diff --git a/.gitignore b/.gitignore index fd14ca42bc2..3f7654a2b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -77,12 +77,12 @@ __pycache__/ # regenerable from committed source — symlink to target/release created by codegen /bin/ -# --- Derived failure-mode roster [owner: gunbc.recurring_failure_mode] --- -# regenerable from committed source — membership list derived from sibling row files before compile; not a second authored authority +# --- Retired failure-mode roster file [owner: gunbc.recurring_failure_mode] --- +# superseded; canonical home: the in-memory roster module contributed by source acquisition (v1_compiler.cli_run.derived_row_roster acquire_dir_files); a file at this path is a legacy artifact of an older gunbc and is refused by the loader — legacy file an older gunbc wrote on every source walk; the roster module is now derived in memory and never written /dag/gunbc/recurring_failure_mode/roster.dag -# --- Derived rung-drop roster [owner: gunbc.rung_drop] --- -# regenerable from committed source — membership list derived from sibling RungDrop row files before compile; not a second authored authority +# --- Retired rung-drop roster file [owner: gunbc.rung_drop] --- +# superseded; canonical home: the in-memory roster module contributed by source acquisition (v1_compiler.cli_run.derived_row_roster acquire_dir_files); a file at this path is a legacy artifact of an older gunbc and is refused by the loader — legacy file an older gunbc wrote on every source walk; the roster module is now derived in memory and never written /dag/gunbc/rung_drop/roster.dag # --- Generator stamp files [owner: gunbc.generated_artifact] --- diff --git a/DESIGN.md b/DESIGN.md index a8d5389b735..bbb9c1624f6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -99,7 +99,7 @@ The domain roster is a modeled fact, not this prose: `gunbc.design_argument` `co - **external facts** — each independently versioned upstream has its own module; this is the strongest instance and was, until this section, the only one with a reviewer. Home authorities: `extdeps.external_authority::ExternalModelScope`. - **materialization / realization** — materialization and realization are separate interfaces with admissible handlers: materialization presents and stores an admitted identity and result contract; realization orders, places and transports work. This row owns whether a handler, provider or placement faithfully realizes that admitted identity and result contract (completeness, readback, retention, transport, refusal, binding obligations) without substituting, truncating or bypassing the admitted key. Cache purity is one discriminator across that boundary, not the whole domain. Key RELATION -- what relation, what scope, what complete canonical preimage, equality and collision disposition, and whether declared inputs are complete -- is owned by conformance-identity. Home authorities: `std.realization::Materialization`, `std.materialization_provider::ArtifactRequest`, `std.artifact_store::ArtifactStore`, `std.cache_interface::StorageSurface`. -- **leasing / locking / grants / privileged access** — exclusive or bounded use and privileged access are one domain: leases with a declared deadline, compare-and-set expectations, scoped authorizations, grants naming who may perform an effect and how it is released, resource handles -- and, since 2026-09-19, which AUTHORIZATION PATTERN performs a privileged effect. That pattern is a §3d selection (gunbc.auth.authorization_pattern_selection consumes std.decision over the effect's frequency, reversibility, surface, workload-identity binding, minted reach and billing consequence): workload identity federation with a per-secret cell for recurring automated effects, one operator approval through the ntfy loop for one-off or witnessed effects, a named human-only step where no API exists, and a refusal for an operator token relayed by chat or between sessions. The census (gunbc.auth.privileged_effect_census) classifies every current site by executing that selection, so the reviewer's three-valued answer is derived for the sites that exist and asked of the site a change adds. A compare-and-set slot's generation line is kept whole by file_compare_and_set and observe_cas_slot_state (gunbc.durable_cas_file_store), because several slots read it as an audit trail; a slot whose only fact is its head -- the materialization store's occupancy index and family holds are the first -- uses the distinct windowed operations (file_compare_and_set_windowed, observe_cas_slot_state_windowed, and the file_hold_*_windowed family) with a CasGenerationWindow of k >= 2, so a bounded store's own bookkeeping does not grow with its commits. They are two contracts, not one operation with a mode: keep-all callers are untouched, and only a caller that names the windowed operation can delete generations. A window read takes the head from a listing of the slot root and verifies head+1 is absent, re-reading on a race and never guessing; a write reclaims below head-k only after its own commit, and a refused delete is reported, never fatal. Exclusion is unchanged -- losers re-read and retry -- so nothing waits on a hold. docs/plans/fabric-storage.md names the head-probe bound this also lifts and assigns its remedy to compaction -- a head that starts from a snapshot; a window is that head-starting-above-1 shape for a slot whose history needs no snapshot, and the fabric heads are not opted in. Home authorities: `std.temporal_effect::HeldLease`, `std.durable_compare_and_set::CasExpectation`, `gunbc.durable_cas_file_store::file_compare_and_set_windowed`, `std.durable_exclusive_hold::DurableHoldState`, `std.scoped_authorization::ScopedAuthorization`, `std.effect_grant::Grant`, `std.resources::ResourceHandle`, `gunbc.auth.authorization_pattern_selection::select_authorization_pattern`, `gunbc.auth.github_gcp_federation::github_wif_provider`, `gunbc.auth.gcp_secret_access::SecretAccessGrant`, `gunbc.auth.access_request::AccessRequest`, `gunbc.auth.approval_capability::ApprovalCapability`, `gunbc.auth.approval_broker::redeem_capability`, `gunbc.auth.access_token_source::select_access_token_source`, `gunbc.auth.privileged_effect_census::privileged_effect_census`. +- **leasing / locking / grants / privileged access** — exclusive or bounded use and privileged access are one domain: leases with a declared deadline, compare-and-set expectations, scoped authorizations, grants naming who may perform an effect and how it is released, resource handles -- and, since 2026-09-19, which AUTHORIZATION PATTERN performs a privileged effect. That pattern is a §3d selection (gunbc.auth.authorization_pattern_selection consumes std.decision over the effect's frequency, reversibility, surface, workload-identity binding, minted reach and billing consequence): workload identity federation with a per-secret cell for recurring automated effects, one operator approval through the ntfy loop for one-off or witnessed effects, and a named human-only step where no API exists. The census (gunbc.auth.privileged_effect_census) classifies every current site by executing that selection, so the reviewer's three-valued answer is derived for the sites that exist and asked of the site a change adds. A compare-and-set slot's generation line is kept whole by file_compare_and_set and observe_cas_slot_state (gunbc.durable_cas_file_store), because several slots read it as an audit trail; a slot whose only fact is its head -- the materialization store's occupancy index and family holds are the first -- uses the distinct windowed operations (file_compare_and_set_windowed, observe_cas_slot_state_windowed, and the file_hold_*_windowed family) with a CasGenerationWindow of k >= 2, so a bounded store's own bookkeeping does not grow with its commits. They are two contracts, not one operation with a mode: keep-all callers are untouched, and only a caller that names the windowed operation can delete generations. A window read takes the head from a listing of the slot root and verifies head+1 is absent, re-reading on a race and never guessing; a write reclaims below head-k only after its own commit, and a refused delete is reported, never fatal. Exclusion is unchanged -- losers re-read and retry -- so nothing waits on a hold. docs/plans/fabric-storage.md names the head-probe bound this also lifts and assigns its remedy to compaction -- a head that starts from a snapshot; a window is that head-starting-above-1 shape for a slot whose history needs no snapshot, and the fabric heads are not opted in. Home authorities: `std.temporal_effect::HeldLease`, `std.durable_compare_and_set::CasExpectation`, `gunbc.durable_cas_file_store::file_compare_and_set_windowed`, `std.durable_exclusive_hold::DurableHoldState`, `std.scoped_authorization::ScopedAuthorization`, `std.effect_grant::Grant`, `std.resources::ResourceHandle`, `gunbc.auth.authorization_pattern_selection::select_authorization_pattern`, `gunbc.auth.github_gcp_federation::github_wif_provider`, `gunbc.auth.gcp_secret_access::SecretAccessGrant`, `gunbc.auth.access_request::AccessRequest`, `gunbc.auth.approval_capability::ApprovalCapability`, `gunbc.auth.approval_broker::redeem_capability`, `gunbc.auth.access_token_source::select_access_token_source`, `gunbc.auth.privileged_effect_census::privileged_effect_census`. - **fabric / compute** — allocation, placement and negotiation of resources. The shared authority is product.capacity (lease, pool, seat events) and product.fabric (priced selection over a provider parameter); the subjects that consume it are compute cells (work contracts, compute layouts, cell reservations, capacity class admission) and inference serving (seat binding, co-tenancy admission). Serving is INSIDE this domain, not beside it -- but the sharing is partial and the boundary is the point: both subjects reach the same priced selection fold, and each carries its OWN occupancy producer and linearization (serving: a termed LeaseGrant settled by fabric_seat_acquire over an event chain; compute: a timeless LeaseIdentity settled by file_compare_and_set). They differ further in what the lease is over -- a compute cell is stateless between grants and settles money, a serving seat is over a replica with loaded state and is quality-conditioned instead. The argument is [the serving capacity home ruling](docs/plans/serving-capacity-home-ruling.md). Home authorities: `product.fabric.selection::select_supply`, `product.capacity.lease::LeaseGrant`, `product.capacity.pool_events::SeatRequest`, `product.fabric.work::ExecutionRequirements`, `gunbc.fabric_event_log::fabric_seat_acquire`, `gunbc.compute.work_request::WorkOperation`, `gunbc.compute.work_provider_local::ComputeLayout`, `gunbc.fabric_control_plane::CellReservation`, `gunbc.fabric_executor_class::CapacityClassAdmission`, `gunbc.harness.harness_seat::harness_bind_seat`, `gunbc.serving.turn_admission::serving_group_admission`. - **decision / selection** — a choice must remain attributable to its subject, candidate field, constraints, evidence and policy, and conformance exposes accidental forks of that authority; the hard selection laws -- no answer outrunning its evidence, no front standing as a winner -- stay with the §3d reviewer, and goal assessment (std.goal_assessment) is a boundary of this row, not a member of it. Home authorities: `std.decision::DecisionSubject`, `std.decision::RealizationSelectionResult`, `std.decision::SelectionReceipt`, `std.decision::select_realization`, `std.pareto::SelectionAxis`, `std.pareto::ParetoEntry`, `std.pareto::DominanceVerdict`. - **keys / hashing** — the row's scope today is the three keying relations that have a consumed home: structural node identity (v2.std.node content_hash over std.content_hash HashFamily), the request and artifact keys of std.materialization_provider (request_key, artifact_request_key), and occurrence identity within one source graph (std.occurrence_identity) -- plus the declared-equality admissibility of a key TYPE (std.algebra algebra_profile_equality_extensional), which governs whether distinct keys of a finite map are decidably distinct (consumed by the map-literal duplicate-key check, docs/plans/map-literal-introduction-design.md). That admissibility is instantiation-blind at this rung: an opaque brand or a type parameter admits unjudged, and its next-rung trigger is instantiation-grain admission (v1 infer equality_admission_wall_note). This row owns the key RELATION -- what relation, what scope, what complete canonical preimage derives it, what equality and collision disposition govern it -- and whether the declared inputs are complete. Faithful realization of an admitted identity and result contract (completeness, readback, retention, transport, refusal, binding; cache purity as one discriminator) is owned by conformance-realization. Two further relations -- demand identity (the question asked) and the dependency read set an answer consumed -- are a declared frontier, not members: they have no home authority yet, are modeled by docs/plans/demand-engine-program.md, and enter this row when M1 of that program lands their carriers; until then a reviewer asks nothing about them under this row (DESIGN §3b: a home that owns only part of a claimed scope is a scope mismatch, so the scope is stated at what the homes own). Each member is a named keying relation within a named scope, derived rather than authored; the relation model is docs/plans/keying-relation-design.md. Home authorities: `std.content_hash::HashFamily`, `std.materialization_provider::request_key`, `v2.std.node::content_hash`, `std.occurrence_identity::OccurrenceIdAllocator`, `extdeps.realization.cache_purity::CachePurityVerdict`, `std.computation_identity::ComputationIdentity`, `std.algebra::algebra_profile_equality_extensional`. diff --git a/ROADMAP.md b/ROADMAP.md index 96317e7e066..17af24b329d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -68,7 +68,7 @@ Every node declares its parent in the functional decomposition: gunbc-project-ro - [ ] **DP-M3: extdeps-scope closure halved** — The legacy extdeps scope manifest (dag/gunbc/legacy_extdeps_scope_frontier.tsv via gunbc.extdeps_scope_frontier) halved, and tools.extdeps_scope_placement_gate enrolled on an executing route before anything is counted. The unimported-bare-provider roster the census pairs with this is not on this line (v2.workflow.floor_unimported_bare_provider_debt_roster exists on main only). Why: An undeclared external-authority placement is a §3 citation nobody can check. [authority](dag/gunbc/extdeps_scope_frontier.dag) - [ ] **DP-M4: closed matches are total** — gunbc.non_fold_residue non_fold_residue_frontier (about 186 rows on this tree) down to at most 100, draining the nfr_reason_mint_era rows first (82). The six seed-Rust wildcard sites are out of scope. Why: A wildcard arm silently absorbs a new variant, which is the floor-level exhaustiveness guarantee missing. [authority](dag/gunbc/non_fold_residue.dag) - [ ] **DP-M5: absence is established, never inferred** — extdeps.filesystem.filesystem_io filesystem_absence_establishment_adoption_standing driven to zero open sites (each routed through filesystem_file_observation / filesystem_entry_presence), and filesystem_read_outcome_adoption_standing re-measured at identity grain. Why: Each unconverted site can report "nothing there" when it could not look. [authority](dag/extdeps/filesystem/filesystem_io.dag) -- [ ] **DP-M6: v2 native-route walls restored** — The native-route rung drops retired and the eval-step drop family shrunk. The three drops the census names (g0_type_decl_modifier_parse_without_sealing_property, variant_fields_unlowered_on_the_native_route, admit_callers_discarded_on_the_native_route) exist on main and not on this line, so this milestone's population is re-derived from gunbc.rung_drop once the line carries them. Why: Each standing drop is a known wall down on the path v2 is meant to run on. [authority](dag/gunbc/rung_drop.dag) +- [ ] **DP-M6: v2 native-route walls restored** — The native-route rung drops retired and the eval-step drop family shrunk. The three drops the census names (g0_type_decl_modifier_parse_without_sealing_property, variant_fields_unlowered_on_the_native_route, admit_callers_discarded_on_the_native_route) existed on main and not on this line. admit_callers_discarded_on_the_native_route is now retired by its own trigger -- the native route checks caller admission from the fn's declared admit_callers list, with the Arrow carrier and its consumer landing together (v2.compiler body_lowering_fold body_lower_fn_admit_clause_edges; v2.compiler.infer infer_caller_admission_diags) -- so it no longer counts. The other two remain, re-derived from gunbc.rung_drop once the line carries them. Why: Each standing drop is a known wall down on the path v2 is meant to run on. [authority](dag/gunbc/rung_drop.dag) - [ ] **The factory: harness, workers, CI/floor and the daily workspace, the machine that lands and verifies work** — The steps every change traverses (orientation, implementation, test, commit, publication, review, verification) are owned by the workflow as durable state, not by an agent's memory. CI and floor verdicts name what they executed and cost what their inputs require. Why: Every obligation left in prose is re-learned per session and dropped under pressure. Every finished candidate that needs hand-integration spends operator time the factory exists to save. [authority](docs/plans/harness-work-lifecycle.md) - [ ] **One request travels the whole factory and comes back as an independently reviewed, published candidate** — The dogfood route as one acceptance case: request, immutable subject, shared computation, placement grant, isolated execution, durable result, metering, independent review, candidate published, cleanup, result returned (production order: the belt reviews a head before publishing it) -- each established by its own typed record from the durable records the real srv2 route wrote for ONE attempt (gunbc.roadmap_dogfood_route DogfoodRouteReceipt). The start receipt is the first complete attempt, selected in refname order, by the first successful recorder to observe one (scanning durable attempt history), recorded once, create-only; attempts operated by hand before the route existed do not qualify. Why: Without one executable case the factory is a set of stages that each pass alone while no request has crossed them all, and the operator's confidence -- the gate for the first external customer -- has nothing to rest on. [authority](dag/gunbc/roadmap/roadmap_dogfood_route.dag) - [ ] **Publish each attempt's worker events to the fabric event log** — The harness's per-attempt worker events, today appended only to a host-local provider-events.jsonl that gunbc.roadmap_provider_events reads, are appended to gunbc.fabric_event_log as a new payload kind over its existing partitions (product.capacity.event_chain), one partition per attempt. Any consumer then reads an attempt's events from the fabric rather than from a file on the worker's host. Why: Without it, every live view depends on sharing a filesystem with the worker, and streaming cannot go through the fabric as ruled. [authority](dag/gunbc/fabric/fabric_event_log.dag) diff --git a/artifacts/bmc/mtcollins1-boot-run-37102391062/SHA256SUMS b/artifacts/bmc/mtcollins1-boot-run-37102391062/SHA256SUMS index c4117401148..08cc6271965 100644 --- a/artifacts/bmc/mtcollins1-boot-run-37102391062/SHA256SUMS +++ b/artifacts/bmc/mtcollins1-boot-run-37102391062/SHA256SUMS @@ -1,4 +1,3 @@ -ff115761d621d17f3c5d23d2869e9f170d8ff0090145df3d2ec633cd496d8306 ./kvm/features.json 357c2410a75c2f46e5679bc17f2c5fa46d2444e3379756037adfa57aaf75557c ./kvm/journal 280137e5d0f80550ddd5ecae177344fce0d7e46ab217137d769ad178ef08a5c6 ./kvm/observer.log 649e3a034f001c421cfa31f5e9d8746e5292ebd6e95aa728201e6e98c5343daf ./kvm/still-1.jpg diff --git a/artifacts/bmc/mtcollins1-boot-run-37102391062/kvm/features.json b/artifacts/bmc/mtcollins1-boot-run-37102391062/kvm/features.json deleted file mode 100644 index 928ce510bf3..00000000000 --- a/artifacts/bmc/mtcollins1-boot-run-37102391062/kvm/features.json +++ /dev/null @@ -1 +0,0 @@ -[{"feature": "RMEDIA_MULTI_IMAGE_SUPPORT"}, {"feature": "BIOS_SUPPORT"}, {"feature": "KVM_SESSION_RECONNECT"}, {"feature": "SYSTEM_FIREWALL"}, {"feature": "OEM_PROPRIETARY_LEVEL_SUPPORT"}, {"feature": "HTML5_KVM"}, {"feature": "DUAL_BIOS_FLASH_SUPPORT"}, {"feature": "IPV6_SUPPORT"}, {"feature": "ALLOW_NON_SECURE_COMMUNICATION"}, {"feature": "SET_SENSOR_THRESHOLDS"}, {"feature": "REPLACE_OEM_LOGO"}, {"feature": "EXTENDEDLOG"}, {"feature": "AUTO_RESIZE_KVM_CLIENT_WINDOW"}, {"feature": "SERVICES"}, {"feature": "RMEDIA"}, {"feature": "AUTOVDORECORD_REMOTE"}, {"feature": "CAPTURE_BSOD_JPEG"}, {"feature": "RADIUS_SUPPORT"}, {"feature": "SYSTEM_FIREWALL_TIMEOUT"}, {"feature": "UARTLOG"}, {"feature": "RUNTIME_HOST_LOCK"}, {"feature": "SOL_SUPPORT"}, {"feature": "HPM_SUPPORT"}, {"feature": "ADVISER_SUPPORT"}, {"feature": "AD_SUPPORT"}, {"feature": "MULTIPLE_USER_VMEDIA"}, {"feature": "LDAP_SUPPORT"}, {"feature": "KVM_SESSION_TIMEOUT"}, {"feature": "FIRMWARE_FLASH_SUPPORT"}, {"feature": "CPLD_SUPPORT"}, {"feature": "PTP_SERVER_SUPPORT"}, {"feature": "BOOTFW_SUPPORT"}, {"feature": "HOST_LOCK_AUTO"}, {"feature": "BACKUP_CONFIG"}, {"feature": "SET_SENSOR_THRESHOLDS_RESET"}, {"feature": "SAVE_SELLOG"}, {"feature": "SESSION_MANAGEMENT"}, {"feature": "RMEDIA_MULTI_CD_SUPPORT"}, {"feature": "TSIG"}, {"feature": "IPV6_COMPLIANCE"}, {"feature": "CD_SERVER_APP"}, {"feature": "WEB_DASHBOARD_WIDGETS"}, {"feature": "FOLDER_REDIRECTION"}, {"feature": "CIRCULAR_SEL"}, {"feature": "FW_IMAGE"}, {"feature": "DISABLE_EMPTY_PASSWORD"}, {"feature": "KB_LANG_SELECT_SUPPORT"}, {"feature": "DYNAMIC_KCS_LAN_SUPPORT"}, {"feature": "LMEDIA"}, {"feature": "DUAL_BOOTFW_EEPROM_SUPPORT"}, {"feature": "BLOCK_ICMP_TIMESTAMP"}, {"feature": "SINGLE_PORT_APP"}, {"feature": "MDNS"}, {"feature": "NCSI_SUPPORT"}, {"feature": "IMG_REDIRECTION"}, {"feature": "HD_SERVER_APP"}, {"feature": "PAM_REORDERING"}, {"feature": "EXTENDED_PRIV"}, {"feature": "SNMP"}, {"feature": "WEB_APP_PROXY"}, {"feature": "LMEDIA_MULTI_IMAGE_SUPPORT"}, {"feature": "VMEDIA_MAX_COUNT_FOR_KVM"}, {"feature": "MEDIA_REDIR_READ_WRITE_ONLY"}, {"feature": "RMEDIA_SESSION_RECONNECT"}, {"feature": "PRESERVECONF"}, {"feature": "TIMEZONE_SUPPORT"}, {"feature": "RMEDIA_MULTI_HD_SUPPORT"}, {"feature": "AUTOVDORECORD"}, {"feature": "YES"}, {"feature": "VERSION_CMP_FLASH"}, {"feature": "BMC_RECOVERY"}, {"feature": "NWLINK"}, {"feature": "KVM_OR_MEDIA_AVAILABLE"}] \ No newline at end of file diff --git a/artifacts/bmc/mtcollins1-megarac-api-surface.txt b/artifacts/bmc/mtcollins1-megarac-api-surface.txt deleted file mode 100644 index ddad31eef40..00000000000 --- a/artifacts/bmc/mtcollins1-megarac-api-surface.txt +++ /dev/null @@ -1,255 +0,0 @@ -api/acd/download_crashdump -api/acd/getcrashdumpstatus -api/acd/set_crashdump_download -api/acd/setcrashdumpstatus -api/actions/chassis-led -api/actions/power -api/aep_inventory/aep_dimm_action -api/aep_inventory/aepdimm_fw_flash_status -api/aep_inventory/aep_dimm_info -api/aep_inventory/aep_namespace_info -api/aep_inventory/aep_pools_info -api/aep_inventory/last_action_info -api/ae_test -api/chassis-status -api/configuration/runtime -api/dashboardsensors -api/detail_sensors_readings -api/fan_control/get_fan_info -api/fan_control/get_manager_status -api/fan_control/set_fan_speed -api/fan_control/set_manager_status -api/firmware-info -api/fru -api/host/get_crash_dump -api/host_inventory/host_interface_baseboard_info -api/host_inventory/host_interface_memory_info -api/host_inventory/host_interface_pcie_device_function_info -api/host_inventory/host_interface_power_info -api/host_inventory/host_interface_processor_info -api/host_inventory/host_interface_storage_info -api/host_inventory/host_interface_system_info -api/host_inventory/host_interface_thermal_info -api/logs/acd -api/logs/audit -api/logs/dashboardevent -api/logs/event -api/logs/event-file -api/logs/eventlog -api/logs/system -api/logs/video -api/logs/video-data -api/logs/video-log -api/logs/video-log/delete -api/maintenance/backup_config -api/maintenance/bios-signed-image-key -api/maintenance/bmc_recovery -api/maintenance/bootfw-signed-image-key -api/maintenance/cpld/enableupdatemode -api/maintenance/cpld/getinfo -api/maintenance/download_config -api/maintenance/dualflashimageconfig -api/maintenance/dual_image_config -api/maintenance/firmware -api/maintenance/firmware/dwldfwimg -api/maintenance/firmware/dwldfwstatus-progress -api/maintenance/firmware/firmware -api/maintenance/firmware/flash-progress -api/maintenance/firmware/upgrade -api/maintenance/firmware/verification -api/maintenance/flash -api/maintenance/flashtrack -api/maintenance/fwimage_location -api/maintenance/hpm/ -api/maintenance/hpm/activatecomponents -api/maintenance/hpm/componentversions -api/maintenance/hpm/exitupdatemode -api/maintenance/hpm/flash -api/maintenance/hpm/freemem -api/maintenance/hpm/getbiosversions -api/maintenance/hpm/preparecomponents -api/maintenance/hpm/updatemode -api/maintenance/hpm/upgradestatus -api/maintenance/hpm/verifyimage -api/maintenance/hpm/verifyimagestatus -api/maintenance/nm_activate -api/maintenance/nm_compareversion -api/maintenance/nm_deactivate -api/maintenance/nm_ecfstatus -api/maintenance/nm_eraseflash -api/maintenance/nm_firmware_update -api/maintenance/nm_upload -api/maintenance/nvme/activatecomponents -api/maintenance/nvme/fwupdate -api/maintenance/nvme/fwupdateprogress -api/maintenance/nvme/nvmemissdsfw -api/maintenance/pldm/ -api/maintenance/pldm/fwupdateprogress -api/maintenance/pldm/fwupdatestatus -api/maintenance/pldm/getpldmfwinfo -api/maintenance/preserve -api/maintenance/reset -api/maintenance/restore_config -api/maintenance/restore_defaults -api/maintenance/signed-image-key -api/maintenance/system_admin -api/maintenance/upload_restore -api/maintenance/ValidBootfwSignImage -api/maintenance/ValidSignImage -api/operations/ssl/certificate/generate -api/pldm/eids -api/pldm/sensors -api/pldm/state_sensors -api/remote_control/get/kvm/launch -api/reset-pass -api/scripts -api/scripts/ -api/sensors -api/session -api/set/dns/registration -api/settings/active-directory-settings -api/settings/active-directory-users -api/settings/channels -api/settings/dashboardntp -api/settings/dashboarduser -api/settings/date-time -api/settings/dns/domain-options -api/settings/dns-info -api/settings/dns/registration -api/settings/dns/restart -api/settings/dns/server-options -api/settings/dns/tsig-private-key -api/settings/download_image -api/settings/firewall -api/settings/firewall/ -api/settings/firewall-ip-rules -api/settings/firewall-port-rules -api/settings/ipmi_disable_interfaces -api/settings/javasol -api/settings/lan_destination/snmptrap_user -api/settings/ldap-certificates -api/settings/ldap-settings -api/settings/ldap-users -api/settings/license -api/settings/licenses -api/settings/log -api/settings/log-certificates -api/settings/log-policy -api/settings/media/active_redirections -api/settings/media/adviser -api/settings/media/general -api/settings/media/instance -api/settings/media/local/configurations -api/settings/media/local/file -api/settings/media/local/image -api/settings/media/local/images -api/settings/media/local/lmediasizecheck -api/settings/media/local/start-media -api/settings/media/local/stop-media -api/settings/media/remote/configurations -api/settings/media/remote/images -api/settings/media/remote/lmediarefreshlist -api/settings/media/remote/rmediarefreshlist -api/settings/media/remotesession -api/settings/media/remote/start-media -api/settings/media/remote/stop-media -api/settings/mouse -api/settings/ncsi-interfaces -api/settings/ncsi/mode -api/settings/network -api/settings/network/activelancfg -api/settings/network-bond -api/settings/network-link -api/settings/nic_management/nic_controller_info -api/settings/nvme_management/nvme_basic_mi_vpd_info -api/settings/nvme_management/nvme_controller_info -api/settings/nvme_management/nvme_p3700_vital_product_data -api/settings/nvme_mi_management/nvme_mi_clear_status_flags -api/settings/nvme_mi_management/nvme_mi_set_status_flags -api/settings/nvme_mi_management/nvme_mi_ssd_info -api/settings/nvme_mi_management/nvme_mi_subsystem_info -api/settings/nvme_mi_management/nvme_mi_vpd_info -api/settings/packages -api/settings/pam-order -api/settings/pcie_switch_management/get_pcie_bifurcation_info -api/settings/pcie_switch_management/pcie_mgmt_dev_info -api/settings/pcie_switch_management/pcie_partition_count -api/settings/pcie_switch_management/pcie_port_bind -api/settings/pcie_switch_management/pcie_port_link_status -api/settings/pcie_switch_management/pcie_port_unbind -api/settings/pcie_switch_management/pcie_switch_reset -api/settings/pcie_switch_management/set_pcie_bifurcation_info -api/settings/pcie_switch_management/validate_port_unbind -api/settings/pcie_switch_mgmt/pcie_partition_info -api/settings/pef/alert_policies -api/settings/pef/alert_string_keys -api/settings/pef/destinations -api/settings/pef/event_filters -api/settings/pef/lan_destinations -api/settings/pef/send_test_alert -api/settings/radius/advanced -api/settings/radius/general -api/settings/raid_management/bbu_info -api/settings/raid_management/clear_all_foreign_cmd -api/settings/raid_management/create_lu_cache -api/settings/raid_management/create_virtual_drive -api/settings/raid_management/dedicated_hot_spare_info -api/settings/raid_management/delete_virtual_drive -api/settings/raid_management/logical_consistency_check_cmd -api/settings/raid_management/logical_device_info -api/settings/raid_management/logical_device_progress_cmd -api/settings/raid_management/logical_initialize_check_cmd -api/settings/raid_management/logical_start_stop_locate_cmd -api/settings/raid_management/manage_array_info -api/settings/raid_management/manage_data_drive_array -api/settings/raid_management/manage_spare_drive_array -api/settings/raid_management/patrol_read_properties -api/settings/raid_management/patrol_read_start_stop_cmd -api/settings/raid_management/physical_dedicated_hotspare_cmd -api/settings/raid_management/physical_device_info -api/settings/raid_management/physical_hotspare_cmd -api/settings/raid_management/physical_onlineoffline_cmd -api/settings/raid_management/physical_removal_cmd -api/settings/raid_management/physical_start_stop_locate_cmd -api/settings/raid_management/physical_state_cmd -api/settings/raid_management/raid_controller_info -api/settings/raid_management/raid_event_log -api/settings/raid_management/raid_levels -api/settings/raid_management/raid_pd_count -api/settings/raid_management/raid_topology_info -api/settings/raid_management/sesenclosureinfo -api/settings/raid_management/sesenclosureinfobrcm -api/settings/raid_management/storage_summary -api/settings/sasit_management/sasit_controller_info -api/settings/sasit_management/sasit_enclosure_info -api/settings/sasit_management/sasit_event_log -api/settings/sasit_management/sasit_physical_device_info -api/settings/sasit_management/sasit_physical_start_stop_locate_cmd -api/settings/sasit_management/sasit_topology_info -api/settings/sensor-threshold -api/settings/services -api/settings/service-sessions -api/settings/smtp -api/settings/smtp/smtpupload -api/settings/sol-recorded-video -api/settings/ssl/certificate -api/settings/ssl/certificate-info -api/settings/static-ipv6 -api/settings/user-preference -api/settings/users -api/settings/users/ -api/settings/users_read_only -api/settings/user/ssh-key-upload/ -api/settings/video/pre-event -api/settings/video/remote-storage -api/settings/video/sol-remote-storage -api/settings/video/sol-triggers -api/settings/video/triggers -api/sol/session -api/sol/solcfg -api/status/uptime -api/system_diagnostics/download_system_diagnostics_log -api/system_diagnostics/generate_system_diagnostics_logs -api/system_diagnostics/log -api/tasks -api/updatenew_password diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/AccountService_Accounts.json b/artifacts/bmc/srv13-first-contact-2026-10-07/AccountService_Accounts.json new file mode 100644 index 00000000000..1e53331cef2 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/AccountService_Accounts.json @@ -0,0 +1,12 @@ +{ + "@odata.id": "/redfish/v1/AccountService/Accounts", + "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection", + "Description": "BMC User Accounts", + "Members": [ + { + "@odata.id": "/redfish/v1/AccountService/Accounts/root" + } + ], + "Members@odata.count": 1, + "Name": "Accounts Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Chassis.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Chassis.json new file mode 100644 index 00000000000..bc18d21c41e --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Chassis.json @@ -0,0 +1,11 @@ +{ + "@odata.id": "/redfish/v1/Chassis", + "@odata.type": "#ChassisCollection.ChassisCollection", + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + } + ], + "Members@odata.count": 1, + "Name": "Chassis Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/FINDINGS.txt b/artifacts/bmc/srv13-first-contact-2026-10-07/FINDINGS.txt new file mode 100644 index 00000000000..f81aa6671a4 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/FINDINGS.txt @@ -0,0 +1,54 @@ +srv13 first contact, 2026-10-07 (ASRock Rack ALTRAD8UD-1L2T), read from srv2. +Each line names the capture file that carries it. + +BMC 192.168.1.247, MAC 9c:6b:00:e7:c9:83 (router reservation bmc-altrad8ud-1l2t-5), + OpenBMC 2.07.00, Redfish 1.17.0, factory credential Managers_bmc.json, service-root.json +BIOS AMI Aptio, UEFI 2.06, Aptio core 2.22.1282, CPLD_MB 0.02 UpdateService_FirmwareInventory_bios_active.json, sol-after-reseat.log +Host NIC Intel I210 Gigabit, 9C:6B:00:E7:CA:63 sol-setup-walk.log (Advanced page) +Redfish Bios resource exposes no Attributes: settings are not readable + or writable over Redfish on this build Systems_system_Bios.json +Boot override targets: None Pxe Hdd Cd Diags BiosSetup Usb (no UefiShell) Systems_system.json +SOL enabled, ADMINISTRATOR (ipmitool sol info, not captured) + +FACTORY DEFAULTS, Aptio Setup + Boot: Hard Disk, NVME, CD/DVD, USB Device, Network, UEFI AP Built-in EFI Shell; + Setup Prompt Timeout 1; NumLock On; Boot Beep Disabled; Full Screen Logo Enabled sol-setup-walk.log + Advanced > Network Stack Configuration: + Network Stack Enabled; IPv4 PXE Disabled; IPv4 HTTP Disabled; + IPv6 PXE Disabled; IPv6 HTTP Disabled; PXE boot wait time 0; Media detect count 1 sol-after-reseat.log + +PXE TEST One-time Pxe override + ForceRestart landed in Aptio Setup, not a PXE client, + because every PXE/HTTP boot client is disabled by default. sol-pxe-override.log + Enabling one is a Setup change; Redfish cannot make it on this build. + +INCIDENT After that test the host went dark (power On, no console). The event log carries + "Undetermined system hardware failure" asserts from 2023-04-03, 2026-10-06 and + 2026-10-07, mostly in pairs ~18 s apart. Operator reseated a DIMM and AC-cycled the + board; it then POSTed to Setup in ~100 s. sel-elist.txt, sol-after-reseat.log + Whether the paired asserts are a fault or this BMC's per-boot noise is not decided. + +THERMAL (2026-10-08) The host had no CPU heatsink fitted. Redfish EventLog records "Thermal trip" + 6-7 min after each successful power-on (23:13, 23:51 on 10-07; 23:39, 23:43 on 10-06), + and the trip latches: later Redfish power-ons leave the host dark (override never + consumed) until an AC cycle. All ten fan headers read "No Reading". The IPMI + "Undetermined system hardware failure" pairs are these thermal and power events. + redfish-eventlog.json, sdr-elist-host-dark.txt + +BMC 03.22.00 (pushed 2026-10-08 01:00Z, host off; member altrad8ud-1l2t_3.22.00.static.mtd.tar + sha256 cb8b8e72...; staged by UpdateService task, active after a Manager reset) + First authenticated request with the published credential: HTTP 403, Redfish + MessageId Base.1.16.0.PasswordChangeRequired (a wrong password is 401). Rotated by + gunbc.tools.bmc_onboard srv13_converge_credential into secret bmc-srv13-admin. + bmc-3.22.00-factory-login-403.json, bmc-push-3.22.00-response.json + Boot override targets unchanged (no UefiShell). Registries gain + BiosAttributeRegistryA3139.2.6.0, but its entry serves no body and Systems/system/Bios + carries no Attributes; Bios/Settings is 404. bmc-3.22.00/ + With BIOS 2.06 the host reached Setup (override consumed ~50 s) and sat ~80 s: no + Attributes published. + +BIOS 4.01 (pushed 2026-10-08 02:26Z, host off; member obmc-bios-altrad8ud-1l2t-4.01.all.tar + sha256 33937359...) bios_active reads 4.01. First boot after the flash took ~135 s to + reach Setup; the second ~85 s. ~2 min in Setup: no Attributes, registry still the + 2.6.0 stub. bios-4.01/ + Not yet tested: whether the BIOS uploads attributes only on leaving Setup for a boot + device (needs a heatsink). diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc.json new file mode 100644 index 00000000000..cf597b77146 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc.json @@ -0,0 +1,99 @@ +{ + "@odata.id": "/redfish/v1/Managers/bmc", + "@odata.type": "#Manager.v1_14_0.Manager", + "Actions": { + "#Manager.Reset": { + "@Redfish.ActionInfo": "/redfish/v1/Managers/bmc/ResetActionInfo", + "target": "/redfish/v1/Managers/bmc/Actions/Manager.Reset" + }, + "#Manager.ResetToDefaults": { + "ResetType@Redfish.AllowableValues": [ + "ResetAll" + ], + "target": "/redfish/v1/Managers/bmc/Actions/Manager.ResetToDefaults" + } + }, + "DateTime": "2026-10-07T23:05:22+00:00", + "DateTimeLocalOffset": "+00:00", + "Description": "Baseboard Management Controller", + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces" + }, + "FirmwareVersion": "2.07.00", + "GraphicalConsole": { + "ConnectTypesSupported": [ + "KVMIP" + ], + "MaxConcurrentSessions": 4, + "ServiceEnabled": true + }, + "HostInterfaces": { + "@odata.id": "/redfish/v1/Managers/bmc/HostInterfaces" + }, + "Id": "bmc", + "LastResetTime": "2026-10-07T23:02:50+00:00", + "Links": { + "ActiveSoftwareImage": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/c1789ba5" + }, + "ManagerForChassis": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + } + ], + "ManagerForChassis@odata.count": 1, + "ManagerForServers": [ + { + "@odata.id": "/redfish/v1/Systems/system" + } + ], + "ManagerForServers@odata.count": 1, + "ManagerInChassis": { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + }, + "SoftwareImages": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/c1789ba5" + } + ], + "SoftwareImages@odata.count": 1 + }, + "LogServices": { + "@odata.id": "/redfish/v1/Managers/bmc/LogServices" + }, + "ManagerDiagnosticData": { + "@odata.id": "/redfish/v1/Managers/bmc/ManagerDiagnosticData" + }, + "ManagerType": "BMC", + "Model": "OpenBmc", + "Name": "OpenBmc Manager", + "NetworkProtocol": { + "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol" + }, + "Oem": { + "@odata.id": "/redfish/v1/Managers/bmc#/Oem", + "@odata.type": "#OemManager.Oem", + "OpenBmc": { + "@odata.id": "/redfish/v1/Managers/bmc#/Oem/OpenBmc", + "@odata.type": "#OemManager.OpenBmc", + "Certificates": { + "@odata.id": "/redfish/v1/Managers/bmc/Truststore/Certificates" + } + } + }, + "PowerState": "On", + "SerialConsole": { + "ConnectTypesSupported": [ + "IPMI", + "SSH" + ], + "MaxConcurrentSessions": 15, + "ServiceEnabled": true + }, + "ServiceEntryPointUUID": "6e884505-ba5e-49f2-af66-cedf82ef8d8c", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "UUID": "ffdfdf52-02ff-41ed-86e1-825b912cfd38" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc_EthernetInterfaces.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc_EthernetInterfaces.json new file mode 100644 index 00000000000..f543b1f924f --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Managers_bmc_EthernetInterfaces.json @@ -0,0 +1,18 @@ +{ + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces", + "@odata.type": "#EthernetInterfaceCollection.EthernetInterfaceCollection", + "Description": "Collection of EthernetInterfaces for this Manager", + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces/eth0" + }, + { + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces/eth1" + }, + { + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces/usb0" + } + ], + "Members@odata.count": 3, + "Name": "Ethernet Network Interface Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/SHA256SUMS b/artifacts/bmc/srv13-first-contact-2026-10-07/SHA256SUMS new file mode 100644 index 00000000000..c342906c2f5 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/SHA256SUMS @@ -0,0 +1,36 @@ +533e714f76ac3392d2d3723214bf93769144c66d65ffb53c3e38652068057f4f ./AccountService_Accounts.json +d529ffd6d7c10336855e028bee8aff53d1aa1b1f0d2a9bd10ad47fa7bcc338de ./bios-4.01/bios-push-4.01-response.json +088b345314c19277031fcc5918b268a6177e3fec1839fce0df90b0a609cfd6f1 ./bios-4.01/bios-update-task-final.json +9ed3cf320675dd014b1791ecb43d05cdcb125c206dbd09a450ae45f5f2df316f ./bmc-3.22.00/bios-attribute-registry-index.json +4e85a95511849bb353b9fb64411a08b818c86ed7500f9db8ce7722788d64a41c ./bmc-3.22.00-factory-login-403.json +356def19a6b04d4ee038ea2f246f5ba404681bd35486fb9137df20ce27b6a20f ./bmc-3.22.00/root_Chassis_ALTRAD8UD_1L2T_Sensors.json +e9bfb2890d8a2dc0cbdf689fab698dafdfceb681120aee2ae47e7734d53b33b1 ./bmc-3.22.00/root_.json +a2a538b9ac8be12980983f7ad968b10a0d95aac7e62de0acdb9d123b2123ae26 ./bmc-3.22.00/root_Managers_bmc.json +03a2c082df8d6e05a6ffa04cbd719c0185a69b6f7af6f5770e4c628d82f96b54 ./bmc-3.22.00/root_Registries.json +5afa31e43baa9883ae0fb8989d1c6309609a31b8df28cbc724f6f8034519bf87 ./bmc-3.22.00/root_Systems_system_Bios.json +b28942659c3286850f3eb778463abce376447de1d673028ceb2e218b0f850c95 ./bmc-3.22.00/root_Systems_system_Bios_Settings.json +fd4ad69ffdbef8db4e21eedb4b7c107627abd662d9f0bc3fd0effff1484469da ./bmc-3.22.00/root_Systems_system.json +33a6b0ee0af70b0c028dd3e7110c307cb2cab48029aaf9f0cc2efcdf39f1de4c ./bmc-3.22.00/root_UpdateService_FirmwareInventory.json +8681e15a07d9bb3e237557626b00bb7ba0c0f4b81c4abe6d4f74baf98539eb73 ./bmc-3.22.00/root_UpdateService.json +d529ffd6d7c10336855e028bee8aff53d1aa1b1f0d2a9bd10ad47fa7bcc338de ./bmc-push-3.22.00-response.json +41761339bcc45b979de0175029ca5cd48845629f9ce9afb45d5a8511d20df98c ./bmc-update-task-last.json +d6ae05a583d44ae630f0d01df364d37849bda917563c6378b20b858054210d8a ./Chassis.json +c5a742c14554ef622a67713e3555d2392759c3e2b91ecfd2fbae2d4a9c8f6bac ./chassis-status.txt +81bb5b9576a26c909ed769ebb3ae75cc85c3397192ec82e2a98613e329041567 ./FINDINGS.txt +6db5fc5581d5b6e0f40df6cddf71095035f65e8256e93640fa7ae67a90fecc99 ./Managers_bmc_EthernetInterfaces.json +bc29928b9410a2ffe8240cd0ba9671f99294b6f146ec436471a03ac81e15aa0b ./Managers_bmc.json +762feb0be02e66bb175180f037d6ececb95bd347b988dafe81b1c7f5d7e01b2f ./redfish-eventlog.json +66ec2013bbb722e232b1ccb8101a9e4096a46490b360fe7650a8cf24c46e35e0 ./sdr-elist-host-dark.txt +2c829c14f85f29b9de21ff262133f75bf8e4a671fc7b7c531e04a7f3133ada77 ./sel-elist.txt +e9bfb2890d8a2dc0cbdf689fab698dafdfceb681120aee2ae47e7734d53b33b1 ./service-root.json +3a3ef6fc68ee7ba2f89d8d2a688da419bc5c515c4d2e07848cba14ab4f49c415 ./sol-after-reseat.log +65e15c97b39d300cf90d90f1cc57195efa635483974cf7b486724a5797349626 ./sol-pxe-override.log +26b07d6326f9e54001fc1559dd42cd3d695939b7a91fdc64c76ac50a575f730e ./sol-setup-walk.log +5b6e9dcf1d96d1077f8eb14dd059917fa9866b9724cb4a329d1eeccaf3a4a665 ./Systems.json +5afa31e43baa9883ae0fb8989d1c6309609a31b8df28cbc724f6f8034519bf87 ./Systems_system_Bios.json +edec8647dc4670711fb5d6eb71f174a16bc0f045cf6bc522719f25a339cf0b42 ./Systems_system_BootOptions.json +b99147a727c4467d9a6ded317e7781d0440b545f495f28460378bcb69a2bdc3b ./Systems_system.json +8681e15a07d9bb3e237557626b00bb7ba0c0f4b81c4abe6d4f74baf98539eb73 ./update-service-before.json +5c04fcde1f060ec620f4b42f30a6491118d44a2589efcd1f57a1f3990bba8e6d ./UpdateService_FirmwareInventory_bios_active.json +37f04ddc817b2741443901c2f9593d04432e0f4a9043a83051934f63ace1231e ./UpdateService_FirmwareInventory_c1789ba5.json +fcd91e7b5f9638004711ae4b6b7835517ef22a4f00392095b60f0551f0b68d4e ./UpdateService_FirmwareInventory.json diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Systems.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems.json new file mode 100644 index 00000000000..5461060bb19 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems.json @@ -0,0 +1,11 @@ +{ + "@odata.id": "/redfish/v1/Systems", + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/system" + } + ], + "Members@odata.count": 1, + "Name": "Computer System Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system.json new file mode 100644 index 00000000000..f9e3fefa874 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system.json @@ -0,0 +1,117 @@ +{ + "@odata.id": "/redfish/v1/Systems/system", + "@odata.type": "#ComputerSystem.v1_16_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "@Redfish.ActionInfo": "/redfish/v1/Systems/system/ResetActionInfo", + "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset" + } + }, + "AssetTag": "", + "Bios": { + "@odata.id": "/redfish/v1/Systems/system/Bios" + }, + "BiosVersion": "null", + "Boot": { + "AutomaticRetryAttempts": 3, + "AutomaticRetryConfig": "Disabled", + "AutomaticRetryConfig@Redfish.AllowableValues": [ + "Disabled", + "RetryAttempts" + ], + "BootSourceOverrideEnabled": "Disabled", + "BootSourceOverrideMode": "UEFI", + "BootSourceOverrideMode@Redfish.AllowableValues": [ + "Legacy", + "UEFI" + ], + "BootSourceOverrideTarget": "None", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Hdd", + "Cd", + "Diags", + "BiosSetup", + "Usb" + ], + "RemainingAutomaticRetryAttempts": 3, + "StopBootOnFault": "Never", + "TrustedModuleRequiredToBoot": "Disabled" + }, + "BootProgress": { + "LastState": "None", + "LastStateTime": "1970-01-01T00:00:00.000000+00:00" + }, + "Description": "Computer System", + "FabricAdapters": { + "@odata.id": "/redfish/v1/Systems/system/FabricAdapters" + }, + "GraphicalConsole": { + "ConnectTypesSupported": [ + "KVMIP" + ], + "MaxConcurrentSessions": 4, + "ServiceEnabled": true + }, + "Id": "system", + "IndicatorLED": "Off", + "LastResetTime": "2026-10-07T23:04:34+00:00", + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/bmc" + } + ] + }, + "LocationIndicatorActive": false, + "LogServices": { + "@odata.id": "/redfish/v1/Systems/system/LogServices" + }, + "Manufacturer": "ASRockRack", + "Memory": { + "@odata.id": "/redfish/v1/Systems/system/Memory" + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 0.0 + }, + "Model": "ALTRAD8UD-1L2T", + "Name": "system", + "PCIeDevices": [], + "PCIeDevices@odata.count": 0, + "PartNumber": "$PRODUCT_PART_NUMBER", + "PowerRestorePolicy": "AlwaysOn", + "PowerState": "On", + "ProcessorSummary": { + "Count": 0 + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/system/Processors" + }, + "SerialConsole": { + "IPMI": { + "ServiceEnabled": true + }, + "MaxConcurrentSessions": 15, + "SSH": { + "HotKeySequenceDisplay": "Press ~. to exit console", + "Port": 2200, + "ServiceEnabled": true + } + }, + "SerialNumber": "$PRODUCT_SERIAL_NUMBER", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/system/Storage" + }, + "SystemType": "Physical" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_Bios.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_Bios.json new file mode 100644 index 00000000000..cbc252c1af7 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_Bios.json @@ -0,0 +1,23 @@ +{ + "@odata.id": "/redfish/v1/Systems/system/Bios", + "@odata.type": "#Bios.v1_1_0.Bios", + "Actions": { + "#Bios.ResetBios": { + "target": "/redfish/v1/Systems/system/Bios/Actions/Bios.ResetBios" + } + }, + "Description": "BIOS Configuration Service", + "Id": "BIOS", + "Links": { + "ActiveSoftwareImage": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + }, + "SoftwareImages": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + } + ], + "SoftwareImages@odata.count": 1 + }, + "Name": "BIOS Configuration" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_BootOptions.json b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_BootOptions.json new file mode 100644 index 00000000000..efdb5419779 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/Systems_system_BootOptions.json @@ -0,0 +1,19 @@ +{ + "error": { + "@Message.ExtendedInfo": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The requested resource of type named 'BootOptions' was not found.", + "MessageArgs": [ + "", + "BootOptions" + ], + "MessageId": "Base.1.16.0.ResourceNotFound", + "MessageSeverity": "Critical", + "Resolution": "Provide a valid resource identifier and resubmit the request." + } + ], + "code": "Base.1.16.0.ResourceNotFound", + "message": "The requested resource of type named 'BootOptions' was not found." + } +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory.json b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory.json new file mode 100644 index 00000000000..a871b347c70 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory.json @@ -0,0 +1,14 @@ +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory", + "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection", + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/c1789ba5" + } + ], + "Members@odata.count": 2, + "Name": "Software Inventory Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_bios_active.json b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_bios_active.json new file mode 100644 index 00000000000..52bb17a0def --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_bios_active.json @@ -0,0 +1,20 @@ +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active", + "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory", + "Description": "Host image", + "Id": "bios_active", + "Name": "Software Inventory", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/system/Bios" + } + ], + "RelatedItem@odata.count": 1, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Updateable": false, + "Version": "2.06" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_c1789ba5.json b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_c1789ba5.json new file mode 100644 index 00000000000..4b0e9f48647 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/UpdateService_FirmwareInventory_c1789ba5.json @@ -0,0 +1,20 @@ +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/c1789ba5", + "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory", + "Description": "BMC image", + "Id": "c1789ba5", + "Name": "Software Inventory", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Managers/bmc" + } + ], + "RelatedItem@odata.count": 1, + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Enabled" + }, + "Updateable": true, + "Version": "2.07.00" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-push-4.01-response.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-push-4.01-response.json new file mode 100644 index 00000000000..c6c3428698e --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-push-4.01-response.json @@ -0,0 +1,8 @@ +{ + "@odata.id": "/redfish/v1/TaskService/Tasks/0", + "@odata.type": "#Task.v1_4_3.Task", + "Id": "0", + "TaskState": "Running", + "TaskStatus": "OK" +} +http:202 diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-update-task-final.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-update-task-final.json new file mode 100644 index 00000000000..a3a769a7245 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bios-4.01/bios-update-task-final.json @@ -0,0 +1,63 @@ +{ + "@odata.id": "/redfish/v1/TaskService/Tasks/0", + "@odata.type": "#Task.v1_4_3.Task", + "EndTime": "2026-10-08T02:29:28+00:00", + "HidePayload": false, + "Id": "0", + "Messages": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has started.", + "MessageArgs": [ + "0" + ], + "MessageId": "TaskEvent.1.0.3.TaskStarted", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has changed to progress 20 percent complete.", + "MessageArgs": [ + "0", + "20" + ], + "MessageId": "TaskEvent.1.0.3.TaskProgressChanged", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has changed to progress 100 percent complete.", + "MessageArgs": [ + "0", + "100" + ], + "MessageId": "TaskEvent.1.0.3.TaskProgressChanged", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has completed.", + "MessageArgs": [ + "0" + ], + "MessageId": "TaskEvent.1.0.3.TaskCompletedOK", + "MessageSeverity": "OK", + "Resolution": "None." + } + ], + "Name": "Task 0", + "Payload": { + "HttpHeaders": [], + "HttpOperation": "POST", + "JsonBody": "null", + "TargetUri": "/redfish/v1/UpdateService/update" + }, + "PercentComplete": 100, + "StartTime": "2026-10-08T02:27:03+00:00", + "TaskMonitor": "/redfish/v1/TaskService/Tasks/0/Monitor", + "TaskState": "Completed", + "TaskStatus": "OK" +} diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00-factory-login-403.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00-factory-login-403.json new file mode 100644 index 00000000000..b913833e224 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00-factory-login-403.json @@ -0,0 +1,14 @@ +{ + "@Message.ExtendedInfo": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The password provided for this account must be changed before access is granted. PATCH the Password property for this account located at the target URI '/redfish/v1/AccountService/Accounts/root' to complete this process.", + "MessageArgs": [ + "/redfish/v1/AccountService/Accounts/root" + ], + "MessageId": "Base.1.16.0.PasswordChangeRequired", + "MessageSeverity": "Critical", + "Resolution": "Change the password for this account using a PATCH to the Password property at the URI provided." + } + ] +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/bios-attribute-registry-index.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/bios-attribute-registry-index.json new file mode 100644 index 00000000000..e9e7f0abe80 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/bios-attribute-registry-index.json @@ -0,0 +1,12 @@ +{ + "@Message.ExtendedInfo": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The request completed successfully.", + "MessageArgs": [], + "MessageId": "Base.1.16.0.Success", + "MessageSeverity": "OK", + "Resolution": "None" + } + ] +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_.json new file mode 100644 index 00000000000..e7b44dd597d --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_.json @@ -0,0 +1,71 @@ +{ + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_15_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "Cables": { + "@odata.id": "/redfish/v1/Cables" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Id": "RootService", + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "ManagerProvidingService": { + "@odata.id": "/redfish/v1/Managers/bmc" + }, + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "ProtocolFeaturesSupported": { + "DeepOperations": { + "DeepPATCH": false, + "DeepPOST": false + }, + "ExcerptQuery": false, + "ExpandQuery": { + "ExpandAll": false, + "Levels": false, + "Links": false, + "NoLinks": false + }, + "FilterQuery": false, + "OnlyMemberQuery": true, + "SelectQuery": true + }, + "RedfishVersion": "1.17.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "UUID": "6e884505-ba5e-49f2-af66-cedf82ef8d8c", + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + } +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Chassis_ALTRAD8UD_1L2T_Sensors.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Chassis_ALTRAD8UD_1L2T_Sensors.json new file mode 100644 index 00000000000..9878201fb7f --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Chassis_ALTRAD8UD_1L2T_Sensors.json @@ -0,0 +1,24 @@ +{ + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors", + "@odata.type": "#SensorCollection.SensorCollection", + "Description": "Collection of Sensors for this Chassis", + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors/power_total_power" + }, + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors/utilization_PSU1_AC_LOST" + }, + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors/utilization_PSU2_AC_LOST" + }, + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors/utilization_PSU3_AC_LOST" + }, + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T/Sensors/utilization_PSU4_AC_LOST" + } + ], + "Members@odata.count": 5, + "Name": "Sensors" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Managers_bmc.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Managers_bmc.json new file mode 100644 index 00000000000..b372782d791 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Managers_bmc.json @@ -0,0 +1,102 @@ +{ + "@odata.id": "/redfish/v1/Managers/bmc", + "@odata.type": "#Manager.v1_14_0.Manager", + "Actions": { + "#Manager.Reset": { + "@Redfish.ActionInfo": "/redfish/v1/Managers/bmc/ResetActionInfo", + "target": "/redfish/v1/Managers/bmc/Actions/Manager.Reset" + }, + "#Manager.ResetToDefaults": { + "ResetType@Redfish.AllowableValues": [ + "ResetAll" + ], + "target": "/redfish/v1/Managers/bmc/Actions/Manager.ResetToDefaults" + } + }, + "DateTime": "2026-10-08T01:48:36+00:00", + "DateTimeLocalOffset": "+00:00", + "Description": "Baseboard Management Controller", + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Managers/bmc/EthernetInterfaces" + }, + "FanControl": { + "@odata.id": "/redfish/v1/Managers/bmc/FanControl" + }, + "FirmwareVersion": "3.22.00", + "GraphicalConsole": { + "ConnectTypesSupported": [ + "KVMIP" + ], + "MaxConcurrentSessions": 4, + "ServiceEnabled": true + }, + "HostInterfaces": { + "@odata.id": "/redfish/v1/Managers/bmc/HostInterfaces" + }, + "Id": "bmc", + "LastResetTime": "2026-10-08T01:05:54+00:00", + "Links": { + "ActiveSoftwareImage": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/14c085a0" + }, + "ManagerForChassis": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + } + ], + "ManagerForChassis@odata.count": 1, + "ManagerForServers": [ + { + "@odata.id": "/redfish/v1/Systems/system" + } + ], + "ManagerForServers@odata.count": 1, + "ManagerInChassis": { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + }, + "SoftwareImages": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/14c085a0" + } + ], + "SoftwareImages@odata.count": 1 + }, + "LogServices": { + "@odata.id": "/redfish/v1/Managers/bmc/LogServices" + }, + "ManagerDiagnosticData": { + "@odata.id": "/redfish/v1/Managers/bmc/ManagerDiagnosticData" + }, + "ManagerType": "BMC", + "Model": "OpenBmc", + "Name": "OpenBmc Manager", + "NetworkProtocol": { + "@odata.id": "/redfish/v1/Managers/bmc/NetworkProtocol" + }, + "Oem": { + "@odata.id": "/redfish/v1/Managers/bmc#/Oem", + "@odata.type": "#OemManager.Oem", + "OpenBmc": { + "@odata.id": "/redfish/v1/Managers/bmc#/Oem/OpenBmc", + "@odata.type": "#OemManager.OpenBmc", + "Certificates": { + "@odata.id": "/redfish/v1/Managers/bmc/Truststore/Certificates" + } + } + }, + "PowerState": "On", + "SerialConsole": { + "ConnectTypesSupported": [ + "IPMI", + "SSH" + ], + "MaxConcurrentSessions": 15, + "ServiceEnabled": true + }, + "ServiceEntryPointUUID": "6e884505-ba5e-49f2-af66-cedf82ef8d8c", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "UUID": "" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Registries.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Registries.json new file mode 100644 index 00000000000..beac7a4a2dc --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Registries.json @@ -0,0 +1,24 @@ +{ + "@odata.id": "/redfish/v1/Registries", + "@odata.type": "#MessageRegistryFileCollection.MessageRegistryFileCollection", + "Description": "Collection of MessageRegistryFiles", + "Members": [ + { + "@odata.id": "/redfish/v1/Registries/Base" + }, + { + "@odata.id": "/redfish/v1/Registries/TaskEvent" + }, + { + "@odata.id": "/redfish/v1/Registries/ResourceEvent" + }, + { + "@odata.id": "/redfish/v1/Registries/OpenBMC" + }, + { + "@odata.id": "/redfish/v1/Registries/BiosAttributeRegistryA3139.2.6.0" + } + ], + "Members@odata.count": 4, + "Name": "MessageRegistryFile Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system.json new file mode 100644 index 00000000000..847b7743dea --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system.json @@ -0,0 +1,116 @@ +{ + "@odata.id": "/redfish/v1/Systems/system", + "@odata.type": "#ComputerSystem.v1_16_0.ComputerSystem", + "Actions": { + "#ComputerSystem.Reset": { + "@Redfish.ActionInfo": "/redfish/v1/Systems/system/ResetActionInfo", + "target": "/redfish/v1/Systems/system/Actions/ComputerSystem.Reset" + } + }, + "Bios": { + "@odata.id": "/redfish/v1/Systems/system/Bios" + }, + "BiosVersion": "2.06", + "Boot": { + "AutomaticRetryAttempts": 3, + "AutomaticRetryConfig": "Disabled", + "AutomaticRetryConfig@Redfish.AllowableValues": [ + "Disabled", + "RetryAttempts" + ], + "BootSourceOverrideEnabled": "Once", + "BootSourceOverrideMode": "UEFI", + "BootSourceOverrideMode@Redfish.AllowableValues": [ + "Legacy", + "UEFI" + ], + "BootSourceOverrideTarget": "BiosSetup", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Hdd", + "Cd", + "Diags", + "BiosSetup", + "Usb" + ], + "RemainingAutomaticRetryAttempts": 3, + "StopBootOnFault": "Never", + "TrustedModuleRequiredToBoot": "Disabled" + }, + "BootProgress": { + "LastState": "None", + "LastStateTime": "1970-01-01T00:00:00.000000+00:00" + }, + "Description": "Computer System", + "FabricAdapters": { + "@odata.id": "/redfish/v1/Systems/system/FabricAdapters" + }, + "GraphicalConsole": { + "ConnectTypesSupported": [ + "KVMIP" + ], + "MaxConcurrentSessions": 4, + "ServiceEnabled": true + }, + "Id": "system", + "IndicatorLED": "Off", + "LastResetTime": "2026-10-08T00:38:37+00:00", + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/ALTRAD8UD_1L2T" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/bmc" + } + ] + }, + "LocationIndicatorActive": false, + "LogServices": { + "@odata.id": "/redfish/v1/Systems/system/LogServices" + }, + "Manufacturer": "ASRockRack", + "Memory": { + "@odata.id": "/redfish/v1/Systems/system/Memory" + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 0.0 + }, + "Model": "ALTRAD8UD-1L2T", + "Name": "system", + "PCIeDevices": [], + "PCIeDevices@odata.count": 0, + "PartNumber": "$BOARD_PART_NUMBER", + "PowerRestorePolicy": "AlwaysOn", + "PowerState": "Off", + "ProcessorSummary": { + "Count": 0 + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/system/Processors" + }, + "SerialConsole": { + "IPMI": { + "ServiceEnabled": true + }, + "MaxConcurrentSessions": 15, + "SSH": { + "HotKeySequenceDisplay": "Press ~. to exit console", + "Port": 2200, + "ServiceEnabled": true + } + }, + "SerialNumber": "BR80S6102300049", + "Status": { + "Health": "OK", + "HealthRollup": "OK", + "State": "Disabled" + }, + "Storage": { + "@odata.id": "/redfish/v1/Systems/system/Storage" + }, + "SystemType": "Physical" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios.json new file mode 100644 index 00000000000..cbc252c1af7 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios.json @@ -0,0 +1,23 @@ +{ + "@odata.id": "/redfish/v1/Systems/system/Bios", + "@odata.type": "#Bios.v1_1_0.Bios", + "Actions": { + "#Bios.ResetBios": { + "target": "/redfish/v1/Systems/system/Bios/Actions/Bios.ResetBios" + } + }, + "Description": "BIOS Configuration Service", + "Id": "BIOS", + "Links": { + "ActiveSoftwareImage": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + }, + "SoftwareImages": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + } + ], + "SoftwareImages@odata.count": 1 + }, + "Name": "BIOS Configuration" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios_Settings.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios_Settings.json new file mode 100644 index 00000000000..c842ea939e8 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_Systems_system_Bios_Settings.json @@ -0,0 +1,19 @@ +{ + "error": { + "@Message.ExtendedInfo": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The requested resource of type named 'Settings' was not found.", + "MessageArgs": [ + "", + "Settings" + ], + "MessageId": "Base.1.16.0.ResourceNotFound", + "MessageSeverity": "Critical", + "Resolution": "Provide a valid resource identifier and resubmit the request." + } + ], + "code": "Base.1.16.0.ResourceNotFound", + "message": "The requested resource of type named 'Settings' was not found." + } +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService.json new file mode 100644 index 00000000000..65e203a2243 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService.json @@ -0,0 +1,19 @@ +{ + "@odata.id": "/redfish/v1/UpdateService", + "@odata.type": "#UpdateService.v1_11_1.UpdateService", + "Description": "Service for Software Update", + "FirmwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" + }, + "HttpPushUri": "/redfish/v1/UpdateService/update", + "HttpPushUriOptions": { + "HttpPushUriApplyTime": { + "ApplyTime": "OnReset" + } + }, + "Id": "UpdateService", + "MaxImageSizeBytes": 68157440, + "MultipartHttpPushUri": "/redfish/v1/UpdateService/update", + "Name": "Update Service", + "ServiceEnabled": true +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService_FirmwareInventory.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService_FirmwareInventory.json new file mode 100644 index 00000000000..82cb92c7721 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-3.22.00/root_UpdateService_FirmwareInventory.json @@ -0,0 +1,17 @@ +{ + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory", + "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection", + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/14c085a0" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/cpld" + } + ], + "Members@odata.count": 3, + "Name": "Software Inventory Collection" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-push-3.22.00-response.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-push-3.22.00-response.json new file mode 100644 index 00000000000..c6c3428698e --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-push-3.22.00-response.json @@ -0,0 +1,8 @@ +{ + "@odata.id": "/redfish/v1/TaskService/Tasks/0", + "@odata.type": "#Task.v1_4_3.Task", + "Id": "0", + "TaskState": "Running", + "TaskStatus": "OK" +} +http:202 diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-update-task-last.json b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-update-task-last.json new file mode 100644 index 00000000000..433577b3c22 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/bmc-update-task-last.json @@ -0,0 +1,63 @@ +{ + "@odata.id": "/redfish/v1/TaskService/Tasks/0", + "@odata.type": "#Task.v1_4_3.Task", + "EndTime": "2026-10-08T01:00:28+00:00", + "HidePayload": false, + "Id": "0", + "Messages": [ + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has started.", + "MessageArgs": [ + "0" + ], + "MessageId": "TaskEvent.1.0.3.TaskStarted", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has changed to progress 10 percent complete.", + "MessageArgs": [ + "0", + "10" + ], + "MessageId": "TaskEvent.1.0.3.TaskProgressChanged", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has changed to progress 100 percent complete.", + "MessageArgs": [ + "0", + "100" + ], + "MessageId": "TaskEvent.1.0.3.TaskProgressChanged", + "MessageSeverity": "OK", + "Resolution": "None." + }, + { + "@odata.type": "#Message.v1_1_1.Message", + "Message": "The task with Id '0' has completed.", + "MessageArgs": [ + "0" + ], + "MessageId": "TaskEvent.1.0.3.TaskCompletedOK", + "MessageSeverity": "OK", + "Resolution": "None." + } + ], + "Name": "Task 0", + "Payload": { + "HttpHeaders": [], + "HttpOperation": "POST", + "JsonBody": "null", + "TargetUri": "/redfish/v1/UpdateService/update" + }, + "PercentComplete": 100, + "StartTime": "2026-10-08T01:00:25+00:00", + "TaskMonitor": "/redfish/v1/TaskService/Tasks/0/Monitor", + "TaskState": "Completed", + "TaskStatus": "OK" +} diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/chassis-status.txt b/artifacts/bmc/srv13-first-contact-2026-10-07/chassis-status.txt new file mode 100644 index 00000000000..3997634a4c0 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/chassis-status.txt @@ -0,0 +1,12 @@ +System Power : on +Power Overload : false +Power Interlock : inactive +Main Power Fault : false +Power Control Fault : false +Power Restore Policy : always-on +Last Power Event : +Chassis Intrusion : inactive +Front-Panel Lockout : inactive +Drive Fault : false +Cooling/Fan Fault : false +Front Panel Control : none diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/redfish-eventlog.json b/artifacts/bmc/srv13-first-contact-2026-10-07/redfish-eventlog.json new file mode 100644 index 00000000000..dc929ec9c4f --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/redfish-eventlog.json @@ -0,0 +1,457 @@ +{ + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries", + "@odata.type": "#LogEntryCollection.LogEntryCollection", + "Description": "Collection of System Event Log Entries", + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/1", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/1/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2023-04-03T21:00:02.839+00:00", + "EntryType": "Event", + "Id": "1", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2023-04-03T21:00:02.839+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/10", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/10/attachment", + "Additiondata": [ + "Over temperature = Assert" + ], + "Created": "2026-10-06T23:31:22.586+00:00", + "EntryType": "Event", + "Id": "10", + "Message": "Thermal trip", + "Modified": "2026-10-06T23:31:22.586+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/11", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/11/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:33:39.774+00:00", + "EntryType": "Event", + "Id": "11", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:33:39.774+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/12", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/12/attachment", + "Additiondata": [ + "Over temperature = Assert" + ], + "Created": "2026-10-06T23:39:02.664+00:00", + "EntryType": "Event", + "Id": "12", + "Message": "Thermal trip", + "Modified": "2026-10-06T23:39:02.664+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/13", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/13/attachment", + "Additiondata": [ + "_PID=281" + ], + "Created": "2026-10-06T23:38:46.201+00:00", + "EntryType": "Event", + "Id": "13", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:38:46.201+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/14", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/14/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:39:04.724+00:00", + "EntryType": "Event", + "Id": "14", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:39:04.724+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/15", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/15/attachment", + "Additiondata": [ + "Over temperature = Assert" + ], + "Created": "2026-10-06T23:43:37.297+00:00", + "EntryType": "Event", + "Id": "15", + "Message": "Thermal trip", + "Modified": "2026-10-06T23:43:37.297+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/16", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/16/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:45:48.253+00:00", + "EntryType": "Event", + "Id": "16", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:45:48.253+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/17", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/17/attachment", + "Additiondata": [ + "_PID=285" + ], + "Created": "2026-10-06T23:49:05.270+00:00", + "EntryType": "Event", + "Id": "17", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:49:05.270+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/18", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/18/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:49:23.943+00:00", + "EntryType": "Event", + "Id": "18", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:49:23.943+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/19", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/19/attachment", + "Additiondata": [ + "_PID=281" + ], + "Created": "2026-10-06T23:54:04.503+00:00", + "EntryType": "Event", + "Id": "19", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:54:04.503+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/2/attachment", + "Additiondata": [ + "_PID=283" + ], + "Created": "2023-04-03T21:02:50.303+00:00", + "EntryType": "Event", + "Id": "2", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2023-04-03T21:02:50.303+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/20", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/20/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:54:23.237+00:00", + "EntryType": "Event", + "Id": "20", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:54:23.237+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/21", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/21/attachment", + "Additiondata": [ + "_PID=286" + ], + "Created": "2026-10-07T02:56:41.255+00:00", + "EntryType": "Event", + "Id": "21", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-07T02:56:41.255+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/22", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/22/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-07T02:56:59.887+00:00", + "EntryType": "Event", + "Id": "22", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-07T02:56:59.887+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/23", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/23/attachment", + "Additiondata": [ + "Over temperature = Assert" + ], + "Created": "2026-10-07T23:13:17.681+00:00", + "EntryType": "Event", + "Id": "23", + "Message": "Thermal trip", + "Modified": "2026-10-07T23:13:17.681+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/24", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/24/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-07T23:20:18.594+00:00", + "EntryType": "Event", + "Id": "24", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-07T23:20:18.594+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/25", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/25/attachment", + "Additiondata": [ + "_PID=285" + ], + "Created": "2026-10-07T23:41:45.868+00:00", + "EntryType": "Event", + "Id": "25", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-07T23:41:45.868+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/26", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/26/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-07T23:42:04.490+00:00", + "EntryType": "Event", + "Id": "26", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-07T23:42:04.490+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/27", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/27/attachment", + "Additiondata": [ + "Over temperature = Assert" + ], + "Created": "2026-10-07T23:51:23.327+00:00", + "EntryType": "Event", + "Id": "27", + "Message": "Thermal trip", + "Modified": "2026-10-07T23:51:23.327+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/28", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/28/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-08T00:30:37.304+00:00", + "EntryType": "Event", + "Id": "28", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-08T00:30:37.304+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/3/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2023-04-03T21:03:08.663+00:00", + "EntryType": "Event", + "Id": "3", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2023-04-03T21:03:08.663+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/4", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/4/attachment", + "Additiondata": [ + "_PID=283" + ], + "Created": "2026-10-06T23:08:15.850+00:00", + "EntryType": "Event", + "Id": "4", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:08:15.850+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/5", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/5/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:08:34.210+00:00", + "EntryType": "Event", + "Id": "5", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:08:34.210+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/6", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/6/attachment", + "Additiondata": [ + "_PID=285" + ], + "Created": "2026-10-06T23:11:02.914+00:00", + "EntryType": "Event", + "Id": "6", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:11:02.914+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/7", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/7/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:11:20.849+00:00", + "EntryType": "Event", + "Id": "7", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:11:20.849+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/8", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/8/attachment", + "Additiondata": [ + "_PID=287" + ], + "Created": "2026-10-06T23:22:21.209+00:00", + "EntryType": "Event", + "Id": "8", + "Message": "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout", + "Modified": "2026-10-06T23:22:21.209+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "Critical" + }, + { + "@odata.id": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/9", + "@odata.type": "#LogEntry.v1_9_0.LogEntry", + "AdditionalDataURI": "/redfish/v1/Systems/system/LogServices/EventLog/Entries/9/attachment", + "Additiondata": [ + "CHASSIS_ID=0" + ], + "Created": "2026-10-06T23:22:39.790+00:00", + "EntryType": "Event", + "Id": "9", + "Message": "xyz.openbmc_project.State.Info.ChassisPowerOnStarted", + "Modified": "2026-10-06T23:22:39.790+00:00", + "Name": "System Event Log Entry", + "Resolved": false, + "Severity": "OK" + } + ], + "Members@odata.count": 28, + "Name": "System Event Log Entries" +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/sdr-elist-host-dark.txt b/artifacts/bmc/srv13-first-contact-2026-10-07/sdr-elist-host-dark.txt new file mode 100644 index 00000000000..9e1ea8ffb51 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/sdr-elist-host-dark.txt @@ -0,0 +1,63 @@ +12V | 01h | ns | 19.0 | No Reading +5V | 02h | ns | 19.0 | No Reading +3V3 | 03h | ok | 19.0 | 3.26 Volts +5VSB | 04h | ok | 19.0 | 4.95 Volts +0V8_VDDC_SOC | 05h | ns | 19.0 | No Reading +BAT | 06h | ok | 19.0 | 3.12 Volts +0V75_PCP_CPU | 07h | ns | 19.0 | No Reading +1V8_PCP | 08h | ns | 19.0 | No Reading +1V2_VDDQ0123 | 09h | ns | 19.0 | No Reading +1V2_VDDQ4567 | 0Ah | ns | 19.0 | No Reading +0V6_VTT0123 | 0Bh | ns | 19.0 | No Reading +0V6_VTT4567 | 0Ch | ns | 19.0 | No Reading +3V3_SOC | 0Dh | ns | 19.0 | No Reading +1V8_VDDH | 0Eh | ns | 19.0 | No Reading +1V5_VDDH | 0Fh | ns | 19.0 | No Reading +0V85_VDDC_RCA | 10h | ns | 19.0 | No Reading +TEMP_SOC | 30h | ns | 6.0 | No Reading +TEMP_SOC_VRD | 31h | ns | 6.0 | No Reading +TEMP_DIMM_VRD | 32h | ns | 6.0 | No Reading +TEMP_Core_VRD | 33h | ns | 6.0 | No Reading +TEMP_DIMM_A1 | 34h | ns | 6.0 | No Reading +TEMP_DIMM_B1 | 35h | ns | 6.0 | No Reading +TEMP_DIMM_C1 | 36h | ns | 6.0 | No Reading +TEMP_DIMM_D1 | 37h | ns | 6.0 | No Reading +TEMP_DIMM_E1 | 38h | ns | 6.0 | No Reading +TEMP_DIMM_F1 | 39h | ns | 6.0 | No Reading +TEMP_DIMM_G1 | 3Ah | ns | 6.0 | No Reading +TEMP_DIMM_H1 | 3Bh | ns | 6.0 | No Reading +TEMP_RCA_VR | 3Ch | ns | 6.0 | No Reading +TEMP_X550 | 40h | ok | 6.0 | 39 degrees C +TEMP_CARD_SIDE | 41h | ok | 6.0 | 33 degrees C +TEMP_VR | 42h | ok | 6.0 | 26 degrees C +TEMP_TR1 | 43h | ok | 6.0 | 0 degrees C +TEMP_MB | 44h | ok | 6.0 | 29 degrees C +TEMP_M2_1 | 45h | ns | 6.0 | No Reading +TEMP_M2_2 | 46h | ns | 6.0 | No Reading +FAN1 | 60h | ns | 29.0 | No Reading +FAN2 | 61h | ns | 29.0 | No Reading +FAN3 | 62h | ns | 29.0 | No Reading +FAN4 | 63h | ns | 29.0 | No Reading +FAN5 | 64h | ns | 29.0 | No Reading +FAN1_1 | 68h | ns | 29.0 | No Reading +FAN2_1 | 69h | ns | 29.0 | No Reading +FAN3_1 | 6Ah | ns | 29.0 | No Reading +FAN4_1 | 6Bh | ns | 29.0 | No Reading +FAN5_1 | 6Ch | ns | 29.0 | No Reading +PWR_Core_VRD | 70h | ns | 19.39 | No Reading +PWR_SOC_VRD | 71h | ns | 19.39 | No Reading +PWR_DIMM_VR1 | 72h | ns | 19.39 | No Reading +PWR_DIMM_VR2 | 73h | ns | 19.39 | No Reading +PWR_RCA_VR | 74h | ns | 19.39 | No Reading +PSU1_VINPUT | E6h | ns | 19.74 | Disabled +PSU2_VINPUT | E7h | ns | 19.75 | Disabled +PSU1_IOUTPUT | ECh | ns | 19.80 | Disabled +PSU2_IOUTPUT | EDh | ns | 19.81 | Disabled +PSU1_PINPUT | EEh | ns | 19.82 | Disabled +PSU2_PINPUT | EFh | ns | 19.83 | Disabled +PSU1_POUTPUT | F0h | ns | 19.84 | Disabled +PSU2_POUTPUT | F1h | ns | 19.85 | Disabled +PSU1_TEMP | F3h | ns | 6.58 | Disabled +PSU2_TEMP | F4h | ns | 6.59 | Disabled +PSU1_FAN | F5h | ns | 29.0 | Disabled +PSU2_FAN | F6h | ns | 29.0 | Disabled diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/sel-elist.txt b/artifacts/bmc/srv13-first-contact-2026-10-07/sel-elist.txt new file mode 100644 index 00000000000..a036bf7de62 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/sel-elist.txt @@ -0,0 +1,24 @@ + 1 | 04/03/2023 | 09:00:02 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 2 | 04/03/2023 | 09:02:50 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 3 | 04/03/2023 | 09:03:08 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 4 | 10/06/2026 | 11:08:15 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 5 | 10/06/2026 | 11:08:34 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 6 | 10/06/2026 | 11:11:02 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 7 | 10/06/2026 | 11:11:20 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 8 | 10/06/2026 | 11:22:21 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 9 | 10/06/2026 | 11:22:39 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + a | 10/06/2026 | 11:31:22 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + b | 10/06/2026 | 11:33:39 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + c | 10/06/2026 | 11:39:02 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + d | 10/06/2026 | 11:38:46 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + e | 10/06/2026 | 11:39:04 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + f | 10/06/2026 | 11:43:37 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 10 | 10/06/2026 | 11:45:48 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 11 | 10/06/2026 | 11:49:05 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 12 | 10/06/2026 | 11:49:23 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 13 | 10/06/2026 | 11:54:04 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 14 | 10/06/2026 | 11:54:23 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 15 | 10/07/2026 | 02:56:41 AM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 16 | 10/07/2026 | 02:56:59 AM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 17 | 10/07/2026 | 11:13:17 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted + 18 | 10/07/2026 | 11:20:18 PM UTC | System Event #0x01 | Undetermined system hardware failure | Asserted diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/service-root.json b/artifacts/bmc/srv13-first-contact-2026-10-07/service-root.json new file mode 100644 index 00000000000..e7b44dd597d --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/service-root.json @@ -0,0 +1,71 @@ +{ + "@odata.id": "/redfish/v1", + "@odata.type": "#ServiceRoot.v1_15_0.ServiceRoot", + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "Cables": { + "@odata.id": "/redfish/v1/Cables" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Id": "RootService", + "JsonSchemas": { + "@odata.id": "/redfish/v1/JsonSchemas" + }, + "Links": { + "ManagerProvidingService": { + "@odata.id": "/redfish/v1/Managers/bmc" + }, + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Name": "Root Service", + "ProtocolFeaturesSupported": { + "DeepOperations": { + "DeepPATCH": false, + "DeepPOST": false + }, + "ExcerptQuery": false, + "ExpandQuery": { + "ExpandAll": false, + "Levels": false, + "Links": false, + "NoLinks": false + }, + "FilterQuery": false, + "OnlyMemberQuery": true, + "SelectQuery": true + }, + "RedfishVersion": "1.17.0", + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "TelemetryService": { + "@odata.id": "/redfish/v1/TelemetryService" + }, + "UUID": "6e884505-ba5e-49f2-af66-cedf82ef8d8c", + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + } +} \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/sol-after-reseat.log b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-after-reseat.log new file mode 100644 index 00000000000..c9112d1fd26 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-after-reseat.log @@ -0,0 +1,107 @@ +[SOL Session operational. Use ~? for help] +Checkpoint 90 +Checkpoint 91 +Checkpoint 92 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 91 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 97 +Checkpoint 98 +Checkpoint 9D +Checkpoint 9A +Checkpoint 9C +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 + +Time for GET - https://169.254.0.17:443/redfish/v1/ operation - 1878 ms, Status - Success, StatusCode - 3 + +Time for GET - https://169.254.0.17:443/redfish/v1/Oem/Ami/InventoryData operation - 3624 ms, Status - Success, StatusCode - 3 + +Time for POST - /redfish/v1/DynamicExtension/RedfishExtensions operation - 2672 ms, Status - Success, StatusCode - 21 + +Time for POST - /redfish/v1/DynamicExtension/RedfishExtensions operation - 2871 ms, Status - Success, StatusCode - 21 +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 +Checkpoint 98 +Checkpoint B4 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 + +Press + <2> or to run UEFI Setup +Press + <6> for Instant Flash +Press + + <1> for Boot Menu +Press to Switch Screen + +Checkpoint A9 +ABCheckpoint AB + Aptio Setup - AMI   Main  Advanced Server Mgmt Security Boot Exit /----------------------------------------------------+-------------------------\| Board Product ALTRAD8UD-1L2T |Motherboard Information || System Product ALTRAD8UD-1L2T | || UEFI Version 2.06 | || BMC Version 2.07.00 | || CPLD_MB Version 0.02 | || | ||> Motherboard Information | ||> Processor Information | ||> Memory Information | || |-------------------------|| System Date [Wed 10/07/2026] |<>: Select Screen || System Time [23:44:57] |^v: Select Item || |Enter: Select || |+/-: Change Option || |F1: General Help || |F7: Discard Changes || |F9: Load UEFI Defaults || |F10: Save and Exit || |ESC: Exit |\----------------------------------------------------+-------------------------/ Version 2.22.1282 Copyright (C) 2024 AMI 895:00123456789 Main  Advanced |> CPU Configuration ^|CPU Configuration ||> RAS Configuration *|Parameters ||> Chipset Configuration *| ||> NVMe Configuration *| ||> ACPI Configuration *| ||> USB Configuration *| ||> Serial Port Console Redirection *| ||> H/W Monitor *| ||> Network Stack Configuration *| ||> Driver Health *|-------------------------|| +|<>: Select Screen ||> Tls Auth Configuration +|^v: Select Item ||> Intel(R) I210 Gigabit Network Connection - +|Enter: Select || 9C:6B:00:E7:CA:63 +|+/-: Change Option ||> VLAN Configuration (MAC:9C6B00E7CA63) +|F1: General Help ||> MAC:9C6B00E7CA63-IPv4 Network Configuration +|F7: Discard Changes ||> MAC:9C6B00E7CA63-IPv6 Network Configuration +|F9: Load UEFI Defaults || v|F10: Save and Exit || |ESC: Exit ||> CPU Configuration ^|RAS Configuration ||> RAS Configuration Configure Chipset ||> RAS Configuration *|Settings ||> Chipset ConfigurationNVMe Device Options ||> Chipset Configuration *| ||> NVMe ConfigurationConfigure ACPI Settings | |> NVMe Configuration *| ||> ACPI ConfigurationUSB Configuration |Parameters|> ACPI Configuration *| ||> USB ConfigurationSerial Port Console |Redirection|> USB Configuration *| ||> Serial Port Console RedirectionMonitor hardware status | |> Serial Port Console Redirection *| ||> H/W MonitorNetwork Stack Settings ||> H/W Monitor *| ||> Network Stack Configuration  Advanced  | Network Stack [Enabled] | || IPv4 PXE Support [Disabled] | || IPv4 HTTP Support [Disabled] | || IPv6 PXE Support [Disabled] | || IPv6 HTTP Support [Disabled] | || PXE boot wait time 0 | || | || | || | || |-------------------------|| | || | || | || | || | || | || | || | || | || Media detect count 1| [Enabled]Enable/Disable UEFI |Network Stack<>: Select Screen |^v: Select ItemEnter: Select+/-: Change Option |F1: General Help |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Main  Advanced  Server Mgmt Security Boot Exit|> CPU Configuration ^|Network Stack Settings ||> RAS Configuration *| ||> Chipset Configuration *| ||> NVMe Configuration *| ||> ACPI Configuration *| ||> USB Configuration *| ||> Serial Port Console Redirection *| ||> H/W Monitor *| ||> Network Stack Configuration *| ||> Driver Health *|-------------------------|| +|<>: Select Screen ||> Tls Auth Configuration +|^v: Select Item ||> Intel(R) I210 Gigabit Network Connection - +|Enter: Select || 9C:6B:00:E7:CA:63 +|+/-: Change Option ||> VLAN Configuration (MAC:9C6B00E7CA63) +|F1: General Help ||> MAC:9C6B00E7CA63-IPv4 Network Configuration +|F7: Discard Changes ||> MAC:9C6B00E7CA63-IPv6 Network Configuration +|F9: Load UEFI Defaults || v|F10: Save and Exit || |ESC: Exit | \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/sol-pxe-override.log b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-pxe-override.log new file mode 100644 index 00000000000..31b7d2e90cd --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-pxe-override.log @@ -0,0 +1,471 @@ +tcgetattr: Inappropriate ioctl for device +[SOL Session operational. Use ~? for help] +Checkpoint A9 +ABCheckpoint AB + Aptio Setup - AMI   Main  Advanced Server Mgmt Security Boot Exit /----------------------------------------------------+-------------------------\| Board Product ALTRAD8UD-1L2T |Motherboard Information || System Product ALTRAD8UD-1L2T | || UEFI Version 2.06 | || BMC Version 2.07.00 | || CPLD_MB Version 0.02 | || | ||> Motherboard Information | ||> Processor Information | ||> Memory Information | || |-------------------------|| System Date [Wed 10/07/2026] |<>: Select Screen || System Time [23:05:02] |^v: Select Item || |Enter: Select || |+/-: Change Option || |F1: General Help || |F7: Discard Changes || |F9: Load UEFI Defaults || |F10: Save and Exit || |ESC: Exit |\----------------------------------------------------+-------------------------/ Version 2.22.1282 Copyright (C) 2024 AMI 345NOTICE: DRAM FW version 220414 +CP: 0ff00900 +MEMC param: + mcu_enable_mask = 00000001 [default: 000000ff] +MCU param: + ecc_en = 00000002 [default: 00000001] +DRAM populated DIMMs: + SK0 MC0 S0: NV-RDIMM[2c:80] 16GB 2666 ECC 1R x4 RCD[b3:80] 18ASF2G72XF12G6V21AB +CP: 3ff00100 +CP: 3ff00200 +CP: 00f01902 +CP: 0ff00a00 +CP: 0ff00b01 +CP: 00f01901 +CP: 00f01904 +CP: 00f02901 +CP: 00f01905 +CP: 00f02902 +CP: 00f02903 +CP: 00f02904 +CP: 00f01907 +CP: 00f01906 +CP: 00f01908 +CP: 00f01909 +CP: 0ff00b02 +CP: 0ff00c00 +CP: 3ff00300 +CP: 00001a02 +CP: 00001a03 +CP: 00001a04 +CP: 00001a05 +CP: 00f01a07 +CP: 00001a09 +CP: 00001a0a +CP: 00001a0b +CP: 00001a0c +CP: 00001a0c +CP: 3ff00400 +CP: 0ff01000 +CP: 0ff01101 +DRAM ECC Initialization: [6] [ ==> ]DRAM ECC Initialization: [12] [ ===> ]DRAM ECC Initialization: [18] [ ====> ]DRAM ECC Initialization: [25] [ ======> ]DRAM ECC Initialization: [31] [ =======> ]DRAM ECC Initialization: [37] [ ========> ]DRAM ECC Initialization: [43] [ =========> ]DRAM ECC Initialization: [50] [ ===========> ]DRAM ECC Initialization: [56] [ ============> ]DRAM ECC Initialization: [62] [ =============> ]DRAM ECC Initialization: [68] [ ==============> ]DRAM ECC Initialization: [75] [ ================> ]DRAM ECC Initialization: [81] [ =================> ]DRAM ECC Initialization: [87] [ ==================> ]DRAM ECC Initialization: [93] [ ===================> ]DRAM ECC Initialization: [100] [ ==================== ] +CP: 0ff01200 +CP: 3ff00800 +DRAM memory regions: + Node[0] Region[0]: 0x000080000000 - 0x0000ffffffff + Node[0] Region[1]: 0x080000000000 - 0x08007fffffff + Node[0] Region[2]: 0x080100000000 - 0x0803ffffffff +DRAM: 16GB DDR4 2666 SYMBOL ECC +Checkpoint 2F +Checkpoint 2E +Checkpoint 31 +Checkpoint 32 +UEFI RC version: 2.10d +SCP FW version : 2.10 +SCP FW build date : 20230517 +Failsafe status : 0 +Reset status : 3 +CPU info + CPU ID : 413FD0C1 + CPU Clock : 3000 MHz + Number of active sockets : 1 + Number of active cores : 80 + Socket[0]: Core voltage : 1050 + Socket[0]: SCU ProductID : A10001 + Socket[0]: Max cores : 80 + Socket[0]: Warranty : 1 + Socket[0]: Subnuma : 0 + Socket[0]: RC disable mask : 0 + Socket[0]: AVS enabled : 1 + Socket[0]: AVS voltage : 1013 + Socket[0]: VDM capability : 0 +SOC info + DDR Frequency : 2666 MHz + Socket[0]: Soc voltage : 750 + Socket[0]: DIMM1 voltage : 1200 + Socket[0]: DIMM2 voltage : 1200 + PCP Clock : 1650 MHz + SOC Clock : 2000 MHz + SYS Clock : 400 MHz + AHB Clock : 200 MHz +User Configuration Setting: + 8028: 0x1 (1) + 8038: 0x0 (0) + 8070: 0x3 (3) + 8080: 0x0 (0) + 8088: 0x0 (0) + 80B0: 0x0 (0) + 80B8: 0x1 (1) + 80C8: 0x1 (1) + 80E0: 0x1 (1) + 8118: 0x2 (2) + 8130: 0x1 (1) + 81B0: 0x0 (0) + 81B8: 0x0 (0) + 81C0: 0x0 (0) + 81C8: 0xFF (255) + 81D0: 0xFF (255) + 8208: 0x78 (120) + 8210: 0x4B0 (1200) + 8218: 0x1 (1) + 8220: 0x5A0 (1440) + 8228: 0x0 (0) + 8230: 0x0 (0) +Board Configuration Setting: + C000: 0xCD3A (52538) + C008: 0x0 (0) + C010: 0x0 (0) + C018: 0x0 (0) + C020: 0xFFFF (65535) + C028: 0x0 (0) + C030: 0x80E8 (33000) + C038: 0x80E8 (33000) + C040: 0x2710 (10000) + C048: 0x2710 (10000) + C050: 0x0 (0) + C058: 0x190 (400) + C060: 0x190 (400) + C068: 0x190 (400) + C070: 0x64 (100) + C078: 0x190 (400) + C080: 0xFFFFFF01 (-255) + C088: 0x2020202 (33686018) + C090: 0x2020202 (33686018) + C098: 0x4 (4) + C0A0: 0x4 (4) + C0A8: 0x20002 (131074) + C0B0: 0x20002 (131074) + C0B8: 0x20002 (131074) + C0C0: 0x20002 (131074) + C0C8: 0x3 (3) + C0D0: 0x3 (3) + C0D8: 0x3 (3) + C0E0: 0x3 (3) + C0E8: 0x0 (0) + C0F0: 0x0 (0) + C0F8: 0x0 (0) + C100: 0x0 (0) + C108: 0x0 (0) + C110: 0x0 (0) + C118: 0x0 (0) + C120: 0x0 (0) + C128: 0x0 (0) + C130: 0x0 (0) + C138: 0x0 (0) + C140: 0x0 (0) + C148: 0x1 (1) + C150: 0x2 (2) + C158: 0x3 (3) + C160: 0x4 (4) + C168: 0xFFFFFFFF (-1) + C170: 0xFFFFFFFE (-2) + C178: 0xFFFFFFFD (-3) + C180: 0x0 (0) + C188: 0x0 (0) + C190: 0x0 (0) + C198: 0x0 (0) + C1A0: 0x0 (0) + C1A8: 0x0 (0) + C1B0: 0x2 (2) + C1B8: 0xC80 (3200) + C1C0: 0x20020000 (537001984) + C1C8: 0x20020000 (537001984) + C1D0: 0x31060177 (822477175) + C1D8: 0x31060177 (822477175) + C1E0: 0x30060070 (805699696) + C1E8: 0x30060070 (805699696) + C1F0: 0x0 (0) + C1F8: 0x0 (0) + C200: 0x0 (0) + C208: 0x0 (0) + C210: 0x44C0CCC (72092876) + C218: 0x84C0CCC (139201740) + C220: 0x4130333 (68354867) + C228: 0x8130333 (135463731) + C230: 0x1130333 (18023219) + C238: 0x2230333 (35849011) + C240: 0x1430333 (21168947) + C248: 0x2830333 (42140467) + C250: 0x55EDEED (90103533) + C258: 0xA5DEDDE (173927902) + C260: 0x55B7BB7 (89881527) + C268: 0xA57B77B (173520763) + C270: 0x5 (5) + C278: 0x90DD90 (9493904) + C280: 0x5 (5) + C288: 0x90DD90 (9493904) + C290: 0x5 (5) + C298: 0x90DD90 (9493904) + C2A0: 0x5 (5) + C2A8: 0x90DD90 (9493904) + C2B0: 0x24 (36) + C2B8: 0x1A001A (1703962) + C2C0: 0x50 (80) + C2C8: 0x240020 (2359328) + C2D0: 0x2800280 (41943680) + C2D8: 0x90909090 (-1869574000) + C2E0: 0x0 (0) + C2E8: 0xC000C0 (12583104) + C2F0: 0x0 (0) + C2F8: 0x0 (0) + C300: 0x0 (0) + C308: 0x0 (0) + C310: 0x2010201 (33620481) + C318: 0x0 (0) + C320: 0x0 (0) + C328: 0x0 (0) + C330: 0x1050106 (17105158) + C338: 0x5F4 (1524) + C340: 0x0 (0) + C348: 0x0 (0) + C350: 0x2020202 (33686018) + C358: 0x2020202 (33686018) + C360: 0x4 (4) + C368: 0x4 (4) + C370: 0xFFFFFFFF (-1) + C378: 0xFFFFFFFF (-1) + C380: 0xFFFFFFFF (-1) + C388: 0xFFFFFFFF (-1) + C390: 0x0 (0) + C398: 0x0 (0) + C3A0: 0x0 (0) + C3A8: 0x0 (0) + C3B0: 0x0 (0) + C3B8: 0x0 (0) + C3C0: 0x0 (0) + C3C8: 0x0 (0) + C3D0: 0xFFFFFFFF (-1) + C3D8: 0xFFFFFFFF (-1) + C3E0: 0xFFFFFFFF (-1) + C3E8: 0xFFFFFFFF (-1) + C3F0: 0x57575757 (1465341783) + C3F8: 0x57575757 (1465341783) + C400: 0x57575757 (1465341783) + C408: 0x57575757 (1465341783) + C410: 0x57575757 (1465341783) + C418: 0x57575757 (1465341783) + C420: 0x57575757 (1465341783) + C428: 0x57575757 (1465341783) + C430: 0x57575757 (1465341783) + C438: 0x57575757 (1465341783) + C440: 0x57575757 (1465341783) + C448: 0x57575757 (1465341783) + C450: 0x57575757 (1465341783) + C458: 0x57575757 (1465341783) + C460: 0x57575757 (1465341783) + C468: 0x57575757 (1465341783) + C470: 0x0 (0) + C478: 0x30003 (196611) + C480: 0x2020202 (33686018) + C488: 0x2020202 (33686018) + C490: 0x2020202 (33686018) + C498: 0xFFFFFFFF (-1) + C4A0: 0xFFFFFFFF (-1) + C4A8: 0x0 (0) + C4B0: 0x0 (0) + C4B8: 0x0 (0) + C4C0: 0x0 (0) + C4C8: 0x0 (0) + C4D0: 0x0 (0) + C4D8: 0x0 (0) + C4E0: 0x0 (0) + C4E8: 0xFFFFFFFF (-1) + C4F0: 0xFFFFFFFF (-1) + C4F8: 0xFFFFFFFF (-1) + C500: 0xFFFFFFFF (-1) + C508: 0x57575757 (1465341783) + C510: 0x57575757 (1465341783) + C518: 0x57575757 (1465341783) + C520: 0x57575757 (1465341783) + C528: 0x57575757 (1465341783) + C530: 0x57575757 (1465341783) + C538: 0x57575757 (1465341783) + C540: 0x57575757 (1465341783) + C548: 0x57575757 (1465341783) + C550: 0x57575757 (1465341783) + C558: 0x57575757 (1465341783) + C560: 0x57575757 (1465341783) + C568: 0x57575757 (1465341783) + C570: 0x57575757 (1465341783) + C578: 0x3 (3) + C580: 0x1A4 (420) + C588: 0x0 (0) + C590: 0x18000 (98304) + C598: 0x80018000 (-2147385344) + C5A0: 0x0 (0) + C5A8: 0x0 (0) + C5B0: 0x0 (0) + C5B8: 0x0 (0) + C5C0: 0x0 (0) + C5C8: 0x0 (0) + C5D0: 0x0 (0) + C5D8: 0x0 (0) + C5E0: 0x550055 (5570645) + C5E8: 0x64 (100) + C5F0: 0x5 (5) + C5F8: 0x1 (1) + C600: 0x2 (2) + C608: 0x5 (5) + C610: 0x0 (0) + C618: 0x0 (0) + C620: 0x0 (0) + C628: 0x0 (0) + C630: 0x0 (0) + C638: 0x0 (0) + C640: 0x0 (0) + C648: 0x0 (0) + C650: 0x0 (0) + C658: 0x0 (0) + C660: 0x0 (0) + C668: 0x1F4 (500) + C670: 0x1388 (5000) + C678: 0x0 (0) + C680: 0x0 (0) + C688: 0x0 (0) + C690: 0x0 (0) + C698: 0x0 (0) + C6A0: 0xFFFFFFFF (-1) + C6A8: 0xFFFFFFFF (-1) + C6B0: 0xFFFFFFFF (-1) + C6B8: 0xFFFFFFFF (-1) + C6C0: 0xFFFFFFFF (-1) + C6C8: 0xFFFFFFFF (-1) + C6D0: 0xFFFFFFFF (-1) + C6D8: 0xFFFFFFFF (-1) + C6E0: 0xFFFFFFFF (-1) + C6E8: 0xFFFFFFFF (-1) + C6F0: 0xFFFFFFFF (-1) + C6F8: 0xFFFFFFFF (-1) + C700: 0xFFFFFFFF (-1) + C708: 0xFFFFFFFF (-1) + C710: 0x57575757 (1465341783) + C718: 0x57575757 (1465341783) + C720: 0x57575757 (1465341783) + C728: 0x57575757 (1465341783) + C730: 0x57575757 (1465341783) + C738: 0x57575757 (1465341783) + C740: 0x57575757 (1465341783) + C748: 0x57575757 (1465341783) + C750: 0x57575757 (1465341783) + C758: 0x57575757 (1465341783) + C760: 0x57575757 (1465341783) + C768: 0x57575757 (1465341783) + C770: 0x57575757 (1465341783) + C778: 0x57575757 (1465341783) + C780: 0x0 (0) + C788: 0x0 (0) + C790: 0x1 (1) + C798: 0x0 (0) + C7A0: 0x0 (0) + C7A8: 0x0 (0) + C7B0: 0x0 (0) + C7B8: 0x0 (0) + C7C0: 0x1 (1) + C7C8: 0x0 (0) + C7D0: 0x0 (0) + C7D8: 0x0 (0) + C7E0: 0x0 (0) + C7E8: 0x0 (0) + C7F0: 0x0 (0) + C7F8: 0x0 (0) +Checkpoint 4F +Checkpoint 60 +Checkpoint 61 +Checkpoint 9A +Checkpoint 90 +Checkpoint 91 +Checkpoint 92 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 94 +Checkpoint 94 +Checkpoint 94 +Checkpoint 95 +Checkpoint 96 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 91 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 97 +Checkpoint 98 +Checkpoint 9D +Checkpoint 9A +Checkpoint 9C +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 +Checkpoint B4 + +Time for GET - https://169.254.0.17:443/redfish/v1/ operation - 295 ms, Status - Success, StatusCode - 3 + +Time for GET - https://169.254.0.17:443/redfish/v1/Oem/Ami/InventoryData operation - 3039 ms, Status - Success, StatusCode - 3 + +Time for POST - /redfish/v1/DynamicExtension/RedfishExtensions operation - 1824 ms, Status - Success, StatusCode - 21 + +Time for POST - /redfish/v1/DynamicExtension/RedfishExtensions operation - 2472 ms, Status - Success, StatusCode - 21 +Checkpoint B4 +Checkpoint B4 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 +Checkpoint 92 + +Press + <2> or to run UEFI Setup +Press + <6> for Instant Flash +Press + + <1> for Boot Menu +Press to Switch Screen + +Checkpoint A9 +ABCheckpoint AB + Aptio Setup - AMI   Main  Advanced Server Mgmt Security Boot Exit /----------------------------------------------------+-------------------------\| Board Product ALTRAD8UD-1L2T |Motherboard Information || System Product ALTRAD8UD-1L2T | || UEFI Version 2.06 | || BMC Version 2.07.00 | || CPLD_MB Version 0.02 | || | ||> Motherboard Information | ||> Processor Information | ||> Memory Information | || |-------------------------|| System Date [Wed 10/07/2026] |<>: Select Screen || System Time [23:07:04] |^v: Select Item || |Enter: Select || |+/-: Change Option || |F1: General Help || |F7: Discard Changes || |F9: Load UEFI Defaults || |F10: Save and Exit || |ESC: Exit |\----------------------------------------------------+-------------------------/ Version 2.22.1282 Copyright (C) 2024 AMI 5678910123456789201234567893012345678940123456789501234567898:0012345678910123456789201234567893012345678940123456789501234567899:00123456789101234567892012345678930123456789401234567895012345678910:0012345678910123456789201234567893012345678940123 \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/sol-setup-walk.log b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-setup-walk.log new file mode 100644 index 00000000000..7b312e263bc --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/sol-setup-walk.log @@ -0,0 +1,3 @@ +tcgetattr: Inappropriate ioctl for device +[SOL Session operational. Use ~? for help] +3456789101234 Main  Advanced |> CPU Configuration ^|CPU Configuration ||> RAS Configuration *|Parameters ||> Chipset Configuration *| ||> NVMe Configuration *| ||> ACPI Configuration *| ||> USB Configuration *| ||> Serial Port Console Redirection *| ||> H/W Monitor *| ||> Network Stack Configuration *| ||> Driver Health *|-------------------------|| +|<>: Select Screen ||> Tls Auth Configuration +|^v: Select Item ||> Intel(R) I210 Gigabit Network Connection - +|Enter: Select || 9C:6B:00:E7:CA:63 +|+/-: Change Option ||> VLAN Configuration (MAC:9C6B00E7CA63) +|F1: General Help ||> MAC:9C6B00E7CA63-IPv4 Network Configuration +|F7: Discard Changes ||> MAC:9C6B00E7CA63-IPv6 Network Configuration +|F9: Load UEFI Defaults || v|F10: Save and Exit || |ESC: Exit | Main Advanced  Server Mgmt | BMC Self Test Status PASSED | || BMC Device ID 32 | || BMC Device Revision 1 | || BMC Firmware Revision 2.07.00 | || IPMI Version 2.0 | || IPMI BMC Interface SSIF | || | || Wait For BMC [Disabled] | || Redfish [Enabled] | || FRB-2 Timer [Disabled] |-------------------------|| FRB-2 Timer timeout 6  | || | || | || | || | || | || | || | || | || FRB-2 Timer Policy [Do Nothing] | OS Watchdog Timer [Disabled]| OS Wtd Timer Timeout 10 |  PASSED^|Wait For BMC response ^|| 32*|for specified time out. *||  1*|In PILOTII, BMC starts *||  2.07.00*|at the same time when *|| 2.0*|BIOS starts during AC *||  SSIF*|power ON. It takes *|*|around 255 seconds to +|| [Disabled]*|initialize Host to BMC v|**|  6 *|   **|  10 *| OS Wtd Timer Policy [Reset]  **|> BMC Network Configuration +|> System Event Log v|  6 <>: Select Screen ||   *|^v: Select ItemEnter: Select|  10 +/-: Change Option ||   *|F1: General Help |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Main Advanced Server Mgmt  Security | Supervisor Password Not Installed |Set or change the ^|| User Password Not Installed |password for the *|| |administrator account. *|| Supervisor Password |Only the administrator *|| User Password |has authority to change *|| |the settings in the *||> Secure Boot |UEFI Setup Utility. +|| |Leave it blank and v|| | || |-------------------------|| | || | || | || | || | || | || | || | || | |<>: Select Screen |^v: Select ItemEnter: Select+/-: Change Option |F1: General Help |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Main Advanced Server Mgmt Security  Boot | | || FIXED BOOT ORDER Priorities  | || Boot Option #1 [Hard Disk] | || Boot Option #2 [NVME] | || Boot Option #3 [CD/DVD] | || Boot Option #4 [USB Device] | || Boot Option #5 [Network] | || Boot Option #6 [UEFI AP:UEFI: | || Built-in EFI Shell] | || |-------------------------||> UEFI Application Boot Priorities | || | || Setup Prompt Timeout 1 | || | || | || | || | || | || | |Sets the system boot ||  |order<>: Select Screen |^v: Select ItemEnter: Select| Bootup NumLock State [On] |+/-: Change Option || Boot Beep [Disabled] |F1: General Help || Full Screen Logo [Enabled] |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Security  Boot Exit | Supervisor Password Not Installed |Set or change the ^|| User Password Not Installed |password for the *|| |administrator account. *|| Supervisor Password |Only the administrator *|| User Password |has authority to change *|| |the settings in the *||> Secure Boot |UEFI Setup Utility. +|| |Leave it blank and v|| | || |-------------------------|| | || | || | || | || | || | || | || | || | |<>: Select Screen |^v: Select ItemEnter: Select+/-: Change Option |F1: General Help |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Server Mgmt  Security Boot Exit | BMC Self Test Status PASSED | || BMC Device ID 32 | || BMC Device Revision 1 | || BMC Firmware Revision 2.07.00 | || IPMI Version 2.0 | || IPMI BMC Interface SSIF | || | || Wait For BMC [Disabled] | || Redfish [Enabled] | || FRB-2 Timer [Disabled] |-------------------------|| FRB-2 Timer timeout 6  | || | || | || | || | || | || | || | || | || FRB-2 Timer Policy [Do Nothing] | OS Watchdog Timer [Disabled]| OS Wtd Timer Timeout 10 |  PASSED^|Wait For BMC response ^|| 32*|for specified time out. *||  1*|In PILOTII, BMC starts *||  2.07.00*|at the same time when *|| 2.0*|BIOS starts during AC *||  SSIF*|power ON. It takes *|*|around 255 seconds to +|| [Disabled]*|initialize Host to BMC v|**|  6 *|   **|  10 *| OS Wtd Timer Policy [Reset]  **|> BMC Network Configuration +|> System Event Log v|  6 <>: Select Screen ||   *|^v: Select ItemEnter: Select|  10 +/-: Change Option ||   *|F1: General Help |F7: Discard Changes |F9: Load UEFI Defaults |F10: Save and Exit |ESC: Exit Main  Advanced  Server Mgmt Security Boot Exit |> CPU Configuration ^|CPU Configuration ||> RAS Configuration *|Parameters ||> Chipset Configuration *| ||> NVMe Configuration *| ||> ACPI Configuration *| ||> USB Configuration *| ||> Serial Port Console Redirection *| ||> H/W Monitor *| ||> Network Stack Configuration *| ||> Driver Health *|-------------------------|| +|<>: Select Screen ||> Tls Auth Configuration +|^v: Select Item ||> Intel(R) I210 Gigabit Network Connection - +|Enter: Select || 9C:6B:00:E7:CA:63 +|+/-: Change Option ||> VLAN Configuration (MAC:9C6B00E7CA63) +|F1: General Help ||> MAC:9C6B00E7CA63-IPv4 Network Configuration +|F7: Discard Changes ||> MAC:9C6B00E7CA63-IPv6 Network Configuration +|F9: Load UEFI Defaults || v|F10: Save and Exit || |ESC: Exit | \ No newline at end of file diff --git a/artifacts/bmc/srv13-first-contact-2026-10-07/update-service-before.json b/artifacts/bmc/srv13-first-contact-2026-10-07/update-service-before.json new file mode 100644 index 00000000000..65e203a2243 --- /dev/null +++ b/artifacts/bmc/srv13-first-contact-2026-10-07/update-service-before.json @@ -0,0 +1,19 @@ +{ + "@odata.id": "/redfish/v1/UpdateService", + "@odata.type": "#UpdateService.v1_11_1.UpdateService", + "Description": "Service for Software Update", + "FirmwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" + }, + "HttpPushUri": "/redfish/v1/UpdateService/update", + "HttpPushUriOptions": { + "HttpPushUriApplyTime": { + "ApplyTime": "OnReset" + } + }, + "Id": "UpdateService", + "MaxImageSizeBytes": 68157440, + "MultipartHttpPushUri": "/redfish/v1/UpdateService/update", + "Name": "Update Service", + "ServiceEnabled": true +} \ No newline at end of file diff --git a/dag/extdeps/ampere/mt_collins_board.dag b/dag/extdeps/ampere/mt_collins_board.dag new file mode 100644 index 00000000000..2ead8aa6c43 --- /dev/null +++ b/dag/extdeps/ampere/mt_collins_board.dag @@ -0,0 +1,43 @@ +module extdeps.ampere.mt_collins_board + +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.board_authority { BoardAuthorityRef } +import extdeps.ampere.mt_collins_getting_started_guide.subject { mt_collins_gsg_authority } +import extdeps.ampere.mt_collins_product_brief.subject { MtCollinsPublicProductBrief, mt_collins_public_product_brief } + +data extdeps_external_authority_anchor: ExternalAuthority = mt_collins_gsg_authority + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.ampere.mt_collins_board", + decl_name: "mt_collins_board", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +// THE MT. COLLINS BOARD AS A SUBJECT, joining the two Ampere documents that already own its facts +// rather than restating either: the Getting Started Guide (figures, orientation) and the public +// product brief. A board-grain subject exists so a bound host's board can reach its per-board +// authority by reference instead of through a closed enum. +type MtCollinsBoard { + getting_started_guide: ExternalAuthority + product_brief: MtCollinsPublicProductBrief +} + +data mt_collins_board: MtCollinsBoard = MtCollinsBoard { + getting_started_guide: mt_collins_gsg_authority, + product_brief: mt_collins_public_product_brief, +} + +data mt_collins_board_authority: BoardAuthorityRef = BoardAuthorityRef { + subject: DeclarationRef { + module_path: "extdeps.ampere.mt_collins_board", + decl_name: "mt_collins_board", + field: WholeDeclaration + } +} diff --git a/dag/extdeps/ampere/mt_collins_firmware_release.dag b/dag/extdeps/ampere/mt_collins_firmware_release.dag index c04568dd11d..b06cab1272d 100644 --- a/dag/extdeps/ampere/mt_collins_firmware_release.dag +++ b/dag/extdeps/ampere/mt_collins_firmware_release.dag @@ -1,10 +1,10 @@ module extdeps.ampere.mt_collins_firmware_release +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import std.types { Int, NonEmptyStr, List } import std.measure { ByteSize, byte_size, Second, second } import extdeps.crypto.hash { sha256_digest } import std.optional { Present } -import extdeps.boards.types { AmpereMtCollins } import extdeps.firmware.types { FirmwareReleaseRow, FirmwareTarget, FirmwareSemanticVersion, BmcController, HostBios, ScpFirmware, MainboardCpld, @@ -42,7 +42,7 @@ data mt_collins_firmware_delivery_authority: ExternalAuthority = ExternalAuthori } data mt_collins_bmc_0_45_3_hpm: FirmwareReleaseRow = FirmwareReleaseRow { - board: AmpereMtCollins, + board: mt_collins_board_authority, target: BmcController, version: firmware_semantic_version(major: 0, minor: 45, patch: 3), released_on: 1703635200000, @@ -56,7 +56,7 @@ data mt_collins_bmc_0_45_3_hpm: FirmwareReleaseRow = FirmwareReleaseRow { } data mt_collins_bios_0acod014_hpm: FirmwareReleaseRow = FirmwareReleaseRow { - board: AmpereMtCollins, + board: mt_collins_board_authority, target: HostBios, version: firmware_semantic_version(major: 0, minor: 14, patch: 0), released_on: 1702339200000, @@ -70,7 +70,7 @@ data mt_collins_bios_0acod014_hpm: FirmwareReleaseRow = FirmwareReleaseRow { } data mt_collins_scp_2_10_hpm: FirmwareReleaseRow = FirmwareReleaseRow { - board: AmpereMtCollins, + board: mt_collins_board_authority, target: ScpFirmware, version: firmware_semantic_version(major: 2, minor: 10, patch: 0), released_on: 1674691200000, @@ -84,7 +84,7 @@ data mt_collins_scp_2_10_hpm: FirmwareReleaseRow = FirmwareReleaseRow { } data mt_collins_mb_cpld_v06_hpm: FirmwareReleaseRow = FirmwareReleaseRow { - board: AmpereMtCollins, + board: mt_collins_board_authority, target: MainboardCpld, version: firmware_semantic_version(major: 6, minor: 0, patch: 0), released_on: 1654560000000, diff --git a/dag/extdeps/bmc/access_profile.dag b/dag/extdeps/bmc/access_profile.dag index 5a196f7dbfa..3c2b37cbe33 100644 --- a/dag/extdeps/bmc/access_profile.dag +++ b/dag/extdeps/bmc/access_profile.dag @@ -8,7 +8,11 @@ import extdeps.external_authority { ExternalAuthority, ExternalModelScope, Exter import extdeps.uri { Uri, Https } import std.credentials { FactoryLogin } import extdeps.bmc.endpoint { BmcControllerEndpoint } -import extdeps.boards.types { BaseboardModel } +import extdeps.board_authority { + BoardAuthorityRef, BoardAuthorityBinding, BoardAuthorityLookup, BoardFruObservation, + board_authority_for, board_facts_authority, board_authority_ref_eq +} +import std.optional { Present, Absent } import extdeps.bmc.capability { BmcFirmwareReleaseCapabilityRow, BmcFirmwareReleaseIdentity, bmc_firmware_release_identity_eq } import extdeps.bmc.megarac { MegaRacImageRedirection } import extdeps.bmc.redfish_virtual_media { RedfishVirtualMediaProbeReceipt } @@ -82,7 +86,7 @@ type ChassisApplicability | AllChassisVariants type BmcAccessProfileKey { - baseboard: BaseboardModel + board: BoardAuthorityRef chassis: ChassisApplicability firmware: BmcFirmwareReleaseIdentity } @@ -147,7 +151,7 @@ fn bmc_access_profile_catalog_row_standing(row: BmcAccessProfileCatalogRow) -> B // alone, so a same-version endpoint cannot be paired with an unrelated board's // profile. The firmware is carried ONCE, here. type BmcObservedIdentity { - baseboard: BaseboardModel + board: BoardFruObservation chassis_part_number: NonEmptyStr? firmware: BmcFirmwareReleaseIdentity } @@ -200,8 +204,17 @@ fn chassis_applicability_admits(applicability: ChassisApplicability, observed: N // The profile key binds baseboard, chassis applicability, and exact release // (family and raw vendor string). Equal firmware alone is not a binding. -fn bmc_access_profile_key_matches(key: BmcAccessProfileKey, identity: BmcObservedIdentity) -> Bool { - key.baseboard == identity.baseboard +// The board is the authority the host's OWN FRU reaches through the supplied bindings, and a profile +// imports board FACTS, so only an evidence-bound board matches. +fn bmc_observed_board_facts_authority(identity: BmcObservedIdentity, bindings: List) -> BoardAuthorityRef? { + board_facts_authority(lookup: board_authority_for(observation: identity.board, bindings: bindings)) +} + +fn bmc_access_profile_key_matches(key: BmcAccessProfileKey, identity: BmcObservedIdentity, bindings: List) -> Bool { + (match bmc_observed_board_facts_authority(identity: identity, bindings: bindings) { + Present { value: a } => board_authority_ref_eq(a: key.board, b: a) + Absent => false + }) && chassis_applicability_admits(applicability: key.chassis, observed: identity.chassis_part_number) && bmc_firmware_release_identity_eq(a: key.firmware, b: identity.firmware) } @@ -211,6 +224,7 @@ fn bmc_access_profile_key_matches(key: BmcAccessProfileKey, identity: BmcObserve // standings (gunbc.machine_intake_access), not arms here. type BmcAccessProfileLookup = AccessProfileKnown { profile: BmcAccessProfile, provenance: BmcAccessProfileProvenance } + | AccessProfileBoardNotFactsBound { observed_identity: BmcObservedIdentity, board: BoardAuthorityLookup } | AccessProfileUncatalogued { observed_identity: BmcObservedIdentity } | AccessProfileAmbiguous { observed_identity: BmcObservedIdentity, count: Int } | AccessProfileCatalogRowRefused { standing: BmcAccessProfileCatalogRowStanding } @@ -238,11 +252,14 @@ fn catalog_first_refused(catalog: List) -> BmcAccess fn bmc_access_profile_for( identity: BmcObservedIdentity, catalog: List, + bindings: List, ) -> BmcAccessProfileLookup { match catalog_first_refused(catalog: catalog) { Present { value: refused } => AccessProfileCatalogRowRefused { standing: refused } - Absent => { - let hits = catalog |> filter(row => bmc_access_profile_key_matches(key: row.profile.key, identity: identity)) + Absent => match bmc_observed_board_facts_authority(identity: identity, bindings: bindings) { + Absent => AccessProfileBoardNotFactsBound { observed_identity: identity, board: board_authority_for(observation: identity.board, bindings: bindings) } + Present { value: _ } => { + let hits = catalog |> filter(row => bmc_access_profile_key_matches(key: row.profile.key, identity: identity, bindings: bindings)) let n = count(hits) if n == 0 { AccessProfileUncatalogued { observed_identity: identity } @@ -255,6 +272,7 @@ fn bmc_access_profile_for( } else { AccessProfileAmbiguous { observed_identity: identity, count: n } } + } } } } @@ -276,8 +294,9 @@ fn bmc_surface_eligibility( profile: BmcAccessProfile, observation: BmcAccessObservation, surface: BmcAccessSurface, + bindings: List, ) -> BmcSurfaceEligibility { - if bmc_access_profile_key_matches(key: profile.key, identity: observation.identity) == false { + if bmc_access_profile_key_matches(key: profile.key, identity: observation.identity, bindings: bindings) == false { SurfaceProfileObservationIdentityMismatch { profile: profile.key, observed: observation.identity } } else if bmc_surface_in(surfaces: profile.candidate_surfaces, surface: surface) == false { SurfaceAbsentFromProfile diff --git a/dag/extdeps/bmc/megarac.dag b/dag/extdeps/bmc/megarac.dag index 9e58a1d2ea3..c8ce1a0937e 100644 --- a/dag/extdeps/bmc/megarac.dag +++ b/dag/extdeps/bmc/megarac.dag @@ -14,7 +14,6 @@ import extdeps.uri { Uri, Https } import extdeps.vendor { Vendor } import extdeps.hardware { Hardware } import extdeps.vendor.ami { ami } -import extdeps.languages.json.emit { JsonValue, json_object, json_array, json_kv, json_int, json_string, serialize_json_array } import std.resources { Network } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -106,7 +105,7 @@ data megarac_op_media_images: MegaRacApiOperation = MegaRacApiOperation { data megarac_op_media_start: MegaRacApiOperation = MegaRacApiOperation { http_method: "POST", path: "/api/settings/media/remote/start-media", - contract_note: "JSON body MUST carry image_redirection:1 beside image_name, image_type (1 = CD) and image_index; without image_redirection the firmware refuses with opaque codes 13410/13460. Recovered from the firmware's own UI bundle (source.min.js), not from documentation. Success attaches a redirection session visible at /api/settings/media/remote/configurations (redirection_status started or connecting via megarac_redirection_status_of_wire, session_index bound)", + contract_note: "JSON body MUST carry image_redirection:1 beside image_name, image_type (1 = CD) and image_index; without image_redirection the firmware refuses with opaque codes (the executed sequence above always carries it). Success attaches a redirection session visible at /api/settings/media/remote/configurations (redirection_status started or connecting via megarac_redirection_status_of_wire, session_index bound)", } data megarac_op_media_stop: MegaRacApiOperation = MegaRacApiOperation { @@ -162,14 +161,8 @@ fn megarac_image_redirection_wire(r: MegaRacImageRedirectionRequired) -> NonEmpt } } -// --------------------------------------------------------------------------------------------- -// SERVED SP-X UI SURFACE CATALOG, keyed to firmware identity 0.32. The capture receipt (path -// and digest) is gunbc.machine_intake_megarac_served_ui_catalog_observation; this module keeps -// only firmware-identity-keyed facts about that surface. The catalog is a FACT ABOUT THIS -// FIRMWARE'S SERVED UI PATHS, not a family-wide MegaRAC claim, and not a redirection_status map: -// AMI advertises Redfish on other builds; this surface does not carry it. Status-code meanings -// are a separate fact, cited on megarac_redirection_status_of_wire. -// --------------------------------------------------------------------------------------------- +// FIRMWARE IDENTITY 0.32: the Foxconn Mt. Collins build these rows were executed against. Status-code +// meanings are cited on megarac_redirection_status_of_wire. data megarac_firmware_0_32: BmcFirmwareReleaseIdentity = bmc_firmware_release_identity_of_wire( family: AmiMegaRac, wire: "0.32", @@ -178,15 +171,15 @@ data megarac_firmware_0_32: BmcFirmwareReleaseIdentity = bmc_firmware_release_id // THE BMC IS THE NFS CLIENT. settings/media/general carries the NFS server, source path and // share type; the host sees a virtual CD. PXE is not a term of this route. The required // image_redirection fact is MegaRacImageRedirectionRequired / megarac_op_media_start. -// REDIRECTION_STATUS. The 255-path catalog does not contain these mappings. The codes that this +// REDIRECTION_STATUS. The codes that this // decoder names are the ones committed observations actually returned on firmware 0.32: // 0 stopped (gunbc.runner_host_media_independence_receipt mtcollins1_independence_evidence), 1 started // and 100 connecting (gunbc.machine_intake_mtcollins1_diskless_boot_receipt // mtcollins1_diskless_boot_artifact_path, and // gunbc.machine_intake_mtcollins1_memory_census_observation mtcollins1_media_redirection_samples). // Connecting is presenting: the same unit booted GRUB from a session that still read 100. -// Any other numeric code on a catalogued firmware is an uncatalogued wire, not a named denial -// recovered from source.min.js (those bytes are not in the tree). A firmware identity this row +// Any other numeric code on a catalogued firmware is an uncatalogued wire, not a named denial. +// A firmware identity this row // does not catalog refuses rather than inheriting 0.32's numbers. type MegaRacRedirectionStatus = RedirectionStopped @@ -241,179 +234,6 @@ fn megarac_redirection_presentation_standing( } } -// CONSOLE RECORDERS. Two recorders, two subjects. video/* records the framebuffer the KVM -// renders; sol-triggers records the SOL stream. Both persist across resets. The SOL object's -// chassis-power-on field is spelled chasis_power_on_events_enable (upstream typo); the video -// object spells chassis_power_on_events_enable. Model what upstream sends. -// -// §3c frontier: megarac_recorder_trigger_power_on_field, megarac_trigger_power_on_field_wire, -// megarac_recorder_reset_survival, and the four data rows they bind. Consumer: the PreOs -// console-recorder / live-SOL axis in gunbc.machine_intake_pre_os_observation_capabilities on -// gunbc#11110 (this PR does not mint that file). Until that lands, an arming PUT cannot be keyed -// to the subject's wire spelling, and live sol activate cannot be distinguished from a recorder -// that survives reset. -type MegaRacConsoleRecorderSubject - = FramebufferRecorder - | SolStreamRecorder - -type MegaRacTriggerPowerOnFieldName - = ChassisPowerOnEventsEnable - | ChasisPowerOnEventsEnable - -fn megarac_recorder_trigger_power_on_field(subject: MegaRacConsoleRecorderSubject) -> MegaRacTriggerPowerOnFieldName { - match subject { - FramebufferRecorder => ChassisPowerOnEventsEnable - SolStreamRecorder => ChasisPowerOnEventsEnable - } -} - -data megarac_video_trigger_power_on_field: MegaRacTriggerPowerOnFieldName = megarac_recorder_trigger_power_on_field(subject: FramebufferRecorder) - -data megarac_sol_trigger_power_on_field: MegaRacTriggerPowerOnFieldName = megarac_recorder_trigger_power_on_field(subject: SolStreamRecorder) - -fn megarac_trigger_power_on_field_wire(name: MegaRacTriggerPowerOnFieldName) -> NonEmptyStr { - match name { - ChassisPowerOnEventsEnable => "chassis_power_on_events_enable" as NonEmptyStr - ChasisPowerOnEventsEnable => "chasis_power_on_events_enable" as NonEmptyStr - } -} - -type MegaRacResetSurvival - = SurvivesHostAndControllerReset - | DiesOnReset - -fn megarac_recorder_reset_survival(subject: MegaRacConsoleRecorderSubject) -> MegaRacResetSurvival { - match subject { - FramebufferRecorder => SurvivesHostAndControllerReset - SolStreamRecorder => SurvivesHostAndControllerReset - } -} - -data megarac_framebuffer_recorder_reset_survival: MegaRacResetSurvival = megarac_recorder_reset_survival(subject: FramebufferRecorder) - -data megarac_sol_recorder_reset_survival: MegaRacResetSurvival = megarac_recorder_reset_survival(subject: SolStreamRecorder) - -data megarac_live_sol_activate_reset_survival: MegaRacResetSurvival = DiesOnReset - -// WHAT THIS SERVED UI CAN AND CANNOT DO, as PATH PRESENCE in the 0.32 catalog. They are not -// omissions and not unit telemetry. Per-DIMM AEP inventory and api/fru ARE in the catalog -// (api/aep_inventory/aep_dimm_info, api/fru); a controller answering nothing through those paths -// is a unit observation, not a served-surface absence. BIOS *settings* (`settings/bios`) are -// absent; flash/version (`api/maintenance/hpm/getbiosversions`) is present. MegaRacWebEventLogOemDescriptionUnknown -// was the same fork: the renderer answering unknown is not a missing path. Ampere OEM SEL -// producer layout is extdeps.ampere.oem_sel. -// -// §3c frontier: megarac_served_ui_surface_standing. Consumer: the PreOs capability axes that -// would claim Redfish or a BIOS/UEFI settings surface, on -// gunbc.machine_intake_pre_os_observation_capabilities (gunbc#11110; this PR does not mint that -// file). Until that lands, a firmware this decoder does not catalog cannot refuse an inherited -// 0.32 presence map. The coproduct carries present surfaces so Absent is not the only -// constructible known answer: a Bool that can only be true is a decoration. -type MegaRacServedUiSurface - = RedfishService - | BiosUefiSettings - | FruInventory - | AepDimmInventory - | BiosVersionFlash - -type MegaRacServedUiSurfacePresence - = ServedUiSurfacePresent - | ServedUiSurfaceAbsent - -// THE CATALOG SUBSTRING EACH SURFACE IS NAMED BY. The presence map below is a transcription of the -// 0.32 served-UI catalog (gunbc.machine_intake_megarac_served_ui_catalog_observation, hashed and -// committed); this row is the join that makes it checkable against the bytes rather than a second -// authored list beside them (review 66694): the integrity witness asks, for every surface, that -// the catalog contains the substring exactly when the map says Present. -fn megarac_served_ui_surface_catalog_token(surface: MegaRacServedUiSurface) -> NonEmptyStr { - match surface { - RedfishService => "redfish" - BiosUefiSettings => "settings/bios" - FruInventory => "api/fru" - AepDimmInventory => "api/aep_inventory/aep_dimm_info" - BiosVersionFlash => "api/maintenance/hpm/getbiosversions" - } -} - -data megarac_served_ui_surfaces: List = [RedfishService, BiosUefiSettings, FruInventory, AepDimmInventory, BiosVersionFlash] - -fn megarac_served_ui_surface_presence_at_0_32(surface: MegaRacServedUiSurface) -> MegaRacServedUiSurfacePresence { - match surface { - RedfishService => ServedUiSurfaceAbsent - BiosUefiSettings => ServedUiSurfaceAbsent - FruInventory => ServedUiSurfacePresent - AepDimmInventory => ServedUiSurfacePresent - BiosVersionFlash => ServedUiSurfacePresent - } -} - -type MegaRacServedUiFactLookup - = ServedUiFactKnown { firmware: BmcFirmwareReleaseIdentity, presence: MegaRacServedUiSurfacePresence } - | ServedUiFactFirmwareUncatalogued { firmware: BmcFirmwareReleaseIdentity } - -fn megarac_served_ui_surface_standing( - firmware: BmcFirmwareReleaseIdentity, - surface: MegaRacServedUiSurface, -) -> MegaRacServedUiFactLookup { - if bmc_firmware_release_identity_eq(a: firmware, b: megarac_firmware_0_32) { - ServedUiFactKnown { - firmware: firmware, - presence: megarac_served_ui_surface_presence_at_0_32(surface: surface), - } - } else { - ServedUiFactFirmwareUncatalogued { firmware: firmware } - } -} - -// SCREEN CAPTURE SURFACES AT 0.32. Read-only probe 2026-09-27 of the bundles served at -// https://192.168.1.228/source.min.js (gzip, sha256 5029fae278f7fc8f4dd4ad5eb8e4457e49fa25d4d0158d4bf9fbab09f57d4bf3) -// and /viewer.min.js (sha256 8bdfbb72c6449eaf64ab232a413abe257c2eec5af5d46fbe960c14534fab3e17). -// THERE IS NO SERVER-SIDE ON-DEMAND STILL. The H5 viewer's "Capture Screen" is captureScreen(): -// document.getElementById("kvm").toDataURL("image/jpeg") saved as CaptureScreen.jpeg. That is a canvas -// the browser painted by decoding the AST2100 stream from wss:///kvm (session token from -// /api/kvm/token), so a still exists only where a KVM client is running. The firmware itself -// keeps two retained screens: the last BSOD JPEG (feature CAPTURE_BSOD_JPEG, GET -// /api/settings/download_image), and framebuffer recordings made by FramebufferRecorder when an -// armed /api/settings/video/triggers event fires (chassis power on/off/reset, lpc reset, watchdog, -// critical/non-critical/non-recoverable, fan state, or a date_time schedule), listed at -// /api/logs/video with bytes at /api/logs/video-data?file=. No IPMI OEM capture command appears -// in either bundle. -type MegaRacScreenCaptureRoute - = KvmClientCanvasJpeg - | LastBsodJpeg - | TriggeredFramebufferRecording - | ServerSideOnDemandStill - -type MegaRacScreenCaptureStanding - = ScreenCaptureServedOnDemand - | ScreenCaptureEventRetainedOnly - | ScreenCaptureClientSideOnly - | ScreenCaptureAbsent - -fn megarac_screen_capture_standing_at_0_32(route: MegaRacScreenCaptureRoute) -> MegaRacScreenCaptureStanding { - match route { - KvmClientCanvasJpeg => ScreenCaptureClientSideOnly - LastBsodJpeg => ScreenCaptureEventRetainedOnly - TriggeredFramebufferRecording => ScreenCaptureEventRetainedOnly - ServerSideOnDemandStill => ScreenCaptureAbsent - } -} - -type MegaRacScreenCaptureLookup - = ScreenCaptureFactKnown { firmware: BmcFirmwareReleaseIdentity, standing: MegaRacScreenCaptureStanding } - | ScreenCaptureFirmwareUncatalogued { firmware: BmcFirmwareReleaseIdentity } - -fn megarac_screen_capture_standing( - firmware: BmcFirmwareReleaseIdentity, - route: MegaRacScreenCaptureRoute, -) -> MegaRacScreenCaptureLookup { - if bmc_firmware_release_identity_eq(a: firmware, b: megarac_firmware_0_32) { - ScreenCaptureFactKnown { firmware: firmware, standing: megarac_screen_capture_standing_at_0_32(route: route) } - } else { - ScreenCaptureFirmwareUncatalogued { firmware: firmware } - } -} - // EVENT LOG SHAPE. The web log and ipmitool are two layouts. GenID1/GenID2 on the standard-event // template are manufacturer bytes; they have no Sender constructor. Ampere's published producer // layout for those manufacturer bytes is extdeps.ampere.oem_sel. @@ -430,47 +250,6 @@ data megarac_web_event_log_layout: MegaRacEventLogLayout = MegaRacWebEventLogOem data megarac_ipmi_sel_layout: MegaRacEventLogLayout = MegaRacIpmiSelPayloadPreserved -// THE WEB SESSION AND KVM VIEWER WIRE, AS THE 0.32 UI READS IT. Recovered from the bundles served at -// https://192.168.1.228 on 2026-09-27 (source.min.js, gzip sha256 -// 5029fae278f7fc8f4dd4ad5eb8e4457e49fa25d4d0158d4bf9fbab09f57d4bf3; viewer.min.js sha256 -// 8bdfbb72c6449eaf64ab232a413abe257c2eec5af5d46fbe960c14534fab3e17): -// - POST /api/session (form fields username, password) answers JSON; the login handler treats -// `ok === 0` as success and reads CSRFToken, privilege, extendedpriv and racsession_id from it, then -// sends X-CSRFTOKEN on every later request. -// - GET /api/settings/services answers a JSON list of service rows; the KVM launcher finds the row -// whose service_name is "kvm" and opens viewer.html only when its viewer_count is 0. -// - DELETE /api/session (X-CSRFTOKEN) ends the session (the viewer's own logout). -// - The H5 viewer opens a WebSocket at /kvm (subprotocols binary, base64) and paints canvas#kvm. -// A REFUSED LOGIN'S BODY AND STATUS ON 0.32 HAVE NOT BEEN CAPTURED, so this module states none: what -// the UI establishes is only that a reply without ok === 0 is not a session. A simulated controller's -// refusal is its renderer's choice (gunbc.bmc_megarac_web_adapter), never this upstream's. -// These are the fields the UI reads, not the whole reply: a reply may carry more. -data megarac_kvm_websocket_path: NonEmptyStr = "/kvm" - -type MegaRacLoginSession { - csrf_token: String - privilege: Int - extended_privilege: Int - racsession_id: Int -} - -fn megarac_login_session_json(session: MegaRacLoginSession) -> JsonValue { - json_object(members: [ - json_kv(key: "ok", value: json_int(n: 0)), - json_kv(key: "CSRFToken", value: json_string(s: session.csrf_token)), - json_kv(key: "privilege", value: json_int(n: session.privilege)), - json_kv(key: "extendedpriv", value: json_int(n: session.extended_privilege)), - json_kv(key: "racsession_id", value: json_int(n: session.racsession_id)), - ]) -} - -fn megarac_services_kvm_json(viewer_count: Int) -> JsonValue { - json_array(elements: [json_object(members: [ - json_kv(key: "service_name", value: json_string(s: "kvm")), - json_kv(key: "viewer_count", value: json_int(n: viewer_count)), - ])]) -} - // THE OPERATIONS ABOVE ARE A DESCRIPTION; THESE ARE THE OPERATIONS. megarac_api_operations records // paths and contract notes and carries no transport, so nothing in this repository could invoke the // MegaRAC media surface — the first attach against a Mt. Collins unit was therefore driven by a @@ -508,8 +287,8 @@ fn megarac_services_kvm_json(viewer_count: Int) -> JsonValue { // positive control that the stored bytes log in. // // StartMedia's json_body MUST CARRY image_redirection AND ITS ABSENCE IS OPAQUE. The contract note -// on the operation row above records that omitting it makes the firmware refuse with codes -// 13410/13460, recovered from the firmware's own UI bundle rather than from documentation. The body +// on the operation row above records that omitting it makes the firmware refuse with opaque +// codes; every executed attach carries it. The body // is a caller parameter rather than a baked literal because image_index is resolved by READING the // images listing, never assumed, and megarac_image_redirection_wire is the authority for the field // itself. @@ -623,41 +402,3 @@ service megarac.Media { } } } - -// THE VALUE THE SP-X UI STORES AS sessionStorage "features": JSON.stringify of its project-configurations -// collection, an array of {"feature": NAME} objects; the viewer's KVM view reads it with unguarded -// indexOf calls. Which names a given build carries is an OBSERVATION this repository made of that -// build's served bundle, so it is a receipt in the observing layer -// (gunbc.machine_intake_megarac_ui_features megarac_ui_features_0_32_receipt), not a fact owned here. -fn megarac_ui_features_json(names: List) -> String { - serialize_json_array(elements: map(names, n => json_object(members: [json_kv(key: "feature", value: json_string(s: n as String))]))) -} - -// THE FIRMWARE'S OWN UI BUNDLE, SERVED AT A FIXED PATH. The SP-X web UI is one minified bundle the -// login page loads before any session exists, so the GET carries no cookie and no token and opens no -// session to release. The codes this module cites as "recovered from source.min.js" (start-media's -// 13410/13460, the served api/ surface, the KVM wire) were read from these bytes; the operation is the -// route that re-reads them instead of a hand-run probe. -// -// THE BYTES ARE WRITTEN AS SERVED. `Accept-Encoding: gzip` without --compressed asks for the encoding -// the 2026-09-27 read recorded (gzip) and leaves it undecoded, so a digest of dest_file is a digest of -// the served bytes, comparable with that read. stdout carries only the write-out: the HTTP status and -// the byte count, space-separated on one line. -data megarac_ui_bundle_path: NonEmptyStr = "/source.min.js" - -service megarac.Ui { - - operation GetServedBundle { - requires Network - input { bmc_host: NonEmptyStr, bundle_path: NonEmptyStr, dest_file: NonEmptyStr, max_time: NonEmptyStr } - output { writeout: String from "stdout", stderr: String from "stderr", exit_code: Int from "exit_code", success: Bool from "exit_success" } - readonly - transport shell { - argv: ["curl", "--fail-with-body", "-sS", "-k", "--max-time", "{max_time}", "-H", "Accept-Encoding: gzip", "-o", "{dest_file}", "-w", "%\{http_code} %\{size_download}", "https://{bmc_host}{bundle_path}"] - } - exit { - 0 => Unit - nonzero => String "megarac UI bundle GET failed" - } - } -} diff --git a/dag/extdeps/bmc/megarac_observed_output.dag b/dag/extdeps/bmc/megarac_observed_output.dag index 6633306a62b..5af3b0a87ee 100644 --- a/dag/extdeps/bmc/megarac_observed_output.dag +++ b/dag/extdeps/bmc/megarac_observed_output.dag @@ -53,15 +53,10 @@ fn megarac_image_row(image_name: String, image_index: Int) -> String { // test.claim.machine_intake.megarac_session_release_witness_test, `{"error":"invalid session"}` with 401. data megarac_invalid_session_body: String = "{\"error\":\"invalid session\"}" -// POST /api/session. NO SESSION REPLY IS RETAINED VERBATIM. The member names are those the firmware's -// own web UI reads (source.min.js, probed 2026-09-27 on branch session/eager-koi-811), and the production -// decoder reads only CSRFToken. -data megarac_session_body_standing: CitedFigureStanding = TranscribedUncited { - read_obligation: "a retained POST /api/session reply from the Mt. Collins MegaRAC, cited by digest" as NonEmptyStr -} - -fn megarac_session_body(racsession_id: Int, csrf_token: String) -> String { - join(["{ \"ok\": 0, \"privilege\": 4, \"extendedpriv\": 259, \"racsession_id\": ", to_string(racsession_id), ", \"CSRFToken\": \"", csrf_token, "\" }"], "") +// POST /api/session: a CONTROLLED FIXTURE, not a reply from the controller. It carries only the one +// field the production decoder reads (CSRFToken); the decoder's contract is the sole ground for its shape. +fn megarac_session_body(csrf_token: String) -> String { + join(["{ \"CSRFToken\": \"", csrf_token, "\" }"], "") } // The replies to start-media, stop-media and DELETE /api/session on success are not decoded by the diff --git a/dag/extdeps/bmc/redfish.dag b/dag/extdeps/bmc/redfish.dag index 53b4f34790c..d2a583593d6 100644 --- a/dag/extdeps/bmc/redfish.dag +++ b/dag/extdeps/bmc/redfish.dag @@ -5,8 +5,13 @@ import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.bmc.types { BmcGroundedHostObservations, BmcTelemetrySnapshot, FanTachSensor, RedfishMemorySummary, RedfishPowerControl, RedfishPowerState, RedfishPowerSubsystem, RedfishProcessorSummary, RedfishSensorReading, RedfishSystemInventory, TemperatureSensor } import std.measure { ByteSize, Gibibyte, HardwareThreadCount, gibibyte_to_byte_size, watt, watt_count } -import std.types { Int, List, NonEmptyStr } +import std.types { Bool, Int, List, NonEmptyStr, String } import std.optional { Present, Absent } +import extdeps.languages.json.parse { + parse_json_document, json_field, json_field_string, + JsonDocumentParsed, JsonDocumentUnreadable, FieldRead, FieldAbsent, FieldMalformed, +} +import extdeps.languages.json.emit { JsonValue, JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { scheme: Https @@ -137,3 +142,62 @@ fn bmc_ground_host_observations(snapshot: BmcTelemetrySnapshot) -> BmcGroundedHo fn redfish_system_total_memory_bytes(inv: RedfishSystemInventory) -> ByteSize { inv.memory.total_memory_bytes } + +// DMTF REDFISH BASE MESSAGE REGISTRY, PasswordChangeRequired (DSP8011, Base 1.x): a service +// answers an authenticated request with this message when the account's password must be changed +// before any other access is granted, and the only admitted request is a PATCH of that account's +// Password property. It is not an authentication failure: the credential was accepted. Observed on +// ASRock Rack ALTRAD8UD-1L2T OpenBMC 3.22.00 (srv13, 2026-10-08) as HTTP 403 whose top-level +// @Message.ExtendedInfo carries MessageId "Base.1.16.0.PasswordChangeRequired"; a wrong password on +// the same service is a bare 401. The registry version varies by service, so the id is matched as +// the "Base." registry prefix plus the ".PasswordChangeRequired" key, and the message is read from +// either the top-level annotation (bmcweb) or the standard error object's annotation. +type RedfishPasswordChangeVerdict + = RedfishPasswordChangeRequired + | RedfishPasswordChangeNotIndicated + +fn redfish_message_id_is_password_change_required(id: String) -> Bool { + starts_with(s: id, prefix: "Base.") && ends_with(s: id, suffix: ".PasswordChangeRequired") +} + +fn redfish_message_requires_password_change(message: JsonValue) -> Bool { + match json_field_string(obj: message, key: "MessageId") { + FieldRead { value: id } => redfish_message_id_is_password_change_required(id: id) + FieldAbsent => false + FieldMalformed { cause: _ } => false + } +} + +fn redfish_extended_info_requires_password_change(holder: JsonValue) -> Bool { + match json_field(obj: holder, key: "@Message.ExtendedInfo") { + FieldRead { value: v } => + match v { + JsonArray { elements: elements } => + fold(elements, init: false, f: fn(acc, m) { acc || redfish_message_requires_password_change(message: m) }) + JsonNull => false + JsonBool { value: _ } => false + JsonNumber { lexeme: _ } => false + JsonString { value: _ } => false + JsonObject { members: _ } => false + } + FieldAbsent => false + FieldMalformed { cause: _ } => false + } +} + +fn redfish_password_change_verdict(body: String) -> RedfishPasswordChangeVerdict { + match parse_json_document(s: body) { + JsonDocumentUnreadable { gap: _ } => RedfishPasswordChangeNotIndicated + JsonDocumentParsed { value: doc } => + if redfish_extended_info_requires_password_change(holder: doc) { + RedfishPasswordChangeRequired + } else { + match json_field(obj: doc, key: "error") { + FieldRead { value: err } => + if redfish_extended_info_requires_password_change(holder: err) { RedfishPasswordChangeRequired } else { RedfishPasswordChangeNotIndicated } + FieldAbsent => RedfishPasswordChangeNotIndicated + FieldMalformed { cause: _ } => RedfishPasswordChangeNotIndicated + } + } + } +} diff --git a/dag/extdeps/board_authority.dag b/dag/extdeps/board_authority.dag new file mode 100644 index 00000000000..3144babf963 --- /dev/null +++ b/dag/extdeps/board_authority.dag @@ -0,0 +1,146 @@ +module extdeps.board_authority + +import std.decl_ref { DeclarationRef, WholeDeclaration, declaration_ref_eq } +import std.types { Bool, Int, List, NonEmptyStr } +import std.content_hash { Sha256Digest } +import v2.std.algebra { filter } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.intel.com/content/dam/www/public/us/en/documents/specification-updates/ipmi-platform-mgt-fru-info-storage-def-v1-0-rev-1-3-spec-update.pdf" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.board_authority", + decl_name: "BoardFruIdentity", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +// THE AGNOSTIC SHAPES OF "WHICH BOARD IS THIS HOST", and nothing else. This module enumerates no +// product (DESIGN section 3, External upstream decomposition): the board a host reports is the +// Board Area of its own FRU (Platform Management FRU Information Storage Definition, Board Area: +// manufacturer, product name, part number), and the board a consumer reasons about is an +// authority REFERENCE to the per-board subject module that owns that board's facts. Adding a board +// is a per-board module plus one binding row in the consumer layer, never an edit here. The rows +// and the table live in gunbc.board_authority_binding; this module owns only the shapes and the one +// fold that reads a table. +type BoardFruIdentity { + manufacturer: NonEmptyStr + product: NonEmptyStr + part_number: NonEmptyStr? +} + +// WHY NO FRU BOARD AREA WAS READ. Never defaulted: a host whose board was not read is a different +// fact from a host whose board is read and unbound. +type BoardFruUnreadCause + = BoardAreaAbsent + | BoardAreaFieldsUnreadable + +type BoardFruObservation + = BoardFruRead { fru: BoardFruIdentity } + | BoardFruNotRead { cause: BoardFruUnreadCause } + +// A BOARD'S AUTHORITY IS THE DECLARATION OF ITS OWN SUBJECT. Equality is declaration_ref_eq, so two +// references agree exactly when they name the same declaration. Each per-board module exports its +// own reference; no consumer spells one. +type BoardAuthorityRef { + subject: DeclarationRef +} + +fn board_authority_ref_eq(a: BoardAuthorityRef, b: BoardAuthorityRef) -> Bool { + declaration_ref_eq(a: a.subject, b: b.subject) +} + +// WHAT JUSTIFIES THE EDGE, in the type so a consumer cannot read the strong standing off prose. +// BoundByNameOnly: the FRU's names agree with the subject's name and nothing else does; that is an +// inference from a name, typed as a bet with the discriminator that would settle it. +// BoundByObservedEvidence: an observation beyond the name (cited receipts) agrees with the +// subject's own cited facts. +type BoardBindingStanding + = BoundByNameOnly { discriminator_obligation: NonEmptyStr } + | BoundByObservedEvidence { first: DeclarationRef, further: List } + +// THE COMMITTED FRU READ the row was authored from: path and sha256 of the bytes. +type BoardFruReceipt { + artifact_path: NonEmptyStr + sha256: Sha256Digest +} + +type BoardAuthorityBinding { + manufacturer: NonEmptyStr + product: NonEmptyStr + authority: BoardAuthorityRef + standing: BoardBindingStanding + fru_receipt: BoardFruReceipt +} + +// IDENTITY-GRADE USES (keying a reading, pairing a reading with a board) accept either bound arm; +// USES THAT IMPORT BOARD FACTS (figures, profile routes) accept BoardEvidenceBound only. +type BoardAuthorityLookup + = BoardIdentityBound { authority: BoardAuthorityRef, discriminator_obligation: NonEmptyStr } + | BoardEvidenceBound { authority: BoardAuthorityRef, first: DeclarationRef, further: List } + | BoardUnbound { fru: BoardFruIdentity } + | BoardAmbiguous { fru: BoardFruIdentity, count: Int } + | BoardNotRead { cause: BoardFruUnreadCause } + +fn binding_names_fru(binding: BoardAuthorityBinding, fru: BoardFruIdentity) -> Bool { + binding.manufacturer == fru.manufacturer && binding.product == fru.product +} + +fn bound_lookup_of(binding: BoardAuthorityBinding) -> BoardAuthorityLookup { + match binding.standing { + BoundByNameOnly { discriminator_obligation: o } => BoardIdentityBound { authority: binding.authority, discriminator_obligation: o } + BoundByObservedEvidence { first: f, further: r } => BoardEvidenceBound { authority: binding.authority, first: f, further: r } + } +} + +// THE ONE TOTAL FOLD. Exact manufacturer and product; the part number is carried, not matched. +// Refuses on both sides of uniqueness; an unread board is never read as unbound. +fn board_authority_for(observation: BoardFruObservation, bindings: List) -> BoardAuthorityLookup { + match observation { + BoardFruNotRead { cause: c } => BoardNotRead { cause: c } + BoardFruRead { fru: f } => { + let hits = bindings |> filter(b => binding_names_fru(binding: b, fru: f)) + let n = count(hits) + if n == 0 { + BoardUnbound { fru: f } + } else if n == 1 { + fold(hits, init: BoardUnbound { fru: f }, f: (_acc, b) => bound_lookup_of(binding: b)) + } else { + BoardAmbiguous { fru: f, count: n } + } + } + } +} + +// The authority a lookup reached for an IDENTITY-GRADE use, absent when the board is not bound. +fn board_identity_authority(lookup: BoardAuthorityLookup) -> BoardAuthorityRef? { + match lookup { + BoardIdentityBound { authority: a, discriminator_obligation: _ } => Present { value: a } + BoardEvidenceBound { authority: a, first: _, further: _ } => Present { value: a } + BoardUnbound { fru: _ } => none + BoardAmbiguous { fru: _, count: _ } => none + BoardNotRead { cause: _ } => none + } +} + +// The authority a lookup reached for a FACTS use: only the evidence-bound arm. +fn board_facts_authority(lookup: BoardAuthorityLookup) -> BoardAuthorityRef? { + match lookup { + BoardEvidenceBound { authority: a, first: _, further: _ } => Present { value: a } + BoardIdentityBound { authority: _, discriminator_obligation: _ } => none + BoardUnbound { fru: _ } => none + BoardAmbiguous { fru: _, count: _ } => none + BoardNotRead { cause: _ } => none + } +} diff --git a/dag/extdeps/boards/asrock_altrad8ud_beta_firmware.dag b/dag/extdeps/boards/asrock_altrad8ud_beta_firmware.dag index 7970f719a77..e259c774880 100644 --- a/dag/extdeps/boards/asrock_altrad8ud_beta_firmware.dag +++ b/dag/extdeps/boards/asrock_altrad8ud_beta_firmware.dag @@ -58,34 +58,40 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope { data asrock_altrad8ud_beta_disclaimer: NonEmptyStr = "Beta firmware is still undergoing final testing; use is at the user's risk and ASRock Rack assumes no responsibility. ASRock Rack recommends keeping current firmware when the system works properly." +data asrock_altrad8ud_beta_bios_4_01_listing: AsrockAltrad8udBetaFirmwareListing = AsrockAltrad8udBetaFirmwareListing { + target: HostBios, + version: SemanticFirmwareVersion { value: firmware_semantic_version(major: 4, minor: 1, patch: 0) }, + listed_on: 1776124800000, + packages: [ + AsrockBetaFirmwarePackage { image_kind: InstantFlashRom, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 5110000), decimal_increment: byte_size(count: 10000) } }, + AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 5110000), decimal_increment: byte_size(count: 10000) } }, + ], + description: "Updated RC to 0ACRB_022", +} + +data asrock_altrad8ud_beta_bmc_3_22_00_listing: AsrockAltrad8udBetaFirmwareListing = AsrockAltrad8udBetaFirmwareListing { + target: BmcController, + version: SemanticFirmwareVersion { value: asrock_altrad8ud_bmc_3_22_00_release.version }, + listed_on: 1772064000000, + packages: [ + AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 27710000), decimal_increment: byte_size(count: 10000) } }, + ], + description: "A password change is enforced upon the first login to the WebUI", +} + +data asrock_altrad8ud_beta_scp_2_10_20260210_listing: AsrockAltrad8udBetaFirmwareListing = AsrockAltrad8udBetaFirmwareListing { + target: ScpFirmware, + version: ScpBuildVersion { major: 2, minor: 10, build_date: Iso8601CivilDate { year: 2026, month: 2, day: 10 } }, + listed_on: 1776729600000, + packages: [ + AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 165250), decimal_increment: byte_size(count: 10) } }, + AsrockBetaFirmwarePackage { image_kind: InstantFlashSlim, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 165010), decimal_increment: byte_size(count: 10) } }, + ], + description: "Ampere SCP firmware", +} + data asrock_altrad8ud_beta_firmware_listings: List = [ - AsrockAltrad8udBetaFirmwareListing { - target: HostBios, - version: SemanticFirmwareVersion { value: firmware_semantic_version(major: 4, minor: 1, patch: 0) }, - listed_on: 1776124800000, - packages: [ - AsrockBetaFirmwarePackage { image_kind: InstantFlashRom, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 5110000), decimal_increment: byte_size(count: 10000) } }, - AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 5110000), decimal_increment: byte_size(count: 10000) } }, - ], - description: "Updated RC to 0ACRB_022", - }, - AsrockAltrad8udBetaFirmwareListing { - target: BmcController, - version: SemanticFirmwareVersion { value: asrock_altrad8ud_bmc_3_22_00_release.version }, - listed_on: 1772064000000, - packages: [ - AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 27710000), decimal_increment: byte_size(count: 10000) } }, - ], - description: "A password change is enforced upon the first login to the WebUI", - }, - AsrockAltrad8udBetaFirmwareListing { - target: ScpFirmware, - version: ScpBuildVersion { major: 2, minor: 10, build_date: Iso8601CivilDate { year: 2026, month: 2, day: 10 } }, - listed_on: 1776729600000, - packages: [ - AsrockBetaFirmwarePackage { image_kind: OpenBmcWebUiTar, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 165250), decimal_increment: byte_size(count: 10) } }, - AsrockBetaFirmwarePackage { image_kind: InstantFlashSlim, displayed_size: AsrockUnresolvedDisplaySize { decimal_interpretation: byte_size(count: 165010), decimal_increment: byte_size(count: 10) } }, - ], - description: "Ampere SCP firmware", - }, + asrock_altrad8ud_beta_bios_4_01_listing, + asrock_altrad8ud_beta_bmc_3_22_00_listing, + asrock_altrad8ud_beta_scp_2_10_20260210_listing, ] diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index 84911d45687..9c816286eb3 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -44,6 +44,7 @@ import std.coordinate_frame { CoordinateFrameIdentity, coordinate_frame_identity import std.affine_space { point2 } import std.orthogonal_geometry { AxisAlignedRectangle2, OrthogonalConstruction, axis_aligned_rectangle2 } import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.board_authority { BoardAuthorityRef } import extdeps.standards.micro_atx { MountingLocationId, LocB, LocC, LocF, LocH, LocJ, LocL, LocM, LocR, LocS } import extdeps.cpu.types { CpuSocket } import extdeps.cpu.ampere { ampere_lga4926_socket } @@ -81,8 +82,16 @@ data asrock_altrad8ud_srv3_nbd_proxy_ws_dry_run_authority: ExternalAuthority = E } } +data asrock_altrad8ud_board_authority: BoardAuthorityRef = BoardAuthorityRef { + subject: DeclarationRef { + module_path: "extdeps.boards.asrock_rack", + decl_name: "asrock_altrad8ud_1l2t_catalog", + field: WholeDeclaration + } +} + data asrock_altrad8ud_1l2t_catalog: BaseboardCatalogRow = BaseboardCatalogRow { - model: AsrockRackAltrad8ud1l2t, + model: asrock_altrad8ud_board_authority, model_label: "ASRock Rack ALTRAD8UD-1L2T", vendor: asrock_rack, bmc_firmware: OpenBmc, @@ -367,7 +376,7 @@ fn live_probe_confirms_nbd_slot_ws_absent() -> Bool { // versioned and separately flashed: srv4 runs BMC 03.22.00 and still fails to // boot, so "this host is on the current firmware" is not a single fact. data asrock_altrad8ud_bios_3_10_instant_flash_release: FirmwareReleaseRow = FirmwareReleaseRow { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, target: HostBios, version: firmware_semantic_version(major: 3, minor: 10, patch: 0), released_on: 1776124800000, @@ -388,7 +397,7 @@ data asrock_altrad8ud_bios_3_10_instant_flash_release: FirmwareReleaseRow = Firm } data asrock_altrad8ud_bios_3_10_openbmc_release: FirmwareReleaseRow = FirmwareReleaseRow { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, target: HostBios, version: firmware_semantic_version(major: 3, minor: 10, patch: 0), released_on: 1776124800000, @@ -429,7 +438,7 @@ data asrock_altrad8ud_bios_3_10_obmc_manifest_purpose: NonEmptyStr = "xyz.openbm data asrock_altrad8ud_bios_3_10_obmc_manifest_machine_name: NonEmptyStr = "altrad8ud-1l2t" data asrock_altrad8ud_bmc_3_22_00_release: FirmwareReleaseRow = FirmwareReleaseRow { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, target: BmcController, version: firmware_semantic_version(major: 3, minor: 22, patch: 0), released_on: 1776124800000, @@ -438,13 +447,33 @@ data asrock_altrad8ud_bmc_3_22_00_release: FirmwareReleaseRow = FirmwareReleaseR scheme: Https locator: "www.asrockrack.com/general/productdetail.asp?Model=ALTRAD8UD-1L2T" }, - image_bytes: byte_size(count: 29056000), - image_digest: Absent, + image_bytes: byte_size(count: 29060210), + image_digest: Present { + value: sha256_digest( + hex: "0c1f458c396aff76ae0259509ea05c53eef57809fa202721875586792fa807bb", + ), + }, release_notes: "1. A password change is enforced upon the first login to the WebUI 2. Improved BMC web information 3. Enhanced BMC system compatibility", prerequisite: NoFirmwarePrerequisite, authority: asrock_altrad8ud_download_authority, } +// The push member inside the 03.22.00 distribution zip: a static-layout OpenBMC image +// (u-boot, kernel, rofs, rwfs, each RSA-SHA256 signed) whose MANIFEST declares +// purpose BMC, version 3.22.00, BuildId 20260217040008, MachineName altrad8ud-1l2t. +// The zip's own bytes are the release row's image_digest; these are the bytes the +// UpdateService consumes. Measured 2026-10-08 and pushed to srv13, which then +// reported BMC image 3.22.00 in its firmware inventory. +data asrock_altrad8ud_bmc_3_22_00_push_artifact_name: NonEmptyStr = "altrad8ud-1l2t_3.22.00.static.mtd.tar" + +data asrock_altrad8ud_bmc_3_22_00_push_artifact_bytes: ByteSize = byte_size(count: 29347840) + +data asrock_altrad8ud_bmc_3_22_00_push_artifact_digest: Digest = sha256_digest( + hex: "cb8b8e727174b8383f90f36e967838c46105bd42bc76cc08c4ac1383a88ca9de", +) + +data asrock_altrad8ud_bmc_3_22_00_manifest_purpose: NonEmptyStr = "xyz.openbmc_project.Software.Version.VersionPurpose.BMC" + data asrock_altrad8ud_firmware_release_catalog: List = [ asrock_altrad8ud_bios_3_10_instant_flash_release, asrock_altrad8ud_bios_3_10_openbmc_release, @@ -474,7 +503,7 @@ data asrock_altrad8ud_firmware_release_catalog: List = [ // the minimum evidence rather than an inference from the vendor page (which // states no such dependency at all). data asrock_altrad8ud_bios_310_push_refused_at_bmc_207: FirmwarePrerequisiteObservation = FirmwarePrerequisiteObservation { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, subject: HostBios, image_kind: OpenBmcWebUiTar, gating_target: BmcController, @@ -486,7 +515,7 @@ data asrock_altrad8ud_bios_310_push_refused_at_bmc_207: FirmwarePrerequisiteObse } data asrock_altrad8ud_bios_310_push_accepted_at_bmc_322: FirmwarePrerequisiteObservation = FirmwarePrerequisiteObservation { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, subject: HostBios, image_kind: OpenBmcWebUiTar, gating_target: BmcController, @@ -515,11 +544,11 @@ data asrock_altrad8ud_bios_openbmc_prerequisite_evidence: List = [ - DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1"], authority: extdeps_external_authority_anchor }, - DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1", "E1"], authority: extdeps_external_authority_anchor }, - DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1", "B1", "E1", "F1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { board: asrock_altrad8ud_board_authority, slots: ["A1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { board: asrock_altrad8ud_board_authority, slots: ["A1", "E1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { board: asrock_altrad8ud_board_authority, slots: ["A1", "B1", "E1", "F1"], authority: extdeps_external_authority_anchor }, DimmPopulation { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, slots: ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], authority: extdeps_external_authority_anchor, }, diff --git a/dag/extdeps/boards/gigabyte.dag b/dag/extdeps/boards/gigabyte.dag index 5670be68a37..a79b823fadf 100644 --- a/dag/extdeps/boards/gigabyte.dag +++ b/dag/extdeps/boards/gigabyte.dag @@ -17,6 +17,7 @@ import extdeps.boards.types { import extdeps.vendor.gigabyte { gigabyte } import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.board_authority { BoardAuthorityRef } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } @@ -53,8 +54,22 @@ data gigabyte_mp72_hb0_marketplace_listing_authority: ExternalAuthority = Extern } } +// The MP72-HB0 is a CANDIDATE reference board with no fleet instance: no PhysicalAsset cites it, no +// host's FRU binds to it (gunbc.board_authority_binding carries no row for it), and it is consumed +// only as the contrast row of the board-profile witness and the forged-probe control. It exists so +// the private machine-tile carrier can compare a dual-socket profile against a vendor-grounded row +// rather than against prose. Dissolution trigger: if no asset cites it once the procurement gate +// closes either way, delete this authority, its catalog row and those two controls. +data gigabyte_mp72_hb0_board_authority: BoardAuthorityRef = BoardAuthorityRef { + subject: DeclarationRef { + module_path: "extdeps.boards.gigabyte", + decl_name: "gigabyte_mp72_hb0_catalog", + field: WholeDeclaration + } +} + data gigabyte_mp72_hb0_catalog: BaseboardCatalogRow = BaseboardCatalogRow { - model: GigabyteMp72Hb0, + model: gigabyte_mp72_hb0_board_authority, model_label: "Gigabyte MP72-HB0 (rev. 3.0)", vendor: gigabyte, bmc_firmware: OpenBmc, diff --git a/dag/extdeps/boards/types.dag b/dag/extdeps/boards/types.dag index 0a6b8277990..ce5a548e9f8 100644 --- a/dag/extdeps/boards/types.dag +++ b/dag/extdeps/boards/types.dag @@ -11,6 +11,7 @@ import std.measure { import extdeps.bmc.endpoint { BmcFirmwareFamily, BmcEndpoint } import extdeps.vendor { Vendor } import extdeps.hardware { Hardware } +import extdeps.board_authority { BoardAuthorityRef } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -22,18 +23,8 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } -// GigabyteMp72Hb0 is a CANDIDATE reference board with no fleet instance: no PhysicalAsset cites it -// and no host resolves to it. It exists so the private machine-tile carrier can compare a -// dual-socket profile against a vendor-grounded row rather than against prose. An arm with no -// instantiation carries a dissolution trigger: if no asset cites it once the procurement gate -// closes either way, delete the arm. -type BaseboardModel - = AsrockRackAltrad8ud1l2t - | GigabyteMp72Hb0 - | AmpereMtCollins - type BaseboardCatalogRow { - model: BaseboardModel + model: BoardAuthorityRef model_label: NonEmptyStr vendor: Vendor bmc_firmware: BmcFirmwareFamily @@ -48,7 +39,7 @@ type ServerBaseboard { // this list is not a smaller version of a supported one — it is an // unsupported configuration whose failure proves nothing about the modules. type DimmPopulation { - board: BaseboardModel + board: BoardAuthorityRef slots: List authority: ExternalAuthority } diff --git a/dag/extdeps/cache.dag b/dag/extdeps/cache.dag index 39e9a9e6a16..a6aecf69def 100644 --- a/dag/extdeps/cache.dag +++ b/dag/extdeps/cache.dag @@ -25,8 +25,6 @@ import extdeps.cache.types { CatalogOpaqueStringKey, CatalogCasContentDigest, } -import extdeps.realization.parse_table_memo { parse_table_memo_facts, parse_table_memo_id } -import extdeps.realization.compile_stage_memo { compile_stage_memo_facts, compile_stage_memo_id } import extdeps.realization.reconcile_in_process { parse_cache_facts, typed_module_cache_facts, @@ -128,8 +126,6 @@ data sccache_local: CacheInterfaceCatalogFacts = sccache_local_facts data buildbuddy_cas: CacheInterfaceCatalogFacts = buildbuddy_cas_facts data cargo_target_dir: CacheInterfaceCatalogFacts = cargo_target_dir_facts data rustup_toolchain_store: CacheInterfaceCatalogFacts = rustup_toolchain_store_facts -data parse_table_memo: CacheInterfaceCatalogFacts = parse_table_memo_facts -data compile_stage_memo: CacheInterfaceCatalogFacts = compile_stage_memo_facts data cache_catalog: List = [ gha_actions_cache, @@ -139,8 +135,6 @@ data cache_catalog: List = [ rustup_toolchain_store, parse_cache_facts, typed_module_cache_facts, - parse_table_memo, - compile_stage_memo_facts, materialization_store_local_facts, ] @@ -148,11 +142,10 @@ data cache_catalog_rows: List = cache_catalog |> map(row => cache_interface_facts_from_catalog(row: row)) data sccache_local_projected: CacheInterfaceFacts = cache_interface_facts_from_catalog(sccache_local) -data parse_table_memo_projected: CacheInterfaceFacts = cache_interface_facts_from_catalog(parse_table_memo) +data parse_cache_projected: CacheInterfaceFacts = cache_interface_facts_from_catalog(parse_cache_facts) data cargo_target_dir_projected: CacheInterfaceFacts = cache_interface_facts_from_catalog(cargo_target_dir) data cache_layer_default_order: List = [ - parse_table_memo_id, parse_cache_id, typed_module_cache_id, cargo_target_dir_id, @@ -160,5 +153,4 @@ data cache_layer_default_order: List = [ rustup_toolchain_store_id, buildbuddy_cas_id, gha_actions_cache_id, - compile_stage_memo_id, ] diff --git a/dag/extdeps/cooling/dynatron.dag b/dag/extdeps/cooling/dynatron.dag index 55e6238bbbb..b7e023df417 100644 --- a/dag/extdeps/cooling/dynatron.dag +++ b/dag/extdeps/cooling/dynatron.dag @@ -1,5 +1,6 @@ module extdeps.cooling.dynatron +import extdeps.boards.asrock_rack { asrock_altrad8ud_board_authority } import std.types { NonEmptyStr, List } import std.nat { Nat } import std.measure { Micrometer, micrometer, watt, rack_unit, Gram, gram, Percent, percent, RevolutionsPerMinute, rpm, MillilitrePerSecond, millilitre_per_second } @@ -7,7 +8,6 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.cooling.types { CpuCoolerCatalogRow } import extdeps.cpu.types { SocketedLandGrid, CpuSocket, LandGridArray } -import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.vendor.dynatron { dynatron } import extdeps.vendor.ampere { ampere_vendor } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef, CitedFigureStanding, CitedToAuthority, TranscribedUncited } @@ -27,7 +27,7 @@ data dynatron_w1_catalog: CpuCoolerCatalogRow = CpuCoolerCatalogRow { active_passive_switchable: true, minimum_rack_units: rack_unit(2), max_design_power: watt(250), - supported_motherboards: [AsrockRackAltrad8ud1l2t], + supported_motherboards: [asrock_altrad8ud_board_authority], } data extdeps_model_scope: ExternalModelScope = ExternalModelScope { diff --git a/dag/extdeps/cooling/types.dag b/dag/extdeps/cooling/types.dag index 57ecae64630..81049ae4d53 100644 --- a/dag/extdeps/cooling/types.dag +++ b/dag/extdeps/cooling/types.dag @@ -1,5 +1,6 @@ module extdeps.cooling.types +import extdeps.board_authority { BoardAuthorityRef } import std.types { NonEmptyStr, Int, Bool, List } import std.measure { Watt, watt, RackUnit } import extdeps.vendor { Vendor } @@ -8,7 +9,6 @@ import extdeps.external_authority { ExternalAuthority, ExternalModelScope, Exter import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.uri { Uri, Https } import extdeps.cpu.types { CpuSocket } -import extdeps.boards.types { BaseboardModel } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -29,10 +29,10 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope { further_citations: [] } -// supported_motherboards is grounded on the existing BaseboardModel authority, not a bare string: a +// supported_motherboards is grounded on the board authority reference (extdeps.board_authority), not a bare string: a // cooler's supported-board population is the same board identities the fleet already binds, so // comparison must not route through label spelling. It was List carrying -// "ALTRAD8UD-1L2T", matching neither the BaseboardModel arm nor the catalog model_label "ASRock +// "ALTRAD8UD-1L2T", matching neither the board authority arm nor the catalog model_label "ASRock // Rack ALTRAD8UD-1L2T" — a String leaf hiding a named part, decomposed at its first consumer, // product.cooling_qualification. type CpuCoolerCatalogRow { @@ -42,5 +42,5 @@ type CpuCoolerCatalogRow { active_passive_switchable: Bool minimum_rack_units: RackUnit max_design_power: Watt - supported_motherboards: List + supported_motherboards: List } diff --git a/dag/extdeps/cron/schedule_model.dag b/dag/extdeps/cron/schedule_model.dag index 47472b62850..5b02e267593 100644 --- a/dag/extdeps/cron/schedule_model.dag +++ b/dag/extdeps/cron/schedule_model.dag @@ -4,6 +4,7 @@ import std.integer { NonNegativeInt, PositiveInt } import std.types { List } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import std.nat { nat_to_decimal_string } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -36,13 +37,13 @@ type CronEntry { fn render_cron_field(f: CronField) -> String { match f { Wildcard => "*" - Exact { value: v } => "{v}" - Listed { values } => values |> map(v => "{v}") |> join(separator: ",") - Ranged { start: s, end: e } => "{s}-{e}" + Exact { value: v } => nat_to_decimal_string(value: v) + Listed { values } => values |> map(v => nat_to_decimal_string(value: v)) |> join(separator: ",") + Ranged { start: s, end: e } => concat(nat_to_decimal_string(value: s), concat("-", nat_to_decimal_string(value: e))) Step { start: st, step: sp } => match st { - 0 => "*/{sp}" - _ => "{st}/{sp}" + 0 => concat("*/", nat_to_decimal_string(value: sp)) + _ => concat(nat_to_decimal_string(value: st), concat("/", nat_to_decimal_string(value: sp))) } } } diff --git a/dag/extdeps/crypto/seeded_filler.dag b/dag/extdeps/crypto/seeded_filler.dag new file mode 100644 index 00000000000..d69439ba684 --- /dev/null +++ b/dag/extdeps/crypto/seeded_filler.dag @@ -0,0 +1,94 @@ +module extdeps.crypto.seeded_filler + +import std.types { String, NonEmptyStr, Int, FilePath } +import std.measure { ByteSize, byte_size, measure_count } +import std.optional { Present, Absent } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } +import extdeps.shell +import extdeps.tools.coreutils_truncate +import extdeps.tools.coreutils_stat +import extdeps.tools.openssl + +// A DETERMINISTIC, NON-SECRET, INCOMPRESSIBLE FILE OF EXACTLY N BYTES, KEYED BY A SEED. +// +// The construction is the GNU coreutils manual's own seeded random source ("Sources of random data"): +// the AES-256-CTR keystream that `openssl enc -aes-256-ctr -pass pass: -nosalt` produces over +// zeros. Here the zeros are a file `truncate -s N` sized exactly, so the keystream is exactly N bytes +// long without a pipe: CTR is a stream mode, so output length equals input length. It is +// reproducible (one seed, one key, one stream), two seeds give unrelated streams, and an AES +// keystream is indistinguishable from random, so neither gzip nor a chunk store's dedup finds +// structure in it -- which is why it, and not zeros or a repeated pattern, stands in for content +// whose SIZE must be carried while its bytes must not be: a compressible filler would understate +// what storing the real content costs. The seed is in argv and is NOT a secret; the filler carries +// no information beyond its seed and its length. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { scheme: Https, locator: "www.gnu.org/software/coreutils/manual/html_node/Random-sources.html" } +} + +data seeded_filler_openssl_enc_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { scheme: Https, locator: "docs.openssl.org/3.0/man1/openssl-enc/" } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { module_path: "extdeps.crypto.seeded_filler", decl_name: "seeded_filler_write", field: WholeDeclaration } + }, + first_citation: extdeps.crypto.seeded_filler.extdeps_external_authority_anchor, + further_citations: [extdeps.crypto.seeded_filler.seeded_filler_openssl_enc_authority] +} + +// WHICH STEP REFUSED. The size readback is a step: a filler whose stat size is not the requested +// size is refused, never returned. +type SeededFillerStep + = FillerZeroSized + | FillerKeystreamWritten + | FillerZeroRemoved + | FillerSizeReadBack + +type SeededFiller + = SeededFillerWritten { path: NonEmptyStr, size: ByteSize } + | SeededFillerRefused { path: NonEmptyStr, step: SeededFillerStep, detail: String } + +fn seeded_filler_step_words(step: SeededFillerStep) -> String { + match step { + FillerZeroSized => "truncate" + FillerKeystreamWritten => "openssl-enc" + FillerZeroRemoved => "zero-input-removal" + FillerSizeReadBack => "size-readback" + } +} + +// WRITE THE FILLER AT `path`, whose parent directory must exist. The zero input is a sibling +// `.zero`, sparse on every filesystem truncate extends, and removed before returning. +fn seeded_filler_write(seed: NonEmptyStr, size: ByteSize, path: NonEmptyStr) -> SeededFiller { + let zero = concat(path as String, ".zero") as NonEmptyStr + let sized = coreutils.Truncate.ToSize(size_bytes: to_string(measure_count(m: size)) as NonEmptyStr, path: zero) + if sized.success == false { + SeededFillerRefused { path: path, step: FillerZeroSized, detail: "truncate could not size the zero input" } + } else { + let enc = openssl.Enc.Aes256CtrPassphrase(passphrase: seed, input_path: zero, output_path: path) + let removed = shell.Remove.FileForce(path: zero as FilePath) + if enc.success == false { + SeededFillerRefused { path: path, step: FillerKeystreamWritten, detail: enc.stderr } + } else if removed.success == false { + SeededFillerRefused { path: path, step: FillerZeroRemoved, detail: concat("the zero input could not be removed: ", zero as String) } + } else { + seeded_filler_readback(path: path, size: size) + } + } +} + +fn seeded_filler_readback(path: NonEmptyStr, size: ByteSize) -> SeededFiller { + let st = coreutils.Stat.PathSize(path: path) + match parse_int(s: replace(st.size_text, "\n", "")) { + Absent => SeededFillerRefused { path: path, step: FillerSizeReadBack, detail: concat("stat printed no size: ", st.size_text) } + Present { value: n } => + if st.success && n == measure_count(m: size) { + SeededFillerWritten { path: path, size: size } + } else { + SeededFillerRefused { path: path, step: FillerSizeReadBack, detail: join(["the filler is ", to_string(n), " bytes, not ", to_string(measure_count(m: size))], "") } + } + } +} diff --git a/dag/extdeps/exec/command.dag b/dag/extdeps/exec/command.dag index 4041e38d681..6df9856730e 100644 --- a/dag/extdeps/exec/command.dag +++ b/dag/extdeps/exec/command.dag @@ -107,6 +107,9 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_extract_command"), decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_create_from_list_command"), decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_list_command"), + decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_create_from_two_lists_command"), + decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_list_sizes_command"), + decl_ref(module_path: "extdeps.tools.tar", decl_name: "tar_extract_listed_command"), decl_ref(module_path: "extdeps.tools.btrfs_progs", decl_name: "btrfs_subvolume_create_command"), decl_ref(module_path: "extdeps.tools.btrfs_progs", decl_name: "btrfs_subvolume_snapshot_read_only_command"), decl_ref(module_path: "extdeps.tools.btrfs_progs", decl_name: "btrfs_subvolume_delete_command"), @@ -115,12 +118,14 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.tools.findutils", decl_name: "find_regular_files_sha256_command"), decl_ref(module_path: "extdeps.tools.findutils", decl_name: "find_owner_executable_files_command"), decl_ref(module_path: "extdeps.tools.findutils", decl_name: "find_symlink_targets_command"), + decl_ref(module_path: "extdeps.tools.findutils", decl_name: "find_regular_file_size_mtime_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mv_exact_destination_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "sort_in_place_command"), decl_ref(module_path: "extdeps.tools.unzip", decl_name: "unzip_extract_command"), decl_ref(module_path: "extdeps.tools.ldd", decl_name: "ldd_command"), decl_ref(module_path: "extdeps.tools.dpkg_deb", decl_name: "dpkg_deb_extract_command"), decl_ref(module_path: "extdeps.tools.node", decl_name: "node_eval_command"), + decl_ref(module_path: "extdeps.tools.node", decl_name: "node_run_file_command"), decl_ref(module_path: "extdeps.tools.node", decl_name: "node_version_command"), decl_ref(module_path: "extdeps.browser.chromium", decl_name: "chromium_version_command"), decl_ref(module_path: "extdeps.tools.squashfs_tools", decl_name: "unsquashfs_extract_command"), @@ -136,6 +141,7 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.iproute2.ip_show", decl_name: "ip_addr_show_command"), decl_ref(module_path: "extdeps.iproute2.ip_show", decl_name: "ip_route_show_command"), decl_ref(module_path: "extdeps.tools.uname", decl_name: "uname_release_command"), + decl_ref(module_path: "extdeps.tools.uname", decl_name: "uname_machine_command"), decl_ref(module_path: "extdeps.tools.make", decl_name: "make_command"), decl_ref(module_path: "extdeps.systemd.systemd_run", decl_name: "systemd_run_command_of"), decl_ref(module_path: "extdeps.package_managers.pip", decl_name: "pip_install_pinned_command"), @@ -153,9 +159,11 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.tools.xorriso", decl_name: "xorriso_list_long_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "cp_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "cat_command"), + decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "nproc_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mktemp_directory_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "dd_overwrite_path_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mkdir_parents_command"), + decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "mkdir_create_only_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "ln_hard_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "ls_long_directory_command"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "ls_long_directory_numeric_command"), @@ -213,6 +221,7 @@ fn argv_command(program: ProgramIdentity, arguments: List) -> ArgvComman decl_ref(module_path: "extdeps.posix.test_utility", decl_name: "test_exists_command"), decl_ref(module_path: "extdeps.posix.test_utility", decl_name: "test_writable_command"), decl_ref(module_path: "extdeps.posix.test_utility", decl_name: "test_executable_command"), + decl_ref(module_path: "extdeps.posix.test_utility", decl_name: "test_directory_command"), decl_ref(module_path: "extdeps.tools.id", decl_name: "id_group_names_command"), decl_ref(module_path: "extdeps.virtualization.firecracker", decl_name: "firecracker_version_command"), decl_ref(module_path: "extdeps.virtualization.qemu", decl_name: "qemu_version_command"), diff --git a/dag/extdeps/exec/program.dag b/dag/extdeps/exec/program.dag index b3b316d7d6f..f4211e3d74c 100644 --- a/dag/extdeps/exec/program.dag +++ b/dag/extdeps/exec/program.dag @@ -69,6 +69,7 @@ fn cataloged_program(invocation: NonEmptyStr) -> ProgramIdentity decl_ref(module_path: "extdeps.tools.busybox", decl_name: "busybox_dd_sparse_image_command"), decl_ref(module_path: "extdeps.tools.unzip", decl_name: "unzip_program"), decl_ref(module_path: "extdeps.tools.node", decl_name: "node_version_command"), + decl_ref(module_path: "extdeps.tools.node", decl_name: "node_run_file_command"), decl_ref(module_path: "extdeps.tools.node", decl_name: "node_eval_command"), decl_ref(module_path: "extdeps.tools.xorriso", decl_name: "xorriso_program"), decl_ref(module_path: "extdeps.nftables.ruleset", decl_name: "nft_program"), @@ -125,6 +126,7 @@ fn uncataloged_program(invocation: NonEmptyStr) -> ProgramIdentity decl_ref(module_path: "extdeps.languages.bash.invocation", decl_name: "bash_program"), decl_ref(module_path: "extdeps.cargo_build", decl_name: "cargo_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "cat_program"), + decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "nproc_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "cp_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "install_program"), decl_ref(module_path: "extdeps.tools.gnu_coreutils", decl_name: "ln_program"), @@ -185,6 +187,7 @@ fn uncataloged_program(invocation: NonEmptyStr) -> ProgramIdentity decl_ref(module_path: "extdeps.tools.uname", decl_name: "uname_program"), decl_ref(module_path: "extdeps.tools.squashfs_tools", decl_name: "unsquashfs_program"), decl_ref(module_path: "extdeps.tools.util_linux_umount", decl_name: "umount_program"), + decl_ref(module_path: "extdeps.tools.util_linux_flock", decl_name: "flock_program"), decl_ref(module_path: "extdeps.systemd.systemctl", decl_name: "systemctl_path_resolved_program"), decl_ref(module_path: "extdeps.systemd.systemd_run", decl_name: "systemd_run_program"), decl_ref(module_path: "extdeps.ntfy.serve", decl_name: "ntfy_version_command"), diff --git a/dag/extdeps/filesystem/filesystem_rust_realization.dag b/dag/extdeps/filesystem/filesystem_rust_realization.dag index 5f0a5d1b2ec..6f17c72c75b 100644 --- a/dag/extdeps/filesystem/filesystem_rust_realization.dag +++ b/dag/extdeps/filesystem/filesystem_rust_realization.dag @@ -1,5 +1,7 @@ module extdeps.filesystem.rust_realization +import std.integer { int_to_decimal_string } + // THE RUST REALIZATION OF create-new PUBLISH, AS ONE AUTHORITY BOTH CONSUMERS COPY. // // `extdeps.filesystem` owns the INTERFACE shape of a filesystem operation. This module owns one @@ -118,7 +120,7 @@ data rust_file_create_staging_candidate_attempt_limit: Int = 1024 fn rust_file_create_staging_candidate_attempt_limit_def() -> String { concat( "pub const GUNBC_CREATE_STAGING_CANDIDATE_ATTEMPT_LIMIT: u32 = ", - "{rust_file_create_staging_candidate_attempt_limit}", + int_to_decimal_string(value: rust_file_create_staging_candidate_attempt_limit), ";\n", ) } diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index f80cc3e6cfe..dd76e6cf32e 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -1,11 +1,11 @@ module extdeps.firmware.types +import extdeps.board_authority { BoardAuthorityRef } import std.nat { Nat } import std.measure { ByteSize, byte_size } import std.types { Bool, EpochMs, NonEmptyStr, List, list_length } import std.optional { Present, Absent } import extdeps.crypto.hash { Digest } -import extdeps.boards.types { BaseboardModel } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } @@ -105,7 +105,7 @@ type FirmwareImageKind // ON the release row so an unappliable push is refused from the catalog, // not discovered from a vendor shell script after the upload. type FirmwareReleaseRow { - board: BaseboardModel + board: BoardAuthorityRef target: FirmwareTarget version: FirmwareSemanticVersion released_on: EpochMs @@ -168,7 +168,7 @@ type FirmwarePushOutcome // same version applied as an Instant Flash ROM never passes through the BMC // and so has no gating target at all. image_kind is carried for that reason. type FirmwarePrerequisiteObservation { - board: BaseboardModel + board: BoardAuthorityRef subject: FirmwareTarget image_kind: FirmwareImageKind gating_target: FirmwareTarget diff --git a/dag/extdeps/git/gitignore.dag b/dag/extdeps/git/gitignore.dag index a9abf914738..23cc97d1da7 100644 --- a/dag/extdeps/git/gitignore.dag +++ b/dag/extdeps/git/gitignore.dag @@ -29,7 +29,7 @@ type GitignoreFile { fn filter_categories(categories: List, build_system: String) -> List { let by_build = filter(categories, c => match c.when_build_system { Absent => true - Present { value: _ } => c.when_build_system == build_system + Present { value: b } => b == build_system }) filter(by_build, c => c.items |> count > 0) } diff --git a/dag/extdeps/git/versioning.dag b/dag/extdeps/git/versioning.dag index 92dcfb7c44b..58b5b33eca9 100644 --- a/dag/extdeps/git/versioning.dag +++ b/dag/extdeps/git/versioning.dag @@ -3,8 +3,8 @@ module extdeps.git.versioning import std.types { NonEmptyStr, brand } import std.integer { NonNegativeInt } import std.algebra { Ordering, Equal, Greater } -import std.nat { nat_compare } -import extdeps.version { VersionIdentity, VersionScheme, VersionConstraint } +import extdeps.version { VersionIdentity, VersionComparison, VersionCompared, VersionComparisonRefused, VersionScheme, VersionConstraint, ComparisonSide, LeftOperand, RightOperand } +import extdeps.version.semver { semver_core_parse, semver_compare_numeric_digits, semver_label_from_code_points, semver_numeric_field, SemVerNumericField } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -16,9 +16,9 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } type GitReleaseVersion { - major: NonNegativeInt - minor: NonNegativeInt - patch: NonNegativeInt + major: SemVerNumericField + minor: SemVerNumericField + patch: SemVerNumericField } type GitVersionPlatformSuffix @@ -30,11 +30,13 @@ type GitCliReportedVersion { suffix: GitVersionPlatformSuffix } -data git_2_0_release: GitReleaseVersion = { - major: 2, - minor: 0, - patch: 0 -} +// THE ANCHOR IS DERIVED THROUGH THE PARSER, never raw lists: the fields come from parsing +// the literal release text, so the confined constructor's admission is the only producer. +// The anchor IS the parse result — an Optional — so a refused label is a refusal carried to +// every consumer, never a fabricated 0.0.0 standing in for a parse that did not happen +// (review 77150: a failure arm must refuse, never widen). The round-trip witness pins the +// admitted label to "2.0.0" by execution, so a forged or control-character field cannot pass. +data git_2_0_release: GitReleaseVersion? = git_release_version_from_text(text: "2.0.0") data git_release_versioning_spec_url: NonEmptyStr = "https://git-scm.com/docs/BreakingChanges.html" @@ -44,8 +46,8 @@ data min_git_version: GitVersionConstraint = ">= 2.0.0" fn git_release_version_label(v: GitReleaseVersion) -> String { concat( - to_string(v.major), - concat(".", concat(to_string(v.minor), concat(".", to_string(v.patch)))) + semver_label_from_code_points(cps: v.major.digits), + concat(".", concat(semver_label_from_code_points(cps: v.minor.digits), concat(".", semver_label_from_code_points(cps: v.patch.digits)))) ) } @@ -54,17 +56,49 @@ fn git_release_to_version_identity(v: GitReleaseVersion) -> VersionIdentity { } fn git_release_version_compare(a: GitReleaseVersion, b: GitReleaseVersion) -> Ordering { - match nat_compare(a: a.major, b: b.major) { - Equal => match nat_compare(a: a.minor, b: b.minor) { - Equal => nat_compare(a: a.patch, b: b.patch) + match semver_compare_numeric_digits(a: a.major, b: b.major) { + Equal => match semver_compare_numeric_digits(a: a.minor, b: b.minor) { + Equal => semver_compare_numeric_digits(a: a.patch, b: b.patch) other => other } other => other } } -fn git_release_compare_identity(a: VersionIdentity, b: VersionIdentity) -> Ordering { - if a == b { Equal } else { Greater } +// ---- identity compare ----------------------------------------------------------------------- +// The label is a VersionScheme's only handle, so the comparison parses the label back into the +// typed form and routes through git_release_version_compare — the ONE implementation of the cited +// numeric rule for this scheme. The LABEL GRAMMAR is not re-invented here: a git release version +// is a bare MAJOR.MINOR.PATCH triple, which is exactly the SemVer 2.0.0 core, so the parse +// consumes extdeps.version.semver's core parse (three dot-separated digit runs, no leading +// zeroes) and refuses anything else. A label outside that grammar is a located refusal: the +// carrier names the failing side, the label, and the cause. + +fn git_release_version_from_text(text: NonEmptyStr) -> GitReleaseVersion? { + match semver_core_parse(cps: chars(s: text as String)) { + Absent => none + Present { value: v } => + Present { value: GitReleaseVersion { major: v.major, minor: v.minor, patch: v.patch } } + } +} + +fn git_release_identity_parse(identity: VersionIdentity) -> GitReleaseVersion? { + match semver_core_parse(cps: chars(s: identity as String)) { + Absent => none + Present { value: v } => + Present { value: GitReleaseVersion { major: v.major, minor: v.minor, patch: v.patch } } + } +} + +fn git_release_compare_identity(a: VersionIdentity, b: VersionIdentity) -> VersionComparison { + match git_release_identity_parse(identity: a) { + Absent => VersionComparisonRefused { side: LeftOperand, identity: a as String, cause: "label is not a git release version: a bare MAJOR.MINOR.PATCH triple of digit runs without leading zeroes is required" } + Present { value: va } => + match git_release_identity_parse(identity: b) { + Absent => VersionComparisonRefused { side: RightOperand, identity: b as String, cause: "label is not a git release version: a bare MAJOR.MINOR.PATCH triple of digit runs without leading zeroes is required" } + Present { value: vb } => VersionCompared { ordering: git_release_version_compare(a: va, b: vb) } + } + } } data git_release_version_scheme: VersionScheme = VersionScheme { diff --git a/dag/extdeps/github/actions_artifacts.dag b/dag/extdeps/github/actions_artifacts.dag new file mode 100644 index 00000000000..48daa030940 --- /dev/null +++ b/dag/extdeps/github/actions_artifacts.dag @@ -0,0 +1,128 @@ +module extdeps.github.actions_artifacts + +import extdeps.github.github { default_api_base, max_per_page } +import extdeps.github.errors { GitHubErrorShape } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import std.credentials { EnvVar } +import std.types { Bool, Bytes, Int, List, String } +import std.resources { Network } +import std.serialization { Text } +import extdeps.ietf.http_semantics { GET } + +// THE GITHUB ACTIONS ARTIFACTS REST API, as the upstream names it: "List workflow run artifacts" +// (operationId actions/list-workflow-run-artifacts) and "Download an artifact" +// (actions/download-artifact), docs.github.com/en/rest/actions/artifacts, read 2026-10-04. +// +// THE DOWNLOAD IS A REDIRECT TO A ZIP, AND BOTH HALVES ARE UPSTREAM FACTS. GitHub answers +// `GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/{archive_format}` with `302 Found` and a +// `Location` header naming a short-lived storage URL (about one minute), and `410 Gone` once the +// artifact has expired; `archive_format` admits exactly `zip`, and the body at the Location is the +// artifact as a zip archive -- upload-artifact always archives, even a single file. Following the +// redirect is the bound REST realization's work, not this interface's: the seed handler +// (v1_interpreter observe_rest_exchange, over ureq's redirect policy) follows it and decides on the +// final response, so the response block below names the answer at the end of the redirect. +// +// THE ARCHIVE IS Bytes AND NO BOUND REALIZATION CAN YET CARRY IT. The REST handler reads every body +// as UTF-8 text (RestBodyObservation carries body: String), so a zip arrives as a body that could not +// be read and the call answers RestBodyUndecodable with the status it came under. That refusal is the +// honest answer today and it is typed; reading a member out of the archive needs a binary body +// carrier on the REST transport and an inflate reader, which is +// the trigger of gunbc.runner.runner_qualification_dispatch qualification_instrument_extraction_frontier_rows' +// parse_claim_cost_tsv row. Its production caller is that module's read_claim_cost_artifact. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "docs.github.com/en/rest/actions/artifacts" + } +} + +// THE FIELDS A READER OF A RUN'S ARTIFACTS ASKS OF ONE, AND NO OTHERS. The claim-cost read +// (gunbc.runner.runner_qualification_dispatch conclude_claim_cost_listing) finds the artifact by +// name, refuses it when upstream's `expired` flag is set -- a download would answer 410 -- and +// downloads it by id. Upstream's artifact object also carries size_in_bytes, archive_download_url, +// created_at and expires_at; nothing here reads them, so they are not modeled (DESIGN 3c), and a +// size that one day is read arrives as std.measure ByteSize, not a bare count. +type ActionsArtifact { + id: Int + name: String + expired: Bool +} + +type ActionsArtifactList { + total_count: Int + artifacts: List +} + +// `archive_format` IS A PATH SEGMENT WHOSE UPSTREAM VOCABULARY HAS ONE MEMBER, `zip`, so the path +// carries it as the literal upstream spelling rather than as an input. An input would make every +// other spelling representable, and a coproduct input interpolated into a path is rendered by the +// seed with its AUTHORED variant name (the regression extdeps.github.workflow_runs +// WorkflowJobAttemptFilter records), so neither would be more faithful than the literal. +// AUTHENTICATION IS THE CALLER'S TOKEN. Accepting an auth_token operation input does not by itself +// bind authentication; auth_input names that input as the credential, as extdeps.github.git_database +// and github.WorkflowRuns declare, so the controller installation token the qualification collector +// passes is the one the request carries rather than the ambient GITHUB_TOKEN. An empty token keeps +// the env fallback, the same dual declaration github.WorkflowRuns uses. +service github.Artifacts { + config { + endpoint: default_api_base + auth: Bearer + auth_input: auth_token + auth_source: EnvVar { name: "GITHUB_TOKEN" } + } + + operation ListWorkflowRunArtifacts { + requires Network + input { + auth_token: Secret + owner: String + repo: String + run_id: String + per_page: Int = max_per_page + } + output { + result: ActionsArtifactList + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/runs/\{run_id\}/artifacts", + query: { per_page: per_page } + } + response { + 200 => ActionsArtifactList + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } + + operation DownloadArtifact { + requires Network + input { + auth_token: Secret + owner: String + repo: String + artifact_id: String + } + output { + archive: Bytes + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/artifacts/\{artifact_id\}/zip", + response_format: Text + } + response { + 200 => Bytes + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 410 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } +} diff --git a/dag/extdeps/github/git_database.dag b/dag/extdeps/github/git_database.dag index cb0e4f68918..fa0dd1869db 100644 --- a/dag/extdeps/github/git_database.dag +++ b/dag/extdeps/github/git_database.dag @@ -38,6 +38,8 @@ type GitRefObjectWire { sha: String } +type GitRefDeletedBody {} + type GitRefWire { ref: String object: GitRefObjectWire @@ -111,10 +113,21 @@ data git_blob_encoding_base64: NonEmptyStr = "base64" // so no caller of UpdateRefFastForward can express a forced update; a moved branch answers 422 // "Update is not a fast forward" and that refusal IS the compare-and-swap. A forced variant would be // a different operation with its own name and its own consumer. +// CreateRef AND DeleteRef (docs.github.com/en/rest/git/refs#create-a-reference and +// #delete-a-reference, read 2026-10-04). CreateRef takes the FULL ref name (`refs/tags/`) in its +// body and answers 201 with the reference; an existing ref answers 422 "Reference already exists", +// so creation never moves a ref and that 422 is the create-once refusal. DeleteRef takes the +// `tags/` form in its path and answers 204 with no body; a ref already gone answers 422 +// "Reference does not exist". Their consumer is gunbc.runner.runner_qualification_ref_pin (a create-once branch per qualification attempt and its removal). +// +// THE BEARER IS AN INPUT WHEN ONE IS GIVEN. Every operation already took `auth_token`; the service read +// only GITHUB_TOKEN. Declaring both lets a caller that holds an installation token present it, and a +// caller passing "" (gunbc.heal_publication) falls through to GITHUB_TOKEN exactly as before. service github.GitDatabase { config { endpoint: default_api_base auth: Bearer + auth_input: auth_token auth_source: EnvVar { name: "GITHUB_TOKEN" } } @@ -306,4 +319,58 @@ service github.GitDatabase { 5xx => GitHubErrorShape } } + + operation CreateRef { + requires Network + input { + auth_token: Secret + owner: String + repo: String + full_ref: String + sha: String + } + output { + reference: GitRefWire + } + transport rest { + method: POST, + path: "/repos/\{owner\}/\{repo\}/git/refs", + body: { ref: full_ref, sha: sha } + } + response { + 201 => GitRefWire + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 409 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } + + operation DeleteRef { + requires Network + input { + auth_token: Secret + owner: String + repo: String + ref: String + } + output { + result: GitRefDeletedBody + } + transport rest { + method: DELETE, + path: "/repos/\{owner\}/\{repo\}/git/refs/\{ref\}" + } + response { + 204 => GitRefDeletedBody + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 409 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } } diff --git a/dag/extdeps/github/org_actions.dag b/dag/extdeps/github/org_actions.dag index cf0a08e88b1..286880ccdd7 100644 --- a/dag/extdeps/github/org_actions.dag +++ b/dag/extdeps/github/org_actions.dag @@ -130,6 +130,16 @@ type OrgRunnerGroupListWire { data org_runner_groups_max_per_page: Int = 100 +// UpdateOrganizationRunnerGroupWorkflows: +// update a self-hosted runner group for an organization (docs.github.com/en/rest/actions/ +// self-hosted-runner-groups#update-a-self-hosted-runner-group-for-an-organization, REST +// 2022-11-28, read 2026-10-04): PATCH sets restricted_to_workflows and selected_workflows and +// answers 200 with the group. Each selected entry is `//@`; per +// docs.github.com/en/enterprise-cloud@latest/actions/how-tos/manage-runners/self-hosted-runners/ +// manage-access (read 2026-10-04), "Pin non-reusable workflows to a branch. Pin reusable +// workflows to a branch, tag, or full SHA" -- a dispatched (non-reusable) workflow is selectable +// only at a branch, which is why a qualification pin is a branch and not a SHA. Its consumer is +// gunbc.runner.runner_qualification_ref_pin, which writes only the dedicated qualification group. service github.OrgRunnerGroupsRest { config { endpoint: default_api_base @@ -161,4 +171,30 @@ service github.OrgRunnerGroupsRest { 5xx => GitHubErrorShape } } + + operation UpdateOrganizationRunnerGroupWorkflows { + requires Network + input { + installation_token: Secret + org: NonEmptyStr + runner_group_id: Int + selected_workflows: List + } + output { + result: OrgRunnerGroupWire + } + transport rest { + method: PATCH, + path: "/orgs/\{org\}/actions/runner-groups/\{runner_group_id\}", + body: { restricted_to_workflows: true, selected_workflows: selected_workflows } + } + response { + 200 => OrgRunnerGroupWire + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 422 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } } diff --git a/dag/extdeps/github/workflow_runs.dag b/dag/extdeps/github/workflow_runs.dag index 7deaed176de..748a013a95e 100644 --- a/dag/extdeps/github/workflow_runs.dag +++ b/dag/extdeps/github/workflow_runs.dag @@ -15,6 +15,7 @@ import std.dissolution { unbound_dissolution } import std.decl_ref { decl_ref } import std.algebra { Cons, Empty } import std.resources { Network } +import std.serialization { Text } import extdeps.transports.rest { RestResult } // WorkflowJobRun.labels are read by gunbc.public_workload_census (observed_job_labels) to @@ -194,6 +195,12 @@ fn workflow_job_attempt_filter_wire(f: WorkflowJobAttemptFilter) -> String { // rendered name. The parenthetical of combination VALUES in axis order is the rendering this // tree observed on those names; it is not a REST field. Standing is TranscribedUncited until // GitHub's Jobs docs state that encoding. +// runner_id AND runner_name ARE WHICH RUNNER TOOK THE JOB, a third fact beside the declared runs-on +// and the observed labels: two runners can carry identical labels. runner_id is the exact identity +// GitHub returned when the JIT registration was minted (gunbc.runner.runner_qualification_dispatch +// job_ran_on joins on it); runner_name is corroboration, since one name can resolve to several ids. +// Upstream publishes both as nullable -- null until a runner takes the job -- and every record in +// this tree written before the fields existed carries none rather than a guessed value. type WorkflowJobRun { id: Int run_id: Int @@ -202,6 +209,8 @@ type WorkflowJobRun { status: WorkflowRunStatus conclusion: WorkflowRunConclusion? labels: List + runner_id: Int? + runner_name: String? created_at: Timestamp? started_at: Timestamp? completed_at: Timestamp? @@ -287,16 +296,32 @@ data structural_coverage_gap_workflow_run_codec_hand_rolled: List = [ // operation is not a module item, so per-operation rationale is not expressible; it belongs here // or nowhere. // +// DOWNLOADJOBLOGSFORWORKFLOWRUN IS UPSTREAM'S "Download job logs for a workflow run" +// (actions/download-job-logs-for-workflow-run, docs.github.com/en/rest/actions/workflow-jobs, read +// 2026-10-04): `GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs` answers `302 Found` with a +// `Location` naming a storage URL that expires after about a minute, and the body there is the job's +// log as plain text, one line per row behind an RFC 3339 timestamp. The redirect is followed by the +// bound REST realization (the seed's ureq handler), so the response block names the text at the end +// of it. A body the handler cannot read -- not UTF-8, or past its read cap -- is RestBodyUndecodable +// with its status, never a truncated log reported as the whole one. +// // THE FILTER IS A CALLER'S CHOICE AND WAS A LITERAL. "latest" returns only the most recent attempt // of each job, which is the right answer for "what is the current state" and the wrong one for any // census of consumed capacity: a re-run mutates a job's record in place, so the attempts "latest" // drops are execution that really happened and really occupied a runner. A caller measuring // occupancy needs "all"; a caller reading current state needs "latest"; neither is a property of // this operation, so the choice moves to the input with "latest" as the compatible default. +// THE BEARER IS THE CALLER'S auth_token WHEN ONE IS GIVEN. Every operation already took auth_token, +// and the service read only GITHUB_TOKEN, so a caller holding an installation token +// (gunbc.github_effect_perform read_run_with / read_attempt_jobs_with) was silently read under the +// ambient Actions credential. Declaring auth_input beside auth_source makes a non-empty auth_token the +// bearer; a caller passing "" (gunbc.pr_base_freshness, gunbc.fleet_desired_admission, +// gunbc.roadmap_launch_deployment_cli) falls through to GITHUB_TOKEN exactly as before. service github.WorkflowRuns { config { endpoint: default_api_base auth: Bearer + auth_input: auth_token auth_source: EnvVar { name: "GITHUB_TOKEN" } } @@ -415,6 +440,33 @@ service github.WorkflowRuns { 500 => GitHubErrorShape } } + operation DownloadJobLogsForWorkflowRun { + requires Network + input { + auth_token: Secret + owner: String + repo: String + job_id: String + } + output { + log: String + } + readonly + transport rest { + method: GET, + path: "/repos/\{owner\}/\{repo\}/actions/jobs/\{job_id\}/logs", + response_format: Text + } + response { + 200 => String + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 410 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } + operation ListJobs { requires Network input { diff --git a/dag/extdeps/github/workflows.dag b/dag/extdeps/github/workflows.dag index c0de4991178..dd7498019af 100644 --- a/dag/extdeps/github/workflows.dag +++ b/dag/extdeps/github/workflows.dag @@ -3,13 +3,9 @@ module extdeps.github.workflows import extdeps.ietf.http_semantics { POST } import extdeps.github.github { default_api_base } import extdeps.github.errors { GitHubErrorShape } -import std.credentials { EnvVar } -import std.types { CommitSha, List, NonEmptyStr } +import std.types { NonEmptyStr, Secret, String } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } -import std.roster_frontier { FrontierRow, frontier_row_decl } -import std.dissolution { unbound_dissolution } -import std.decl_ref { decl_ref } import std.resources { Network } import extdeps.ietf.http_semantics { POST } import extdeps.transports.rest { RestResult } @@ -42,40 +38,37 @@ type WorkflowDispatchReceipt { // WorkflowDispatchReceipt would fabricate a run that was refused. The owning workflow may be // retriggered after diagnosis; this operation never assumes a git push created a run. // -// FROZEN UPSTREAM SHAPE: tools.ci_heal_dispatch, the only production caller, is deleted. Restoring -// heal auto-dispatch does not consume this operation. `expected_healed_sha` is the `inputs` -// key GitHub accepts for this repository's workflow_dispatch contract; it is not a second heal -// policy and must not grow more heal-specific keys here. Disposition: -// create_dispatch_unconsumed_frontier_rows. - -data create_dispatch_unconsumed_frontier_rows: List = [ - frontier_row_decl( - ref: decl_ref(module_path: "extdeps.github.workflows", decl_name: "WorkflowDispatchReceipt"), - reason: "github.Workflows CreateDispatch and WorkflowDispatchReceipt are the cited REST shape with no production caller after tools.ci_heal_dispatch was deleted; frozen: no new heal-specific keys or auto-invoke rows", - dissolution: unbound_dissolution(description: "a production fold that is not heal auto-dispatch calls github.Workflows CreateDispatch, or CreateDispatch and WorkflowDispatchReceipt are deleted. NOT satisfied by restoring tools.ci_heal_dispatch, nor by a witness constructing WorkflowDispatchReceipt"), - ) -] - +// `inputs` IS THE UPSTREAM'S OWN SHAPE: an object of input name to value, at most 25 properties, +// each key a workflow_dispatch input the target workflow declares (extdeps.github.actions +// DispatchInput). It once carried exactly one key, expected_healed_sha, because the deleted +// tools.ci_heal_dispatch was its only caller -- a consumer's policy fused into the interface +// (DESIGN section 3). Which keys a dispatch carries is the dispatching workflow's fact, so the +// caller supplies the object and this module names no key. GitHub answers an undeclared key with +// 422, which is the refusal arm below, never a silently dropped input. +// +// THE CREDENTIAL IS AN INPUT, NOT AN AMBIENT VARIABLE. It once read GITHUB_TOKEN from the +// environment, which fixed the executing credential to whichever job happened to run the call. +// Which credential performs a dispatch is the caller's authorization fact (gunbc.auth), so the +// operation takes the bearer as auth_token, as the self-hosted-runner and gist services do. service github.Workflows { config { endpoint: default_api_base auth: Bearer - auth_source: EnvVar { name: "GITHUB_TOKEN" } + auth_input: auth_token } operation CreateDispatch { requires Network input { + auth_token: Secret owner: String repo: String workflow_id: String ref: String - expected_healed_sha: CommitSha + inputs: Map } output { - workflow_run_id: Int - run_url: NonEmptyStr - html_url: NonEmptyStr + result: WorkflowDispatchReceipt } transport rest { method: POST, @@ -83,7 +76,7 @@ service github.Workflows { headers: { "X-GitHub-Api-Version": workflows_api_version }, body: { ref: ref, - inputs: { expected_healed_sha: expected_healed_sha } + inputs: inputs } } response { diff --git a/dag/extdeps/http/client.dag b/dag/extdeps/http/client.dag index 30d078bd951..227ac12dc3c 100644 --- a/dag/extdeps/http/client.dag +++ b/dag/extdeps/http/client.dag @@ -96,6 +96,13 @@ fn http_client_get_unix_socket_argv(socket: NonEmptyStr, url: NonEmptyStr, max_t // for endpoints that read application/x-www-form-urlencoded (the OAuth token endpoint among // them), one content-type line differs, and the body still rides --data @{file} so a client // secret in the form body is never an argv word (gunbc.credential_argv_exposure). +// PostJsonFromFileWithHeaderFile IS PostJsonFromFile FOR AN ENDPOINT THAT AUTHENTICATES, with the +// credential carried exactly as GetFollowRedirectsBoundedWithHeaderFile carries it: one header line in +// a caller-owned file read by curl -H @file, so the bearer is never an argv word +// (gunbc.credential_argv_exposure) and the body still rides --data @{file}. --fail-with-body keeps an +// error status's body, which is where a hosted API's typed refusal lives. The response headers are dumped +// to a caller-owned file (curl -D) because a refusal's retry hints (Retry-After, X-RateLimit-*) are headers +// and --fail-with-body returns only the body. // PostStdinWithinUnixSocket, THE SAME BOUNDED POST OVER A UNIX SOCKET (curl --unix-socket, curl(1)): // the URL still names the scheme, authority and path the server routes on, but the connection is // made to the socket file and the kernel attests the caller to the server. The exit codes are @@ -309,6 +316,33 @@ service http.Client { } } + operation PostJsonFromFileWithHeaderFile { + requires Network + input { url: NonEmptyStr, request_body_file: NonEmptyStr, header_file: NonEmptyStr, response_headers_file: NonEmptyStr, max_seconds: NonEmptyStr } + output { body: String from "stdout", success: Bool from "exit_success" } + transport shell { + argv: [ + "curl", + "--fail-with-body", + "-sS", + "--max-time", "{max_seconds}", + "-D", "{response_headers_file}", + "-X", "POST", + "{url}", + "-H", "Content-Type: application/json", + "-H", "@{header_file}", + "--data", "@{request_body_file}", + ] + } + exit { + 0 => Unit + nonzero => String "curl POST JSON from file with header file failed" + } + mock_response { + 0 => { body: "", success: false } "hermetic: no live endpoint; a transport that never connects, so the caller answers unreachable rather than fabricating a completion" + } + } + operation PostFormFromFile { input { url: NonEmptyStr, request_body_file: NonEmptyStr, max_seconds: NonEmptyStr } output { body: String from "stdout", success: Bool from "exit_success" } diff --git a/dag/extdeps/languages/go/module.dag b/dag/extdeps/languages/go/module.dag index fc814b911af..c7b859c4de6 100644 --- a/dag/extdeps/languages/go/module.dag +++ b/dag/extdeps/languages/go/module.dag @@ -2,7 +2,8 @@ module extdeps.languages.go.module import std.types { NonEmptyStr, brand } import std.algebra { Ordering, Less, Equal, Greater } -import extdeps.version { VersionIdentity, VersionScheme } +import extdeps.version { VersionIdentity, VersionComparison, VersionCompared, VersionComparisonRefused, VersionScheme, ComparisonSide, LeftOperand, RightOperand } +import extdeps.version.semver { semver_compare, semver_version_parse, SemVerVersion } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -38,37 +39,42 @@ fn go_semver_to_version_identity(v: GoSemVer) -> VersionIdentity { go_semver_label(v: v) } -fn go_semver_compare(a: GoSemVer, b: GoSemVer) -> Ordering { - let mc = if a.major < b.major { Less } else { if a.major > b.major { Greater } else { Equal } } - let nc = if a.minor < b.minor { Less } else { if a.minor > b.minor { Greater } else { Equal } } - let pc = if a.patch < b.patch { Less } else { if a.patch > b.patch { Greater } else { Equal } } - let pre = match a.prerelease { - Absent => match b.prerelease { - Absent => Equal - Present { value: _bp } => Greater - } - Present { value: _ap } => match b.prerelease { - Absent => Less - Present { value: _bp } => Equal - } +// ---- identity compare ----------------------------------------------------------------------- +// Go module versions are "v" followed by a SemVer 2.0.0 version (the cited authority: +// module versions are semantic versions with a leading "v"), so the scheme strips the prefix and +// reuses the SemVer grammar and semver_compare — the ONE implementation of the precedence rule. +// The former go-local comparator is deleted: it ranked ANY two pre-release versions Equal +// (v1.0.0-alpha == v1.0.0-beta), the exact widening the cited rule forbids, and its parser +// accepted labels SemVer refuses (a leading-zero pre-release identifier, an empty identifier). +// A label outside the grammar (no "v" prefix, or a non-SemVer remainder) is a located refusal: +// the carrier names the failing side, the label, and the cause. + +fn go_module_semver_identity_parse(identity: VersionIdentity) -> SemVerVersion? { + let cps = chars(s: identity as String) + match cps.first() { + Absent => none + Present { value: h } => + if h != 118 { + none + } else { + semver_version_parse(cps: cps.skip(n: 1)) + } } - match mc { - Less => Less - Greater => Greater - Equal => match nc { - Less => Less - Greater => Greater - Equal => match pc { - Less => Less - Greater => Greater - Equal => pre +} + +fn go_module_compare_identity(a: VersionIdentity, b: VersionIdentity) -> VersionComparison { + match go_module_semver_identity_parse(identity: a) { + Absent => VersionComparisonRefused { side: LeftOperand, identity: a as String, cause: "label is not a go module version: a go module version is 'v' followed by a SemVer 2.0.0 version" } + Present { value: va } => + match go_module_semver_identity_parse(identity: b) { + Absent => VersionComparisonRefused { side: RightOperand, identity: b as String, cause: "label is not a go module version: a go module version is 'v' followed by a SemVer 2.0.0 version" } + Present { value: vb } => VersionCompared { ordering: semver_compare(a: va, b: vb) } } - } } } data go_module_version_scheme: VersionScheme = VersionScheme { - compare: (a, b) => if a == b { Equal } else { Less } + compare: go_module_compare_identity } type GoModulePath = NonEmptyStr where brand("GoModulePath") diff --git a/dag/extdeps/languages/rust/emit.dag b/dag/extdeps/languages/rust/emit.dag index 2bffcb57d7d..69034eef1fa 100644 --- a/dag/extdeps/languages/rust/emit.dag +++ b/dag/extdeps/languages/rust/emit.dag @@ -76,8 +76,8 @@ data rust_simple_method_specs: List = [ { method_name: "first", template: "\{recv\}.first().cloned()", wraps_result: false }, { method_name: "enumerate", template: "\{recv\}.iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()", wraps_result: true }, { method_name: "chars", template: "\{recv\}.chars().map(|c| c as i64).collect::>()", wraps_result: true }, - { method_name: "skip", template: "\{recv\}.iter().cloned().skip(\{arg\} as usize).collect::>()", wraps_result: true }, - { method_name: "take", template: "\{recv\}.iter().cloned().take(\{arg\} as usize).collect::>()", wraps_result: true }, + { method_name: "skip", template: "v1_rt::list_skip(&\{recv\}, \{arg\})", wraps_result: true }, + { method_name: "take", template: "v1_rt::list_take(&\{recv\}, \{arg\})", wraps_result: true }, { method_name: "is_empty", template: "\{recv\}.is_empty()", wraps_result: false } ] diff --git a/dag/extdeps/llm/codex_auth.dag b/dag/extdeps/llm/codex_auth.dag index b8762766e94..732f8e5b6b6 100644 --- a/dag/extdeps/llm/codex_auth.dag +++ b/dag/extdeps/llm/codex_auth.dag @@ -251,3 +251,33 @@ fn codex_default_organization(token: CodexIdToken) -> CodexOrganizationMembershi fn codex_organization_ids(token: CodexIdToken) -> List { token.openai_auth.organizations |> map(o => o.id) } + +// WHAT THE SOURCE SAYS, BESIDE WHAT ONE HOST SHOWED. codex_observed_refresh_persistence records an +// observation on codex-cli 0.145.0 and stays as observed; this row records a separate fact, read from +// the CLI's source at a pinned revision, so neither one gets rewritten into the other. At +// github.com/openai/codex 7f892275e31002f0422477c6219189284560e689 (2026-10-04), crate codex-rs/login, +// module auth/manager.rs: AuthManager::auth refreshes when should_refresh_proactively holds, meaning +// the access_token JWT exp falls within CHATGPT_ACCESS_TOKEN_REFRESH_WINDOW_MINUTES (5) of now, or +// last_refresh is older than TOKEN_REFRESH_INTERVAL (8 days). refresh_and_persist_chatgpt_token then +// calls persist_tokens, which writes id_token, access_token, the refresh_token the response returned +// and last_refresh back to the auth storage (CODEX_HOME/auth.json under the default +// AuthCredentialsStoreMode::File). So at this revision the file is rewritten about once an hour +// during use, not only at login.\n\nRotation stays a separate fact. The response's refresh_token is +// an Option, and the client maps the issuer's error code refresh_token_reused to +// RefreshTokenFailedReason::Exhausted, so the issuer detects reuse. The source shows the CLIENT is +// ready for rotation; whether the issuer actually rotates on every exchange is server behaviour, and +// codex_observed_refresh_rotation stays CodexRotationUnobserved until it is observed.\n\nThe source +// also has no cross-process lock on the file: the refresh lock is per process, and the guarded +// reload compares account ids, not a lock token. So two processes refreshing one copy can both +// present the same refresh token. +data codex_source_revision: NonEmptyStr = "github.com/openai/codex@7f892275e31002f0422477c6219189284560e689" as NonEmptyStr +data codex_source_refresh_persistence: CodexRefreshPersistence = CodexPersistsOnRefresh +data codex_source_access_token_refresh_window: Second = second(count: 300) + +// THE auth.json KEYS THE HARNESS READS to decide a write-back, as named by codex-rs/login +// token_data.rs TokenData and auth/storage.rs AuthDotJson. They are wire names of the observed file layout above, not new +// vocabulary. +data codex_auth_file_auth_mode_key: NonEmptyStr = "auth_mode" as NonEmptyStr +data codex_auth_file_tokens_key: NonEmptyStr = "tokens" as NonEmptyStr +data codex_auth_file_refresh_token_key: NonEmptyStr = "refresh_token" as NonEmptyStr +data codex_auth_file_account_id_key: NonEmptyStr = "account_id" as NonEmptyStr diff --git a/dag/extdeps/llm/cursor_cli.dag b/dag/extdeps/llm/cursor_cli.dag index 8d1dad9e086..47cbbe602ec 100644 --- a/dag/extdeps/llm/cursor_cli.dag +++ b/dag/extdeps/llm/cursor_cli.dag @@ -108,8 +108,11 @@ data cursor_model_grok_high_fast: CursorModelId = "cursor-grok-4.5-high-fast" as data cursor_observed_model_roster_size: Int = 193 // DELIBERATELY NOT USED: -w/--worktree starts the agent in an isolated git worktree the CLI creates under ~/.cursor/worktrees//. This repository's belt already creates and owns attempt worktrees under the dashboard instance's own attempts root, with the branch name and attempt identity bound to the roadmap node.\n\nAccepting the CLI's worktree would hand that ownership to the provider: the location would move outside the instance root the model treats as owned, the naming would stop deriving from the attempt, and teardown would belong to a directory nobody declared. The flag is recorded so the choice is visible rather than looking like an oversight — a later reader finding two worktree mechanisms should know only one is ours. -type CursorInvocation { - auth: CursorAuth +// THE RUN AND THE CREDENTIAL ARE TWO FACTS. Everything but the key decides the argv; the key decides +// only where the credential travels. Keeping them apart lets a caller whose key is not in hand (a +// spawn that exports it from a custody file in the child, gunbc.roadmap_dispatch_actuator) build the +// same argv this module builds, rather than spelling cursor-agent's flags a second time. +type CursorRunShape { model: CursorModelId mode: CursorExecutionMode sandbox: CursorSandboxChoice @@ -120,6 +123,11 @@ type CursorInvocation { prompt: NonEmptyStr } +type CursorInvocation { + auth: CursorAuth + run: CursorRunShape +} + // THE AUTH FIELD WAS NEVER READ, SO BOTH ARMS PRODUCED THE SAME PROCESS. shape_cursor_agent_argv // built an argv and returned a List of String, and its accompanying note explained at length why // the key is deliberately absent from that argv — which was true, and which meant a @@ -147,20 +155,20 @@ type CursorProcessInvocation { environment: List } -fn cursor_common_argv(inv: CursorInvocation) -> List { +fn cursor_common_argv(run: CursorRunShape) -> List { concat( [cursor_cli_program, "--print"], concat( - ["--output-format", cursor_output_format_wire(f: inv.output_format)], + ["--output-format", cursor_output_format_wire(f: run.output_format)], concat( - ["--model", inv.model as String], + ["--model", run.model as String], concat( - cursor_mode_args(mode: inv.mode), + cursor_mode_args(mode: run.mode), concat( - cursor_sandbox_args(choice: inv.sandbox), + cursor_sandbox_args(choice: run.sandbox), concat( - if inv.force { ["--force"] } else { [] }, - if inv.trust_workspace { ["--trust"] } else { [] }, + if run.force { ["--force"] } else { [] }, + if run.trust_workspace { ["--trust"] } else { [] }, ), ), ), @@ -169,28 +177,34 @@ fn cursor_common_argv(inv: CursorInvocation) -> List { ) } -fn cursor_tail_argv(inv: CursorInvocation) -> List { +fn cursor_tail_argv(run: CursorRunShape) -> List { concat( - ["--workspace", inv.workspace as String], - [inv.prompt as String], + ["--workspace", run.workspace as String], + [run.prompt as String], ) } +// The argv of a run whose key reaches the process through CURSOR_API_KEY set by someone else: the +// environment arm's argv, with nothing about the key in it. +fn cursor_environment_authenticated_argv(run: CursorRunShape) -> List { + concat(cursor_common_argv(run: run), cursor_tail_argv(run: run)) +} + fn shape_cursor_invocation(inv: CursorInvocation) -> CursorProcessInvocation { match inv.auth { CursorApiKeyArgument { key } => CursorProcessInvocation { program: cursor_cli_program, argv: concat( - cursor_common_argv(inv: inv), - concat(["--api-key", key as String], cursor_tail_argv(inv: inv)), + cursor_common_argv(run: inv.run), + concat(["--api-key", key as String], cursor_tail_argv(run: inv.run)), ), environment: [], } CursorApiKeyEnvironment { key } => CursorProcessInvocation { program: cursor_cli_program, - argv: concat(cursor_common_argv(inv: inv), cursor_tail_argv(inv: inv)), + argv: cursor_environment_authenticated_argv(run: inv.run), environment: [ CursorEnvBinding { name: cursor_api_key_env_var, value: key }, ], @@ -212,14 +226,16 @@ fn cursor_automation_invocation( ) -> CursorInvocation { CursorInvocation { auth: CursorApiKeyEnvironment { key: key }, - model: model, - mode: mode, - sandbox: sandbox, - output_format: output_format, - force: force, - trust_workspace: trust_workspace, - workspace: workspace, - prompt: prompt, + run: CursorRunShape { + model: model, + mode: mode, + sandbox: sandbox, + output_format: output_format, + force: force, + trust_workspace: trust_workspace, + workspace: workspace, + prompt: prompt, + }, } } diff --git a/dag/extdeps/nvidia/api_trial_terms.dag b/dag/extdeps/nvidia/api_trial_terms.dag new file mode 100644 index 00000000000..b6a13ddbdd7 --- /dev/null +++ b/dag/extdeps/nvidia/api_trial_terms.dag @@ -0,0 +1,99 @@ +module extdeps.nvidia.api_trial_terms + +import std.types { NonEmptyStr, List } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } + +// THE TERMS THAT GOVERN AN NVIDIA-HOSTED API SERVICE, as their own upstream because they are an +// independently versioned document (DESIGN section 3, external upstream decomposition). They are not +// a property of a model -- the Nemotron 3 Ultra weights are licensed under OpenMDW-1.1, a different +// document with different obligations -- and not of a router that forwards to the service: OpenRouter +// links this PDF as the terms of service of its Nvidia endpoint for the free slug, and the obligations +// below are NVIDIA's whoever relays the request. +// +// READ 2026-10-05 from the PDF itself; the document's own last line is "v. September 19, 2025". Every +// obligation row below cites the section that states it, and the section text was read from that +// version, not transcribed from a summary. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf", + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.nvidia.api_trial_terms", + decl_name: "nvidia_api_trial_terms_v20250919", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [], +} + +type NvidiaApiTrialTermsVersion20250919 {} + +type NvidiaApiTrialTermsVersion = NvidiaApiTrialTermsVersion20250919 + +// WHAT THE TERMS OBLIGE OF A USER, one arm per obligation a caller's conduct can breach, each named for +// the section that states it. A closed set rather than prose because a consumer deciding whether a use +// is admissible must be able to ask about each obligation separately; a paragraph cannot be asked. +// +// - 1.2 Trial Access Rights: access "for limited trial purposes only and without use of the API +// Service or Generated Content in production"; 1.4 repeats the trial limit for credits. +// - 2.6(a): User Content may not include "any confidential information, controlled or sensitive +// data, including protected health information, personal data"; 4.3 forbids uploading personal, +// financial, health or governmental information. +// - 3.3(iv): NVIDIA collects "User Content and Generated Content to improve NVIDIA products and +// services, including AI models". This is a disclosure rather than a user duty, carried because a +// caller choosing what to send needs it as much as the prohibitions. +// - 4.2: no copying, selling, sublicensing, transferring or distributing the service or Generated +// Content to others. +// - 4.12: no use of the service or Generated Content "to develop or improve products or services +// that compete with the API Service". +type NvidiaApiTrialObligation + = TrialPurposeNoProduction + | NoConfidentialOrPersonalData + | ContentUsedToImproveNvidiaProducts + | NoRedistributionOfServiceOrOutput + | NoCompetingProductDevelopment + +fn nvidia_api_trial_obligation_section(o: NvidiaApiTrialObligation) -> NonEmptyStr { + match o { + TrialPurposeNoProduction => "1.2, 1.4" as NonEmptyStr + NoConfidentialOrPersonalData => "2.6(a), 4.3" as NonEmptyStr + ContentUsedToImproveNvidiaProducts => "3.3(iv)" as NonEmptyStr + NoRedistributionOfServiceOrOutput => "4.2" as NonEmptyStr + NoCompetingProductDevelopment => "4.12" as NonEmptyStr + } +} + +type NvidiaApiTrialTerms { + version: NvidiaApiTrialTermsVersion + version_label: NonEmptyStr + obligations: List + terms: ExternalAuthority +} + +data nvidia_api_trial_terms_v20250919: NvidiaApiTrialTerms = NvidiaApiTrialTerms { + version: NvidiaApiTrialTermsVersion20250919 {}, + version_label: "2025-09-19" as NonEmptyStr, + obligations: [ + TrialPurposeNoProduction, + NoConfidentialOrPersonalData, + ContentUsedToImproveNvidiaProducts, + NoRedistributionOfServiceOrOutput, + NoCompetingProductDevelopment, + ], + terms: extdeps_external_authority_anchor, +} + +fn nvidia_api_trial_terms_wire(t: NvidiaApiTrialTerms) -> NonEmptyStr { + join([ + "NVIDIA API Trial Terms of Service v. ", t.version_label as String, " (", + join(map(t.obligations, o => nvidia_api_trial_obligation_section(o: o) as String), "; "), ")", + ], "") as NonEmptyStr +} diff --git a/dag/extdeps/nvidia/nemotron_3_ultra.dag b/dag/extdeps/nvidia/nemotron_3_ultra.dag new file mode 100644 index 00000000000..c6bc8a62ad5 --- /dev/null +++ b/dag/extdeps/nvidia/nemotron_3_ultra.dag @@ -0,0 +1,46 @@ +module extdeps.nvidia.nemotron_3_ultra + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } + +// NVIDIA NEMOTRON 3 ULTRA, as its own extdeps authority for the reason extdeps.deepseek.deepseek_v4 and +// extdeps.zhipu.glm_5_3_flash are: an independently versioned upstream model is not a property of +// whatever serves it. What a HOSTED endpoint of this model admits -- its window, its output ceiling, the +// reasoning efforts its router accepts -- is the endpoint's fact and lives with the router +// (extdeps.openrouter.openrouter); what the endpoint's operator requires of its users lives with those +// terms (extdeps.nvidia.api_trial_terms). This module carries only what the model card says about the +// model. +// +// READ 2026-10-05 from the model card at the anchor: 550B total / 55B active parameters, a hybrid +// Transformer-Mamba mixture-of-experts, text in and text out, weights under OpenMDW-1.1. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16", + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.nvidia.nemotron_3_ultra", + decl_name: "nemotron_ultra_thinking_end_token", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [], +} + +// THE CARD'S REASONING MODES ARE NOT MODELLED HERE, AND THE ABSENCE IS DELIBERATE. The card names three -- +// off and full via chat_template_kwargs enable_thinking, and "Medium-effort reasoning" via medium_effort, +// which "uses significantly fewer reasoning tokens than full thinking mode" -- but those keys address a +// chat template, and the only route this repository has to the model is a router that takes its own +// reasoning parameter and translates it unseen. Nothing here can send them, so a type for them would be +// a declaration with no consumer. A self-hosted Nemotron unit is the consumer that would add it. + +// THE END-OF-THINKING CLOSER. The card's budget client splits a completion on "" and appends it +// when a capped trace never produced one, so it is the model's own delimiter. +data nemotron_ultra_thinking_end_token: NonEmptyStr = "" as NonEmptyStr diff --git a/dag/extdeps/ocp/mt_jade/platform.dag b/dag/extdeps/ocp/mt_jade/platform.dag index d09442f52c0..6a9f66ccc24 100644 --- a/dag/extdeps/ocp/mt_jade/platform.dag +++ b/dag/extdeps/ocp/mt_jade/platform.dag @@ -33,7 +33,7 @@ import extdeps.ocp.mt_jade.subject { data extdeps_external_authority_anchor: ExternalAuthority = mt_jade_subject_authority_anchor -// This module does not widen extdeps.boards.types BaseboardModel. Mt. Jade identity lives here +// This module does not widen any generic board enum (extdeps.board_authority carries references, not products). Mt. Jade identity lives here // and in extdeps.ocp.mt_jade.subject; Wiwynn SV328R is a separate product. Adding a board must // not edit a generic product enum (same standing as extdeps.ocp.mt_mitchell.platform). diff --git a/dag/extdeps/ocp/mt_jade/subject.dag b/dag/extdeps/ocp/mt_jade/subject.dag index 568ef866d9b..0f02d79d5e1 100644 --- a/dag/extdeps/ocp/mt_jade/subject.dag +++ b/dag/extdeps/ocp/mt_jade/subject.dag @@ -5,6 +5,7 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } import extdeps.vendor { Vendor } +import extdeps.board_authority { BoardAuthorityRef } import extdeps.hardware { Hardware } import extdeps.vendor.ampere { ampere_vendor } import extdeps.publication { SpecificationDocumentDate } @@ -99,6 +100,16 @@ data mt_jade_reference_platform: MtJadeReferencePlatform = MtJadeReferencePlatfo excluded_appendices: mt_jade_excluded_non_normative_appendices, } +// THE BOARD-AUTHORITY REFERENCE to this subject, exported so a binding row names the subject by +// importing it rather than spelling a module path. +data mt_jade_board_authority: BoardAuthorityRef = BoardAuthorityRef { + subject: DeclarationRef { + module_path: "extdeps.ocp.mt_jade.subject", + decl_name: "mt_jade_reference_platform", + field: WholeDeclaration + } +} + data mt_jade_model_scope: ExternalModelScope = ExternalModelScope { subject: ExternalSubjectRef { declaration: DeclarationRef { diff --git a/dag/extdeps/ocp/mt_mitchell/platform.dag b/dag/extdeps/ocp/mt_mitchell/platform.dag index 1f7786f8a41..0ecda166c13 100644 --- a/dag/extdeps/ocp/mt_mitchell/platform.dag +++ b/dag/extdeps/ocp/mt_mitchell/platform.dag @@ -22,7 +22,7 @@ import extdeps.ocp.mt_mitchell.subject { data extdeps_external_authority_anchor: ExternalAuthority = mt_mitchell_subject_authority_anchor -data platform_subject_edge_note: String = "THE SPECIFICATION IDENTITY IS NOT RE-DECLARED HERE. extdeps.ocp.mt_mitchell.subject owns the marketing name, revision, version label, contributor and licence rows; this module carries the board facts the specification states and binds them to that declaration through the subject edge below — the extdeps.ocp.mt_jade.platform shape. Mt. Mitchell and the Ampere Altra Platform Hardware Design Specification stay separate authorities for the reason DESIGN section 3 gives: OCP publishes and licenses this document under the Open Web Foundation agreements with its own revision series and contributor population, Ampere publishes the other under a proprietary marking, and neither may require editing the other's module. Nothing here dispatches between them and neither module imports the other; a consumer that needs both joins them downstream. This module likewise does not widen extdeps.boards.types BaseboardModel — adding a board must not edit a generic product enum." +data platform_subject_edge_note: String = "THE SPECIFICATION IDENTITY IS NOT RE-DECLARED HERE. extdeps.ocp.mt_mitchell.subject owns the marketing name, revision, version label, contributor and licence rows; this module carries the board facts the specification states and binds them to that declaration through the subject edge below — the extdeps.ocp.mt_jade.platform shape. Mt. Mitchell and the Ampere Altra Platform Hardware Design Specification stay separate authorities for the reason DESIGN section 3 gives: OCP publishes and licenses this document under the Open Web Foundation agreements with its own revision series and contributor population, Ampere publishes the other under a proprietary marking, and neither may require editing the other's module. Nothing here dispatches between them and neither module imports the other; a consumer that needs both joins them downstream. This module likewise does not widen any generic board enum — adding a board must not edit a generic product enum." data mt_mitchell_platform_subject: MtMitchellReferencePlatform = mt_mitchell_reference_platform diff --git a/dag/extdeps/openrouter/nvidia_nemotron_3_ultra_free.dag b/dag/extdeps/openrouter/nvidia_nemotron_3_ultra_free.dag new file mode 100644 index 00000000000..17bec64cc62 --- /dev/null +++ b/dag/extdeps/openrouter/nvidia_nemotron_3_ultra_free.dag @@ -0,0 +1,77 @@ +module extdeps.openrouter.nvidia_nemotron_3_ultra_free + +import std.types { NonEmptyStr, Bool, List } +import std.measure { TokenCount, token_count, MoneyAmountMicro, money_amount_micro } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { + ExternalAuthority, ExternalModelScope, ExternalSubjectRef, +} +import extdeps.uri { Uri, Https } +import extdeps.openrouter.openrouter { OpenRouterReasoningEffort, EffortHigh, EffortMedium } +import extdeps.nvidia.api_trial_terms { NvidiaApiTrialTerms, nvidia_api_trial_terms_v20250919 } + +// OPENROUTER'S LISTING OF NEMOTRON 3 ULTRA'S FREE VARIANT. What a listing admits is OpenRouter's fact +// about its endpoint, not NVIDIA's about the model (extdeps.nvidia.nemotron_3_ultra), so it is a row in +// OpenRouter's module family; a second listing is a second file, never an edit to this one. +// +// READ 2026-10-05 from the endpoints read at the anchor and from GET /api/v1/models: one endpoint, +// provider Nvidia, upstream snapshot nvidia/nemotron-3-ultra-550b-a55b-20260604:free, context_length +// 1000000, max_completion_tokens 65536, pricing prompt 0 / completion 0, supported_parameters reasoning, +// include_reasoning, temperature, max_tokens, seed, top_p, tools, tool_choice, reasoning_effort; and on +// the models listing, reasoning {mandatory: false, default_enabled: true, supported_efforts: ["high", +// "medium"], default_effort: "high"}. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "openrouter.ai/api/v1/models/nvidia/nemotron-3-ultra-550b-a55b:free/endpoints", + } +} + +data openrouter_models_listing_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "openrouter.ai/api/v1/models", + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.openrouter.nvidia_nemotron_3_ultra_free", + decl_name: "nemotron_ultra_free_model_id", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [openrouter_models_listing_authority], +} + +data nemotron_ultra_free_model_id: NonEmptyStr = "nvidia/nemotron-3-ultra-550b-a55b:free" as NonEmptyStr + +// THE ENDPOINT'S WINDOW AND OUTPUT CEILING. Both are the endpoint's admission, which is why they are here +// and not on the model: the paid listing of the same model publishes 262144 and 16384. +data nemotron_ultra_free_context_length: TokenCount = token_count(count: 1000000) + +data nemotron_ultra_free_max_completion_tokens: TokenCount = token_count(count: 65536) + +// THE EFFORTS THIS LISTING ACCEPTS, transcribed from supported_efforts in the listing's own order +// (highest first), and the two facts beside them a request needs: reasoning is not mandatory, so +// `reasoning.enabled: false` is a request the listing admits, and the default effort is high. +data nemotron_ultra_free_supported_efforts: List = [EffortHigh, EffortMedium] + +data nemotron_ultra_free_reasoning_mandatory: Bool = false + +// THE PRICE IS ZERO PER TOKEN (prompt and completion both read "0" USD) AND THE LIMIT IS PER REQUEST, which is what makes reasoning depth cost +// neither money nor quota on this listing: a deeper effort spends latency and completion budget only. +data nemotron_ultra_free_price_per_token: MoneyAmountMicro = money_amount_micro(count: 0) + +// WHICH MODEL MODE EACH ACCEPTED EFFORT REACHES IS NOT STATED, AND NOTHING HERE ASSERTS IT. The two +// efforts coincide with the card's two thinking modes (full and medium), and "high reaches full" is the +// obvious reading -- but neither OpenRouter nor NVIDIA publishes the translation, so it is a bet (DESIGN +// section 4d) and no row consumes it. What IS published is the ORDER: supported_efforts is "returned in +// descending effort order (highest first)" (the reasoning-tokens guide), so a consumer ranking depth reads +// the position in this list, which is cited, rather than a mode, which is not. + +// THE TERMS THE ENDPOINT IS GOVERNED BY. OpenRouter links NVIDIA's API Trial Terms as this provider's +// terms of service; the obligations are NVIDIA's (extdeps.nvidia.api_trial_terms). +data nemotron_ultra_free_terms: NvidiaApiTrialTerms = nvidia_api_trial_terms_v20250919 diff --git a/dag/extdeps/openrouter/openrouter.dag b/dag/extdeps/openrouter/openrouter.dag new file mode 100644 index 00000000000..6ab8e3954af --- /dev/null +++ b/dag/extdeps/openrouter/openrouter.dag @@ -0,0 +1,630 @@ +module extdeps.openrouter.openrouter + +import std.types { String, Bool, Int, List, NonEmptyStr } +import std.algebra { trim } +import std.nat { Nat, nat_range_inclusive } +import std.measure { Second, second, second_count } +import std.checked_arithmetic { checked_int_to_nat } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import extdeps.uri { Uri, Https } +import extdeps.api_rate_limit { UpstreamRateLimit, PerMinute, PerDay } +import extdeps.languages.json.emit { JsonValue, JsonObject, JsonNumber, JsonNull, JsonBool, JsonString, JsonArray } +import extdeps.languages.json.parse { + JsonDocumentParsed, JsonDocumentUnreadable, parse_json_document, json_document_gap_text, + JsonFieldRead, FieldAbsent, FieldRead, FieldMalformed, json_field, json_field_string, json_field_int, json_field_bool, +} + +// OPENROUTER, as its own extdeps authority: an independently governed router that relays an +// OpenAI-compatible chat-completions request to one of several providers. It is not OpenAI (its +// errors, limits, reasoning parameter and key surface are its own), so it does not live in +// extdeps.llm.openai_rest, and it is not any provider it relays to. A model OpenRouter lists is a row in +// its own module beside this one (extdeps.openrouter.nvidia_nemotron_3_ultra_free), so a second listing +// is a new file rather than an edit here. +// +// READ 2026-10-05: the API reference overview (the anchor), the limits page and the errors page. The +// facts below cite which. +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "openrouter.ai/docs/api-reference/overview", + } +} + +data openrouter_limits_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "openrouter.ai/docs/api-reference/limits", + } +} + +data openrouter_reasoning_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https, + locator: "openrouter.ai/docs/guides/best-practices/reasoning-tokens", + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.openrouter.openrouter", + decl_name: "openrouter_chat_completions_url", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [openrouter_limits_authority, openrouter_reasoning_authority], +} + +// THE TWO ENDPOINTS THIS REPOSITORY CALLS. Chat completions is OpenAI-compatible in shape; the key read +// answers what the presented key may still spend. Both authenticate with `Authorization: Bearer `, +// and both are HTTPS -- there is no plain-http door. +data openrouter_chat_completions_url: NonEmptyStr = "https://openrouter.ai/api/v1/chat/completions" as NonEmptyStr + +data openrouter_key_url: NonEmptyStr = "https://openrouter.ai/api/v1/key" as NonEmptyStr + +fn openrouter_authorization_header_line(key: NonEmptyStr) -> String { + join(["Authorization: Bearer ", key as String, "\n"], "") +} + +// ── THE FREE-MODEL LIMITS, FROM THE LIMITS PAGE ─────────────────────────────────────────────── +// +// Free variants (model ids ending in ":free") are limited to 20 requests per minute, and per day to 50 +// when the account has purchased fewer than 10 credits all-time or 1000 when it has purchased at least +// 10. Both limits bind at once, so a caller leases BOTH pools before each request. +// +// THE DAILY TIER IS SELECTED BY A TYPED FACT, NOT CHOSEN AS A LITERAL. Which row applies is a property +// of the account, and the account says which: the key read below returns free_model_daily_requests.limit, +// so the fact is DERIVED from what OpenRouter reports for the presented key rather than asserted beside +// it. Both tiers share one scope and therefore one quota partition, because they are one pool on the +// upstream whose ceiling moves with the account -- two scopes would let a caller spend both. +type OpenRouterCreditsPurchased + = CreditsPurchasedBelowTen + | CreditsPurchasedAtLeastTen + +data openrouter_free_models_per_minute: UpstreamRateLimit = UpstreamRateLimit { + upstream: "openrouter" as NonEmptyStr, + scope: "free-models-minute" as NonEmptyStr, + requests: 20, + period: PerMinute, +} + +fn openrouter_free_models_per_day(credits: OpenRouterCreditsPurchased) -> UpstreamRateLimit { + UpstreamRateLimit { + upstream: "openrouter" as NonEmptyStr, + scope: "free-models-day" as NonEmptyStr, + requests: match credits { + CreditsPurchasedBelowTen => 50 + CreditsPurchasedAtLeastTen => 1000 + }, + period: PerDay, + } +} + +// THE TIER IS READ OFF THE DAILY LIMIT THE KEY REPORTS, and a limit that is neither published tier is a +// refusal rather than the nearer one: a third tier is a change to the limits page this module has not +// read, and guessing it would lease against a ceiling nobody published. +type OpenRouterCreditsReading + = CreditsTierObserved { credits: OpenRouterCreditsPurchased } + | CreditsTierUnrecognized { daily_limit: Int } + +fn openrouter_credits_from_daily_limit(daily_limit: Int) -> OpenRouterCreditsReading { + if daily_limit == 1000 { + CreditsTierObserved { credits: CreditsPurchasedAtLeastTen } + } else if daily_limit == 50 { + CreditsTierObserved { credits: CreditsPurchasedBelowTen } + } else { + CreditsTierUnrecognized { daily_limit: daily_limit } + } +} + +// ── THE KEY READ ───────────────────────────────────────────────────────────────────────────── +// +// GET /api/v1/key answers data.{limit, limit_remaining, usage, is_free_tier, free_model_daily_requests +// {used, limit, remaining}}. Only the members a consumer reads are decoded: whether the key is accepted +// at all (a 200 that decodes), the key's own credit limit, and the free-model daily counters the tier +// is derived from. +// +// THE KEY'S CREDIT LIMIT IS A DIFFERENT FACT FROM THE ACCOUNT'S TIER. data.limit is a per-KEY spending +// cap set on the key (null when the key has none); the daily free-model ceiling is per ACCOUNT, set by +// credits purchased. A key capped at zero credits can spend nothing on a priced model and is still +// metered by the account's free tier, which is exactly the guard a free-only caller wants: a request +// that reaches a priced model answers 402 from openrouter_key_limit instead of charging anything. +// +// ZERO IS READ OFF THE LEXEME, NOT A FLOAT. The wire carries a JSON number ("0", "0.0"); a lexeme whose +// digits are all zero is zero in every notation JSON admits for it, and anything else is a positive cap +// a free-only caller must refuse. +type OpenRouterKeySpendLimit + = KeySpendUncapped + | KeySpendCappedAtZero + | KeySpendCappedAbove { lexeme: String } + +fn openrouter_lexeme_is_zero(lexeme: String) -> Bool { + lexeme != "" && trim_zero_digits(s: lexeme) == "" +} + +fn trim_zero_digits(s: String) -> String { + replace(s: replace(s: replace(s: replace(s: replace(s: replace(s: s, from: "0", to: ""), from: ".", to: ""), from: "-", to: ""), from: "+", to: ""), from: "e", to: ""), from: "E", to: "") +} + +fn openrouter_decode_key_spend_limit(key_data: JsonValue) -> OpenRouterKeySpendLimit? { + match json_field(obj: key_data, key: "limit") { + FieldAbsent => Present { value: KeySpendUncapped } + FieldMalformed { cause: _ } => none + FieldRead { value: v } => + match v { + JsonNumber { lexeme: lx } => + if openrouter_lexeme_is_zero(lexeme: lx) { Present { value: KeySpendCappedAtZero } } else { Present { value: KeySpendCappedAbove { lexeme: lx } } } + JsonNull => Present { value: KeySpendUncapped } + JsonBool { value: _ } => none + JsonString { value: _ } => none + JsonArray { elements: _ } => none + JsonObject { members: _ } => none + } + } +} + +fn openrouter_key_spend_limit_wire(l: OpenRouterKeySpendLimit) -> String { + match l { + KeySpendUncapped => "no key credit limit" + KeySpendCappedAtZero => "key credit limit 0" + KeySpendCappedAbove { lexeme: lx } => join(["key credit limit ", lx], "") + } +} + +type OpenRouterKeyRead { + is_free_tier: Bool + spend_limit: OpenRouterKeySpendLimit + free_daily_used: Int + free_daily_limit: Int + free_daily_remaining: Int +} + +type OpenRouterKeyDecode + = OpenRouterKeyDecoded { key: OpenRouterKeyRead } + | OpenRouterKeyUndecodable { cause: String } + +fn openrouter_int_member(obj: JsonValue, key: String, subject: String) -> JsonFieldRead { + match json_field_int(obj: obj, key: key) { + FieldAbsent => FieldMalformed { cause: join([subject, " carries no ", key], "") } + FieldMalformed { cause: c } => FieldMalformed { cause: c } + FieldRead { value: n } => FieldRead { value: n } + } +} + +fn openrouter_decode_key_daily(key_data: JsonValue, is_free_tier: Bool, spend_limit: OpenRouterKeySpendLimit) -> OpenRouterKeyDecode { + match json_field(obj: key_data, key: "free_model_daily_requests") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "the key read carries no free_model_daily_requests" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: daily } => + match openrouter_int_member(obj: daily, key: "used", subject: "free_model_daily_requests") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "free_model_daily_requests carries no used" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: used } => + match openrouter_int_member(obj: daily, key: "limit", subject: "free_model_daily_requests") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "free_model_daily_requests carries no limit" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: limit } => + match openrouter_int_member(obj: daily, key: "remaining", subject: "free_model_daily_requests") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "free_model_daily_requests carries no remaining" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: remaining } => + OpenRouterKeyDecoded { key: OpenRouterKeyRead { is_free_tier: is_free_tier, spend_limit: spend_limit, free_daily_used: used, free_daily_limit: limit, free_daily_remaining: remaining } } + } + } + } + } +} + +fn openrouter_decode_key_read(body: String) -> OpenRouterKeyDecode { + match parse_json_document(s: body) { + JsonDocumentUnreadable { gap: g } => OpenRouterKeyUndecodable { cause: join(["the key read is not a JSON document: ", json_document_gap_text(gap: g)], "") } + JsonDocumentParsed { value: doc } => + match json_field(obj: doc, key: "data") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "the key read carries no data member" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: key_data } => + match json_field_bool(obj: key_data, key: "is_free_tier") { + FieldAbsent => OpenRouterKeyUndecodable { cause: "the key read carries no is_free_tier" } + FieldMalformed { cause: c } => OpenRouterKeyUndecodable { cause: c } + FieldRead { value: free } => + match openrouter_decode_key_spend_limit(key_data: key_data) { + Absent => OpenRouterKeyUndecodable { cause: "the key read's limit member is not a number or null" } + Present { value: spend } => openrouter_decode_key_daily(key_data: key_data, is_free_tier: free, spend_limit: spend) + } + } + } + } +} + +// ── THE ERROR BODY, FROM THE ERRORS AND LIMITS PAGES ────────────────────────────────────────── +// +// Every error is {error: {code, message, metadata?}}. Two codes carry metadata a caller must branch on: +// +// - 402: metadata.limit_source names WHICH budget refused -- openrouter_in_flight_budget, +// openrouter_key_limit or openrouter_credits -- and metadata.reason refines the in-flight case as +// in_flight_budget_exhausted or weight_exceeds_budget. metadata.remedy_hint is documented as +// log-only, so it is carried as text and never branched on. Free models are exempt from the +// in-flight budget, but a negative balance still answers 402 from openrouter_credits. +// - 429: metadata.error_type is rate_limit_exceeded, with provider_code when the limit was the +// provider's rather than OpenRouter's own. +// +// THE ENUMS ARE CLOSED AND AN UNKNOWN SPELLING REFUSES. A fourth limit_source decoded as the nearest of +// three would send a caller to repair the wrong budget, which is the whole reason the field exists. +type OpenRouterLimitSource + = InFlightBudgetLimit + | KeySpendLimit + | CreditsLimit + +fn openrouter_limit_source_wire(s: OpenRouterLimitSource) -> NonEmptyStr { + match s { + InFlightBudgetLimit => "openrouter_in_flight_budget" as NonEmptyStr + KeySpendLimit => "openrouter_key_limit" as NonEmptyStr + CreditsLimit => "openrouter_credits" as NonEmptyStr + } +} + +type OpenRouterLimitReason + = InFlightBudgetExhausted + | WeightExceedsBudget + +fn openrouter_limit_reason_wire(r: OpenRouterLimitReason) -> NonEmptyStr { + match r { + InFlightBudgetExhausted => "in_flight_budget_exhausted" as NonEmptyStr + WeightExceedsBudget => "weight_exceeds_budget" as NonEmptyStr + } +} + +type OpenRouterError + = OpenRouterPaymentRequired { limit_source: OpenRouterLimitSource, reason: OpenRouterLimitReason?, message: String, remedy_hint: String? } + | OpenRouterRateLimited { message: String, provider_code: String? } + | OpenRouterOtherError { code: Int, message: String } + +type OpenRouterErrorDecode + = OpenRouterErrorDecoded { error: OpenRouterError } + | OpenRouterErrorUndecodable { cause: String } + +type OpenRouterLimitSourceRead + = LimitSourceRead { source: OpenRouterLimitSource } + | LimitSourceUnrecognized { wire: String } + +fn openrouter_limit_source_from_wire(wire: String) -> OpenRouterLimitSourceRead { + if wire == "openrouter_in_flight_budget" { + LimitSourceRead { source: InFlightBudgetLimit } + } else if wire == "openrouter_key_limit" { + LimitSourceRead { source: KeySpendLimit } + } else if wire == "openrouter_credits" { + LimitSourceRead { source: CreditsLimit } + } else { + LimitSourceUnrecognized { wire: wire } + } +} + +type OpenRouterLimitReasonRead + = LimitReasonAbsent + | LimitReasonRead { reason: OpenRouterLimitReason } + | LimitReasonUnrecognized { wire: String } + +fn openrouter_limit_reason_from_field(field: JsonFieldRead) -> OpenRouterLimitReasonRead { + match field { + FieldAbsent => LimitReasonAbsent + FieldMalformed { cause: c } => LimitReasonUnrecognized { wire: c } + FieldRead { value: w } => + if w == "in_flight_budget_exhausted" { + LimitReasonRead { reason: InFlightBudgetExhausted } + } else if w == "weight_exceeds_budget" { + LimitReasonRead { reason: WeightExceedsBudget } + } else { + LimitReasonUnrecognized { wire: w } + } + } +} + +fn openrouter_optional_string(field: JsonFieldRead) -> String? { + match field { + FieldRead { value: s } => Present { value: s } + FieldAbsent => none + FieldMalformed { cause: _ } => none + } +} + +fn openrouter_decode_402(message: String, metadata: JsonValue) -> OpenRouterErrorDecode { + match json_field_string(obj: metadata, key: "limit_source") { + FieldAbsent => OpenRouterErrorUndecodable { cause: "a 402 whose metadata carries no limit_source names no budget to repair" } + FieldMalformed { cause: c } => OpenRouterErrorUndecodable { cause: join(["the 402 limit_source is unreadable: ", c], "") } + FieldRead { value: source_wire } => + match openrouter_limit_source_from_wire(wire: source_wire) { + LimitSourceUnrecognized { wire: w } => OpenRouterErrorUndecodable { cause: join(["the 402 names an unpublished limit_source '", w, "'"], "") } + LimitSourceRead { source: source } => + match openrouter_limit_reason_from_field(field: json_field_string(obj: metadata, key: "reason")) { + LimitReasonUnrecognized { wire: w } => OpenRouterErrorUndecodable { cause: join(["the 402 names an unpublished reason '", w, "'"], "") } + LimitReasonAbsent => + OpenRouterErrorDecoded { error: OpenRouterPaymentRequired { limit_source: source, reason: none, message: message, remedy_hint: openrouter_optional_string(field: json_field_string(obj: metadata, key: "remedy_hint")) } } + LimitReasonRead { reason: r } => + OpenRouterErrorDecoded { error: OpenRouterPaymentRequired { limit_source: source, reason: Present { value: r }, message: message, remedy_hint: openrouter_optional_string(field: json_field_string(obj: metadata, key: "remedy_hint")) } } + } + } + } +} + +fn openrouter_decode_error_fields(error: JsonValue) -> OpenRouterErrorDecode { + match json_field_int(obj: error, key: "code") { + FieldAbsent => OpenRouterErrorUndecodable { cause: "the error object carries no code" } + FieldMalformed { cause: c } => OpenRouterErrorUndecodable { cause: c } + FieldRead { value: code } => { + let message = match json_field_string(obj: error, key: "message") { + FieldRead { value: m } => m + FieldAbsent => "" + FieldMalformed { cause: c } => c + } + let metadata = match json_field(obj: error, key: "metadata") { + FieldRead { value: m } => m + FieldAbsent => JsonObject { members: [] } + FieldMalformed { cause: _ } => JsonObject { members: [] } + } + if code == 402 { + openrouter_decode_402(message: message, metadata: metadata) + } else if code == 429 { + OpenRouterErrorDecoded { error: OpenRouterRateLimited { message: message, provider_code: openrouter_optional_string(field: json_field_string(obj: metadata, key: "provider_code")) } } + } else { + OpenRouterErrorDecoded { error: OpenRouterOtherError { code: code, message: message } } + } + } + } +} + +fn openrouter_decode_error_body(body: String) -> OpenRouterErrorDecode { + match parse_json_document(s: body) { + JsonDocumentUnreadable { gap: g } => OpenRouterErrorUndecodable { cause: join(["the error body is not a JSON document: ", json_document_gap_text(gap: g)], "") } + JsonDocumentParsed { value: doc } => + match json_field(obj: doc, key: "error") { + FieldAbsent => OpenRouterErrorUndecodable { cause: "the body carries no error member" } + FieldMalformed { cause: c } => OpenRouterErrorUndecodable { cause: c } + FieldRead { value: e } => openrouter_decode_error_fields(error: e) + } + } +} + +// WHAT A 402'S SOURCE MEANS FOR A FREE-ONLY KEY. openrouter_key_limit on a key capped at zero credits +// is not a budget to top up: it says the request reached a PRICED model, which a free-only caller must +// refuse and never retry -- retrying sends the same paid request again. The other two sources keep +// their published meaning. +fn openrouter_limit_source_meaning(s: OpenRouterLimitSource) -> String { + match s { + KeySpendLimit => "a priced model was requested through a key whose credit limit forbids spending (never retried); " + InFlightBudgetLimit => "" + CreditsLimit => "" + } +} + +// ── A 429 IS A TYPED REFUSAL THAT CARRIES THE UPSTREAM'S OWN RETRY HINTS ────────────────────────── +// +// The limits page says an error OpenRouter itself returns for a platform limit carries X-RateLimit-Limit, +// X-RateLimit-Remaining and X-RateLimit-Reset, and that Retry-After is added when every attempted provider +// returned a retry hint. The headers are the upstream's word about when it will answer again, so they ride +// the refusal instead of being dropped with the transport. +// +// WHAT IS NOT PUBLISHED IS NOT ASSERTED (DESIGN section 4d). The page gives neither the unit of +// X-RateLimit-Reset nor says Retry-After is seconds rather than an HTTP date (RFC 9110 allows both). So +// Retry-After is read as delta-seconds only when it is all digits and otherwise carried as the text it +// was; and the reset value is carried VERBATIM, never converted to an instant. A consumer that waits +// computes its wait from Retry-After alone; the reset is evidence for the operator, with a live reading +// the thing that would ground its unit. +type OpenRouterRetryAfter + = RetryAfterSeconds { seconds: Second } + | RetryAfterUnparsed { text: String } + | RetryAfterAbsent + | RetryAfterUnread { cause: String } + +// THE HEADER DUMP IS READ OR IT IS NOT, AND THE TWO ARE DIFFERENT FACTS. A dump that was read and carries no +// Retry-After says the upstream gave no hint; a dump that could not be read says nothing about whether it did. +// Reading a failed read as "no hint" would invent an absence the caller then acts on, so the failure is carried. +type OpenRouterHeaderDump + = HeaderDumpRead { text: String } + | HeaderDumpUnread { cause: String } + +type OpenRouterRateLimitHints { + retry_after: OpenRouterRetryAfter + reset: String? + limit: String? + remaining: String? +} + +// A header dump is the status line and headers of each response curl saw, in order; only the LAST block +// describes the answer the caller got, so a status line starts a fresh reading. +type HeaderScan { + retry_after: String? + reset: String? + limit: String? + remaining: String? +} + +// HTTP FIELD NAMES ARE CASE-INSENSITIVE (RFC 9110 section 5.1): HTTP/2 sends them lowercase, HTTP/1.1 may +// not, and a dump is read the same either way. Folded over ASCII letters only. +data openrouter_ascii_upper: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +data openrouter_ascii_lower: String = "abcdefghijklmnopqrstuvwxyz" + +fn openrouter_ascii_fold(s: String) -> String { + fold(nat_range_inclusive(lo: 0, hi: 25), init: s, f: (acc, i) => + replace(acc, substring(s: openrouter_ascii_upper, start: i, end: i + 1), substring(s: openrouter_ascii_lower, start: i, end: i + 1))) +} + +fn openrouter_header_scan_line(acc: HeaderScan, line: String) -> HeaderScan { + let t = trim(s: line) + if openrouter_ascii_fold(s: t).starts_with("http/") { + HeaderScan { retry_after: none, reset: none, limit: none, remaining: none } + } else { + match split(s: t, delimiter: ":").first() { + Absent => acc + Present { value: raw_name } => { + let name = openrouter_ascii_fold(s: trim(s: raw_name)) + let value = if length(t) > length(raw_name) { trim(s: substring(s: t, start: length(raw_name) + 1, end: length(t))) } else { "" } + if length(t) <= length(raw_name) { + acc + } else if name == "retry-after" { + HeaderScan { retry_after: Present { value: value }, reset: acc.reset, limit: acc.limit, remaining: acc.remaining } + } else if name == "x-ratelimit-reset" { + HeaderScan { retry_after: acc.retry_after, reset: Present { value: value }, limit: acc.limit, remaining: acc.remaining } + } else if name == "x-ratelimit-limit" { + HeaderScan { retry_after: acc.retry_after, reset: acc.reset, limit: Present { value: value }, remaining: acc.remaining } + } else if name == "x-ratelimit-remaining" { + HeaderScan { retry_after: acc.retry_after, reset: acc.reset, limit: acc.limit, remaining: Present { value: value } } + } else { + acc + } + } + } + } +} + +// HTTP delay-seconds IS 1*DIGIT (RFC 9110 section 10.2.3): no sign, no fraction, no unit. The integer parser +// below accepts a leading '+' or '-' (so '+30' and '-0' would read as numbers), so the lexical shape is +// checked first and a value that is not all ASCII digits is carried as the text it was. +fn openrouter_is_ascii_digits(text: String) -> Bool { + length(text) > 0 && length(fold(nat_range_inclusive(lo: 0, hi: 9), init: text, f: (acc, i) => replace(acc, to_string(i), ""))) == 0 +} + +fn openrouter_retry_after_from_text(text: String) -> OpenRouterRetryAfter { + if !openrouter_is_ascii_digits(text: text) { + RetryAfterUnparsed { text: text } + } else { + openrouter_retry_after_from_digits(text: text) + } +} + +fn openrouter_retry_after_from_digits(text: String) -> OpenRouterRetryAfter { + match parse_int(s: text) { + Absent => RetryAfterUnparsed { text: text } + Present { value: n } => + match checked_int_to_nat(n: n) { + Absent => RetryAfterUnparsed { text: text } + Present { value: sec } => RetryAfterSeconds { seconds: second(count: sec) } + } + } +} + +fn openrouter_rate_limit_hints(dump: OpenRouterHeaderDump) -> OpenRouterRateLimitHints { + match dump { + HeaderDumpUnread { cause: c } => OpenRouterRateLimitHints { retry_after: RetryAfterUnread { cause: c }, reset: none, limit: none, remaining: none } + HeaderDumpRead { text: t } => openrouter_rate_limit_hints_of_text(dump: t) + } +} + +fn openrouter_rate_limit_hints_of_text(dump: String) -> OpenRouterRateLimitHints { + let scan = fold(split(s: dump, delimiter: "\n"), init: HeaderScan { retry_after: none, reset: none, limit: none, remaining: none }, f: (acc, line) => + openrouter_header_scan_line(acc: acc, line: line)) + OpenRouterRateLimitHints { + retry_after: match scan.retry_after { Absent => RetryAfterAbsent Present { value: t } => openrouter_retry_after_from_text(text: t) }, + reset: scan.reset, + limit: scan.limit, + remaining: scan.remaining, + } +} + +// THE REFUSAL ITSELF: an OpenRouter 429 with its hints. It is never retried by the code that decodes it; +// carrying the hint is how a caller that CHOOSES to wait learns how long. +type OpenRouterRateLimitRefusal { + message: String + provider_code: String? + hints: OpenRouterRateLimitHints +} + +fn openrouter_decode_rate_limited(body: String, header_dump: OpenRouterHeaderDump) -> OpenRouterRateLimitRefusal? { + match openrouter_decode_error_body(body: body) { + OpenRouterErrorUndecodable { cause: _ } => none + OpenRouterErrorDecoded { error: e } => + match e { + OpenRouterRateLimited { message: m, provider_code: p } => + Present { value: OpenRouterRateLimitRefusal { message: m, provider_code: p, hints: openrouter_rate_limit_hints(dump: header_dump) } } + OpenRouterPaymentRequired { limit_source: _, reason: _, message: _, remedy_hint: _ } => none + OpenRouterOtherError { code: _, message: _ } => none + } + } +} + +fn openrouter_retry_after_wire(r: OpenRouterRetryAfter) -> String { + match r { + RetryAfterSeconds { seconds: n } => join(["Retry-After ", to_string(second_count(s: n)), " s"], "") + RetryAfterUnparsed { text: t } => join(["Retry-After '", t, "' (not delta-seconds; not interpreted)"], "") + RetryAfterAbsent => "no Retry-After" + RetryAfterUnread { cause: c } => join(["Retry-After unknown: the response headers could not be read (", c, ")"], "") + } +} + +fn openrouter_rate_limit_wire(r: OpenRouterRateLimitRefusal) -> String { + join([ + "OpenRouter answered 429 rate_limit_exceeded", + match r.provider_code { Present { value: pc } => join([" from provider ", pc], "") Absent => " from OpenRouter" }, + ": ", r.message, "; ", openrouter_retry_after_wire(r: r.hints.retry_after), + match r.hints.reset { Present { value: v } => join(["; X-RateLimit-Reset=", v, " (unit not documented; carried verbatim)"], "") Absent => "" }, + match r.hints.remaining { Present { value: v } => join(["; X-RateLimit-Remaining=", v], "") Absent => "" }, + "; the request was NOT retried", + ], "") +} + +fn openrouter_error_wire(e: OpenRouterError) -> String { + match e { + OpenRouterPaymentRequired { limit_source: s, reason: r, message: m, remedy_hint: _ } => + join([ + openrouter_limit_source_meaning(s: s), + "402 from ", openrouter_limit_source_wire(s: s) as String, + match r { Present { value: rv } => join([" (", openrouter_limit_reason_wire(r: rv) as String, ")"], "") Absent => "" }, + ": ", m, + ], "") + OpenRouterRateLimited { message: m, provider_code: p } => + join(["429 rate_limit_exceeded", match p { Present { value: pc } => join([" from provider ", pc], "") Absent => " from OpenRouter" }, ": ", m], "") + OpenRouterOtherError { code: c, message: m } => join([to_string(c), ": ", m], "") + } +} + +// ── THE REASONING PARAMETER, FROM THE REASONING-TOKENS GUIDE ───────────────────────────────── +// +// reasoning.effort is one of max, xhigh, high, medium, low, minimal or none; reasoning.enabled false +// turns reasoning off where the model does not make it mandatory. Which efforts a given model honours +// is published per model in GET /api/v1/models as reasoning.supported_efforts -- so the vocabulary is +// OpenRouter's and the admitted subset is each listing's row, never this module's guess. +type OpenRouterReasoningEffort + = EffortMax + | EffortXhigh + | EffortHigh + | EffortMedium + | EffortLow + | EffortMinimal + | EffortNone + +fn openrouter_reasoning_effort_wire(e: OpenRouterReasoningEffort) -> NonEmptyStr { + match e { + EffortMax => "max" as NonEmptyStr + EffortXhigh => "xhigh" as NonEmptyStr + EffortHigh => "high" as NonEmptyStr + EffortMedium => "medium" as NonEmptyStr + EffortLow => "low" as NonEmptyStr + EffortMinimal => "minimal" as NonEmptyStr + EffortNone => "none" as NonEmptyStr + } +} + +fn openrouter_effort_eq(a: OpenRouterReasoningEffort, b: OpenRouterReasoningEffort) -> Bool { + (openrouter_reasoning_effort_wire(e: a) as String) == (openrouter_reasoning_effort_wire(e: b) as String) +} + +// AN EFFORT OUTSIDE THE LISTING'S SUPPORTED SET IS REFUSED, NOT SENT. OpenRouter's own guide shows what +// an unsupported effort becomes on another provider -- Gemini maps xhigh down to high -- so sending it +// is not an error the wire reports, it is a different setting the caller never sees. +type OpenRouterEffortAdmission + = EffortAdmitted { effort: OpenRouterReasoningEffort } + | EffortNotSupported { effort: OpenRouterReasoningEffort, supported: List } + +fn openrouter_effort_admitted(effort: OpenRouterReasoningEffort, supported: List) -> OpenRouterEffortAdmission { + if count(filter(supported, s => openrouter_effort_eq(a: s, b: effort))) > 0 { + EffortAdmitted { effort: effort } + } else { + EffortNotSupported { effort: effort, supported: supported } + } +} + +// THE MESSAGE MEMBER REASONING RIDES ON. The guide: "Reasoning tokens will appear in the reasoning field +// of each message", and the same field is how prior reasoning is passed back. +data openrouter_message_reasoning_field: NonEmptyStr = "reasoning" as NonEmptyStr diff --git a/dag/extdeps/posix/identity.dag b/dag/extdeps/posix/identity.dag index 660656bd2a7..efe5c6ea048 100644 --- a/dag/extdeps/posix/identity.dag +++ b/dag/extdeps/posix/identity.dag @@ -5,7 +5,8 @@ import extdeps.external_authority { ExternalAuthority, ExternalModelScope, Exter import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.uri { Uri, Https } import std.optional { Optional, Present, Absent } -import v2.std.integer { integer_lexeme_to_int_optional, integer_nat_to_decimal_string } +import v2.std.integer { integer_lexeme_to_int_optional } +import std.nat { nat_to_decimal_string } import std.algebra { trim } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -100,11 +101,11 @@ fn posix_group_id_from_text(text: String) -> Optional { } fn posix_user_id_text(user: PosixUserId) -> String { - integer_nat_to_decimal_string(value: user.value) + nat_to_decimal_string(value: user.value) } fn posix_group_id_text(group: PosixGroupId) -> String { - integer_nat_to_decimal_string(value: group.value) + nat_to_decimal_string(value: group.value) } // THE ONE SPELLING OF chown(1)'s OWNER[:GROUP] SEPARATOR. Both forms below -- numeric ids and names diff --git a/dag/extdeps/posix/signal.dag b/dag/extdeps/posix/signal.dag index 68ad9509ad7..a0b29185b73 100644 --- a/dag/extdeps/posix/signal.dag +++ b/dag/extdeps/posix/signal.dag @@ -1,6 +1,6 @@ module extdeps.posix.signal -import std.types { NonEmptyStr, String, Bool } +import std.types { NonEmptyStr, String, Bool, Int } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.uri { Uri, Https } @@ -48,3 +48,122 @@ service posix.Signal { } } } + +// THE LINUX SIGNAL NUMBERS FOR THE STANDARD SIGNALS, by the name systemd reports them under +// ($EXIT_STATUS when $EXIT_CODE is killed or dumped: the name without its SIG prefix). signal(7), +// "Standard signals": the numbering below is the one shared by x86, ARM and most other Linux +// architectures (the table's middle column); alpha, sparc, mips and parisc number some of these +// differently, and no host this repository runs on is one of them. A name not listed is refused +// with Absent rather than guessed -- including the realtime signals, which have no fixed number. +data linux_signal_numbers_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "man7.org/linux/man-pages/man7/signal.7.html" + } +} + +// The closed set of names, so a misspelled or unlisted signal has no constructor; the one string +// parse is linux_standard_signal_of_name, at the systemd $EXIT_STATUS boundary. +type LinuxStandardSignal + = SignalHup + | SignalInt + | SignalQuit + | SignalIll + | SignalTrap + | SignalAbrt + | SignalBus + | SignalFpe + | SignalKill + | SignalUsr1 + | SignalSegv + | SignalUsr2 + | SignalPipe + | SignalAlrm + | SignalTerm + | SignalStkflt + | SignalChld + | SignalCont + | SignalStop + | SignalTstp + | SignalTtin + | SignalTtou + | SignalUrg + | SignalXcpu + | SignalXfsz + | SignalVtalrm + | SignalProf + | SignalWinch + | SignalIo + | SignalPwr + | SignalSys + +fn linux_standard_signal_of_name(name: String) -> LinuxStandardSignal? { + if name == "HUP" { Present { value: SignalHup } } + else if name == "INT" { Present { value: SignalInt } } + else if name == "QUIT" { Present { value: SignalQuit } } + else if name == "ILL" { Present { value: SignalIll } } + else if name == "TRAP" { Present { value: SignalTrap } } + else if name == "ABRT" { Present { value: SignalAbrt } } + else if name == "BUS" { Present { value: SignalBus } } + else if name == "FPE" { Present { value: SignalFpe } } + else if name == "KILL" { Present { value: SignalKill } } + else if name == "USR1" { Present { value: SignalUsr1 } } + else if name == "SEGV" { Present { value: SignalSegv } } + else if name == "USR2" { Present { value: SignalUsr2 } } + else if name == "PIPE" { Present { value: SignalPipe } } + else if name == "ALRM" { Present { value: SignalAlrm } } + else if name == "TERM" { Present { value: SignalTerm } } + else if name == "STKFLT" { Present { value: SignalStkflt } } + else if name == "CHLD" { Present { value: SignalChld } } + else if name == "CONT" { Present { value: SignalCont } } + else if name == "STOP" { Present { value: SignalStop } } + else if name == "TSTP" { Present { value: SignalTstp } } + else if name == "TTIN" { Present { value: SignalTtin } } + else if name == "TTOU" { Present { value: SignalTtou } } + else if name == "URG" { Present { value: SignalUrg } } + else if name == "XCPU" { Present { value: SignalXcpu } } + else if name == "XFSZ" { Present { value: SignalXfsz } } + else if name == "VTALRM" { Present { value: SignalVtalrm } } + else if name == "PROF" { Present { value: SignalProf } } + else if name == "WINCH" { Present { value: SignalWinch } } + else if name == "IO" { Present { value: SignalIo } } + else if name == "PWR" { Present { value: SignalPwr } } + else if name == "SYS" { Present { value: SignalSys } } + else { none } +} + +fn linux_standard_signal_number(signal: LinuxStandardSignal) -> Int { + match signal { + SignalHup => 1 + SignalInt => 2 + SignalQuit => 3 + SignalIll => 4 + SignalTrap => 5 + SignalAbrt => 6 + SignalBus => 7 + SignalFpe => 8 + SignalKill => 9 + SignalUsr1 => 10 + SignalSegv => 11 + SignalUsr2 => 12 + SignalPipe => 13 + SignalAlrm => 14 + SignalTerm => 15 + SignalStkflt => 16 + SignalChld => 17 + SignalCont => 18 + SignalStop => 19 + SignalTstp => 20 + SignalTtin => 21 + SignalTtou => 22 + SignalUrg => 23 + SignalXcpu => 24 + SignalXfsz => 25 + SignalVtalrm => 26 + SignalProf => 27 + SignalWinch => 28 + SignalIo => 29 + SignalPwr => 30 + SignalSys => 31 + } +} diff --git a/dag/extdeps/posix/test_utility.dag b/dag/extdeps/posix/test_utility.dag index 5a13b74b18d..79bbf36e0bb 100644 --- a/dag/extdeps/posix/test_utility.dag +++ b/dag/extdeps/posix/test_utility.dag @@ -60,6 +60,11 @@ fn test_executable_command(path: String) -> ArgvCommand { argv_command(program: test_program(), arguments: ["-x", path]) } +// -d IS "EXISTS AND IS A DIRECTORY" (POSIX test, OPERANDS -d). +fn test_directory_command(path: String) -> ArgvCommand { + argv_command(program: test_program(), arguments: ["-d", path]) +} + // WHETHER A PATH IS THERE, OR WHY THAT COULD NOT BE DECIDED -- ONE DECLARATION, AT THE BOUNDARY THAT // OWNS THE UTILITY. // diff --git a/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag b/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag index 8858a8e3c4e..3dbbf320283 100644 --- a/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag +++ b/dag/extdeps/provisioning/ubuntu_seeded_install_media.dag @@ -1,6 +1,6 @@ module extdeps.provisioning.ubuntu_seeded_install_media -import std.types { Int, List, NonEmptyStr, String } +import std.types { Bool, Int, List, NonEmptyStr, String, path_segment_is_safe } import std.content_hash { Fnv1a64Structural, content_hash_atom, @@ -12,6 +12,7 @@ import std.content_hash { import extdeps.provisioning.ubuntu_install_media { UbuntuInstallMediaArtifactRow } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import extdeps.crypto.hash { Digest, Sha256, Sha512 } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -28,6 +29,77 @@ type NoCloudSeedFile { content: NonEmptyStr } +// A BINARY SEED MEMBER, NAMED BY ITS CONTENT DIGEST. A NoCloudSeedFile is text the caller renders; +// an artifact is bytes someone else publishes, so the input carries where they are fetched from and +// the Digest (algorithm and hex, extdeps.crypto.hash) they must have, and the build fetches them into the seed directory and refuses unless +// the readback digest equals it. The digest is in the build key and the URL is not: the URL is +// transport, and two URLs serving the same bytes build the same image. +type NoCloudSeedArtifact { + name: NonEmptyStr + source_url: NonEmptyStr + digest: Digest +} + +// AN ARTIFACT NAME IS ADMITTED BEFORE ANY BYTE MOVES, because the remaster writes it under the seed +// directory and a name is a path: `../../boot/grub/grub.cfg` would land outside the seed, and +// `user-data` or a seed file's own name would replace a member the input separately claims the image +// carries -- a correct digest only proves the unintended bytes arrived. So the fetch takes an +// AdmittedSeedArtifact, and the one constructor of its basename is the admission below: a single +// path segment (std.types path_segment_is_safe, the one law for that), none of the NoCloud datasource's own file names, +// no collision with a text seed file, and no duplicate among the artifacts. +type SeedArtifactBasename sole_constructor { + value: NonEmptyStr +} + +type AdmittedSeedArtifact sole_constructor { + basename: SeedArtifactBasename + source_url: NonEmptyStr + digest: Digest +} + +type SeedArtifactAdmissionRefusal + = SeedArtifactNameNotABasename { name: NonEmptyStr } + | SeedArtifactNameReserved { name: NonEmptyStr } + | SeedArtifactNameCollidesWithSeedFile { name: NonEmptyStr } + | SeedArtifactNameDuplicated { name: NonEmptyStr } + +type SeedArtifactAdmission + = SeedArtifactsAdmitted { artifacts: List } + | SeedArtifactsRefused { refusals: List } + +// The NoCloud datasource reads these names from the seed directory (cloud-init NoCloud: user-data, +// meta-data, vendor-data, network-config); an artifact may not occupy one. +data nocloud_seed_reserved_names: List = ["user-data", "meta-data", "vendor-data", "network-config"] + +fn seed_artifact_name_refusals(input: UbuntuSeededInstallMediaBuildInput, artifact: NoCloudSeedArtifact) -> List { + let name = artifact.name as String + let shape = if path_segment_is_safe(raw: name) { [] } else { [SeedArtifactNameNotABasename { name: artifact.name }] } + let reserved = if any(nocloud_seed_reserved_names, r => r == name) { [SeedArtifactNameReserved { name: artifact.name }] } else { [] } + let collides = if any(input.seed_files, f => (f.name as String) == name) { [SeedArtifactNameCollidesWithSeedFile { name: artifact.name }] } else { [] } + let duplicated = if count(filter(input.seed_artifacts, a => (a.name as String) == name)) > 1 { [SeedArtifactNameDuplicated { name: artifact.name }] } else { [] } + append(append(append(shape, items: reserved), items: collides), items: duplicated) +} + +fn seeded_install_media_seed_artifact_admission(input: UbuntuSeededInstallMediaBuildInput) -> SeedArtifactAdmission { + let refusals = flat_map(input.seed_artifacts, a => seed_artifact_name_refusals(input: input, artifact: a)) + if count(refusals) == 0 { + SeedArtifactsAdmitted { + artifacts: map(input.seed_artifacts, a => AdmittedSeedArtifact { basename: SeedArtifactBasename { value: a.name }, source_url: a.source_url, digest: a.digest }), + } + } else { + SeedArtifactsRefused { refusals: refusals } + } +} + +fn seed_artifact_admission_refusal_text(refusal: SeedArtifactAdmissionRefusal) -> String { + match refusal { + SeedArtifactNameNotABasename { name: n } => concat("seed artifact name ", concat(n as String, " is not a single path component")) + SeedArtifactNameReserved { name: n } => concat("seed artifact name ", concat(n as String, " is a NoCloud datasource file")) + SeedArtifactNameCollidesWithSeedFile { name: n } => concat("seed artifact name ", concat(n as String, " is already a text seed file")) + SeedArtifactNameDuplicated { name: n } => concat("seed artifact name ", concat(n as String, " is named by more than one artifact")) + } +} + // WHAT A DERIVED SEEDED IMAGE IS BUILT FROM, and nothing else: every field below reaches the output // bytes, so each one is in the derivation key. The payload is a PARAMETER -- an install autoinstall // (srv3) and a live-session census program (Mt. Collins) are both a rendered user-data body plus seed @@ -39,6 +111,7 @@ type UbuntuSeededInstallMediaBuildInput { nocloud_dir_on_iso: NonEmptyStr user_data_body: NonEmptyStr seed_files: List + seed_artifacts: List grub_kernel_cmdline: NonEmptyStr volume_id: NonEmptyStr pinned_dates: SeededInstallMediaPinnedDates @@ -98,8 +171,29 @@ fn seeded_install_media_seed_file_key(file: NoCloudSeedFile) -> Fnv1a64Structura ) } +fn seeded_install_media_seed_artifact_key(artifact: NoCloudSeedArtifact) -> Fnv1a64Structural { + content_hash_combine_structural( + content_hash_atom(value: artifact.name), + content_hash_combine_structural( + content_hash_atom(value: seed_artifact_digest_algorithm_token(digest: artifact.digest)), + content_hash_atom(value: artifact.digest.hex), + ), + ) +} + +// THE ALGORITHM IS PART OF THE PIN, so it is part of the key: the same hex under another algorithm is +// another artifact. A closed match over extdeps.crypto.hash HashAlgorithm, no fallback arm. +fn seed_artifact_digest_algorithm_token(digest: Digest) -> NonEmptyStr { + match digest.algorithm { + Sha256 => "sha256" + Sha512 => "sha512" + } +} + +// An input with no artifacts folds to the key it had before artifacts existed, so every image built +// without one keeps its key and its published digest. fn seeded_install_media_build_key(input: UbuntuSeededInstallMediaBuildInput) -> NonEmptyStr { - let seed = fold( + let text_seed = fold( input.seed_files, init: content_hash_combine_structural( content_hash_atom(value: input.nocloud_dir_on_iso), @@ -107,6 +201,11 @@ fn seeded_install_media_build_key(input: UbuntuSeededInstallMediaBuildInput) -> ), f: (acc, file) => content_hash_combine_structural(acc, seeded_install_media_seed_file_key(file: file)), ) + let seed = fold( + input.seed_artifacts, + init: text_seed, + f: (acc, artifact) => content_hash_combine_structural(acc, seeded_install_media_seed_artifact_key(artifact: artifact)), + ) let boot = content_hash_combine_structural( content_hash_combine_structural( content_hash_atom(value: input.grub_kernel_cmdline), diff --git a/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag b/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag index 133e5b6fbae..d47d898aa7a 100644 --- a/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag +++ b/dag/extdeps/provisioning/ubuntu_seeded_install_media_remaster.dag @@ -6,13 +6,17 @@ import extdeps.tools.xorriso { xorriso_extract_tree_command, xorriso_replay_buil import extdeps.tools.gnu_coreutils { mkdir_parents_command, mktemp_directory_command, rm_recursive_force_one_file_system_command } import extdeps.tools.chmod { chmod_recursive_set_mode_command, chmod_path_resolved_program } import extdeps.posix.test_utility { test_regular_file_command } -import extdeps.crypto.hash { sha256sum_file_command } +import extdeps.crypto.hash { Digest, Sha256, Sha512, sha256sum_file_command } +import extdeps.tools.curl { curl_download_command } import extdeps.provisioning.ubuntu_install_media { UbuntuInstallMediaGrubConfig, ubuntu_install_media_grub_cfg_for } import extdeps.provisioning.ubuntu_seeded_install_media { NoCloudSeedFile, + AdmittedSeedArtifact, SeedArtifactsAdmitted, SeedArtifactsRefused, + seeded_install_media_seed_artifact_admission, seed_artifact_admission_refusal_text, UbuntuSeededInstallMediaBuildInput, } import std.types { Bool, Int, List, String, NonEmptyStr } +import std.decl_ref { decl_ref } import std.algebra { trim } import std.content_hash { sha256_hex_digest } import std.optional { Present } @@ -114,7 +118,7 @@ fn install_media_remaster_seed_directory_levels(nocloud_dir_on_iso: NonEmptyStr) // The seed files as the image holds them (grub.cfg is the other mapped file). fn install_media_remaster_seed_files(input: UbuntuSeededInstallMediaBuildInput) -> List { let seed_root = concat("/", input.nocloud_dir_on_iso as String) - concat([concat(seed_root, "/user-data") as NonEmptyStr, concat(seed_root, "/meta-data") as NonEmptyStr], map(input.seed_files, f => concat(seed_root, "/", f.name as String) as NonEmptyStr)) + concat([concat(seed_root, "/user-data") as NonEmptyStr, concat(seed_root, "/meta-data") as NonEmptyStr], concat(map(input.seed_files, f => concat(seed_root, "/", f.name as String) as NonEmptyStr), map(input.seed_artifacts, a => concat(seed_root, "/", a.name as String) as NonEmptyStr))) } // Owner and group are pinned recursively from the TOPMOST directory the seed creates -- for a nested @@ -253,12 +257,64 @@ fn install_media_remaster_write_seeds(host: CommandHost, writes: List }) } -fn install_media_remaster_write_nocloud_seed(host: CommandHost, work_dir: NonEmptyStr, input: UbuntuSeededInstallMediaBuildInput) -> String? { - let nocloud_dir = concat(work_dir as String, "/", input.nocloud_dir_on_iso as String) as NonEmptyStr - match host_command(host: host, command: mkdir_parents_command(path: nocloud_dir as String)) { +// EACH ARTIFACT IS FETCHED, THEN READ BACK: the fetch's success is not the bytes' identity, so the +// member is admitted only when its measured sha256 equals the one the input names. The first +// artifact that does not hold ends the seed. +// THE READBACK IS sha256sum'S, so it holds only against a Sha256 pin with that hex: a Sha512 pin can +// never be satisfied by it and refuses rather than being compared by its hex alone. +fn seed_artifact_readback_holds(read_back_sha256_hex: String, pin: Digest) -> Bool { + match pin.algorithm { + Sha256 => read_back_sha256_hex == (pin.hex as String) + Sha512 => false + } +} + +fn install_media_remaster_fetch_artifact(host: CommandHost, nocloud_dir: NonEmptyStr, artifact: AdmittedSeedArtifact) -> String? admit_callers: [ + decl_ref(module_path: "extdeps.provisioning.ubuntu_seeded_install_media_remaster", decl_name: "install_media_remaster_fetch_artifacts"), +] { + let path = concat(nocloud_dir as String, "/", artifact.basename.value as String) as NonEmptyStr + match host_command(host: host, command: curl_download_command(url: artifact.source_url as String, destination: path as String)) { HostCommandDone { stdout: _ } => - install_media_remaster_write_seeds(host: host, writes: install_media_remaster_seed_writes(nocloud_dir: nocloud_dir, input: input), scope_root: work_dir) - other => Present { value: concat("mkdir -p ", nocloud_dir as String, ": ", host_command_outcome_text(e: other)) } + match install_media_remaster_digest(host: host, path: path) { + Absent => Present { value: install_media_remaster_digest_refusal(host: host, path: path) } + Present { value: d } => + if seed_artifact_readback_holds(read_back_sha256_hex: d, pin: artifact.digest) { none } else { Present { value: concat("seed artifact ", artifact.basename.value as String, " read back sha256 ", d, " but the input names ", artifact.digest.hex as String) } } + } + other => Present { value: concat("fetching seed artifact ", artifact.basename.value as String, ": ", host_command_outcome_text(e: other)) } + } +} + +// THE FETCH IS SEALED TO THE CHECKED ROUTE. An AdmittedSeedArtifact was admitted against ONE input's +// seed population, and the destination is that input's seed directory under the remaster's work +// directory; a caller that could hand either helper a different directory could replay an +// admission against a seed it was never checked against (a name refused there as a collision would +// become a write) or point a valid basename outside the seed. So the only caller of the pair is +// install_media_remaster_write_nocloud_seed, which admits, derives the directory, and fetches in one +// body (compile probe: test.probe.seed_artifact_fetch_forged_probe). +fn install_media_remaster_fetch_artifacts(host: CommandHost, nocloud_dir: NonEmptyStr, artifacts: List) -> String? admit_callers: [ + decl_ref(module_path: "extdeps.provisioning.ubuntu_seeded_install_media_remaster", decl_name: "install_media_remaster_write_nocloud_seed"), +] { + fold(artifacts, init: none, f: (acc, a) => match acc { + Present { value: r } => Present { value: r } + Absent => install_media_remaster_fetch_artifact(host: host, nocloud_dir: nocloud_dir, artifact: a) + }) +} + +// THE NAMES ARE ADMITTED BEFORE THE SEED DIRECTORY IS EVEN CREATED, so a refused name moves no byte. +fn install_media_remaster_write_nocloud_seed(host: CommandHost, work_dir: NonEmptyStr, input: UbuntuSeededInstallMediaBuildInput) -> String? { + match seeded_install_media_seed_artifact_admission(input: input) { + SeedArtifactsRefused { refusals: rs } => Present { value: join(map(rs, r => seed_artifact_admission_refusal_text(refusal: r)), "; ") } + SeedArtifactsAdmitted { artifacts: admitted } => { + let nocloud_dir = concat(work_dir as String, "/", input.nocloud_dir_on_iso as String) as NonEmptyStr + match host_command(host: host, command: mkdir_parents_command(path: nocloud_dir as String)) { + HostCommandDone { stdout: _ } => + match install_media_remaster_write_seeds(host: host, writes: install_media_remaster_seed_writes(nocloud_dir: nocloud_dir, input: input), scope_root: work_dir) { + Present { value: r } => Present { value: r } + Absent => install_media_remaster_fetch_artifacts(host: host, nocloud_dir: nocloud_dir, artifacts: admitted) + } + other => Present { value: concat("mkdir -p ", nocloud_dir as String, ": ", host_command_outcome_text(e: other)) } + } + } } } diff --git a/dag/extdeps/realization/compile_stage_memo.dag b/dag/extdeps/realization/compile_stage_memo.dag deleted file mode 100644 index 6f1d761b158..00000000000 --- a/dag/extdeps/realization/compile_stage_memo.dag +++ /dev/null @@ -1,78 +0,0 @@ -module extdeps.realization.compile_stage_memo - -import extdeps.external_authority { ExternalAuthority } -import extdeps.uri { Uri, Https } -import std.optional { Absent } - -import extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding } -import std.cache_identity { CacheInterfaceId } -import std.cache_interface { - ContentAddressLookup, - OverwriteAllowed, - MissThenCreate, - StructuredArtifact, - InProcess, - PerFile, - None, - InProcessNs, - Strong, - OperatorObserved, - ManualPurge, - Repo, - CapacityUnbounded, - ReleasedAtProviderScopeExit, -} - -data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { - uri: Uri { - scheme: Https - locator: "github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization" - } -} - -data compile_stage_memo_id: CacheInterfaceId = "compile_stage_memo_cache" - -data compile_stage_memo_facts: CacheInterfaceCatalogFacts = { - identity: compile_stage_memo_id - backing_surface: "gunbc-v2-compile-stage-in-process" - transport_encoding: CatalogCasContentEncoding - key_derivation: { - - key_inputs: ContentAddressed - overwritable: true - prefix_fallback_allowed: false - content_verified_on_read: false - visibility_scope: Repo - invalidation_triggers: [ManualPurge] - } - io: { - lookup_semantics: ContentAddressLookup - write_semantics: OverwriteAllowed - miss_semantics: MissThenCreate - } - placement: { - value_shape: StructuredArtifact - locality: InProcess - retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded } - atomicity: PerFile - auth: None - read_latency: InProcessNs - consistency: Strong - } - evidence: [ - { - field: "locality" - evidence: OperatorObserved { - note: "scoped to compile_ingest_staging pipeline; drops with the ingest walk — within-run stage memo (v2.std.staging.cached_stage)" - observed_at: Absent - } - }, - { - field: "key_derivation" - evidence: OperatorObserved { - note: "stage input content-addressed at the cached_stage lookup site; miss runs the stage, hit skips recompute" - observed_at: Absent - } - }, - ] -} diff --git a/dag/extdeps/realization/materialization_store_local.dag b/dag/extdeps/realization/materialization_store_local.dag index 9551444a2ec..69c9b474502 100644 --- a/dag/extdeps/realization/materialization_store_local.dag +++ b/dag/extdeps/realization/materialization_store_local.dag @@ -81,6 +81,7 @@ import gunbc.durable_cas_file_store { CasWindowReclamation, CasReclamationNotCommitted, CasAttemptAdmission, cas_window_generations, CasWindowGenerations, CasWindowGenerationsListed, CasWindowGenerationsUnlisted, cas_reclaim_below, CasGenerationWindow, + cas_verify_named_window_head, NamedWindowHeadCurrent, NamedWindowHeadMoved, } import std.durable_exclusive_hold { DurableHoldOwnerRef, DurableHoldReleaseRef, DurableHoldHolderReport, DurableHoldReleaseRefusal, DurableHoldRecoveryRefusal, @@ -91,10 +92,10 @@ import std.durable_exclusive_hold { import gunbc.durable_exclusive_hold_file_store { file_hold_acquire_windowed, FileHoldAcquireOutcome, FileHoldAcquired, FileHoldOccupied, FileHoldAcquireLost, FileHoldAcquireStoreUnavailable, FileHoldSlotUndecodable, FileHoldKeyNotSlotAddressable, - file_hold_release_assess_windowed, FileHoldReleasePlanned, FileHoldReleaseNotEligible, + file_hold_release_assess_windowed_at, FileHoldWindowedReleasePlanned, FileHoldWindowedReleaseNotEligible, FileHoldWindowedReleaseNamedHeadMoved, file_hold_release_commit, FileHoldReleased, FileHoldReleaseLost, FileHoldReleaseStoreUnavailable, FileHoldReleaseKeyNotSlotAddressable, - file_hold_recovery_assess_windowed, FileHoldRecoveryPlanned, FileHoldRecoveryNotEligible, - observe_file_hold_windowed, + file_hold_recovery_assess_windowed, FileHoldWindowedRecoveryPlanned, FileHoldWindowedRecoveryNotEligible, FileHoldWindowedRecoveryNamedHeadMoved, + hold_observation_of_cas, } import gunbc.process_hold_identity { process_hold_text, process_hold_marker, process_hold_decode, ProcessHoldHolder, @@ -907,6 +908,7 @@ type LocalStoreIndexRefusal | LocalStoreIndexContended { attempts: Int } | LocalStoreIndexUnsettled { settlement: LocalStoreSlotSettlement } | LocalStoreIndexHoldStale { observed: DurableHoldObservation } + | LocalStoreIndexHoldNamedWindowHeadMoved { named: CasGeneration } fn local_store_index_refusal_text(refusal: LocalStoreIndexRefusal) -> String { match refusal { @@ -927,6 +929,7 @@ fn local_store_index_refusal_text(refusal: LocalStoreIndexRefusal) -> String { LocalStoreIndexContended { attempts: n } => concat("the occupancy index kept moving under concurrent writers past the attempt bound: ", n as String) LocalStoreIndexUnsettled { settlement: st } => concat("the occupancy index cannot advance: ", local_store_settlement_text(s: st)) LocalStoreIndexHoldStale { observed: _ } => "the occupancy index write was attempted under a hold that is no longer live" + LocalStoreIndexHoldNamedWindowHeadMoved { named: _ } => "the occupancy index write was attempted after the named window head moved" } } @@ -974,6 +977,7 @@ fn local_store_write_index(held: LocalStoreFamilyHeld, expected: CasExpectation, { match local_store_hold_check(held: held) { LocalStoreHoldNotCurrent { observed: o } => LocalStoreIndexWriteRefused { refusal: LocalStoreIndexHoldStale { observed: o } } + LocalStoreHoldNamedWindowMoved { named: n } => LocalStoreIndexWriteRefused { refusal: LocalStoreIndexHoldNamedWindowHeadMoved { named: n } } LocalStoreHoldCurrent => local_store_write_index_current(held: held, expected: expected, index: index) } } @@ -1033,6 +1037,20 @@ fn local_store_write_index_settled(held: LocalStoreFamilyHeld, expected: CasExpe // on this host, and only a holder observed dead is recovered through file_hold_recovery_assess. An // unobservable holder stays held -- a family that cannot be shown free is not written. The hold slot // is windowed like the occupancy index (k = 2), so its bookkeeping does not grow with commits. +// +// HOLD CHECK AND RELEASE READ THE SLOT BY NAME. The windowed verify step is: generation G present +// and G+1 absent. LocalStoreFamilyHeld already names G, so those two reads are cas_file_slot_path +// Filesystem.Read -- O(1) in the number of objects in the store root. That two-read proof is sound +// only while a surviving eligible prefix cannot have holes: cas_reclaim_below deletes in generation +// order and stops at the first host refusal, so it cannot remove G+1 while G stays. A successor +// present still means the slot moved; an absent G is not this hold. Unknown-head observation +// (acquire, index) still lists, as DESIGN §3b states for a window whose generation is not named. +// +// ACQUIRE STILL LISTS. Windowed presence is not a prefix: generations at or below head-k are +// deleted, so a named gallop from generation 1 would read NotFound and treat a live head at 100 as +// an empty slot, then create-new generation 1 beside it. Listing the root to discover this key's +// generation files is the unknown-head observation cas_observe_window already uses. A held session +// pays that list once, not once per commit. data local_store_hold_owner_prefix: String = "materialization-store" @@ -1056,6 +1074,7 @@ type LocalStoreHoldRefusal | LocalStoreHoldWindowTooNarrow { k: Int } | LocalStoreHoldUnsettled { settlement: LocalStoreSlotSettlement } | LocalStoreHoldStale { observed: DurableHoldObservation } + | LocalStoreHoldNamedWindowHeadMoved { named: CasGeneration } | LocalStoreHoldWrongFamily { held: ArtifactKindId, requested: ArtifactKindId } fn local_store_hold_refusal_text(refusal: LocalStoreHoldRefusal) -> String { @@ -1071,6 +1090,7 @@ fn local_store_hold_refusal_text(refusal: LocalStoreHoldRefusal) -> String { LocalStoreHoldWindowTooNarrow { k: k } => concat("the family hold window is narrower than its floor: k = ", k as String) LocalStoreHoldUnsettled { settlement: st } => concat("the family hold slot cannot advance: ", local_store_settlement_text(s: st)) LocalStoreHoldStale { observed: _ } => "the hold this write was given is no longer the family's live hold" + LocalStoreHoldNamedWindowHeadMoved { named: _ } => "the named window head moved; this hold is no longer the family's live hold" LocalStoreHoldWrongFamily { held: h, requested: r } => concat(concat("the hold is for family ", h as String), concat(", the write is for ", r as String)) } } @@ -1163,8 +1183,9 @@ fn local_store_hold_occupied(binding: LocalStoreBinding, family: ArtifactKindId, LocalStoreHoldRefused { refusal: LocalStoreHoldContended } } else { match file_hold_recovery_assess_windowed(root: root_path as NonEmptyStr, slot_key: key, report: DurableHoldHolderReport { holder: holder, generation: generation, liveness: liveness }, released_by: local_store_hold_recovery_ref, window: window) { - FileHoldRecoveryNotEligible { slot_key: _, refusal: r } => LocalStoreHoldRefused { refusal: LocalStoreHoldRecoveryRefused { refusal: r } } - FileHoldRecoveryPlanned { plan: pl, dead_holder: _, evidence: _ } => + FileHoldWindowedRecoveryNotEligible { slot_key: _, refusal: r } => LocalStoreHoldRefused { refusal: LocalStoreHoldRecoveryRefused { refusal: r } } + FileHoldWindowedRecoveryNamedHeadMoved { slot_key: _, named: n } => LocalStoreHoldRefused { refusal: LocalStoreHoldNamedWindowHeadMoved { named: n } } + FileHoldWindowedRecoveryPlanned { plan: pl, dead_holder: _, evidence: _ } => match file_hold_release_commit(plan: pl) { FileHoldReleased { slot_key: _, generation: _ } => local_store_acquire_family_hold(binding: binding, family: family, recovered: true) FileHoldReleaseLost { slot_key: _ } => LocalStoreHoldRefused { refusal: LocalStoreHoldContended } @@ -1186,6 +1207,7 @@ fn local_store_hold_occupied(binding: LocalStoreBinding, family: ArtifactKindId, type LocalStoreHoldCheck = LocalStoreHoldCurrent | LocalStoreHoldNotCurrent { observed: DurableHoldObservation } + | LocalStoreHoldNamedWindowMoved { named: CasGeneration } fn local_store_hold_check(held: LocalStoreFamilyHeld) -> LocalStoreHoldCheck admit_callers: [ @@ -1196,10 +1218,25 @@ fn local_store_hold_check(held: LocalStoreFamilyHeld) -> LocalStoreHoldCheck decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_commit_verified"), ] { - let observed = observe_file_hold_windowed(root: materialization_store_root_path(root: held.binding.root) as NonEmptyStr, key: local_store_hold_key(family: held.family), window: held.window) + match cas_verify_named_window_head(root: materialization_store_root_path(root: held.binding.root) as NonEmptyStr, key: local_store_hold_key(family: held.family), generation: held.generation) { + NamedWindowHeadMoved { named_generation: n } => LocalStoreHoldNamedWindowMoved { named: n } + NamedWindowHeadCurrent { observation: o } => + local_store_hold_from_current_observation(held: held, observed: hold_observation_of_cas(observed: o)) + } +} + +fn local_store_hold_from_current_observation(held: LocalStoreFamilyHeld, observed: DurableHoldObservation) -> LocalStoreHoldCheck + admit_callers: [ + decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_hold_check"), + ] +{ match observed { - HoldSlotHeld { generation: g, owner: o } => - if cas_generation_count(g: g) == cas_generation_count(g: held.generation) && durable_hold_owner_equal(left: o, right: held.owner) { LocalStoreHoldCurrent } else { LocalStoreHoldNotCurrent { observed: observed } } + HoldSlotHeld { generation: g, owner: owner } => + if cas_generation_count(g: g) == cas_generation_count(g: held.generation) && durable_hold_owner_equal(left: owner, right: held.owner) { + LocalStoreHoldCurrent + } else { + LocalStoreHoldNotCurrent { observed: observed } + } HoldSlotAbsent => LocalStoreHoldNotCurrent { observed: observed } HoldSlotFree { generation: _, released_by: _ } => LocalStoreHoldNotCurrent { observed: observed } HoldSlotUndecodable { generation: _, detail: _ } => LocalStoreHoldNotCurrent { observed: observed } @@ -1223,6 +1260,7 @@ fn local_store_with_family_of(binding: LocalStoreBinding, family: ArtifactKin admit_callers: [ decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_with_family"), decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_record_recency"), + decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_held_session"), ] { match local_store_acquire_family_hold(binding: binding, family: family, recovered: false) { @@ -1234,11 +1272,36 @@ fn local_store_with_family_of(binding: LocalStoreBinding, family: ArtifactKin } } +// A HELD SESSION: acquire the family once, run many lookups and commits under LocalStoreFamilyHeld, +// release once. Distinct from local_store_commit, which still brackets every write; this operation +// does not change that signature. +// Consumed now by test.claim.materialization_store_local_wet_witness +// n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution (inhabitance). +// Named later consumer: v2.compiler.compile, when its typecheck writes leave per-commit +// local_store_commit (the host typed_module_cache still does) and take one session around the +// family's writes; that change adds an admit_callers row here. Until then this is not a second +// name for local_store_with_family_of -- it is the sealed session operation. +// +// DEADLINE AND RENEWAL follow std.durable_exclusive_hold, not a clock. That protocol recovers a +// hold only when its holder is OBSERVED dead -- there is no timestamp, duration or silent expiry. +// This process is the holder (gunbc.process_hold_identity). Renewal is the named re-observation +// local_store_hold_check performs before every mutation: the slot must still be held at the +// generation and by the owner the session recorded, or the mutation refuses as stale. A session +// whose process has died is recovered by the next writer's liveness observation, never by age. +fn local_store_held_session(capability: LocalStoreCapability, family: ArtifactKindId, run: fn(LocalStoreFamilyHeld) -> T) -> LocalStoreBracket + admit_callers: [ + decl_ref(module_path: "test.claim.materialization_store_local_wet_witness", decl_name: "n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution"), + ] +{ + local_store_with_family_of(binding: capability.binding, family: family, run: run) +} + // The release is reported, never fatal to the write it closes: a hold that could not be freed stays // held by this process, and once this process is gone the next writer recovers it by observation. type LocalStoreHoldRelease = LocalStoreHoldReleased | LocalStoreHoldReleaseNotEligible { refusal: DurableHoldReleaseRefusal } + | LocalStoreHoldReleaseNamedWindowHeadMoved { named: CasGeneration } | LocalStoreHoldReleaseLost | LocalStoreHoldReleaseStoreUnavailable { cause: CasStoreFailure } | LocalStoreHoldReleaseKeyNotAddressable @@ -1250,9 +1313,10 @@ fn local_store_release_family_hold(held: LocalStoreFamilyHeld) -> LocalStoreHold decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_acquire_all"), ] { - match file_hold_release_assess_windowed(root: materialization_store_root_path(root: held.binding.root) as NonEmptyStr, slot_key: local_store_hold_key(family: held.family), owner: held.owner, acquired_generation: held.generation, released_by: local_store_hold_release_ref, window: held.window) { - FileHoldReleaseNotEligible { slot_key: _, refusal: r } => LocalStoreHoldReleaseNotEligible { refusal: r } - FileHoldReleasePlanned { plan: pl } => + match file_hold_release_assess_windowed_at(root: materialization_store_root_path(root: held.binding.root) as NonEmptyStr, slot_key: local_store_hold_key(family: held.family), owner: held.owner, acquired_generation: held.generation, released_by: local_store_hold_release_ref, window: held.window) { + FileHoldWindowedReleaseNotEligible { slot_key: _, refusal: r } => LocalStoreHoldReleaseNotEligible { refusal: r } + FileHoldWindowedReleaseNamedHeadMoved { slot_key: _, named: n } => LocalStoreHoldReleaseNamedWindowHeadMoved { named: n } + FileHoldWindowedReleasePlanned { plan: pl } => match file_hold_release_commit(plan: pl) { FileHoldReleased { slot_key: _, generation: _ } => LocalStoreHoldReleased FileHoldReleaseLost { slot_key: _ } => LocalStoreHoldReleaseLost @@ -1644,6 +1708,7 @@ fn local_store_sweep_held(all: LocalStoreAllFamiliesHeld) -> LocalStoreSweep Absent => match local_store_hold_check(held: h) { LocalStoreHoldCurrent => none LocalStoreHoldNotCurrent { observed: o } => Present { value: LocalStoreSweepHoldRefused { family: h.family, refusal: LocalStoreHoldStale { observed: o }, released: [] } } + LocalStoreHoldNamedWindowMoved { named: n } => Present { value: LocalStoreSweepHoldRefused { family: h.family, refusal: LocalStoreHoldNamedWindowHeadMoved { named: n }, released: [] } } } }) match stale { @@ -1724,6 +1789,7 @@ fn local_store_record_recency_held(held: LocalStoreFamilyHeld, served: List LocalStoreRecencyHoldRefused { family: family, refusal: LocalStoreHoldStale { observed: o } } + LocalStoreHoldNamedWindowMoved { named: n } => LocalStoreRecencyHoldRefused { family: family, refusal: LocalStoreHoldNamedWindowHeadMoved { named: n } } LocalStoreHoldCurrent => local_store_record_recency_current(held: held, served: served) } } @@ -1984,6 +2050,7 @@ fn local_store_commit_verified( let root_path = materialization_store_root_path(root: root) match local_store_hold_check(held: held) { LocalStoreHoldNotCurrent { observed: o } => local_store_commit_hold_refused(key: key, refusal: LocalStoreHoldStale { observed: o }) + LocalStoreHoldNamedWindowMoved { named: n } => local_store_commit_hold_refused(key: key, refusal: LocalStoreHoldNamedWindowHeadMoved { named: n }) LocalStoreHoldCurrent => local_store_commit_refused(commit: store_commit_settle( store: held.binding.store, @@ -2250,6 +2317,7 @@ fn local_store_commit_under( } else { match local_store_hold_check(held: held) { LocalStoreHoldNotCurrent { observed: o } => local_store_commit_hold_refused(key: request_key(req: req), refusal: LocalStoreHoldStale { observed: o }) + LocalStoreHoldNamedWindowMoved { named: n } => local_store_commit_hold_refused(key: request_key(req: req), refusal: LocalStoreHoldNamedWindowHeadMoved { named: n }) LocalStoreHoldCurrent => { let staged = parts |> map(p => local_store_read_part(id: p.id, path: p.staged)) match local_store_first_unreadable(readings: staged) { diff --git a/dag/extdeps/realization/parse_table_memo.dag b/dag/extdeps/realization/parse_table_memo.dag deleted file mode 100644 index a7ed67f4682..00000000000 --- a/dag/extdeps/realization/parse_table_memo.dag +++ /dev/null @@ -1,89 +0,0 @@ -module extdeps.realization.parse_table_memo - -import extdeps.external_authority { ExternalAuthority } -import extdeps.uri { Uri, Https } -import std.optional { Absent } - -import extdeps.cache.types { CacheInterfaceCatalogFacts, ContentAddressed, CatalogCasContentEncoding } -import std.cache_identity { CacheInterfaceId } -import std.cache_interface { - ContentAddressLookup, - OverwriteAllowed, - MissThenCreate, - StructuredArtifact, - InProcess, - PerFile, - None, - InProcessNs, - Strong, - OperatorObserved, - ManualPurge, - Repo, - CapacityUnbounded, - ReleasedAtProviderScopeExit, -} -import std.disposition { Disposition, Scaffold, RealizationDispatch } -import std.decl_ref { DeclarationRef, WholeDeclaration } - -data parse_table_memo_seed_handler_dissolution_trigger: Disposition = Scaffold { - dissolves_to: RealizationDispatch, - bind: DeclarationRef { - module_path: "v2.compiler.materialization_carriers", - decl_name: "materialization_allows_memo_store", - field: WholeDeclaration - } -} - -data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { - uri: Uri { - scheme: Https - locator: "github.com/gunb-ai/gunbc/tree/main/dag/extdeps/realization" - } -} - -data parse_table_memo_id: CacheInterfaceId = "parse_table_memo_cache" - -data parse_table_memo_facts: CacheInterfaceCatalogFacts = { - identity: parse_table_memo_id - backing_surface: "gunbc-parse-table-in-process" - transport_encoding: CatalogCasContentEncoding - key_derivation: { - - key_inputs: ContentAddressed - overwritable: true - prefix_fallback_allowed: false - content_verified_on_read: false - visibility_scope: Repo - invalidation_triggers: [ManualPurge] - } - io: { - lookup_semantics: ContentAddressLookup - write_semantics: OverwriteAllowed - miss_semantics: MissThenCreate - } - placement: { - value_shape: StructuredArtifact - locality: InProcess - retention: { release_policy: ReleasedAtProviderScopeExit, capacity: CapacityUnbounded } - atomicity: PerFile - auth: None - read_latency: InProcessNs - consistency: Strong - } - evidence: [ - { - field: "locality" - evidence: OperatorObserved { - note: "scoped to InterpContext; drops with evaluation — within-run memo (design-recompute-memoization §4.1)" - observed_at: Absent - } - }, - { - field: "key_derivation" - evidence: OperatorObserved { - note: "ParseTableSubject = grammar_digest + token_stream_digest (full stream content-address incl. lexemes) + position + production" - observed_at: Absent - } - }, - ] -} diff --git a/dag/extdeps/systemd/service_exec.dag b/dag/extdeps/systemd/service_exec.dag new file mode 100644 index 00000000000..98eb50cd5fc --- /dev/null +++ b/dag/extdeps/systemd/service_exec.dag @@ -0,0 +1,201 @@ +module extdeps.systemd.service_exec + +import std.types { String, NonEmptyStr, List, Bool } +import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.uri { Uri, Https } +import extdeps.exec.command { ArgvCommand, argv_words } + +// WHAT THE SERVICE MANAGER HANDS A STOP COMMAND, and how a command line is spelled to it. Two +// upstream facts, both from systemd's own manuals, and nothing about any one consumer: which unit +// carries a stop hook, what it writes and where are workflow facts that live with the workflow. + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Environment%20Variables%20Set%20or%20Propagated%20by%20the%20Service%20Manager" + } +} + +data systemd_service_command_lines_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.freedesktop.org/software/systemd/man/latest/systemd.service.html#Command%20lines" + } +} + +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.systemd.service_exec", + decl_name: "ServiceStopVariable", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [systemd_service_command_lines_authority] +} + +// systemd.exec(5), "Environment Variables Set or Propagated by the Service Manager": +// $INVOCATION_ID -- set for every process the manager spawns for the unit: the unit's current +// invocation, a 128-bit id formatted as 32 lowercase hex characters. +// $SERVICE_RESULT -- set for ExecStop= and ExecStopPost= only: the service's result, one of the +// words below. +// $EXIT_CODE -- set for ExecStop= and ExecStopPost= only: how the main process ended: +// "exited", "killed" or "dumped" (empty if it never ran). +// $EXIT_STATUS -- set for ExecStop= and ExecStopPost= only: the numeric exit status when +// $EXIT_CODE is "exited", otherwise the signal NAME (e.g. "TERM"). +// They exist only while the stop command runs, so a record of the main process's end that survives +// the unit has to be written by that command, from these values. +type ServiceStopVariable + = InvocationIdVariable + | ServiceResultVariable + | ExitCodeVariable + | ExitStatusVariable + +fn service_stop_variable_name(variable: ServiceStopVariable) -> NonEmptyStr { + match variable { + InvocationIdVariable => "INVOCATION_ID" as NonEmptyStr + ServiceResultVariable => "SERVICE_RESULT" as NonEmptyStr + ExitCodeVariable => "EXIT_CODE" as NonEmptyStr + ExitStatusVariable => "EXIT_STATUS" as NonEmptyStr + } +} + +// $SERVICE_RESULT's documented words (systemd.exec(5)). An unlisted word is carried, not dropped: +// a manager newer than this model may add one, and the reader must say so rather than guess. +type ServiceResult + = ServiceResultSuccess + | ServiceResultProtocol + | ServiceResultTimeout + | ServiceResultExitCode + | ServiceResultSignal + | ServiceResultCoreDump + | ServiceResultWatchdog + | ServiceResultStartLimitHit + | ServiceResultResources + | ServiceResultOomKill + | ServiceResultUnlisted { word: String } + +fn service_result_of_wire(word: String) -> ServiceResult { + if word == "success" { ServiceResultSuccess } + else if word == "protocol" { ServiceResultProtocol } + else if word == "timeout" { ServiceResultTimeout } + else if word == "exit-code" { ServiceResultExitCode } + else if word == "signal" { ServiceResultSignal } + else if word == "core-dump" { ServiceResultCoreDump } + else if word == "watchdog" { ServiceResultWatchdog } + else if word == "start-limit-hit" { ServiceResultStartLimitHit } + else if word == "resources" { ServiceResultResources } + else if word == "oom-kill" { ServiceResultOomKill } + else { ServiceResultUnlisted { word: word } } +} + +// $EXIT_CODE's documented words. "Exited" carries a numeric status; the other two carry a signal +// name, and the two are different facts -- a process killed by SIGTERM did not exit 15. +type ServiceExitCode + = ServiceExitCodeExited + | ServiceExitCodeKilled + | ServiceExitCodeDumped + | ServiceExitCodeUnlisted { word: String } + +fn service_exit_code_of_wire(word: String) -> ServiceExitCode { + if word == "exited" { ServiceExitCodeExited } + else if word == "killed" { ServiceExitCodeKilled } + else if word == "dumped" { ServiceExitCodeDumped } + else { ServiceExitCodeUnlisted { word: word } } +} + +// ONE PIECE OF A COMMAND-LINE WORD: text the manager passes through unchanged, or a variable it +// substitutes. systemd.service(5), "Command lines": "${FOO}" used as part of a word is replaced by +// the variable's value with no word splitting, which is the form used here; the bare "$FOO" form +// word-splits and is not modeled. Keeping the two pieces apart is what lets the renderer refuse a +// literal that would be read as an expansion, instead of trusting that a string happens not to +// contain one. +type ExecWordPart + = ExecLiteral { text: String } + | ExecVariable { variable: ServiceStopVariable } + +type ExecWordRendering + = ExecWordRendered { word: NonEmptyStr } + | ExecWordRefused { reason: NonEmptyStr } + +// A literal is refused, never escaped, when it holds a character the command-line parser would +// treat as syntax: whitespace or a quote (word boundaries), a backslash (escapes), "$" (expansion) +// or "%" (unit specifiers). Escaping would be a second grammar to get right; every literal this +// repository mints is a path or an identifier that has no need of those characters. +fn exec_literal_admissible(text: String) -> Bool { + !(string_contains(s: text, pattern: " ") || string_contains(s: text, pattern: "\t") || string_contains(s: text, pattern: "\n") || string_contains(s: text, pattern: "\"") + || string_contains(s: text, pattern: "'") || string_contains(s: text, pattern: "\\") || string_contains(s: text, pattern: "$") || string_contains(s: text, pattern: "%")) +} + +fn exec_word_render(parts: List) -> ExecWordRendering { + let refused = parts |> flat_map(p => match p { + ExecLiteral { text } => if exec_literal_admissible(text: text) { [] as List } else { [text] } + ExecVariable { variable: _ } => [] as List + }) + match refused.first() { + Present { value: bad } => + ExecWordRefused { reason: join(["command-line literal carries a character systemd would read as syntax: ", bad], "") as NonEmptyStr } + Absent => { + let word = join(parts |> map(p => match p { + ExecLiteral { text } => text + ExecVariable { variable } => join(["${", service_stop_variable_name(variable: variable) as String, "}"], "") + }), "") + if string_length(s: word) == 0 { + ExecWordRefused { reason: "an empty command-line word cannot be spelled to systemd" as NonEmptyStr } + } else { + ExecWordRendered { word: word as NonEmptyStr } + } + } + } +} + +type ExecCommandLineRendering + = ExecCommandLineRendered { line: NonEmptyStr } + | ExecCommandLineRefused { reason: NonEmptyStr } + +// AN ARGV AS ONE ExecStart=/ExecStopPost= VALUE. The manager splits the value on whitespace into +// words, the first naming the program, and runs it directly: no shell is involved at any point +// (systemd.service(5), "Command lines"), which is why a typed argv can be handed over at all. Every +// word passes the same admissibility test as a literal part, except that a word rendered from parts +// by exec_word_render may carry the "${...}" its variables were spelled as; that form is re-checked +// below rather than trusted. +fn exec_command_line(command: ArgvCommand) -> ExecCommandLineRendering { + let words = argv_words(command: command) + let refused = words |> filter(w => !exec_literal_admissible(text: w) && !exec_word_is_rendered_expansion(word: w)) + match refused.first() { + Present { value: bad } => + ExecCommandLineRefused { reason: join(["argv word carries a character systemd would read as syntax: ", bad], "") as NonEmptyStr } + Absent => ExecCommandLineRendered { line: join(words, " ") as NonEmptyStr } + } +} + +// THE "+" COMMAND-LINE PREFIX, from the special executable prefixes systemd.service(5) documents +// ("Command lines"): it runs that one command with full privileges -- outside the unit's file-system +// namespacing and sandboxing (ProtectSystem=, ProtectHome=, ReadWritePaths= and the rest), while the +// main process keeps them. That is what lets a stop hook write where the unit's own process may not. +// In a user manager "full privileges" are the user's own; the prefix never grants more than the +// manager's principal holds. It is the only prefix this repository uses, so it is one function rather +// than a coproduct of one. +data exec_full_privileges_prefix: String = "+" + +fn exec_command_line_full_privileges(command: ArgvCommand) -> ExecCommandLineRendering { + match exec_command_line(command: command) { + ExecCommandLineRefused { reason } => ExecCommandLineRefused { reason: reason } + ExecCommandLineRendered { line } => ExecCommandLineRendered { line: join([exec_full_privileges_prefix, line as String], "") as NonEmptyStr } + } +} + +// A WORD WHOSE ONLY SYNTAX IS "${NAME}" FOR A MODELED VARIABLE. Re-reading the rendered word, not +// trusting a flag beside it: strip every modeled expansion, and the remainder must be an +// admissible literal. A "$" anywhere else -- an unmodeled variable, a bare $FOO -- survives the +// strip and refuses. +fn exec_word_is_rendered_expansion(word: String) -> Bool { + let stripped = fold( + [InvocationIdVariable, ServiceResultVariable, ExitCodeVariable, ExitStatusVariable], + init: word, + f: (acc, v) => replace(acc, join(["${", service_stop_variable_name(variable: v) as String, "}"], ""), ""), + ) + stripped != word && exec_literal_admissible(text: stripped) +} diff --git a/dag/extdeps/systemd/systemctl.dag b/dag/extdeps/systemd/systemctl.dag index 1acf1739c59..b1e644d3f9e 100644 --- a/dag/extdeps/systemd/systemctl.dag +++ b/dag/extdeps/systemd/systemctl.dag @@ -225,8 +225,14 @@ fn systemctl_set_property_argv(unit: NonEmptyStr, property: NonEmptyStr, value: ] } +// `systemctl revert UNIT...` takes UNIT NAMES ONLY and removes every drop-in of each named unit +// (systemctl(1), systemd 255). Property names are not arguments: an earlier argv appended +// "MemoryMax" "MemoryHigh", which systemctl read as two more units (MemoryMax.service, +// MemoryHigh.service) and reverted silently with exit 0 -- so the argv claimed a property-scoped +// revert it never performed. What this reverts is the unit's whole drop-in set, including the +// control shadows set-property wrote, and that is what the caller is relying on. fn systemctl_revert_memory_caps_argv(unit: NonEmptyStr) -> List { - ["systemctl", "revert", unit as String, "MemoryMax", "MemoryHigh"] + ["systemctl", "revert", unit as String] } // THE USER MANAGER IS A DIFFERENT MANAGER, NOT A FLAG ON THE SAME QUERY. `systemctl show` without @@ -597,10 +603,10 @@ service systemd.Systemctl { success: Bool from "exit_success" stderr: String from "stderr" } - transport shell { argv: ["systemctl", "revert", "{unit}", "MemoryMax", "MemoryHigh"] } + transport shell { argv: ["systemctl", "revert", "{unit}"] } exit { 0 => Unit - nonzero => String "systemctl revert MemoryMax MemoryHigh failed" + nonzero => String "systemctl revert failed" } mock_response { 0 => { success: true, stderr: "" } "hermetic systemd.Systemctl.RevertMemoryCaps" diff --git a/dag/extdeps/systemd/systemd.dag b/dag/extdeps/systemd/systemd.dag index 1e514352c9d..a72ed451ba2 100644 --- a/dag/extdeps/systemd/systemd.dag +++ b/dag/extdeps/systemd/systemd.dag @@ -6,7 +6,7 @@ import std.algebra { trim } import std.types { List } import std.nat { Nat } import std.measure { - ByteSize, byte_size, byte_size_count, Microsecond, microsecond, microsecond_count, + ByteSize, byte_size, byte_size_count, Microsecond, microsecond, microsecond_count, Second, second_count, HardwareThreadCount, hardware_thread_count_value, milli_per_unit, micro_per_unit, seconds_per_minute, minutes_per_hour, } @@ -71,6 +71,7 @@ type SystemdUnitProperty | Result | ExecMainStatus | ExecStartProperty + | ExecStopPostProperty | User | WorkingDirectoryProperty | RuntimeDirectoryProperty @@ -96,6 +97,8 @@ type SystemdUnitProperty | PrivateTmpProperty | ReadWritePathsProperty | RemainAfterExitProperty + | RuntimeMaxSecProperty + | CollectModeProperty | JobProperty // THE WIRE SPELLING AS A SEALED WORD, SO THE CLOSED TYPE SURVIVES ALL THE WAY TO THE ARGV. @@ -147,6 +150,7 @@ fn systemd_unit_property_wire(property: SystemdUnitProperty) -> NonEmptyStr { Result => "Result" as NonEmptyStr ExecMainStatus => "ExecMainStatus" as NonEmptyStr ExecStartProperty => "ExecStart" as NonEmptyStr + ExecStopPostProperty => "ExecStopPost" as NonEmptyStr User => "User" as NonEmptyStr WorkingDirectoryProperty => "WorkingDirectory" as NonEmptyStr RuntimeDirectoryProperty => "RuntimeDirectory" as NonEmptyStr @@ -172,6 +176,8 @@ fn systemd_unit_property_wire(property: SystemdUnitProperty) -> NonEmptyStr { PrivateTmpProperty => "PrivateTmp" as NonEmptyStr ReadWritePathsProperty => "ReadWritePaths" as NonEmptyStr RemainAfterExitProperty => "RemainAfterExit" as NonEmptyStr + RuntimeMaxSecProperty => "RuntimeMaxSec" as NonEmptyStr + CollectModeProperty => "CollectMode" as NonEmptyStr JobProperty => "Job" as NonEmptyStr } } @@ -257,6 +263,13 @@ fn systemd_time_span_split(part: String) -> SystemdTimeSpanPart? { } } +// THE TYPED QUANTITY RENDERED TO SYSTEMD'S TIME-SPAN SPELLING, at the boundary and nowhere else: a +// duration is authored as a Second and only its systemd text is a string, so no consumer parses text to +// recover the number. Whole seconds with the `s` unit are a spelling systemd_duration_usec reads back. +fn systemd_time_span_of_seconds(d: Second) -> NonEmptyStr { + join([to_string(second_count(s: d)), "s"], "") as NonEmptyStr +} + fn systemd_duration_usec(text: String) -> Microsecond? { let parts = filter(split(s: trim(s: text), delimiter: " "), p => p != "") if list_length(items: parts) == 0 { @@ -522,3 +535,7 @@ fn systemd_slice_cgroup_path(mount_point: NonEmptyStr, slice: NonEmptyStr) -> Sy } } } + +// THE DIRECTORY sd_booted(3) TESTS: it exists exactly when systemd is the running service manager +// (PID 1), which is what systemd-run needs to start a transient unit. +data systemd_booted_runtime_dir: String = "/run/systemd/system" diff --git a/dag/extdeps/systemd/udevd.dag b/dag/extdeps/systemd/udevd.dag index db46aaf7794..0da2ef6433b 100644 --- a/dag/extdeps/systemd/udevd.dag +++ b/dag/extdeps/systemd/udevd.dag @@ -39,3 +39,8 @@ data systemd_log_target_env_name: NonEmptyStr = "SYSTEMD_LOG_TARGET" data systemd_log_target_kmsg: NonEmptyStr = "kmsg" data systemd_log_level_debug: NonEmptyStr = "debug" + +// THE LABEL INDEX udev MAINTAINS: 60-persistent-storage.rules links each filesystem label under +// /dev/disk/by-label (systemd udev rules, v255), so listing the directory reads the labels the +// booted host's block devices carry. +data udev_disk_by_label_dir: String = "/dev/disk/by-label" diff --git a/dag/extdeps/systemd/unit_file.dag b/dag/extdeps/systemd/unit_file.dag index 725d146f464..f943f50772e 100644 --- a/dag/extdeps/systemd/unit_file.dag +++ b/dag/extdeps/systemd/unit_file.dag @@ -513,6 +513,20 @@ type SystemdTimerDirective | RandomizedDelaySec { duration: NonEmptyStr } | Persistent { enabled: Bool } +// A .path UNIT'S DIRECTIVES (systemd.path(5)). A path unit activates its unit when a condition on a +// watched path becomes true, through inotify, with no polling and no clock -- which is what makes it +// the event source for a directory other processes write into. The two conditions modeled differ in +// a way that matters to their consumers: DirectoryNotEmpty= is a LEVEL (it is re-checked when the +// activated unit finishes, so an entry that arrived while it ran starts it again), PathChanged= is an +// EDGE (a change while the activated unit is still running is coalesced into that run, and is not +// remembered after it). PathUnit= names the unit to activate; MakeDirectory= creates the watched +// directory if it is missing. +type SystemdPathDirective + = PathUnit { target: NonEmptyStr } + | DirectoryNotEmpty { path: NonEmptyStr } + | PathChanged { path: NonEmptyStr } + | MakeDirectory { enabled: Bool } + type SystemdInstallDirective = WantedBy { target: NonEmptyStr } | RequiredBy { target: NonEmptyStr } @@ -774,6 +788,15 @@ fn systemd_timer_directive_line(directive: SystemdTimerDirective) -> String { } } +fn systemd_path_directive_line(directive: SystemdPathDirective) -> String { + match directive { + PathUnit { target: v } => join(["Unit=", v as String], "") + DirectoryNotEmpty { path: v } => join(["DirectoryNotEmpty=", v as String], "") + PathChanged { path: v } => join(["PathChanged=", v as String], "") + MakeDirectory { enabled: v } => if v { "MakeDirectory=yes" } else { "MakeDirectory=no" } + } +} + // THE LINE HAS NO MATCH LEFT TO GET WRONG. Both halves come from an authority: the knob name from // `systemd_unit_property_wire` and the value from `systemd_directive_value_wire`, so a fifth knob is // a mint above and no edit here. @@ -836,6 +859,11 @@ type SystemdUnitFile slice: List, install: SystemdInstallSection, } + | PathUnitFile { + unit: List, + path: List, + install: SystemdInstallSection, + } // THE INSTANCE SPECIFIER IS systemd's, SO IT IS SPELLED HERE AND NOWHERE ELSE. // @@ -858,6 +886,8 @@ data systemd_timer_section_header: String = "[Timer]" data systemd_slice_section_header: String = "[Slice]" +data systemd_path_section_header: String = "[Path]" + data systemd_install_section_header: String = "[Install]" fn systemd_install_section_block(section: SystemdInstallSection) -> List> { @@ -933,6 +963,16 @@ fn systemd_unit_file_lines(file: SystemdUnitFile) -> List { systemd_install_section_block(section: inst), ), ) + PathUnitFile { unit: u, path: pd, install: inst } => + systemd_section_lines( + sections: concat( + [ + concat([systemd_unit_section_header], map(u, d => systemd_unit_directive_line(directive: d))), + concat([systemd_path_section_header], map(pd, d => systemd_path_directive_line(directive: d))), + ], + systemd_install_section_block(section: inst), + ), + ) } } diff --git a/dag/extdeps/tailscale/serve.dag b/dag/extdeps/tailscale/serve.dag index 069bb40421e..9b97024572b 100644 --- a/dag/extdeps/tailscale/serve.dag +++ b/dag/extdeps/tailscale/serve.dag @@ -4,7 +4,7 @@ import extdeps.exec.program { ProgramIdentity, uncataloged_program } import std.types { NonEmptyStr, Port, List, Bool, Unit } import std.algebra { Less, Equal, Greater } -import extdeps.version.semver { SemVerVersion, semver_compare, semver_version_label } +import extdeps.version.semver { SemVerVersion, semver_compare, semver_version_label, semver_version_parse } import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } @@ -295,13 +295,11 @@ fn tailscale_serve_status_route_proxy_targets(status_json: String, key: Tailscal // guards is a teardown that silently removes nothing and reports success.\n\nThe boundary is stated // as the version the upstream itself names, not as a version this repo picked. -data tailscale_scoped_serve_off_minimum: SemVerVersion = SemVerVersion { - major: 1, - minor: 52, - patch: 0, - pre_release: [], - build: [], -} +// THE BOUNDARY IS THE PARSE RESULT, never a hand-built record: the fields of a SemVerVersion +// are confined code-point runs (semver_numeric_field is the only admission), so the boundary +// arrives through the parser and an unadmitted label is carried as a refusal to every +// consumer, not a fabricated version (review 77150). +data tailscale_scoped_serve_off_minimum: SemVerVersion? = semver_version_parse(cps: chars(s: "1.52.0" as String)) type TailscaleServeCapability = ScopedRemovalSupported { observed: SemVerVersion } @@ -310,10 +308,14 @@ type TailscaleServeCapability // THE VERDICT CARRIES WHAT IT WAS COMPUTED FROM. A Bool capability cannot be re-derived when the boundary moves or the host is upgraded, and cannot say WHICH client it is describing — so the observed version rides on both decided arms and only the undecided arm carries prose.\n\nIt is a SemVerVersion rather than the version string tailscale prints, grounded on extdeps.version.semver, because an earlier draft of this row held both a display string and a parsed major/minor pair — two representations of one fact, free to disagree, which is the exact defect class this module is being repaired for. The label is a projection (semver_version_label), so there is nothing to keep in sync.\n\nComparison is semver_compare rather than a hand-written major/minor ladder, for the same reason: the ordering is already modeled, and a second ladder would be a second answer. fn tailscale_scoped_removal_capability(observed: SemVerVersion) -> TailscaleServeCapability { - match semver_compare(a: observed, b: tailscale_scoped_serve_off_minimum) { - Less => ScopedRemovalUnsupported { observed: observed } - Equal => ScopedRemovalSupported { observed: observed } - Greater => ScopedRemovalSupported { observed: observed } + match tailscale_scoped_serve_off_minimum { + Absent => ScopedRemovalUnobserved { reason: "the serve-off boundary label failed to parse — a defect in the boundary row, not an observation gap" } + Present { value: minimum } => + match semver_compare(a: observed, b: minimum) { + Less => ScopedRemovalUnsupported { observed: observed } + Equal => ScopedRemovalSupported { observed: observed } + Greater => ScopedRemovalSupported { observed: observed } + } } } diff --git a/dag/extdeps/tools/coreutils_truncate.dag b/dag/extdeps/tools/coreutils_truncate.dag new file mode 100644 index 00000000000..9c2a3072cec --- /dev/null +++ b/dag/extdeps/tools/coreutils_truncate.dag @@ -0,0 +1,29 @@ +module extdeps.tools.coreutils_truncate + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import std.types { Bool, NonEmptyStr, Unit } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.gnu.org/software/coreutils/manual/html_node/truncate-invocation.html" + } +} + +// `truncate -s N FILE` creates FILE if it is absent and sets its size to EXACTLY N bytes; bytes it +// adds read as zero (coreutils "truncate invocation"). -s takes a bare decimal byte count here, never +// a suffixed one, so the size is the caller's integer and nothing the tool reinterprets. -- ends the +// options, so a path beginning with '-' is a path. +service coreutils.Truncate { + operation ToSize { + requires none + input { size_bytes: NonEmptyStr, path: NonEmptyStr } + output { success: Bool from "exit_success" } + transport shell { argv: ["truncate", "-s", "{size_bytes}", "--", "{path}"] } + exit { + 0 => Unit + nonzero => String "truncate could not size the path" + } + } +} diff --git a/dag/extdeps/tools/findutils.dag b/dag/extdeps/tools/findutils.dag index 971008edf0f..595cd5faeb4 100644 --- a/dag/extdeps/tools/findutils.dag +++ b/dag/extdeps/tools/findutils.dag @@ -101,3 +101,27 @@ fn find_owner_executable_files_command(root: String) -> ArgvCommand { fn find_symlink_targets_command(root: String) -> ArgvCommand { argv_command(program: find_program(), arguments: [root, "-type", "l", "-printf", "l %P -> %l\\n"]) } + +// THE REGULAR FILES UNDER A ROOT WITH THEIR SIZE AND MODIFICATION TIME, ONE +// `\t\t` LINE EACH, AND NOTHING READ BUT METADATA. Every -printf +// directive here is answered from the inode find already holds from its own stat(2) of the entry +// (find manual, "Format Directives"): %s is st_size in bytes, %T@ is st_mtime as seconds since the +// epoch with a fractional part, %P is the path with the starting point removed. No directive opens +// the file, and the expression carries no action that runs another program, so the command reads +// no file content -- find_command_runs_another_program below is how a consumer checks the second +// half on the value rather than on this note. The path is LAST so a name holding a tab survives as +// the rest of the line; the escapes are find's own, so the argv word carries a backslash and a +// letter, never a control character. +fn find_regular_file_size_mtime_command(root: String) -> ArgvCommand { + argv_command(program: find_program(), arguments: [root, "-type", "f", "-printf", "%s\\t%T@\\t%P\\n"]) +} + +// THE ACTIONS THAT MAKE find RUN ANOTHER PROGRAM ON WHAT IT MATCHED (find manual, "Run Commands"): +// -exec and -execdir, and their prompting forms -ok and -okdir. find itself reads directory entries +// and inodes; every content reader it can reach -- cat, sha256sum, grep -- arrives through one of +// these four. find_regular_files_sha256_command above is a real builder that uses one. +data find_program_running_actions: List = ["-exec", "-execdir", "-ok", "-okdir"] + +fn find_command_runs_another_program(command: ArgvCommand) -> Bool { + any(command.arguments, a => any(find_program_running_actions, x => a == x)) +} diff --git a/dag/extdeps/tools/gnu_coreutils.dag b/dag/extdeps/tools/gnu_coreutils.dag index 9d301182af4..e9f8de17282 100644 --- a/dag/extdeps/tools/gnu_coreutils.dag +++ b/dag/extdeps/tools/gnu_coreutils.dag @@ -38,6 +38,7 @@ data min_coreutils_version: VersionConstraint = ">= 8.0" // criterion for climbing to an absolute row, and the counter-example that makes the distinction // load-bearing are recorded once at extdeps.exec.command program_spelling_is_an_observation_claim. fn cat_program() -> ProgramIdentity = uncataloged_program(invocation: "cat") +fn nproc_program() -> ProgramIdentity = uncataloged_program(invocation: "nproc") fn mktemp_program() -> ProgramIdentity = uncataloged_program(invocation: "mktemp") fn cp_program() -> ProgramIdentity = uncataloged_program(invocation: "cp") fn printf_program() -> ProgramIdentity = uncataloged_program(invocation: "printf") @@ -78,6 +79,15 @@ fn mkdir_parents_command(path: String) -> ArgvCommand { argv_command(program: mkdir_program(), arguments: ["-p", "--", path]) } +// mkdir WITHOUT -p IS CREATE-ONLY: it creates exactly one directory and REFUSES with "File exists" +// when the name is already taken, and it refuses when the parent is missing rather than creating it +// (coreutils manual, "mkdir invocation"). The kernel's mkdir(2) is atomic, so two invocations +// racing on one name leave one winner and one refusal and never a merged result -- which is what +// lets the NAME of the directory be the record and a repeated invocation be harmless. +fn mkdir_create_only_command(path: String) -> ArgvCommand { + argv_command(program: mkdir_program(), arguments: ["--", path]) +} + // ln WITHOUT -s IS A HARD LINK, and without -f it REFUSES when the destination exists (coreutils // "ln invocation"): the create-only publication step. A second name for the same inode, never a // replacement of the bytes behind an existing name. @@ -100,6 +110,12 @@ fn cat_command(path: String) -> ArgvCommand { argv_command(program: cat_program(), arguments: [path]) } +// nproc WITH NO OPERANDS PRINTS THE PROCESSING UNITS AVAILABLE TO THE CURRENT PROCESS (GNU coreutils +// nproc), so a reading taken inside a cgroup or affinity mask is that bound, not the machine's count. +fn nproc_command() -> ArgvCommand { + argv_command(program: nproc_program(), arguments: []) +} + // od(1) OVER ONE BYTE SPAN, AS HEX: -j skips `offset` bytes and -N reads at most `length`, both byte // counts (coreutils "od invocation"); -An drops the address column, -tx1 prints one two-digit lowercase // hex byte per unit, and -v prints every line rather than folding repeats into `*` -- without it a run diff --git a/dag/extdeps/tools/gzip.dag b/dag/extdeps/tools/gzip.dag index a2a3e845cb9..d0a78421957 100644 --- a/dag/extdeps/tools/gzip.dag +++ b/dag/extdeps/tools/gzip.dag @@ -58,3 +58,19 @@ service gzip.Gzip { } } } + +// COMPRESS ONE FILE BESIDE ITSELF: `--keep --best --force`, which writes .gz at level 9, keeps +// the input, and replaces a stale .gz (gzip manual, "Invoking gzip"). The compressed size is a +// stat of .gz; it is how a caller asks whether bytes are compressible without carrying them. +service gzip.Compress { + operation BesideKeepBest { + requires none + input { path: NonEmptyStr } + output { success: Bool from "exit_success" } + transport shell { argv: ["gzip", "--keep", "--best", "--force", "--", "{path}"] } + exit { + 0 => Unit + nonzero => String "gzip could not compress the path" + } + } +} diff --git a/dag/extdeps/tools/hostname.dag b/dag/extdeps/tools/hostname.dag index 922621bbc0e..b4dbb0fb777 100644 --- a/dag/extdeps/tools/hostname.dag +++ b/dag/extdeps/tools/hostname.dag @@ -38,6 +38,17 @@ data extdeps_hostnamectl_external_authority_anchor: ExternalAuthority = External } } +// TWO READ ROWS BECAUSE THEY ANSWER DIFFERENT QUESTIONS. Bare `hostname` prints whatever the host is +// configured to call itself, which may be fully qualified; `hostname -s` prints the first label only. +// The reach probe compares its read for EXACT equality against the enrolled fleet slot label (the +// short name a host is known by on the LAN), so it needs the configured name the host itself reports +// — not a re-derived first label — and the probe therefore owns this row, the bare spelling, while a +// converge that matches by short label owns `hostname_short_read_argv`. Collapsing the two would +// silently change which name a caller compared. +fn hostname_read_argv() -> List { + [hostname_program().invocation as String] +} + fn hostname_short_read_argv() -> List { ["hostname", "-s"] } diff --git a/dag/extdeps/tools/id.dag b/dag/extdeps/tools/id.dag index 5230ceffcdc..16b79d1ecc8 100644 --- a/dag/extdeps/tools/id.dag +++ b/dag/extdeps/tools/id.dag @@ -28,6 +28,19 @@ fn id_uid_argv() -> List { [id_program().invocation as String, "-u"] } +// NO OPERAND, DELIBERATELY: id(1) with no operand answers from the CALLING PROCESS's own +// credentials, established at its exec and never changing afterwards. The reach probe asks "which +// account did the transport actually reach", and the only account that can say is the one the session +// IS — so the probe supplies no user and reads the login name the session itself carries. A user +// operand would ask the account database about a different account and answer the wrong question. +// +// -un, NOT -u: -n selects NAMES over numeric uids (the probe records the login name it was granted, +// not a per-host uid), and -u then prints that name alone rather than the full `id` record. One flag +// for the name, one for the field; the probe trims the newline the remote shell leaves on stdout. +fn id_user_name_argv() -> List { + [id_program().invocation as String, "-un"] +} + fn id_gid_argv() -> List { [id_program().invocation as String, "-g"] } diff --git a/dag/extdeps/tools/node.dag b/dag/extdeps/tools/node.dag index d6772b9402a..c688a65814b 100644 --- a/dag/extdeps/tools/node.dag +++ b/dag/extdeps/tools/node.dag @@ -114,3 +114,8 @@ fn node_version_command(node_binary: NonEmptyStr) -> ArgvCommand { fn node_eval_command(node_binary: NonEmptyStr, script: String, operands: List) -> ArgvCommand { argv_command(program: cataloged_program(invocation: node_binary), arguments: concat(["-e", script], operands)) } + +// `node \"}function refuse(r,key)\{r.writeHead(599,\{'Content-Type':'text/plain'});r.end('unmodeled request: '+key)}function answer(r,key)\{const t=apply(key);if(!t)return refuse(r,key);r.writeHead(t.status,\{'Content-Type':'application/json'});r.end(t.body)}const s=http.createServer((q,r)=>\{let body='';q.on('data',d=>body+=d);q.on('end',()=>\{ if(q.url==='/')\{log('spa-root served');r.writeHead(200,\{'Content-Type':'text/html'});r.end(\"\");return} if(q.url==='/login.html')\{r.writeHead(200,\{'Content-Type':'text/html'});r.end('');return} if(q.url==='/viewer.html')\{r.writeHead(200,\{'Content-Type':'text/html'});r.end(viewer());return} if(q.url==='/api/session'&&q.method==='POST')\{const p=new URLSearchParams(body);return answer(r,key('login',[p.get('username')||'',p.get('password')||'']))} if(q.url==='/api/session'&&q.method==='DELETE')\{return answer(r,key('logout',[q.headers['x-csrftoken']||'']))} if(q.url==='/api/settings/services')\{return answer(r,key('services',[]))} r.writeHead(404);r.end()})});s.on('upgrade',(q,sock)=>\{const u=new URL(q.url,'http://x');if(u.pathname!==kvmPath)\{sock.destroy();return} const t=apply(key('connect',[u.searchParams.get('token')||'']));if(!t||t.status!==101)\{log('kvm refused');sock.destroy();return} const k=crypto.createHash('sha1').update(q.headers['sec-websocket-key']+'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64'); sock.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: '+k+'\\r\\n\\r\\n'); sock.write(Buffer.from([0x81,0x05,0x66,0x72,0x61,0x6d,0x65]));socket=sock;log('kvm-open');sock.on('close',()=>\{if(socket===sock)socket=null})});const started=Date.now();for(const a of advances)\{setTimeout(()=>apply(key('advance',[String(a)])),a*1000)}img.listen(0,'127.0.0.1',()=>s.listen(0,'127.0.0.1',()=>log('listening start='+selfStart+' port='+s.address().port)));" - -// WHETHER THE TRANSPORT IS SERVING, AND IF NOT WHY. Readiness is bound to the whole launch INSTANCE: -// only a listening line carrying BOTH the pid and the start time the owned launch recorded counts, so a -// stale line from an earlier process -- even one the kernel gave the same pid -- never reads as ready. -type MegaRacWebTransportCause - = TransportLaunchRefused { detail: String } - | TransportPidUnpublished - | TransportCleanupUnresolved { line: String } - | TransportExitedBeforeReady { exit_line: String } - | TransportReadinessForeign { lines: List } - | TransportReadinessUnobserved { standing: OwnedProcessStanding } - -type MegaRacWebTransportStart - = TransportServing { port: String, pid: String } - | TransportNotServing { cause: MegaRacWebTransportCause } - -fn lines_of(text: String) -> List { - filter(split(s: text, delimiter: "\n"), l => l != "") -} - -fn listening_port(line: String) -> String { - match split(s: line, delimiter: " port=").skip(n: 1).first() { - Present { value: p } => trim(s: p) - Absent => "" - } -} - -// A process that is not running is a transport that exited before ready, once its exit line is in the output. -fn megarac_web_transport_exit_readiness(exit_lines: List) -> MegaRacWebTransportStart? { - match exit_lines.first() { - Present { value: l } => Present { value: TransportNotServing { cause: TransportExitedBeforeReady { exit_line: l } } } - Absent => none - } -} - -// ONE READINESS OBSERVATION, pure over what was read. -fn megarac_web_transport_readiness(standing: OwnedProcessStanding, out: String, events: String) -> MegaRacWebTransportStart? { - let listening: List = filter(lines_of(text: events), l => string_contains(s: l, pattern: " listening start=")) - let exit_lines: List = filter(lines_of(text: out), l => string_contains(s: l, pattern: owned_process_exit_prefix)) - if string_contains(s: out, pattern: owned_process_unpublished_survived_line) { - Present { value: TransportNotServing { cause: TransportCleanupUnresolved { line: owned_process_unpublished_survived_line } } } - } else if string_contains(s: out, pattern: owned_process_unpublished_line) { - Present { value: TransportNotServing { cause: TransportPidUnpublished } } - } else { - match standing { - OwnedRunning { pid: p, start_time: st } => { - let mine: List = filter(listening, l => string_contains(s: l, pattern: concat(" pid=", concat(p, concat(" listening start=", concat(st, " port=")))))) - match mine.first() { - Present { value: l } => Present { value: TransportServing { port: listening_port(line: l), pid: p } } - Absent => none - } - } - OwnedGone { pid: _ } => megarac_web_transport_exit_readiness(exit_lines: exit_lines) - OwnedForeign { pid: _, observed_start: _ } => megarac_web_transport_exit_readiness(exit_lines: exit_lines) - OwnedUnreadable { pid: _, detail: _ } => megarac_web_transport_exit_readiness(exit_lines: exit_lines) - OwnedUnparseable { pid: _, detail: _ } => megarac_web_transport_exit_readiness(exit_lines: exit_lines) - OwnedNotRecorded { detail: _ } => megarac_web_transport_exit_readiness(exit_lines: exit_lines) - } - } -} - -fn megarac_web_transport_await(pid_path: String, out_path: String, log_path: String, remaining: Int) -> MegaRacWebTransportStart { - let standing = owned_process_standing(pid_path: pid_path) - let out = Filesystem.Read(path: out_path) - let events = Filesystem.Read(path: log_path) - match megarac_web_transport_readiness(standing: standing, out: if out.success { out.content } else { "" }, events: if events.success { events.content } else { "" }) { - Present { value: answer } => answer - Absent => - if remaining <= 0 { - let listening: List = filter(lines_of(text: if events.success { events.content } else { "" }), l => string_contains(s: l, pattern: " listening start=")) - if count(listening) > 0 { TransportNotServing { cause: TransportReadinessForeign { lines: listening } } } - else { TransportNotServing { cause: TransportReadinessUnobserved { standing: standing } } } - } else { - let _paced = sleep_delay_seconds_second_carrier_projection(duration: second(count: 1)) - megarac_web_transport_await(pid_path: pid_path, out_path: out_path, log_path: log_path, remaining: remaining - 1) - } - } -} - -data megarac_web_transport_ready_polls: Int = 10 - -// SERVE a table from a directory the caller owns, under the node binary it names (the declared runner -// toolchain's). The launch is gunbc.owned_process's; readiness is awaited and typed. -fn megarac_web_transport_serve(dir: String, table: String, node_binary: String) -> MegaRacWebTransportStart { - let pid_path = concat(dir, "/transport.pid") - let out_path = concat(dir, "/transport.out") - let log_path = concat(dir, "/transport.log") - let table_path = concat(dir, "/table.json") - let wrote = Filesystem.Write(path: table_path, content: table) - if !wrote.success { return TransportNotServing { cause: TransportLaunchRefused { detail: concat("the table could not be written: ", wrote.error) } } } - let launched = gunbc.owned_process.launch.LaunchOwned( - pid_path: pid_path as NonEmptyStr, - log_path: out_path as NonEmptyStr, - command: [node_binary, "-e", megarac_web_transport_script, table_path, log_path], - ) - if !launched.success { return TransportNotServing { cause: TransportLaunchRefused { detail: trim(s: launched.transport_stderr) } } } - megarac_web_transport_await(pid_path: pid_path, out_path: out_path, log_path: log_path, remaining: megarac_web_transport_ready_polls) -} - -fn megarac_web_transport_stop(dir: String) -> OwnedRelease { - owned_process_release(pid_path: concat(dir, "/transport.pid")) -} diff --git a/dag/gunbc/bmc_model.dag b/dag/gunbc/bmc_model.dag index f78873b1d0f..18f983c77a0 100644 --- a/dag/gunbc/bmc_model.dag +++ b/dag/gunbc/bmc_model.dag @@ -119,11 +119,11 @@ fn bmc_world(power: BmcPower) -> BmcWorld { } } -// THE WEB AND KVM SURFACE OF A MegaRAC SP-X CONTROLLER (firmware 0.32; operations and wire shapes in -// extdeps.bmc.megarac, cited there to the served source.min.js and viewer.min.js). World state only: +// THE WEB AND KVM SURFACE OF A MANAGEMENT CONTROLLER, as this repository's own model (no vendor wire +// shape is carried here). World state only: // the web sessions the controller holds, the account it accepts, the KVM viewers other clients hold, // and the one KVM stream this world's client may open. `canvas_readable` is a BROWSER-side fact the -// protocol adapter reads -- whether the page's canvas can be exported -- and no transition changes it. +// a protocol adapter would read -- whether the page's canvas can be exported -- and no transition changes it. type BmcWebSession { id: Int csrf_token: String @@ -243,7 +243,7 @@ fn bmc_web_session_of_token(world: BmcWorld, csrf_token: String) -> List s.csrf_token == csrf_token) } -// GET /api/settings/services, the kvm row: every viewer the controller is serving, ours included. +// The KVM viewer count: every viewer the controller is serving, ours included. fn bmc_kvm_viewer_count(world: BmcWorld) -> Int { world.web.other_kvm_viewers + match world.web.kvm { BmcKvmStreaming { session: _ } => 1 @@ -259,12 +259,11 @@ type BmcKvmConnectStep { // The /kvm WebSocket for a session this world holds. A token naming no session is refused. // -// OCCUPIED SEAT, A MODELING DECISION (not a captured 0.32 fact): while a stream is held -- by this +// OCCUPIED SEAT, A MODELING DECISION (not a captured fact): while a stream is held -- by this // world's client or by another viewer (other_kvm_viewers) -- a further connect is REFUSED, never -// allowed to take the stream over. The 0.32 UI checks viewer_count before it opens viewer.html and -// reports "only one KVM session is allowed", but a read of viewer_count does not exclude a competing -// connect arriving after it, so the model answers the connect itself. A captured multi-viewer answer -// from the controller replaces this decision. +// allowed to take the stream over. A read of the viewer count does not exclude a competing connect +// arriving after it, so the model answers the connect itself. A captured multi-viewer answer from a +// controller replaces this decision. fn bmc_kvm_connect(world: BmcWorld, csrf_token: String) -> BmcKvmConnectStep { let occupied = bmc_kvm_viewer_count(world: world) > 0 match bmc_web_session_of_token(world: world, csrf_token: csrf_token).first() { diff --git a/dag/gunbc/board_authority_binding.dag b/dag/gunbc/board_authority_binding.dag new file mode 100644 index 00000000000..77e7f955a01 --- /dev/null +++ b/dag/gunbc/board_authority_binding.dag @@ -0,0 +1,137 @@ +module gunbc.board_authority_binding + +import std.types { String, NonEmptyStr, List } +import std.optional { Present, Absent } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import std.content_hash { Sha256Digest } +import extdeps.board_authority { + BoardAuthorityBinding, BoardAuthorityLookup, BoardFruIdentity, BoardFruObservation, BoardFruRead, BoardFruNotRead, + BoardAreaAbsent, BoardAreaFieldsUnreadable, BoardFruReceipt, + BoundByNameOnly, BoundByObservedEvidence, board_authority_for +} +import gunbc.ipmitool_fru_text { FruCaptureGap, FruCaptureUnreadable, FruCaptureDigestAgrees, FruCaptureDigestDiffers, fru_capture_digest_check, FruFieldValue, FruFieldGap, FruFieldAbsent, FruFieldRepeated, FruFieldEmpty, fru_field } +import extdeps.filesystem.filesystem_io { Filesystem, FilesystemReadRefused, FilesystemReadSucceeded, filesystem_read_outcome } +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } +import extdeps.ocp.mt_jade.subject { mt_jade_board_authority } + +// THE CONSUMER-LAYER BINDING TABLE (DESIGN section 3, External upstream decomposition). A generic +// hub may not enumerate concrete products or dispatch among their authorities, so the rows that +// join a FRU's names to a per-board authority live here, in the workflow layer. Adding a board is +// one per-board module plus one row here; no enum is edited. +// +// MT. COLLINS (FOXCONN, "Mt. Collins"): evidence-bound. The FRU names agree AND the host's own +// physical receipts match the Getting Started Guide's figure (the 32-DIMM bring-up and the +// physical-orientation anchors), so board FACTS may be read off this authority. +data mtcollins1_fru_receipt: BoardFruReceipt = BoardFruReceipt { + artifact_path: "artifacts/bmc/mtcollins1-controller-fru-2026-09-12.txt", + sha256: Sha256Digest { hex: "dc63dabe07764dcf7688a74230e7fe1a6151ea384435f10460a1979eda637d3c" }, +} + +data mt_collins_board_binding: BoardAuthorityBinding = BoardAuthorityBinding { + manufacturer: "FOXCONN", + product: "Mt. Collins", + authority: mt_collins_board_authority, + standing: BoundByObservedEvidence { + first: DeclarationRef { + module_path: "gunbc.machine_intake_mtcollins1_32dimm_bringup_observation", + decl_name: "mtcollins1_dram_training_capture", + field: WholeDeclaration + }, + further: [DeclarationRef { + module_path: "gunbc.machine_intake_mtcollins1_physical_orientation", + decl_name: "mtcollins1_three_stick_anchor", + field: WholeDeclaration + }] + }, + fru_receipt: mtcollins1_fru_receipt, +} + +// MT. JADE (WIWYNN, "Mt.Jade Motherboard", part B81.03010.0041): name-only. The FRU's names agree +// with the OCP Mt. Jade reference platform's name and nothing else does yet, so this is an +// inference from a name (DESIGN 4d), typed as a bet with the discriminator that would settle it. +// Identity-grade uses accept it; uses that import board facts refuse it. +data mtjade1_fru_receipt: BoardFruReceipt = BoardFruReceipt { + artifact_path: "artifacts/bmc/mtjade1-ipmitool-fru-print-192.168.1.246-2026-10-04T110207Z.txt", + sha256: Sha256Digest { hex: "1800e7394dc06a023dc4d9cda481a63fe3cf9f07ed20cb0491746c61e9baf46b" }, +} + +data mt_jade_board_binding: BoardAuthorityBinding = BoardAuthorityBinding { + manufacturer: "WIWYNN", + product: "Mt.Jade Motherboard", + authority: mt_jade_board_authority, + standing: BoundByNameOnly { + discriminator_obligation: "a measured reading on the host (DIMM population or SMBIOS slot map) agreeing with a CITED public Mt. Jade source, not the FRU name" + }, + fru_receipt: mtjade1_fru_receipt, +} + +data board_authority_bindings: List = [mt_collins_board_binding, mt_jade_board_binding] + +fn observed_board_authority(observation: BoardFruObservation) -> BoardAuthorityLookup { + board_authority_for(observation: observation, bindings: board_authority_bindings) +} + +// THE BOARD AREA OF `ipmitool fru print` TEXT, read into the observation the lookup consumes: Board +// Mfg and Board Product are required (exact text, one line each); Board Part Number is carried when +// present. Never defaulted: no board area is BoardAreaAbsent, a present area with an empty or +// repeated required field is BoardAreaFieldsUnreadable. +fn board_fru_observation_of_text(content: String) -> BoardFruObservation { + match fru_field(content: content, field: "Board Mfg") { + FruFieldGap { gap: mg } => + match fru_field(content: content, field: "Board Product") { + FruFieldGap { gap: pg } => + match mg { + FruFieldAbsent { field: _ } => match pg { + FruFieldAbsent { field: _ } => BoardFruNotRead { cause: BoardAreaAbsent } + FruFieldRepeated { field: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + FruFieldEmpty { field: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + } + FruFieldRepeated { field: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + FruFieldEmpty { field: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + } + FruFieldValue { value: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + } + FruFieldValue { value: mfg } => + match fru_field(content: content, field: "Board Product") { + FruFieldGap { gap: _ } => BoardFruNotRead { cause: BoardAreaFieldsUnreadable } + FruFieldValue { value: product } => + BoardFruRead { fru: BoardFruIdentity { + manufacturer: mfg, + product: product, + part_number: match fru_field(content: content, field: "Board Part Number") { + FruFieldValue { value: pn } => Present { value: pn } + FruFieldGap { gap: _ } => none + }, + } } + } + } +} + +fn receipt_sha256_hex(receipt: BoardFruReceipt) -> String { + receipt.sha256.hex as String +} + +// A COMMITTED CAPTURE EITHER YIELDS A BOARD OBSERVATION OR A CAPTURE-LEVEL GAP. The gap is the one +// gunbc.ipmitool_fru_text FruCaptureGap (unreadable file, digest mismatch) the Mt. Jade identity read +// also uses, with the one digest check fru_capture_digest_check; the board hub carries only what the +// FRU Board Area itself can fail to say. +type BoardCaptureRead + = BoardCaptureObserved { observation: BoardFruObservation } + | BoardCaptureRefused { gap: FruCaptureGap } + +// THE DIGEST-CHECKED READ of a committed FRU capture: content whose sha256 differs from the receipt +// the binding row was authored from is a refusal, never decoded. +fn board_fru_observation_of_capture(content: String, receipt: BoardFruReceipt) -> BoardCaptureRead { + match fru_capture_digest_check(content: content, expected_sha256: receipt_sha256_hex(receipt: receipt) as NonEmptyStr) { + FruCaptureDigestDiffers { gap: g } => BoardCaptureRefused { gap: g } + FruCaptureDigestAgrees => BoardCaptureObserved { observation: board_fru_observation_of_text(content: content) } + } +} + +fn read_board_fru_observation(receipt: BoardFruReceipt) -> BoardCaptureRead uses fs: std.resources.Filesystem { + let read = Filesystem.Read(path: receipt.artifact_path as String) + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error: e } => BoardCaptureRefused { gap: FruCaptureUnreadable { error: e } } + FilesystemReadSucceeded { content: text } => board_fru_observation_of_capture(content: text, receipt: receipt) + } +} diff --git a/dag/gunbc/boot_artifact_delivery.dag b/dag/gunbc/boot_artifact_delivery.dag index 0b0f1f14590..0e2da32f776 100644 --- a/dag/gunbc/boot_artifact_delivery.dag +++ b/dag/gunbc/boot_artifact_delivery.dag @@ -58,6 +58,7 @@ import extdeps.bmc.access_profile { } import gunbc.machine_intake_access { AccessContextBound, + AccessContextBoardNotFactsBound, AccessContextCatalogRefused, AccessContextControlRouteEvidenceMismatch, AccessContextObservationEvidenceMismatch, @@ -91,6 +92,7 @@ import gunbc.boot_artifact { OperatingSystemInstaller, intake_artifact_for, } +import gunbc.board_authority_binding { board_authority_bindings } import gunbc.machine_intake_subject { MachineIntakeSubject } import gunbc.machine_intake_receipt { EvidenceRef, same_intake_subject } import gunbc.install_transport_qualification { @@ -523,7 +525,7 @@ fn bind_boot_delivery_target(target: BootDeliveryTarget, access: BoundBmcAccessC // context's capability row must NAME the capability, and the profile and the // observation the context was bound from must both admit the surface. fn surface_candidate_standing(ctx: BoundBmcAccessContext, surface: BmcAccessSurface) -> BootDeliveryCandidateStanding { - match bmc_surface_eligibility(profile: ctx.profile, observation: ctx.observation, surface: surface) { + match bmc_surface_eligibility(profile: ctx.profile, observation: ctx.observation, surface: surface, bindings: board_authority_bindings) { SurfaceEligible => CandidateEligible SurfaceAbsentFromProfile => CandidateIneligible { cause: CandidateSurfaceAbsentFromProfile { surface: surface } } SurfaceAbsentFromLiveObservation => CandidateIneligible { cause: CandidateLiveSurfaceAbsent { surface: surface } } @@ -614,7 +616,7 @@ fn nbd_proxy_websocket_candidate(ctx: BoundBmcAccessContext, request: BootDelive // MegaRAC's proprietary remote media: the OEM capability, the OEM surface, AND // the route fact its start-media contract calls required. A profile whose // MegaRAC route leaves image_redirection Unset cannot plan this arm — that is -// the executed refusal (opaque codes 13410/13460) modeled as a standing. +// the executed refusal modeled as a standing. fn megarac_rest_remote_media_candidate(ctx: BoundBmcAccessContext, request: BootDeliveryRequest) -> BootDeliveryCandidateStanding { if release_row_has_capability(row: ctx.capability_row, cap: CapabilityOemRemoteMedia) == false { CandidateIneligible { cause: CandidateCapabilityAbsent } @@ -1054,6 +1056,7 @@ fn solve_boot_artifact_delivery( AccessContextObservationEvidenceMismatch { endpoint: _, manifest: _, raw_response: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } AccessContextProbeEvidenceMismatch { endpoint: _, manifest: _, raw_response: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } AccessContextControlRouteEvidenceMismatch { endpoint: _, manifest: _, raw_response: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } + AccessContextBoardNotFactsBound { observed_identity: _, board: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } AccessContextProfileUncatalogued { observed_identity: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } AccessContextProfileAmbiguous { observed_identity: _, count: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } AccessContextCatalogRefused { standing: _ } => BootDeliveryRefused { cause: DeliveryAccessUnbound { standing: ev.access } } diff --git a/dag/gunbc/build_target.dag b/dag/gunbc/build_target.dag index 133aebf5742..8d4c7bad6cf 100644 --- a/dag/gunbc/build_target.dag +++ b/dag/gunbc/build_target.dag @@ -500,10 +500,12 @@ type TargetStandingLookup | StandingLookupLabelNotCanonical { target: Label, cause: LabelCanonicalityCause } // THE LOOKUP SPELLING IS `lookup`, NOT `map_get`, AND THE REASON IS RESOLUTION RATHER THAN TASTE. -// Two declarations answer to the bare name `map_get` — the builtin, returning `Optional`, and -// `v2.std.collection.map_get`, returning `Outcome>` — so in a module whose imports make -// both visible the reference is ambiguous and refuses outright. `lookup` is the dual-dispatch -// chokepoint, has one declaration, and already returns the `Optional` these matches read. +// HISTORICALLY the bare name `map_get` answered to two declarations — the builtin, returning +// `Optional`, and `v2.std.collection`'s declared projection, returning `Outcome>` — +// so in a module whose imports made both visible the reference was ambiguous and refused outright. +// The declared projection now answers to `map_get_checked` alone (the fork is dissolved), and +// `lookup` remains the dual-dispatch chokepoint: one declaration, and it already returns the +// `Optional` these matches read. fn target_standing_lookup(index: TargetStandingIndex, target: Label) -> TargetStandingLookup { match label_canonicality(label: target) { LabelNonCanonical { cause: cause } => diff --git a/dag/gunbc/census_closure_frontier.dag b/dag/gunbc/census_closure_frontier.dag index bd31e98983b..1b075493367 100644 --- a/dag/gunbc/census_closure_frontier.dag +++ b/dag/gunbc/census_closure_frontier.dag @@ -21,11 +21,11 @@ import extdeps.docker.container_stats { } import extdeps.docker.endpoint { endpoint_string_type_frontier_rows } import extdeps.github.merge_state { merge_state_graphql_transport_frontier_rows } +import gunbc.runner.runner_qualification_dispatch { qualification_instrument_extraction_frontier_rows } import extdeps.github.workflow_runs { conclusion_authority_consolidation_frontier_rows, } import extdeps.github.app { modeled_webhook_event_vocabulary_frontier_rows } -import extdeps.github.workflows { create_dispatch_unconsumed_frontier_rows } import gunbc.public_workload_census { public_workload_census_replay_consumer_frontier_rows, public_workload_census_run_attempt_consumer_frontier_rows, @@ -110,7 +110,7 @@ fn census_closure_frontier_row_groups() -> List> { endpoint_string_type_frontier_rows, merge_state_graphql_transport_frontier_rows, conclusion_authority_consolidation_frontier_rows, - create_dispatch_unconsumed_frontier_rows, + qualification_instrument_extraction_frontier_rows, modeled_webhook_event_vocabulary_frontier_rows, census_app_installation_route_frontier_rows, census_app_installation_population_frontier_rows, diff --git a/dag/gunbc/ci/ci_deploy_access.dag b/dag/gunbc/ci/ci_deploy_access.dag index feeb259bd91..07213a79511 100644 --- a/dag/gunbc/ci/ci_deploy_access.dag +++ b/dag/gunbc/ci/ci_deploy_access.dag @@ -405,3 +405,21 @@ fn srv1_live_deploy_privileged_commands() -> List { p: fleet_posix_ci_runner_principal() ) } + +// EVERY ACCESS THAT APPLIES A DEPLOYMENT, AS ONE ROSTER. A host-shared resource whose content is a +// function of who deploys to that host -- /etc/sudoers.d/gunbc-deploy -- is derived from this list +// filtered by host, never from the one access that happens to be running, so applying any +// deployment through any access renders the same bytes (gunbc.ci_deploy_sudoers +// deploy_sudoers_dropin_body). A new access is a row here; one left out is a principal whose grants +// the next apply through another access would delete. +fn deploy_access_roster() -> List { + [ + ci_deploy_srv1_access, + ci_deploy_srv1_operator_access, + ci_deploy_srv1_operator_local_access, + ] +} + +fn deploy_accesses_on_host(host: ComputeHost) -> List { + filter(xs: deploy_access_roster(), predicate: a => (a.host.identity as String) == (host.identity as String)) +} diff --git a/dag/gunbc/ci/ci_deploy_sudoers.dag b/dag/gunbc/ci/ci_deploy_sudoers.dag index f2169cb8442..612da35305a 100644 --- a/dag/gunbc/ci/ci_deploy_sudoers.dag +++ b/dag/gunbc/ci/ci_deploy_sudoers.dag @@ -1,7 +1,8 @@ module gunbc.ci_deploy_sudoers import std.types { String, NonEmptyStr, List, Bool } -import v2.std.algebra { fold_list, any } +import v2.std.algebra { fold_list, any, list_flat_map } +import std.list { distinct_by_key } import extdeps.sudo.elevation { sudo_elevate_argv } import v2.extdeps.languages.bash_build { bash_build_command_from_lits, @@ -30,6 +31,7 @@ import extdeps.sudo.nopasswd_execute_probe_check_op { import gunbc.ci_deploy_access { DeployAccess, PrivilegedCommand, + deploy_accesses_on_host, gunbc_deploy_sudoers_dropin, deploy_access_sudoers_line, posix_sudo_grant_to_privileged, @@ -100,15 +102,27 @@ fn deploy_sudoers_line_for_grant(access: DeployAccess, g: PosixSudoGrant) -> Str ) } -// One line per grant, header first. The heredoc row (v2.workflow.bash_emit bash_emit_heredoc) +// THE DROP-IN IS HOST-SHARED, SO ITS BODY IS THE UNION OVER EVERY ACCESS THAT DEPLOYS TO THE HOST +// (gunbc.ci_deploy_access deploy_accesses_on_host), not the running access's own grants. Rendering +// from the running access alone made an operator-local apply rewrite the file to the operator's one +// line and delete the CI runner's grants. One line per (principal, grant), first occurrence kept +// in roster order, header first. The heredoc row (v2.workflow.bash_emit bash_emit_heredoc) // terminates the body itself, so the lines are joined without a trailing newline. -fn deploy_sudoers_dropin_body(access: DeployAccess) -> String { - join( - concat([deploy_sudoers_dropin_header()], map(access.principal.sudo_grants, g => deploy_sudoers_line_for_grant(access: access, g: g))), - "\n" +fn deploy_sudoers_dropin_lines_for(accesses: List) -> List { + distinct_by_key( + xs: list_flat_map(xs: accesses, f: a => map(a.principal.sudo_grants, g => deploy_sudoers_line_for_grant(access: a, g: g))), + key: l => l, ) } +fn deploy_sudoers_dropin_body_for(accesses: List) -> String { + join(concat([deploy_sudoers_dropin_header()], deploy_sudoers_dropin_lines_for(accesses: accesses)), "\n") +} + +fn deploy_sudoers_dropin_body(access: DeployAccess) -> String { + deploy_sudoers_dropin_body_for(accesses: deploy_accesses_on_host(host: access.host)) +} + // THE ELEVATION PREFIX IS READ FROM extdeps.sudo.elevation sudo_elevate_argv, not typed here, so the // two tokens that cross the privilege boundary have one authority in the repository. fn deploy_sudoers_elevated(command: List, tail: List) -> Node { diff --git a/dag/gunbc/ci/ci_failure_class.dag b/dag/gunbc/ci/ci_failure_class.dag index 5f9adffb8ed..9f5e6ff0de8 100644 --- a/dag/gunbc/ci/ci_failure_class.dag +++ b/dag/gunbc/ci/ci_failure_class.dag @@ -7,7 +7,7 @@ import gunbc.recurring_failure_mode.command_not_found_exit_cannot_distinguish_ev } import v2.std.node { Node } import v2.std.diagnostic { Outcome } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import v2.extdeps.languages.bash_build { bash_build_and_then, bash_build_assign, @@ -254,7 +254,7 @@ fn floor_outcome_wire(o: FloorOutcome) -> String { } fn posix_exit_status_decimal(code: Int) -> String { - integer_int_to_decimal_string(value: code) + int_to_decimal_string(value: code) } fn infra_signature_wire(sig: InfraSignature) -> String { diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index 78617e3ebb5..72cdc0daf96 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -321,6 +321,10 @@ data excl_bin_wet_reason: String = "bin-execution witness class: fns run compile data excl_bin_wet_dissolve: DissolutionCondition = unbound_dissolution(description: "the file migrates under the execution-corpus scope prefix and enrolls as ExecutionWitnessKind rows in gunbc.commit_workflow; then this row and the roster entries delete (bin_witness_wet_note)") +data excl_immutable_pin_no_ci_wet_lane_reason: String = "ioctl-set IMMUTABLE leaked-hold composition (test.claim.materialization_store_local_immutable_pin_wet_witness): pin G, second acquire/release, reuse leaked G. Needs CAP_LINUX_IMMUTABLE. No required wet lane grants that cap, so the file is BinWitnessWet and named in gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci_population: a changed identity is DeclinedNoCiWetLane, not PlannedAsChangedWitness without a terminal. The executing floor wall for the no-hole reclaim half is reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution on the LocalRepoWetLane sibling file." + +data excl_immutable_pin_no_ci_wet_lane_dissolve: DissolutionCondition = unbound_dissolution(description: "a required wet lane grants CAP_LINUX_IMMUTABLE, or an unlink-refusal construction that needs none; then this file re-enrolls on local_repo_wet_schedule as ExpectedToHold, this exclusion flips to LocalRepoWetLane or deletes, and the pattern leaves edited_bin_witness_wet_rows_not_executed_by_ci_population") + data excl_run_verdict_exit_status_reason: String = "SUBSTANTIATED per-row (found 2026-08-16, PR #8286 CI red, run 31922786399, batch 3 discovery-corpus, 3 of 9782): every fn in this file invokes the compiled `gunbc` seed as a SUBPROCESS and reads the status of that process, so the hermetic envelope refuses them — 'no mock_response for operation Run' and 'for operation IsExecutable', the class excl_install_media_reason names. The refusal is correct and the file cannot be made hermetic: the subject IS the process exit status, which no in-process assertion can observe, and mocking the invocation would let the witness pass against a fabricated status — precisely the defect it exists to detect. The file was authored into dag/test/claim/ and so was swept into unshrunk hermetic discovery by path; the witnesses were right and the enrolment was wrong. Per DESIGN 'witness cost derives from purpose', this row alone would only remove them from hermetic discovery, which is NOT an executing consumer — so all three fns are ALSO enrolled in bin_witness_wet_entries below, which is their real per-PR executing consumer under WitnessHasExecutingConsumer standing. Size is derived, not chosen: three subprocess invocations of a three-constructor fixture, one per arm of the driver's verdict map." data excl_run_verdict_exit_status_dissolve: DissolutionCondition = unbound_dissolution(description: "the driver's exit-status seam becomes observable without a subprocess — i.e. the verdict boundary migrates under the execution-corpus scope prefix and enrolls as ExecutionWitnessKind rows in gunbc.commit_workflow, the same terminal excl_bin_wet_dissolve names; then this row and the three roster entries delete together. Publishing a mock_response for Run/IsExecutable does NOT discharge it: a mocked status is authored data, so the assertions would be re-checked against the fixture rather than against the seam, which is the vacuous pass this file's refusal-polarity note already records once.") @@ -534,11 +538,31 @@ data witness_exclusion_frontier: List = [ classification: LocalRepoWetLane, reason: excl_local_repo_wet_digest_stdin_reason, dissolution: excl_local_repo_wet_digest_stdin_dissolve}, + WitnessExclusionRow { + pattern: "workspace_source_pack_archive_real_execution_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_tempdir_write_reason, + dissolution: excl_local_repo_wet_tempdir_write_dissolve}, + WitnessExclusionRow { + pattern: "seeded_filler_real_execution_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_tempdir_write_reason, + dissolution: excl_local_repo_wet_tempdir_write_dissolve}, + WitnessExclusionRow { + pattern: "workspace_transcript_rate_real_execution_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_tempdir_write_reason, + dissolution: excl_local_repo_wet_tempdir_write_dissolve}, WitnessExclusionRow { pattern: "megarac_spx_ui_surface_artifact_integrity_witness_test.dag", classification: LocalRepoWetLane, reason: excl_local_repo_wet_tempdir_write_reason, dissolution: excl_local_repo_wet_tempdir_write_dissolve}, + WitnessExclusionRow { + pattern: "mtcollins_sol_host_payload_integrity_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_tempdir_write_reason, + dissolution: excl_local_repo_wet_tempdir_write_dissolve}, WitnessExclusionRow { pattern: "emit_source_store_test.dag", classification: OfflineLocalRecipe, @@ -1110,6 +1134,11 @@ data witness_exclusion_frontier: List = [ classification: LocalRepoWetLane, reason: excl_local_repo_wet_tempdir_write_reason, dissolution: excl_local_repo_wet_dissolve}, + WitnessExclusionRow { + pattern: "materialization_store_local_immutable_pin_wet_witness_test.dag", + classification: BinWitnessWet, + reason: excl_immutable_pin_no_ci_wet_lane_reason, + dissolution: excl_immutable_pin_no_ci_wet_lane_dissolve}, WitnessExclusionRow { pattern: "host_capacity_wet_witness_test.dag", classification: LocalRepoWetLane, @@ -1130,6 +1159,11 @@ data witness_exclusion_frontier: List = [ classification: LocalRepoWetLane, reason: excl_local_repo_wet_tempdir_write_reason, dissolution: excl_local_repo_wet_dissolve}, + WitnessExclusionRow { + pattern: "roadmap_belt_exit_drain_wet_witness_test.dag", + classification: LocalRepoWetLane, + reason: excl_local_repo_wet_tempdir_write_reason, + dissolution: excl_local_repo_wet_dissolve}, WitnessExclusionRow { pattern: "roadmap_dogfood_route_wet_witness_test.dag", classification: LocalRepoWetLane, @@ -1155,11 +1189,6 @@ data witness_exclusion_frontier: List = [ classification: LocalRepoWetLane, reason: excl_local_repo_wet_tempdir_write_reason, dissolution: excl_local_repo_wet_dissolve}, - WitnessExclusionRow { - pattern: "mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - classification: LocalRepoWetLane, - reason: "SUBSTANTIATED per-row: every fn launches the real held KVM observer -- the pinned node, Playwright and headless Chromium that gunbc.runner_browser_toolchain runner_browser_toolchain_here_wet resolves on the floor pool (converged green on srv1, run 36377598915) -- against a loopback controller whose every answer is a gunbc.bmc_model transition served by gunbc.bmc_megarac_web_adapter on ephemeral ports, and writes only inside a shell.Mktemp.Dir directory it removes. shell.Mktemp.Dir, Filesystem.Write, the transport launch and the observer launch carry no mock_response, so hermetic discovery refuses them by construction; their subject is what a real browser and a real WebSocket report (seat busy, a controller-closed /kvm stream, an unexportable canvas), which no mock honestly supplies. No network beyond loopback, no remote host, no BMC, no credential." as String, - dissolution: unbound_dissolution(description: "a .dag browser-automation realization can drive a page and observe its WebSocket lifecycle hermetically against gunbc.bmc_model, so these assertions re-run without a real browser or process; then the fns re-enrol as ordinary hermetic discovery rows, drop off local_repo_wet_schedule, and this row deletes")}, WitnessExclusionRow { pattern: "sol_hold_stdin_wet_witness_test.dag", classification: LocalRepoWetLane, @@ -1728,7 +1757,8 @@ data bin_witness_wet_entries: List = [ bin_wet(entry: "dag/test/claim/push_event_witness_wet_test.dag", f: "witness_push_before_payload_read_refused_on_missing_path"), bin_wet(entry: "dag/test/claim/host/host_cli_dependency_wet_witness_test.dag", f: "observe_echo_wet_posix_command_v_check_does_not_crash"), bin_wet(entry: "dag/test/claim/host/host_cli_dependency_wet_witness_test.dag", f: "observe_npm_wet_posix_command_v_check_does_not_crash"), - bin_wet(entry: "dag/test/claim/stage0_rust_maintenance_census_report_live_witness_test.dag", f: "maintenance_census_report_positive_control_derives_from_current_head") + bin_wet(entry: "dag/test/claim/stage0_rust_maintenance_census_report_live_witness_test.dag", f: "maintenance_census_report_positive_control_derives_from_current_head"), + bin_wet(entry: "dag/test/claim/materialization_store_local_immutable_pin_wet_witness_test.dag", f: "a_leaked_hold_stays_stale_when_reclaim_cannot_remove_its_generation_by_real_execution") ] data bin_witness_wet_per_row_wall_budget_seconds: Int = 60 diff --git a/dag/gunbc/ci/ci_materialization.dag b/dag/gunbc/ci/ci_materialization.dag index 869d0f066e5..198f38271e8 100644 --- a/dag/gunbc/ci/ci_materialization.dag +++ b/dag/gunbc/ci/ci_materialization.dag @@ -238,7 +238,7 @@ fn ci_sccache_provider_id() -> String { sccache_local_id } -data ci_floor_resolve_receipt_note: String = "The counted cold-resolve receipt of the lens-infra case study (operator go, 2026-07-09; the N-lenses-recompute-one-resolve pathology loyal-wren-398 root-caused from the affected-set OOM): make the floor's cold-resolve spend a COUNTED, gated number. MEASURED (CI run 29059860791 @ 93f6a21, 2026-07-10): resolves_total = 3 with #6422's two v2 witness entry classes enrolled (compiler_materialization_witness_test.dag, parse_table_memo_governed_witness_test.dag) — the pin HELD; those entries pooled warm against the shared index and paid zero additional cold resolves ([receipt] floor resolves: 3 entry resolve(s), 21771ms on that job log). The count was already 3 before #6422's entries existed (run 29058798771 semantics: compile-clean plus two dag/test/claim entry classes on the ladder lane); per-entry attribution is not observable at the receipt's current total-grain, so no decomposition claims beyond what the ledger shows. LIVE RECEIPT of ladder enrollment semantics (run 29058798771): enrolling the ladder witness file moved the count 1 -> 3 — each enrolled entry pays its own closure resolve against the shared index — and the gate redded that exact run; the bump to 3 acknowledges witness-entry resolves as declared debt. Ratchet direction: the shared-resolve rewire (one closure resolve per source-roots set, handed to every consumer as a readonly reference — the loyal-wren lane) collapses the per-entry resolves back toward 1, lowering this row consciously. So this gate PINS measured debt consciously: a fourth cold entry resolve (a new single-claim entry class, a bypass of the shared index, a walk_memo regression) reds the same run that introduces it. claim_executor writes target/floor-resolve-receipt.txt (resolves_total = per-result entry resolves actually paid, walk-memo hits excluded; discovery_corpus_resolve_ms observational on its own key, never conflated); the derived ci.yml step compares resolves_total to the declared row. The duplication this lane still owes lives OUTSIDE this receipt: the affected-set whole-tree resolve class (loyal-wren lane: value-tier declaration plus the persistent content-keyed store) and cross-run cold compiles (the fleet frame, provider rows with warm-equals-cold receipts). The gate lives on the ci job only; the regen job runs a different plan and enrolls when its count is pinned. Naming note: an earlier draft tagged this receipt M1 — retired 2026-07-10 (operator no-codename rule; and DESIGN's floor-shared-compute-memoization thread already uses M1/M2 for a different pair). EMPIRICAL LAW (three receipts, 2026-07-10, recorded here because this row is the measuring authority and successor sessions start from carriers, not message threads): resolves_total = 1 (the batch-1 whole-tree materialization) + the number of enrolled entry classes whose closure ESCAPES what batch-1 already materialized (an escaping entry pays a cold resolve with resolve_nanos > 0; a pooled entry hits walk_memo and pays zero). Receipt 1: ladder-lane enrollment paid +2 (run 29058798771 redded 1 -> 3, conscious bump). Receipt 2: #6422's two v2 entry classes paid +0 (run 29059860791 held at 3 — pooled warm). Receipt 3: #6432's entry paid +0 and its predicted 4 was REFUSED by this gate and retracted to the measured 3 — the full declare -> measure -> refuse -> retract loop. Receipt 4: #7088 cheap-gate-early reorder (run 29970856958 @ bd5885f6, 2026-07-23): resolves_total 3 -> 4 — cheap gates now run in batch 1 before compile-clean in batch 2, so dag/gunbc/instruments/floor_effect_gate_witness.dag pays a cold resolve before the compile anchor's walk_memo warms; gate redded 4 != declared 3, conscious bump to 4. Receipt 5: the one-tree-one-resolve rewire (2026-07-23, redundant-work ledger levers 2/3): the compile-clean receipt's typed store (the main-thread process_shared_index the eager install warms) is now CONSUMED across batches by construction — the executor's lane decision (claim_executor batch_unit_lane, clause c) routes any resolve-group sharing an (entry, mode) some batch resolves on the memo path onto that same path, so receipt 4's cheap-gate cold resolve dissolves back into the one anchor resolve: the promoted cheap-gate group pays THE counted whole-tree-entry resolve fresh against the warm typed store, and the compile anchor + emit-host consume its walk_memo context as hits (excluded by this receipt's own rule); declared count 4 -> 3 consciously in the same PR, per this gate's discipline. The sibling duplication OUTSIDE this receipt closed in the same change and was then DEEPENED per the section-9 ledger correction (the cold-child class: batch walls were the SUM of serial cold children, ~26/run, invisible to this receipt by process boundary): the per-claim child-process fold (tools.host_prelude run_gunbc_claims — one cold gunbc process per CLAIM ROW) is realized as ONE claim_batch child per call, and the cheap-gate transports consolidated to one call per GATE (layering 7 rows, extdeps 5 rows — claim_batch's pre-existing pooled --entry/--function grammar, one shared index per child, per-claim verdicts named, no short-circuit; srv1 counterfactuals: single cold child 47.2s, 12 claims pooled 93.4s vs 480s serial). The vehicle stays a separate process deliberately — never in-executor evaluation (the executor is the 16GiB-pinned process; a child dies and frees). Residue — one pool build per call — is counted in the redundancy ledger (receipt TSV deleted 2026-08-16 (operator ruling: delete anything not actively derived) rows) and dissolves on the same terminal store below. Terminal condition for the shared-resolve rewire (the persistent content-keyed node store, the affected-set lane's follow-on): when the store covers the whole tree the escape set is empty BY CONSTRUCTION and this row pins at 1 permanently — resolve once, share by reference, realized at the resolve grain; the rewire PR declares the count's drop consciously in the same PR, per this gate's discipline.RECEIPT 6 — CONSCIOUS LOWERING 3 -> 1 (claude, 2026-07-26, D3b gate flips; run 30188211737 @ 8b1fd6c redded 'floor resolve count 1 differs from declared 3'). NOT the terminal store rewire, and the distinction matters for what happens next: the count fell because the two ESCAPING ENTRY CLASSES LEFT THE PER-PR FLOOR, not because the escape set became empty by construction. This row's own EMPIRICAL LAW predicts it exactly — resolves_total = 1 (the batch-1 whole-tree materialization) + the entries whose closure escapes it — and SourceRootIngestGate (floor_source_root_ingest_gate_entry) and SelfHostReadsRealBytesGate (floor_self_host_realized_comparison_gate_entry) were both such entries, each a RunnableSingleClaim on its own entry file paying its own cold closure resolve. Re-homing them to the falsifier cadence (gunbc.commit_workflow gate_gauntlet_flip_basis_note) removed both, leaving the anchor resolve alone. So this is the ratchet direction this note already names ('collapses the per-entry resolves back toward 1, lowering this row consciously'), arrived at by placement rather than by the store — and it is a MEASUREMENT of the flip's benefit, independent of wall-clock: two whole-tree cold resolves per PR, gone. CONSEQUENCE, deliberately left as a live wall rather than smoothed over: 1 is now the floor's declared debt, so if either gate ever returns to the per-PR surface — or any new escaping entry class is enrolled — this gate reds on the run that does it. That is the gate working, not a brittleness to relax; the number must go back up consciously in the PR that re-enrolls, exactly as it came down here. It does NOT discharge the terminal condition below: the escape set is empty today by ROSTER, not by construction, so the persistent content-keyed store still owes its own permanent pin at 1." +data ci_floor_resolve_receipt_note: String = "The counted cold-resolve receipt of the lens-infra case study (operator go, 2026-07-09; the N-lenses-recompute-one-resolve pathology loyal-wren-398 root-caused from the affected-set OOM): make the floor's cold-resolve spend a COUNTED, gated number. MEASURED (CI run 29059860791 @ 93f6a21, 2026-07-10): resolves_total = 3 with #6422's two v2 witness entry classes enrolled (compiler_materialization_witness_test.dag, parse_table_memo_governed_witness_test.dag; the latter deleted with the parse memo carrier, operator ruling R2 2026-10-08) — the pin HELD; those entries pooled warm against the shared index and paid zero additional cold resolves ([receipt] floor resolves: 3 entry resolve(s), 21771ms on that job log). The count was already 3 before #6422's entries existed (run 29058798771 semantics: compile-clean plus two dag/test/claim entry classes on the ladder lane); per-entry attribution is not observable at the receipt's current total-grain, so no decomposition claims beyond what the ledger shows. LIVE RECEIPT of ladder enrollment semantics (run 29058798771): enrolling the ladder witness file moved the count 1 -> 3 — each enrolled entry pays its own closure resolve against the shared index — and the gate redded that exact run; the bump to 3 acknowledges witness-entry resolves as declared debt. Ratchet direction: the shared-resolve rewire (one closure resolve per source-roots set, handed to every consumer as a readonly reference — the loyal-wren lane) collapses the per-entry resolves back toward 1, lowering this row consciously. So this gate PINS measured debt consciously: a fourth cold entry resolve (a new single-claim entry class, a bypass of the shared index, a walk_memo regression) reds the same run that introduces it. claim_executor writes target/floor-resolve-receipt.txt (resolves_total = per-result entry resolves actually paid, walk-memo hits excluded; discovery_corpus_resolve_ms observational on its own key, never conflated); the derived ci.yml step compares resolves_total to the declared row. The duplication this lane still owes lives OUTSIDE this receipt: the affected-set whole-tree resolve class (loyal-wren lane: value-tier declaration plus the persistent content-keyed store) and cross-run cold compiles (the fleet frame, provider rows with warm-equals-cold receipts). The gate lives on the ci job only; the regen job runs a different plan and enrolls when its count is pinned. Naming note: an earlier draft tagged this receipt M1 — retired 2026-07-10 (operator no-codename rule; and DESIGN's floor-shared-compute-memoization thread already uses M1/M2 for a different pair). EMPIRICAL LAW (three receipts, 2026-07-10, recorded here because this row is the measuring authority and successor sessions start from carriers, not message threads): resolves_total = 1 (the batch-1 whole-tree materialization) + the number of enrolled entry classes whose closure ESCAPES what batch-1 already materialized (an escaping entry pays a cold resolve with resolve_nanos > 0; a pooled entry hits walk_memo and pays zero). Receipt 1: ladder-lane enrollment paid +2 (run 29058798771 redded 1 -> 3, conscious bump). Receipt 2: #6422's two v2 entry classes paid +0 (run 29059860791 held at 3 — pooled warm). Receipt 3: #6432's entry paid +0 and its predicted 4 was REFUSED by this gate and retracted to the measured 3 — the full declare -> measure -> refuse -> retract loop. Receipt 4: #7088 cheap-gate-early reorder (run 29970856958 @ bd5885f6, 2026-07-23): resolves_total 3 -> 4 — cheap gates now run in batch 1 before compile-clean in batch 2, so dag/gunbc/instruments/floor_effect_gate_witness.dag pays a cold resolve before the compile anchor's walk_memo warms; gate redded 4 != declared 3, conscious bump to 4. Receipt 5: the one-tree-one-resolve rewire (2026-07-23, redundant-work ledger levers 2/3): the compile-clean receipt's typed store (the main-thread process_shared_index the eager install warms) is now CONSUMED across batches by construction — the executor's lane decision (claim_executor batch_unit_lane, clause c) routes any resolve-group sharing an (entry, mode) some batch resolves on the memo path onto that same path, so receipt 4's cheap-gate cold resolve dissolves back into the one anchor resolve: the promoted cheap-gate group pays THE counted whole-tree-entry resolve fresh against the warm typed store, and the compile anchor + emit-host consume its walk_memo context as hits (excluded by this receipt's own rule); declared count 4 -> 3 consciously in the same PR, per this gate's discipline. The sibling duplication OUTSIDE this receipt closed in the same change and was then DEEPENED per the section-9 ledger correction (the cold-child class: batch walls were the SUM of serial cold children, ~26/run, invisible to this receipt by process boundary): the per-claim child-process fold (tools.host_prelude run_gunbc_claims — one cold gunbc process per CLAIM ROW) is realized as ONE claim_batch child per call, and the cheap-gate transports consolidated to one call per GATE (layering 7 rows, extdeps 5 rows — claim_batch's pre-existing pooled --entry/--function grammar, one shared index per child, per-claim verdicts named, no short-circuit; srv1 counterfactuals: single cold child 47.2s, 12 claims pooled 93.4s vs 480s serial). The vehicle stays a separate process deliberately — never in-executor evaluation (the executor is the 16GiB-pinned process; a child dies and frees). Residue — one pool build per call — is counted in the redundancy ledger (receipt TSV deleted 2026-08-16 (operator ruling: delete anything not actively derived) rows) and dissolves on the same terminal store below. Terminal condition for the shared-resolve rewire (the persistent content-keyed node store, the affected-set lane's follow-on): when the store covers the whole tree the escape set is empty BY CONSTRUCTION and this row pins at 1 permanently — resolve once, share by reference, realized at the resolve grain; the rewire PR declares the count's drop consciously in the same PR, per this gate's discipline.RECEIPT 6 — CONSCIOUS LOWERING 3 -> 1 (claude, 2026-07-26, D3b gate flips; run 30188211737 @ 8b1fd6c redded 'floor resolve count 1 differs from declared 3'). NOT the terminal store rewire, and the distinction matters for what happens next: the count fell because the two ESCAPING ENTRY CLASSES LEFT THE PER-PR FLOOR, not because the escape set became empty by construction. This row's own EMPIRICAL LAW predicts it exactly — resolves_total = 1 (the batch-1 whole-tree materialization) + the entries whose closure escapes it — and SourceRootIngestGate (floor_source_root_ingest_gate_entry) and SelfHostReadsRealBytesGate (floor_self_host_realized_comparison_gate_entry) were both such entries, each a RunnableSingleClaim on its own entry file paying its own cold closure resolve. Re-homing them to the falsifier cadence (gunbc.commit_workflow gate_gauntlet_flip_basis_note) removed both, leaving the anchor resolve alone. So this is the ratchet direction this note already names ('collapses the per-entry resolves back toward 1, lowering this row consciously'), arrived at by placement rather than by the store — and it is a MEASUREMENT of the flip's benefit, independent of wall-clock: two whole-tree cold resolves per PR, gone. CONSEQUENCE, deliberately left as a live wall rather than smoothed over: 1 is now the floor's declared debt, so if either gate ever returns to the per-PR surface — or any new escaping entry class is enrolled — this gate reds on the run that does it. That is the gate working, not a brittleness to relax; the number must go back up consciously in the PR that re-enrolls, exactly as it came down here. It does NOT discharge the terminal condition below: the escape set is empty today by ROSTER, not by construction, so the persistent content-keyed store still owes its own permanent pin at 1." data ci_floor_resolve_receipt_path: String = "target/floor-resolve-receipt.txt" diff --git a/dag/gunbc/ci/ci_spec.dag b/dag/gunbc/ci/ci_spec.dag index d86d550ed34..1bdb4b84f0c 100644 --- a/dag/gunbc/ci/ci_spec.dag +++ b/dag/gunbc/ci/ci_spec.dag @@ -78,6 +78,7 @@ import gunbc.machine_intake_mtcollins1_sol_notice { mtcollins1_sol_notice_stop_s import gunbc.machine_intake_mtcollins1_fan_observe { mtcollins1_fan_credential_path_env, mtcollins1_fan_observation_receipt_path } import gunbc.fabric_writer_identity_observe { fabric_writer_identity_receipt_path } import gunbc.fabric.workspace_source { workspace_source_receipt_path } +import gunbc.fabric.workspace_transcript_rate { workspace_transcript_rate_receipt_path } import gunbc.fabric.workspace_checkpoint_instrument { workspace_measure_receipt_path, workspace_restore_receipt_path } import gunbc.machine_intake_mtcollins1_census_image_publish { mtcollins1_census_image_receipt_path } import gunbc.machine_intake_mtcollins1_census_member_readback { census_member_readback_receipt_path } @@ -856,6 +857,11 @@ data gunbc_ci_workspace_checkpoint_restore_target: GunbcRunStepTarget = GunbcRun function: "workspace_checkpoint_restore_wet", } +data gunbc_ci_workspace_transcript_rate_observe_target: GunbcRunStepTarget = GunbcRunStepTarget { + entry: "dag/gunbc/fabric/workspace_transcript_rate.dag", + function: "workspace_transcript_rate_observe_wet", +} + // THE MICRO-VM GUEST IMAGE HAS TWO TARGETS BECAUSE OBSERVE AND CONVERGE ARE TWO OPERATIONS, and // this repository's ordering law is what keeps them apart: an observation reports a host's standing // and repairs nothing, so it may run anywhere at any time; a converge builds an image on the host it @@ -943,6 +949,16 @@ data gunbc_ci_namecheap_observe_target: GunbcRunStepTarget = GunbcRunStepTarget function: "namecheap_observe_ci_wet", } +data gunbc_ci_arrival_admit_target: GunbcRunStepTarget = GunbcRunStepTarget { + entry: "dag/gunbc/machine_intake/arrival_admission.dag", + function: "arrival_admit_executor", +} + +data gunbc_ci_arrival_converge_target: GunbcRunStepTarget = GunbcRunStepTarget { + entry: "dag/gunbc/machine_intake/arrival_run.dag", + function: "arrival_converge_wet", +} + data gunbc_ci_host_credential_custody_converge_target: GunbcRunStepTarget = GunbcRunStepTarget { entry: "dag/gunbc/fleet/host_credential_custody_converge.dag", function: "host_credential_custody_converge_ci_wet", @@ -1053,11 +1069,6 @@ data gunbc_ci_mtcollins1_fan_observe_target: GunbcRunStepTarget = GunbcRunStepTa function: "mtcollins1_fan_observe_wet", } -data gunbc_ci_mtcollins1_ui_bundle_observe_target: GunbcRunStepTarget = GunbcRunStepTarget { - entry: "dag/gunbc/machine_intake/mtcollins1_ui_bundle_observe.dag", - function: "mtcollins1_ui_bundle_observe_wet", -} - data gunbc_ci_mtcollins1_kvm_observer_observe_target: GunbcRunStepTarget = GunbcRunStepTarget { entry: "dag/gunbc/machine_intake/mtcollins1_kvm_observer_observe.dag", function: "mtcollins1_kvm_observer_observe_wet", @@ -1397,6 +1408,7 @@ fn gunbc_run_step_targets() -> List { gunbc_ci_workspace_source_pack_target, gunbc_ci_workspace_checkpoint_measure_target, gunbc_ci_workspace_checkpoint_restore_target, + gunbc_ci_workspace_transcript_rate_observe_target, gunbc_ci_runner_guest_image_observe_target, gunbc_ci_runner_guest_image_converge_target, gunbc_ci_runner_microvm_host_converge_target, @@ -1419,7 +1431,6 @@ fn gunbc_run_step_targets() -> List { gunbc_ci_host_reset_return_target, gunbc_ci_host_reset_dispatch_admission_target, gunbc_ci_mtcollins1_fan_observe_target, - gunbc_ci_mtcollins1_ui_bundle_observe_target, gunbc_ci_mtcollins1_kvm_observer_observe_target, gunbc_ci_mtcollins1_kvm_observer_release_target, gunbc_ci_mtcollins1_census_image_publish_target, @@ -1456,7 +1467,9 @@ fn gunbc_run_step_targets() -> List { gunbc_ci_pair_serving_d0_target, gunbc_ci_pair_serving_d0_admit_target, gunbc_ci_gcp_iam_converge_request_target, - gunbc_ci_gcp_iam_converge_apply_target + gunbc_ci_gcp_iam_converge_apply_target, + gunbc_ci_arrival_admit_target, + gunbc_ci_arrival_converge_target ] } @@ -2018,10 +2031,6 @@ fn gunbc_ci_mtcollins1_probe_invoke(stem: String, target: GunbcRunStepTarget) -> ], "") } -fn gunbc_ci_mtcollins1_ui_bundle_observe_invoke() -> String { - gunbc_ci_mtcollins1_probe_invoke(stem: "mtcollins1-ui-bundle", target: gunbc_ci_mtcollins1_ui_bundle_observe_target) -} - fn gunbc_ci_mtcollins1_kvm_observer_observe_invoke() -> String { gunbc_ci_mtcollins1_probe_invoke(stem: "mtcollins1-kvm-observer", target: gunbc_ci_mtcollins1_kvm_observer_observe_target) } @@ -2764,6 +2773,16 @@ fn gunbc_ci_workspace_checkpoint_restore_invoke() -> String { ) } +fn gunbc_ci_workspace_transcript_rate_observe_invoke() -> String { + gunbc_run_step_script( + source_roots: witness_layer_roots, + entry: gunbc_ci_workspace_transcript_rate_observe_target.entry, + function: gunbc_ci_workspace_transcript_rate_observe_target.function, + claim_run: false, + receipt_rel: workspace_transcript_rate_receipt_path + ) +} + fn gunbc_ci_runner_guest_image_observe_invoke() -> String { gunbc_run_step_script( source_roots: witness_layer_roots, @@ -4063,6 +4082,26 @@ fn gunbc_ci_namecheap_observe_invoke() -> String { ) } +fn gunbc_ci_arrival_admit_invoke() -> String { + gunbc_run_step_script( + source_roots: witness_layer_roots, + entry: gunbc_ci_arrival_admit_target.entry, + function: gunbc_ci_arrival_admit_target.function, + claim_run: false, + receipt_rel: none + ) +} + +fn gunbc_ci_arrival_converge_invoke() -> String { + gunbc_run_step_script( + source_roots: witness_layer_roots, + entry: gunbc_ci_arrival_converge_target.entry, + function: gunbc_ci_arrival_converge_target.function, + claim_run: false, + receipt_rel: none + ) +} + fn gunbc_ci_printer_workflow_invoke() -> String { gunbc_run_step_script(source_roots: witness_layer_roots, entry: gunbc_ci_printer_workflow_target.entry, diff --git a/dag/gunbc/cli_dispatch_surface.dag b/dag/gunbc/cli_dispatch_surface.dag index da903b440b1..e765c93e092 100644 --- a/dag/gunbc/cli_dispatch_surface.dag +++ b/dag/gunbc/cli_dispatch_surface.dag @@ -550,7 +550,7 @@ fn gunbc_cli_subcommands() -> List { arity: CliRepeated, doc: [ "Named argument for the entry function, repeatable: `--arg name=value`.", - "Values enter as String; a missing `=` refuses rather than guessing." + "Bound against the parameter's resolved type identity: kernel String, Int, and Bool, and std.types NonEmptyStr (String where string_non_empty). Other types refuse." ], emission: CarriedByGeneratedDispatch } diff --git a/dag/gunbc/codex_app_server_press.dag b/dag/gunbc/codex_app_server_press.dag index a4916844438..7157325ae78 100644 --- a/dag/gunbc/codex_app_server_press.dag +++ b/dag/gunbc/codex_app_server_press.dag @@ -73,6 +73,7 @@ import extdeps.llm.codex_app_server { import extdeps.shell import extdeps.shell.exec import extdeps.tools.env { env_path_resolved_program, env_prefixed_args } +import extdeps.tools.util_linux_flock { flock_program, flock_exclusive_nonblocking_words } import v2.std.orchestration { EnvSet } import v2.std.compilers.cli_surface { ProcessArgvExpansion, @@ -544,16 +545,45 @@ fn codex_press_account_trip_request_lines() -> String { ) + "\n" } +// A TRIP THAT MUST NOT RACE A TURN runs under the turn's exclusive flock: account/read may refresh +// the token, and a refresh rotates it (gunbc.codex_harness_credential). The lock is one more argv +// layer in front of the same env-prefixed trip, so the program becomes flock and its arguments carry +// the env invocation; with no lock the trip is exactly as before. +type CodexTripLock { + lock_path: NonEmptyStr + conflict_exit_code: Int +} + +fn codex_press_account_trip_program(lock: CodexTripLock?) -> String { + match lock { + Absent => env_path_resolved_program().invocation as String + Present { value: _ } => flock_program().invocation as String + } +} + fn codex_press_account_trip_invocation( executable: FilePath, codex_home: FilePath, + lock: CodexTripLock?, ) -> ProcessArgvExpansion { + let env_args = env_prefixed_args( + bindings: [EnvSet { name: "CODEX_HOME", value: codex_home as String }], + command_argv: [executable as String, "app-server", "--stdio"], + ) process_argv_expansion( surface: cli_surface_of_literal_words( - words: env_prefixed_args( - bindings: [EnvSet { name: "CODEX_HOME", value: codex_home as String }], - command_argv: [executable as String, "app-server", "--stdio"], - ), + words: match lock { + Absent => env_args + Present { value: l } => + skip( + flock_exclusive_nonblocking_words( + lock_path: l.lock_path, + conflict_exit_code: l.conflict_exit_code, + command: concat([env_path_resolved_program().invocation as String], env_args), + ), + 1, + ) + }, ), ) } @@ -1608,6 +1638,7 @@ fn observe_codex_account_preflight( arguments: codex_press_account_trip_invocation( executable: projected.executable.path, codex_home: codex_home, + lock: none, ), stdin_payload: codex_press_account_trip_request_lines(), ) diff --git a/dag/gunbc/codex_harness_credential.dag b/dag/gunbc/codex_harness_credential.dag new file mode 100644 index 00000000000..a5764aab794 --- /dev/null +++ b/dag/gunbc/codex_harness_credential.dag @@ -0,0 +1,325 @@ +module gunbc.codex_harness_credential + +import std.types { String, NonEmptyStr, Secret, FilePath, Int, List } +import extdeps.filesystem.filesystem_io { + FilesystemFileObservation, + FilesystemFileAbsent, + FilesystemFileRead, + FilesystemFileIndeterminate, + FilesystemFileObservationsDisagree, + FilesystemFileSubjectRefused, +} +import extdeps.cloud.gcp.secret_ref { SecretRef, secret_ref_secret_resource } +import extdeps.cloud.gcp.secret_manager +import extdeps.cloud.gcp.secret_manager { encode_sm_access_version_payload_wire } +import extdeps.transports.rest { + classify_rest_refusal, + RestMutationExchange, + RestAnswered, + RestRefused, + RestExchangeStatusRefused, + RestExchangeCommitAmbiguous, + RestExchangeUnreached, + RestExchangeUndecodable, +} +import extdeps.languages.json.emit { JsonValue, JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject } +import extdeps.languages.json.parse { + parse_json_document, + JsonDocumentParsed, + JsonDocumentUnreadable, + json_object_unique_member, + JsonMemberFound, + JsonMemberAbsent, + JsonMemberDuplicated, + JsonMemberNotAnObject, +} +import extdeps.llm.codex_auth { + codex_auth_mode_of_wire, + CodexChatGptMode, + CodexApiKeyMode, + codex_auth_file_relative, + codex_auth_file_auth_mode_key, + codex_auth_file_tokens_key, + codex_auth_file_refresh_token_key, + codex_auth_file_account_id_key, +} +import gunbc.secret_provision { fleet_secret_ref, secret_ref_pin_version } +import gunbc.auth.access_token_source { + AccessTokenSource, + ensure_access_token, + AccessTokenReady, + AccessTokenUpsertRequired, + AccessTokenEnsureRefused, +} +import extdeps.tools.util_linux_flock { flock_exclusive_nonblocking_words } +import gunbc.codex_app_server_press { AmbientCodexCredentialContext } +import product.host_identity { HostIdentity } +import gunbc.fleet_host_identity { operator_host_srv2 } + +// ONE CODEX SUBSCRIPTION CREDENTIAL, HELD ON ONE HOST AND WRITTEN BACK WHEN CODEX ROTATES IT. +// The ambient route binds a CODEX_HOME someone logged into on the host; nothing in it says where the +// login came from or who else holds a copy. This credential comes from ONE secret, codex-auth-harness, +// whose payload is the auth.json a ChatGPT login wrote, byte for byte. +// +// Why the host is authoritative after placement: extdeps.llm.codex_auth +// codex_source_refresh_persistence records that codex rewrites auth.json about once an hour while it +// runs, replacing the refresh token with whatever the issuer returned, and that the issuer refuses a +// reused refresh token. So the copy codex last refreshed is the only live one. The custody converge +// (gunbc.host_credential_custody_converge CodexWorkerTurnAuth) places the file once on the one custody +// host, never overwrites it afterwards, and on every run reads it back and adds it to the store as a +// new version when its refresh token moved on. Turns on that host run under the flock below, so one +// codex process refreshes at a time. +// +// Operator rulings this follows (relayed by swift-ibex-601, 2026-10-05): subscription (chatgpt) mode, +// not API-key mode; never codex's upstream-labelled-internal external-token login; credential delivery +// by host custody under the fleet-converge run's federated grant, with no per-turn Secret Manager +// access; exactly one custody host. +// +// RESIDUAL, stated: the store lags the host until the next custody converge. A host lost after a +// rotation and before that converge loses the live token; the next placement on a rebuilt host then +// carries a spent one, codex reports login required, and dispatch refuses the credential until the +// operator logs in again. +// +// DECLARED FRONTIER (operator ruling, 2026-10-05): the narrower window is a workload identity bound +// to the codex turn runner on the custody host, holding only secretAccessor and secretVersionAdder on +// codex-auth-harness, so a turn could add the rotated file as a version the moment codex writes it +// rather than at the next converge. TRIGGER: gunbc.auth.access_token_source select_access_token_source +// resolves a federated token on the custody host outside an Actions run. Until then delivery is the +// custody converge, and nothing in a turn reaches Secret Manager. + +data codex_auth_harness_secret_id: NonEmptyStr = "codex-auth-harness" as NonEmptyStr + +// latest, because the credential moves: every write-back mints the next version. The fetch resolves +// the alias to an exact version and the receipt carries that version. +data codex_auth_harness_secret_ref: SecretRef = secret_ref_pin_version( + ref: fleet_secret_ref(secret_id: codex_auth_harness_secret_id), + version: "latest", +) + +// THE ONE HOST THAT HOLDS THE CODEX SUBSCRIPTION CREDENTIAL, and the CODEX_HOME on it. The store lags +// the host until the next custody converge reads the host file back, so a second host would receive a +// spent token: gunbc.host_credential_custody_converge refuses to place a host-authoritative row on +// more than one host, and codex harness dispatch is pinned here. +data codex_worker_turn_custody_host: HostIdentity = operator_host_srv2 +data codex_worker_turn_codex_home: NonEmptyStr = "/etc/gunbc-codex-harness" as NonEmptyStr +data codex_worker_turn_auth_host_path: NonEmptyStr = "/etc/gunbc-codex-harness/auth.json" as NonEmptyStr + +// WHAT THE HARNESS NEEDS FROM auth.json, AND NOTHING ELSE. The refresh token decides whether a +// write-back is owed; the account id decides whether the file still belongs to the account the secret +// holds. The whole document is what gets written back, so nothing else needs reading. +type CodexHarnessAuthIdentity { + account_id: NonEmptyStr + refresh_token: Secret +} + +type CodexHarnessAuthFileRefusal + = CodexHarnessAuthFileUnparseable + | CodexHarnessAuthModeNotChatgpt { observed: String } + | CodexHarnessAuthModeAbsent + | CodexHarnessAuthMemberMissing { member: NonEmptyStr } + +type CodexHarnessAuthRead + = CodexHarnessAuthAdmitted { identity: CodexHarnessAuthIdentity } + | CodexHarnessAuthRefused { cause: CodexHarnessAuthFileRefusal } + +fn codex_harness_string_member(v: JsonValue, key: NonEmptyStr) -> String? { + match json_object_unique_member(v: v, key: key as String) { + JsonMemberFound { value } => + match value { + JsonString { value: s } => if s == "" { none } else { Present { value: s } } + JsonNull => none + JsonBool { value: _ } => none + JsonNumber { lexeme: _ } => none + JsonArray { elements: _ } => none + JsonObject { members: _ } => none + } + JsonMemberAbsent => none + JsonMemberDuplicated { count: _ } => none + JsonMemberNotAnObject => none + } +} + +fn codex_harness_member_missing(key: NonEmptyStr) -> CodexHarnessAuthRead { + CodexHarnessAuthRefused { cause: CodexHarnessAuthMemberMissing { member: key } } +} + +fn codex_harness_tokens_identity(tokens: JsonValue) -> CodexHarnessAuthRead { + match codex_harness_string_member(v: tokens, key: codex_auth_file_refresh_token_key) { + Absent => codex_harness_member_missing(key: codex_auth_file_refresh_token_key) + Present { value: refresh } => + match codex_harness_string_member(v: tokens, key: codex_auth_file_account_id_key) { + Absent => codex_harness_member_missing(key: codex_auth_file_account_id_key) + Present { value: account } => + CodexHarnessAuthAdmitted { + identity: CodexHarnessAuthIdentity { + account_id: account as NonEmptyStr, + refresh_token: refresh as Secret, + }, + } + } + } +} + +fn codex_harness_chatgpt_identity(doc: JsonValue) -> CodexHarnessAuthRead { + match json_object_unique_member(v: doc, key: codex_auth_file_tokens_key as String) { + JsonMemberFound { value: tokens } => codex_harness_tokens_identity(tokens: tokens) + JsonMemberAbsent => codex_harness_member_missing(key: codex_auth_file_tokens_key) + JsonMemberDuplicated { count: _ } => codex_harness_member_missing(key: codex_auth_file_tokens_key) + JsonMemberNotAnObject => CodexHarnessAuthRefused { cause: CodexHarnessAuthFileUnparseable } + } +} + +// THE PAYLOAD FORMAT IS CHECKED BEFORE ANY TURN RUNS. An API-key auth.json, or anything that is not +// an auth.json, refuses here with the reason, rather than starting codex against a file it will reject. +fn codex_harness_auth_read(content: String) -> CodexHarnessAuthRead { + match parse_json_document(s: content) { + JsonDocumentUnreadable { gap: _ } => CodexHarnessAuthRefused { cause: CodexHarnessAuthFileUnparseable } + JsonDocumentParsed { value: doc } => + match codex_harness_string_member(v: doc, key: codex_auth_file_auth_mode_key) { + Absent => CodexHarnessAuthRefused { cause: CodexHarnessAuthModeAbsent } + Present { value: wire } => + match codex_auth_mode_of_wire(raw: wire) { + Present { value: mode } => + match mode { + CodexChatGptMode => codex_harness_chatgpt_identity(doc: doc) + CodexApiKeyMode => CodexHarnessAuthRefused { cause: CodexHarnessAuthModeNotChatgpt { observed: wire } } + } + Absent => CodexHarnessAuthRefused { cause: CodexHarnessAuthModeNotChatgpt { observed: wire } } + } + } + } +} + +// THE WRITE-BACK DECISION, from the identity that was materialized and the file read back after the +// turn. A changed refresh token owes a new version carrying the whole file. An unchanged one owes +// nothing. A file that now names a different account, or that cannot be read, is refused rather than +// written: storing it would replace the operator's credential with something this run cannot vouch +// for, and NOT storing it is only safe because the refusal is reported. +type CodexHarnessWriteBackRefusal + = CodexHarnessReadBackUnobservable { detail: String } + | CodexHarnessReadBackAbsent + | CodexHarnessReadBackRefused { cause: CodexHarnessAuthFileRefusal } + | CodexHarnessReadBackAccountChanged { materialized: NonEmptyStr, read_back: NonEmptyStr } + +type CodexHarnessWriteBackDecision + = CodexHarnessWriteBackNotOwed + | CodexHarnessWriteBackOwed { payload: Secret } + | CodexHarnessWriteBackRefused { cause: CodexHarnessWriteBackRefusal } + +fn codex_harness_write_back_decision( + materialized: CodexHarnessAuthIdentity, + read_back: FilesystemFileObservation, +) -> CodexHarnessWriteBackDecision { + match read_back { + FilesystemFileAbsent(_) => CodexHarnessWriteBackRefused { cause: CodexHarnessReadBackAbsent } + FilesystemFileIndeterminate { cause } => + CodexHarnessWriteBackRefused { cause: CodexHarnessReadBackUnobservable { detail: cause } } + FilesystemFileObservationsDisagree { path: _, cause } => + CodexHarnessWriteBackRefused { cause: CodexHarnessReadBackUnobservable { detail: cause } } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => + CodexHarnessWriteBackRefused { cause: CodexHarnessReadBackUnobservable { detail: cause } } + FilesystemFileRead { path: _, content } => + match codex_harness_auth_read(content: content) { + CodexHarnessAuthRefused { cause } => + CodexHarnessWriteBackRefused { cause: CodexHarnessReadBackRefused { cause: cause } } + CodexHarnessAuthAdmitted { identity: after } => + if (after.account_id as String) != (materialized.account_id as String) { + CodexHarnessWriteBackRefused { + cause: CodexHarnessReadBackAccountChanged { + materialized: materialized.account_id, + read_back: after.account_id, + }, + } + } else if (after.refresh_token as String) == (materialized.refresh_token as String) { + CodexHarnessWriteBackNotOwed + } else { + CodexHarnessWriteBackOwed { payload: content as Secret } + } + } + } +} + +// WHAT HAPPENED TO THE STORE, as a receipt the outcome carries. VersionAddAmbiguous is the case worth +// naming: the store may or may not hold the rotated token, so the next acquire may read a spent one. +type CodexHarnessWriteBackReceipt + = CodexHarnessStoreUnchanged + | CodexHarnessVersionAdded { version_name: String } + | CodexHarnessVersionAddRefused { detail: String } + | CodexHarnessVersionAddAmbiguous { detail: NonEmptyStr } + | CodexHarnessWriteBackWithheld { cause: CodexHarnessWriteBackRefusal } + +// THE CONVERGE'S WRITE-BACK. gunbc.host_credential_custody_converge calls this when the host's +// auth.json carries a refresh token the store does not, under the fleet-converge run's federated +// token and the secretVersionAdder grant in gunbc.auth.fleet_secret_accessor_roster. +fn codex_harness_add_version(token_source: AccessTokenSource, payload: Secret) -> CodexHarnessWriteBackReceipt { + match ensure_access_token(source: token_source) { + AccessTokenUpsertRequired { plan: _ } => + CodexHarnessVersionAddRefused { detail: "no Secret Manager access token: access upsert required" } + AccessTokenEnsureRefused { reason } => + CodexHarnessVersionAddRefused { detail: reason as String } + AccessTokenReady { token } => { + match gcp.SecretManager.AddVersion( + access_token: token, + secret_name: secret_ref_secret_resource(ref: codex_auth_harness_secret_ref), + payload_b64: encode_sm_access_version_payload_wire(credential: payload), + ) { + RestAnswered { answer: added } => CodexHarnessVersionAdded { version_name: added.name as String } + RestRefused { refusal } => + match classify_rest_refusal(standing: RestMutationExchange { refusal: refusal }) { + RestExchangeStatusRefused { status: _, body } => CodexHarnessVersionAddRefused { detail: body } + RestExchangeCommitAmbiguous { detail } => CodexHarnessVersionAddAmbiguous { detail: detail } + RestExchangeUnreached { cause } => CodexHarnessVersionAddAmbiguous { detail: cause } + RestExchangeUndecodable { status: _, cause } => CodexHarnessVersionAddAmbiguous { detail: cause } + } + } + } + } +} + +fn codex_harness_write_back_refusal_wire(cause: CodexHarnessWriteBackRefusal) -> String { + match cause { + CodexHarnessReadBackUnobservable { detail } => concat("host auth.json unobservable: ", detail) + CodexHarnessReadBackAbsent => "host auth.json absent" + CodexHarnessReadBackRefused { cause: _ } => "host auth.json is not a chatgpt-mode auth.json" + CodexHarnessReadBackAccountChanged { materialized, read_back } => + join(["host auth.json names account ", read_back as String, ", the store names ", materialized as String], "") + } +} + +fn codex_harness_write_back_receipt_wire(r: CodexHarnessWriteBackReceipt) -> String { + match r { + CodexHarnessStoreUnchanged => "unchanged" + CodexHarnessVersionAdded { version_name } => concat("version-added ", version_name) + CodexHarnessVersionAddRefused { detail } => concat("REFUSED ", detail) + CodexHarnessVersionAddAmbiguous { detail } => concat("AMBIGUOUS ", detail as String) + CodexHarnessWriteBackWithheld { cause } => concat("WITHHELD ", codex_harness_write_back_refusal_wire(cause: cause)) + } +} + +// ── The turn side: one turn at a time over the custody CODEX_HOME ─────────────────────────────── +// +// The turn reads nothing from Secret Manager. It runs codex with CODEX_HOME set to the directory the +// custody converge placed on the one custody host, under an exclusive flock(1) on a lock file in that +// directory, because codex has no cross-process lock on auth.json (extdeps.llm.codex_auth +// codex_source_revision note) and two concurrent refreshes would present one refresh token twice. +// The lock is held by the codex process for exactly its lifetime and the kernel drops it if the +// process dies, so a crashed turn leaves nothing to recover. A second turn while one runs does not +// wait: it exits codex_harness_turn_busy_exit, which names the credential as busy rather than as a +// codex failure. + +data codex_harness_turn_lock_path: NonEmptyStr = "/etc/gunbc-codex-harness/turn.lock" as NonEmptyStr + +// EX_TEMPFAIL (sysexits.h 75): the credential is in use by another turn; retrying later is the remedy. +data codex_harness_turn_busy_exit: Int = 75 + +fn codex_harness_credential_context() -> AmbientCodexCredentialContext { + AmbientCodexCredentialContext { codex_home: codex_worker_turn_codex_home as FilePath } +} + +fn codex_harness_turn_argv(command: List) -> List { + flock_exclusive_nonblocking_words( + lock_path: codex_harness_turn_lock_path, + conflict_exit_code: codex_harness_turn_busy_exit, + command: command, + ) +} diff --git a/dag/gunbc/commit_workflow.dag b/dag/gunbc/commit_workflow.dag index 19fe9b4534c..8ea88743217 100644 --- a/dag/gunbc/commit_workflow.dag +++ b/dag/gunbc/commit_workflow.dag @@ -1450,41 +1450,6 @@ data commit_gate_roster: List = [ }, surfaces: [GithubActionsCiJob] }, - CommitCheckEnrollment { - check: CommitWitnessClaim { - entry: "src/v2/test/claim/compiler_materialization_witness_test.dag", - check_fns: [ - "compiler_materialization_ladder_holds", - "parse_table_memo_provider_preserves_catalog_retention", - "compile_stage_memo_provider_preserves_catalog_retention", - "parse_table_carrier_inhabits_realization", - "compile_stage_carrier_inhabits_realization", - "parse_table_empty_uses_catalog_identity", - "single_site_demands_are_accepted_single_recompute", - "parse_table_memo_plural_reds_without_provider" - ], - kind: CorpusWitnessKind, - span: SpanUndeclared - }, - surfaces: [GithubActionsCiJob] - }, - CommitCheckEnrollment { - check: CommitWitnessClaim { - entry: "src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag", - check_fns: [ - "witness_materialization_gate_recompute_denies_store", - "witness_recompute_fixture_carries_recompute_verdict", - "witness_door_insert_refused_for_recompute_table", - "witness_door_lookup_refused_for_recompute_table", - "parse_table_memo_bypass_store_reds", - "witness_memoize_carrier_allows_store", - "witness_content_key_subject_stable" - ], - kind: CorpusWitnessKind, - span: SpanUndeclared - }, - surfaces: [GithubActionsCiJob] - }, CommitCheckEnrollment { check: CommitWitnessClaim { entry: "dag/test/claim/ci_materialization_witness_test.dag", @@ -2980,7 +2945,7 @@ fn realization_artifact_for_surface(surface: CommitWorkflowSurface) -> Generated // enforcement-coverage precedent in gunbc.ci_layer_roots). The files live under // src/v2/test/claim/long/ with the local recipe; dissolve-on: the long-lane scheduled trigger // (falsifier follow-on) enrolls them and this note retires. Contrast: the parse_table_memo_governed -// enrollment above was REPOINTED to its long/ home, not deleted — its four enrolled check_fns each +// enrollment (since deleted with the parse memo carrier, operator ruling R2 2026-10-08) was REPOINTED to its long/ home, not deleted — its four enrolled check_fns each // measured 0ms (audit 2026-07-12); only unenrolled siblings in that file (warm_equals_cold) are // over budget, and an explicit row runs exactly its named check_fns. Delete when the enrolled fns // themselves are slow; repoint when only the file moved. Wave 1 Gate 1 body-producer keystones diff --git a/dag/gunbc/compute/host_capacity.dag b/dag/gunbc/compute/host_capacity.dag index 9e1062d483a..38af2e04c97 100644 --- a/dag/gunbc/compute/host_capacity.dag +++ b/dag/gunbc/compute/host_capacity.dag @@ -19,8 +19,9 @@ import gunbc.fabric_event_log { PoolEventAppend, PoolEventAppended, PoolEventContended, PoolEventAppendRefused, fabric_pool_event_append, pool_event_append_wire, } import gunbc.fabric_event_log_host { now_epoch_seconds } +import gunbc.owned_directory { OwnedDirectoryTarget, owned_directory_path } import gunbc.roadmap_dashboard_instance { - HostDashboardInstance, dashboard_instance_compute_root, + HostDashboardInstance, dashboard_instance_shared_directory, DashboardInstanceByHost, DashboardInstanceForHost, DashboardInstanceHostUnknown, dashboard_instance_for_host, } @@ -96,8 +97,23 @@ type ComputeSubjectResolution = ComputeSubjectResolved { subject: ComputeCapacitySubject } | ComputeSubjectHostUnresolved { host: NonEmptyStr } +// THE COMPUTE ROOT THE WORK PROVIDER AND THIS POOL DEMAND: leases, checkouts, the shared per-host +// target directory, the content store and the capacity partition all live under it +// (gunbc.compute.work_provider_local reads it from here). Declared at this, the lower of the two +// modules, as an owned-directory target both deployment routes ensure +// (gunbc.roadmap.dashboard_instance_directories); under a root-owned /opt/gunbc a worker's first +// request would otherwise refuse StoreUnavailable at `install -d`. The subdirectories are the +// provider's own and it creates them under this root. +fn compute_root_directory(instance: HostDashboardInstance) -> OwnedDirectoryTarget { + dashboard_instance_shared_directory(instance: instance, member: "compute-root" as NonEmptyStr, child: "compute") +} + +fn compute_root(instance: HostDashboardInstance) -> NonEmptyStr { + owned_directory_path(t: compute_root_directory(instance: instance)) as String as NonEmptyStr +} + fn compute_capacity_root(host_instance: HostDashboardInstance) -> NonEmptyStr { - join([dashboard_instance_compute_root(instance: host_instance) as String, "/capacity"], "") as NonEmptyStr + join([compute_root(instance: host_instance) as String, "/capacity"], "") as NonEmptyStr } fn compute_memory_partition(host: NonEmptyStr) -> PartitionId { diff --git a/dag/gunbc/compute/work_provider_local.dag b/dag/gunbc/compute/work_provider_local.dag index def52cc09c9..d682ded4bf0 100644 --- a/dag/gunbc/compute/work_provider_local.dag +++ b/dag/gunbc/compute/work_provider_local.dag @@ -48,7 +48,6 @@ import gunbc.compute.attempt_lifecycle { } import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_for_repo_root, DashboardInstanceResolved, DashboardInstanceRootUnknown, - dashboard_instance_compute_root, } import gunbc.roadmap_dispatch_actuator { dispatch_capability_program } import gunbc.roadmap_execution_contract { @@ -65,16 +64,19 @@ import gunbc.compute.host_capacity { ComputeRelease, ComputeReleased, ComputeNothingToRelease, ComputeReleaseRefused, compute_reserve, compute_release, compute_reservation_wire, compute_memory_ceiling, ComputeCapacityStanding, ComputeCapacityStandingRead, ComputeCapacityStandingUnread, compute_capacity_standing, compute_capacity_standing_wire, ComputeCapacitySubject, ComputeSubjectResolution, ComputeSubjectResolved, ComputeSubjectHostUnresolved, - compute_capacity_subject, compute_capacity_store, compute_release_on, + compute_capacity_subject, compute_capacity_store, compute_release_on, compute_root, } import std.measure { kibibyte } +import std.content_hash { serialize_content_hash } +import std.materialization_provider { ComputeWorkRequest, request_key, compute_work_demand_nature } +import std.materialization_ladder { FreshEffect, IdempotentEffect, PureComputation, WorldRead } import gunbc.compute.work_request { WorkSubject, ExactTree, WorkOperation, BuildGunbcBinaries, CompileEntry, RunClaims, CargoProfile, CargoRelease, CargoDebug, - WorkToolchainIdentity, work_identity, work_identity_hex, work_default_build, work_declared_outputs, + WorkToolchainIdentity, work_toolchain_observed, rustc_verbose_host_triple, work_default_build, work_declared_outputs, DeclaredOutput, WorkOutputRoot, CheckoutRoot, TargetRoot, WorkOutput, WorkOutcome, WorkSucceeded, WorkFailed, WorkRefusedByInfrastructure, WorkCancelled, WorkOutputMismatch, WorkInfrastructureRefusal, ProducerInFlight, HostComputeCapacityFull, HostBelowMemoryFloor, ComputeCapacityLedgerUnavailable, - CheckoutUnavailable, StoreUnavailable, + CheckoutUnavailable, StoreUnavailable, FreshEffectNotServed, ToolchainUnobserved, DependencyNotSucceeded, SubjectUnresolved, UnitMemoryGrantExceeded, UnitEndingUnclassifiable, work_outcome_wire, work_outcome_ending, work_outcome_ending_of, work_operation_canonical, work_outcome_identity, StoredOutcomeRead, StoredOutcomeFound, StoredOutcomeUnreadable, stored_outcome_decode, stored_outcome_is_success, } @@ -112,7 +114,7 @@ type ComputeLayout { } fn compute_layout(instance: HostDashboardInstance, identity_hex: String) -> ComputeLayout { - let root = dashboard_instance_compute_root(instance: instance) as String + let root = compute_root(instance: instance) as String let work_dir = join([root, "/work/", identity_hex], "") ComputeLayout { root: root, @@ -156,16 +158,47 @@ type ToolchainObservation | ToolchainObservationRefused { detail: String } fn compute_observe_toolchain(instance: HostDashboardInstance) -> ToolchainObservation { - match compute_exec(program: compute_program(instance: instance, cap: RustcCapability), workdir: instance.repo_root as String, args: ["--version"]) { - ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["rustc --version exited ", to_string(value: exit_code), ": ", stderr], "") } - ComputeExecOk { stdout: rustc_line, stderr: _ } => - match compute_exec(program: sccache_installed_binary_path, workdir: instance.repo_root as String, args: ["--version"]) { - ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["sccache --version exited ", to_string(value: exit_code), ": ", stderr], "") } - ComputeExecOk { stdout: cache_line, stderr: _ } => - if trim(s: rustc_line) == "" || trim(s: cache_line) == "" { - ToolchainObservationRefused { detail: "rustc or sccache printed an empty version line" } - } else { - ToolchainObserved { toolchain: WorkToolchainIdentity { rustc_version: trim(s: rustc_line) as NonEmptyStr, cache_release: trim(s: cache_line) as NonEmptyStr } } + let repo = instance.repo_root as String + compute_toolchain_decide( + rustc: compute_exec(program: compute_program(instance: instance, cap: RustcCapability), workdir: repo, args: ["-vV"]), + cargo: compute_exec(program: compute_program(instance: instance, cap: CargoCapability), workdir: repo, args: ["--version"]), + cache: compute_exec(program: sccache_installed_binary_path, workdir: repo, args: ["--version"]), + provider: compute_exec(program: compute_program(instance: instance, cap: GitWorkspaceCapability), workdir: repo, args: ["rev-parse", "HEAD:dag/gunbc/compute"]), + ) +} + +// PURE: four readings in, one execution identity or a located refusal out. Every axis must have been +// read and be non-empty, and the triple must be found in rustc's own report; a missing axis is a +// refusal, never a default, because a key with a hole names a different computation than it claims. +fn compute_toolchain_decide(rustc: ComputeExec, cargo: ComputeExec, cache: ComputeExec, provider: ComputeExec) -> ToolchainObservation { + match rustc { + ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["rustc -vV exited ", to_string(value: exit_code), ": ", stderr], "") } + ComputeExecOk { stdout: verbose, stderr: _ } => + match cargo { + ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["cargo --version exited ", to_string(value: exit_code), ": ", stderr], "") } + ComputeExecOk { stdout: cargo_line, stderr: _ } => + match cache { + ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["sccache --version exited ", to_string(value: exit_code), ": ", stderr], "") } + ComputeExecOk { stdout: cache_line, stderr: _ } => + match provider { + ComputeExecFailed { exit_code, stdout: _, stderr } => ToolchainObservationRefused { detail: join(["the provider's source revision could not be read (git rev-parse exited ", to_string(value: exit_code), "): ", stderr], "") } + ComputeExecOk { stdout: provider_tree, stderr: _ } => + match rustc_verbose_host_triple(verbose: verbose) { + Absent => ToolchainObservationRefused { detail: "rustc -vV reported no host: line, so the target triple is unobserved" } + Present { value: triple } => + if trim(s: verbose) == "" || triple == "" || trim(s: cargo_line) == "" || trim(s: cache_line) == "" || trim(s: provider_tree) == "" { + ToolchainObservationRefused { detail: "an execution-identity reading was empty (rustc -vV, its host triple, cargo, sccache or the provider revision)" } + } else { + ToolchainObserved { toolchain: work_toolchain_observed( + rustc_verbose: trim(s: verbose) as NonEmptyStr, + target_triple: triple as NonEmptyStr, + cargo_version: trim(s: cargo_line) as NonEmptyStr, + cache_release: trim(s: cache_line) as NonEmptyStr, + provider_revision: trim(s: provider_tree) as NonEmptyStr, + ) } + } + } + } } } } @@ -990,7 +1023,7 @@ fn compute_list(path: String) -> ComputeListing { } fn compute_grant_exceeded_count(instance: HostDashboardInstance, class: WorkClass) -> GrantExceededCount { - let root = dashboard_instance_compute_root(instance: instance) as String + let root = compute_root(instance: instance) as String grant_exceeded_count_decide( root: compute_list(path: root), parent: compute_list(path: compute_grant_exceeded_parent(root: root)), @@ -1488,6 +1521,15 @@ fn compute_lease_drop_record(dropped: LeaseDrop, layout: ComputeLayout) -> HostR } } +// THE WORK'S IDENTITY IS THE MATERIALIZATION PROVIDER'S KEY, not a second one. It is derived from +// the ComputeWorkRequest family's judgment contract (std.materialization_provider), so the tree, +// the operation and the observed execution identity are the only things that move it, and the key a +// store will be asked for at the cutover is byte-for-byte the name this provider already files the +// work under. +fn compute_work_identity(subject: WorkSubject, op: WorkOperation, toolchain: WorkToolchainIdentity) -> String { + serialize_content_hash(hash: request_key(req: ComputeWorkRequest { subject: subject, operation: op, toolchain: toolchain })) as String +} + fn compute_provide_leaf( instance: HostDashboardInstance, commit: String, @@ -1496,7 +1538,7 @@ fn compute_provide_leaf( toolchain: WorkToolchainIdentity, dependency_store: String, ) -> ProvideResult { - let identity = work_identity_hex(id: work_identity(subject: subject, op: op, toolchain: toolchain)) + let identity = compute_work_identity(subject: subject, op: op, toolchain: toolchain) let layout = compute_layout(instance: instance, identity_hex: identity) let attached = compute_read_stored_outcome(layout: layout) if stored_outcome_is_success(r: attached) { @@ -1832,7 +1874,33 @@ fn compute_unspawned_summary() -> SystemdRunWaitSummary { // PROVIDE, WITH THE BUILD DEPENDENCY RESOLVED FIRST. The build of the same subject is itself a // request: attached if stored, produced once otherwise, and compile/claims consume its store. +// A WET CLAIM RUN IS A FRESH EFFECT AND THIS PROVIDER DOES NOT SERVE ONE (ruling on gunbc#13072, +// item 2). Its identity is the request key, and every request with that key shares one lease, one +// work directory and one stored outcome -- so a second identical wet run would attach to the first, +// memoizing what std.materialization_provider classifies FreshEffect and the ladder answers Recompute. +// Serving it needs an execution-occurrence identity beside the demand identity, which the ladder +// cutover introduces; until then the request refuses before anything is observed or run. The +// classification is the contract's own (std.materialization_provider compute_work_demand_nature, a +// fold over the operation alone), read here rather than re-derived, so a future FreshEffect operation +// refuses without this function changing. +fn compute_operation_admission(op: WorkOperation) -> String? { + match compute_work_demand_nature(operation: op) { + FreshEffect => Present { value: "the request's contract classifies it FreshEffect, and this provider has no occurrence identity, so a repeat would attach to a prior run's outcome; run it hermetic or through the materialization cutover" } + IdempotentEffect => none + PureComputation => none + WorldRead { staleness_envelope_declared: _ } => Present { value: "the request's contract classifies it a WorldRead, which this provider's key does not cover" } + } +} + fn compute_provide(instance: HostDashboardInstance, commit: String, op: WorkOperation) -> ProvideResult { + match compute_operation_admission(op: op) { + Present { value: why } => + compute_fresh(outcome: WorkRefusedByInfrastructure { identity: "", cause: FreshEffectNotServed { detail: why } }, subject: ExactTree { tree: compute_null_tree() }, op: op) + Absent => compute_provide_admitted(instance: instance, commit: commit, op: op) + } +} + +fn compute_provide_admitted(instance: HostDashboardInstance, commit: String, op: WorkOperation) -> ProvideResult { match compute_observe_toolchain(instance: instance) { ToolchainObservationRefused { detail } => compute_fresh(outcome: WorkRefusedByInfrastructure { identity: "", cause: ToolchainUnobserved { detail: detail } }, subject: ExactTree { tree: compute_null_tree() }, op: op) @@ -1864,7 +1932,7 @@ fn compute_provide_dependent(instance: HostDashboardInstance, commit: String, su r: compute_provide_leaf(instance: instance, commit: commit, subject: subject, op: op, toolchain: toolchain, dependency_store: compute_layout(instance: instance, identity_hex: build_identity).store_dir), carried: provide_result_host_record(r: build)) } else { - let identity = work_identity_hex(id: work_identity(subject: subject, op: op, toolchain: toolchain)) + let identity = compute_work_identity(subject: subject, op: op, toolchain: toolchain) provide_result_with_host_record( r: compute_fresh(outcome: WorkRefusedByInfrastructure { identity: identity, cause: DependencyNotSucceeded { dependency_identity: provide_result_identity(r: build), ending: provide_result_ending(r: build) } }, subject: subject, op: op), carried: provide_result_host_record(r: build)) @@ -1946,7 +2014,7 @@ fn compute_request_cli(repo_root: String, commit: String, operation: String, ent // holding a reference, not an identity. Deriving the root through a layout with an empty identity // would work and would also be a second spelling of the same path (DESIGN 3). fn compute_attempts_root(instance: HostDashboardInstance) -> NonEmptyStr { - join([dashboard_instance_compute_root(instance: instance) as String, "/attempts"], "") as NonEmptyStr + join([compute_root(instance: instance) as String, "/attempts"], "") as NonEmptyStr } // THE TERMINAL TRANSITION AND THE RELEASE ARE TWO FACTS AND THE MIDDLE ARM IS THE ONE THAT diff --git a/dag/gunbc/compute/work_request.dag b/dag/gunbc/compute/work_request.dag index e90a806a33b..a73efc395ce 100644 --- a/dag/gunbc/compute/work_request.dag +++ b/dag/gunbc/compute/work_request.dag @@ -1,8 +1,8 @@ module gunbc.compute.work_request import std.types { String, Bool, Int, List, NonEmptyStr, FilePath } +import std.algebra { trim } import std.measure { Kibibyte, kibibyte_count } -import std.content_hash { Fnv1a64Structural, content_hash_atom, content_hash_tagged_structural } import extdeps.systemd.systemd_run { SystemdRunOptionWordsRefusedCause, systemd_run_option_words_refused_cause_detail } import extdeps.git.object_store { GitObjectId, git_object_id_wire_hex } import extdeps.languages.json.emit { @@ -95,28 +95,64 @@ fn work_declared_outputs(op: WorkOperation) -> List { } } -// The toolchain half of the identity: the OBSERVED rustc version line and the sccache release the -// host serves, because the same tree under a different compiler is different work. A provider -// observes these before computing an identity; it never spells them. -type WorkToolchainIdentity { - rustc_version: NonEmptyStr +// THE EXECUTION HALF OF THE IDENTITY: everything that can change what the same tree and operation +// produce on a host, OBSERVED rather than spelled. Hidden fleet uniformity is not an identity axis +// (ruling on gunbc#13072, item 4): two hosts with the same rustc release line can still target +// different triples, run a different cargo, or interpret the operation through a different provider. +// rustc_verbose -- `rustc -vV` whole: release, commit hash, LLVM version and host triple +// target_triple -- the `host:` line of that output, the platform every built binary targets +// cargo_version -- `cargo --version` of the cargo the build actually runs +// cache_release -- the sccache release that wraps rustc +// provider_revision -- the git tree of the compute provider's own source (dag/gunbc/compute) in the +// repository the provider runs from, which decides how an operation becomes a +// command without changing the subject tree +// +// THE PROVENANCE WALL IS NOT YET STRUCTURAL ON THE NATIVE ROUTE, and this says so rather than claiming +// a seal. sole_constructor and admit_callers are enforced by the seed; on the native route both are +// standing rung drops (gunbc.rung_drop admit_callers_discarded_on_the_native_route and +// g0_type_decl_modifier_parse_without_sealing_property), so there the mint below is callable by any +// declaration. The provider's observation fold is its only production caller. +type WorkToolchainIdentity sole_constructor { + rustc_verbose: NonEmptyStr + target_triple: NonEmptyStr + cargo_version: NonEmptyStr cache_release: NonEmptyStr + provider_revision: NonEmptyStr } -fn work_identity(subject: WorkSubject, op: WorkOperation, toolchain: WorkToolchainIdentity) -> Fnv1a64Structural { - let tree = match subject { ExactTree { tree } => git_object_id_wire_hex(oid: tree) as String } - content_hash_tagged_structural( - tag: "gunbc-compute-work-v1" as NonEmptyStr, - payload: content_hash_atom(value: join([ - tree, "|", work_operation_canonical(op: op), "|", toolchain.rustc_version as String, "|", toolchain.cache_release as String, - ], "") as NonEmptyStr), - ) -} +fn work_toolchain_observed( + rustc_verbose: NonEmptyStr, + target_triple: NonEmptyStr, + cargo_version: NonEmptyStr, + cache_release: NonEmptyStr, + provider_revision: NonEmptyStr, +) -> WorkToolchainIdentity + admit_callers: [ + decl_ref(module_path: "gunbc.compute.work_provider_local", decl_name: "compute_toolchain_decide"), + decl_ref(module_path: "test.claim.compute.work_request_witness_test", decl_name: "tc"), + ] + = WorkToolchainIdentity { + rustc_verbose: rustc_verbose, + target_triple: target_triple, + cargo_version: cargo_version, + cache_release: cache_release, + provider_revision: provider_revision, + } -fn work_identity_hex(id: Fnv1a64Structural) -> String { - id.digest as String +// The host triple a `rustc -vV` reports, read from its one `host: ` line; absent is a refusal. +fn rustc_verbose_host_triple(verbose: String) -> String? { + match verbose.split(sep: "\n") |> filter(l => starts_with(s: l, prefix: "host: ")) |> first() { + Present { value: l } => Present { value: trim(s: l.replace(from: "host: ", to: "")) } + Absent => none + } } +// THERE IS NO work_identity HERE ANY MORE. It was a second key for one computation -- a private +// tagged hash beside std.materialization_provider's request_key -- and the identity of a compute +// request is now that provider's derived key over the ComputeWorkRequest family +// (gunbc.compute.work_provider_local compute_work_identity). This module keeps the work VOCABULARY; +// the materialization contract owns the key. + // One returned output: the declared path, the blob identity of its bytes (git's blob object id, a // content hash the same authority every subject already uses), and where the store holds it. type WorkOutput { @@ -179,6 +215,7 @@ type WorkInfrastructureRefusal | SubjectUnresolved { detail: String } | UnitMemoryGrantExceeded { class: String, granted: Kibibyte, peak_upper: Kibibyte?, grant_was_ceiling: Bool } | UnitEndingUnclassifiable { exit_code: Int, cause: String } + | FreshEffectNotServed { detail: String } | UnitLaunchRefused { cause: SystemdRunOptionWordsRefusedCause } fn work_outcome_identity(o: WorkOutcome) -> String { @@ -246,6 +283,7 @@ fn work_refusal_detail(r: WorkInfrastructureRefusal) -> String { ToolchainUnobserved { detail } => join(["toolchain unobserved: ", detail], "") DependencyNotSucceeded { dependency_identity, ending } => join(["dependency ", dependency_identity, " ended ", ending], "") SubjectUnresolved { detail } => join(["subject unresolved: ", detail], "") + FreshEffectNotServed { detail } => join(["this provider does not serve a fresh effect: ", detail], "") UnitEndingUnclassifiable { exit_code, cause } => join(["the unit exited ", to_string(value: exit_code), " and its ending could not be classified, so it is neither reported as failed nor as grant-exceeded: ", cause], "") UnitLaunchRefused { cause } => diff --git a/dag/gunbc/contributor_onboarding_path.dag b/dag/gunbc/contributor_onboarding_path.dag index fcd643dd75f..32e2d6bb1a5 100644 --- a/dag/gunbc/contributor_onboarding_path.dag +++ b/dag/gunbc/contributor_onboarding_path.dag @@ -1,7 +1,7 @@ module gunbc.contributor_onboarding_path import std.types { String, List, NonEmptyStr } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import std.optional { Optional, Absent, Present } import std.markdown { MarkdownDocument, MarkdownBlock } import gunbc.plans.md_helpers { h1, h2, h3, p, li, ul, ol, hr } @@ -111,11 +111,12 @@ type OnboardingBlocker // THE ORDINAL IS AN Int, AND THAT IS A CORRECTION RATHER THAN A PREFERENCE (review 64609). // It was first declared `std.nat` `Nat` -- `CommutativeSemiring` -- and rendered with -// `integer_nat_to_decimal_string`, whose parameter is `v2.std.nat.Nat`, the Peano `Zero | Succ` +// `integer_nat_to_decimal_string` (then in `v2.std.integer`, since moved to `std.nat` as +// `nat_to_decimal_string` by gunbc#13436), whose parameter was `v2.std.nat.Nat`, the Peano `Zero | Succ` // coproduct. Two materially different types sharing one spelling is the DESIGN section 3 meaning // fork, and this instance is invisible exactly where it is authored: under the interpreter's // numeric grounding the projection rendered 1 through 10 correctly, while the emitted closure -// refuses it. `v2.std.integer` `integer_int_to_decimal_string` records that same class as already +// refuses it. `std.integer` `int_to_decimal_string` records that same class as already // repaired for two Int-valued callers, so re-introducing it would have been a silent regression of // that fix. Every other `dag/gunbc` caller consumes the Int renderer; this one now does too. // @@ -653,7 +654,7 @@ fn obstacles_with_undeclared_step() -> List { // repair the rule roster above received, and the step ordinal was the last authored position left. fn step_blocks(s: OnboardingStep, position: Int) -> List { - let head = h3(text: join([integer_int_to_decimal_string(value: position), ". ", (s.id as String)], "")) + let head = h3(text: join([int_to_decimal_string(value: position), ". ", (s.id as String)], "")) let goal = p(text: (s.goal as String)) let who = p(text: join(["Who is needed: ", blocker_rendered(b: s.blocker)], "")) let verify = p(text: join(["Done when: ", (s.verification as String)], "")) @@ -690,7 +691,7 @@ fn onboarding_preamble_blocks() -> List { p(text: "Generated from `gunbc.contributor_onboarding_path`. Do not hand-edit — the generated-artifact phase adjudicates this file against that authority. Every command below is derived from the authority that owns it, so this page moves when the build, the lane partition or the source roots move."), p(text: "This page is an index of a path, not the proof that it works. What establishes the path is walking it: clone, build, run the lanes, make one change, get it reviewed, land it. If a step here does not work, that is a defect in the path and worth reporting as one — the obstacle roster below exists because each of its rows was hit rather than imagined."), p(text: "`DESIGN.md` is the authority for every judgement about a change and is not a first-day document. The reading order at the end of this page says what you need before your first build, what you need before your first commit, and what can wait."), - p(text: join(["**", integer_int_to_decimal_string(value: count(onboarding_rules)), " RULES APPLY TO EVERY STEP BELOW, because three strangers walking this page lost their build to them.**"], "")), + p(text: join(["**", int_to_decimal_string(value: count(onboarding_rules)), " RULES APPLY TO EVERY STEP BELOW, because three strangers walking this page lost their build to them.**"], "")), ol(items: map(onboarding_rules, r => li(text: join(["**", (r.name as String), ".** ", (r.rule as String)], "")))), hr(), ] @@ -723,7 +724,7 @@ fn onboarding_step_blocks() -> List { [ [ h2(text: "The path"), - p(text: join([integer_int_to_decimal_string(value: count(onboarding_steps)), " steps in order. Run each verification before moving on: a step that half-worked is the expensive kind, because the failure surfaces several steps later attached to the wrong cause."], "")), + p(text: join([int_to_decimal_string(value: count(onboarding_steps)), " steps in order. Run each verification before moving on: a step that half-worked is the expensive kind, because the failure surfaces several steps later attached to the wrong cause."], "")), ], rendered_step_blocks(steps: onboarding_steps), ], diff --git a/dag/gunbc/cursor_harness_credential.dag b/dag/gunbc/cursor_harness_credential.dag new file mode 100644 index 00000000000..8d2a789f02a --- /dev/null +++ b/dag/gunbc/cursor_harness_credential.dag @@ -0,0 +1,206 @@ +module gunbc.cursor_harness_credential + +import std.types { String, NonEmptyStr, Bool, List, FilePath } +import std.upsert_decision { ObservationVerdict, Converged, Drifted, UnknownRefused } +import extdeps.cloud.gcp.secret_ref { SecretRef } +import extdeps.cloud.gcp.secret_manager { SmVersionState } +import extdeps.languages.json.emit { JsonValue, JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject } +import extdeps.languages.json.parse { + parse_json_document, + JsonDocumentParsed, + JsonDocumentUnreadable, + json_object_unique_member, + JsonMemberFound, + JsonMemberAbsent, + JsonMemberDuplicated, + JsonMemberNotAnObject, +} +import extdeps.llm.cursor_cli { + cursor_api_key_env_var, + CursorRunShape, + cursor_model_auto, + CursorDefaultEditMode, + CursorSandboxEnabled, + CursorStreamJson, + cursor_environment_authenticated_argv, +} +import extdeps.posix.sh_invocation { posix_sh_export_from_file_then_exec_command, PosixShExportExecReady, PosixShExportExecEmitRefused, PosixShRemoveFileAfterRead } +import extdeps.posix.shell_command_language { admit_posix_shell_name, PosixShellNameAdmitted, PosixShellNameRefused } +import extdeps.exec.command { argv_words } +import product.host_identity { HostIdentity } +import gunbc.fleet_host_identity { operator_host_srv2 } +import gunbc.secret_provision { fleet_secret_ref, secret_ref_pin_version } +import gunbc.readback_independence { ReadBackProbe, ProbeInert } +import gunbc.auth.materialized_secret { + EnvVar, + MaterializedSecret, + MaterializationOutcome, + with_materialized_secret, +} + +// ONE CURSOR API KEY, READ FROM gunbai-secrets, BOUND AS CURSOR_API_KEY. extdeps.llm.cursor_cli +// records that the CLI authenticates per invocation with a value, so the fleet credential is a secret +// and nothing else: no directory, no rotation, nothing to write back. The dispatch credential is the +// existing CursorApiKeySecretRef arm (gunbc.dispatch_selection declared_provider_inventory_for_instance +// builds it from this ref), and gunbc.cursor_sdk_local_binding admits it unchanged. +// +// The version is EXACT, not latest: gunbc.cursor_sdk_secret_admission refuses latest for Cursor, and +// that ruling stands. Version 1 is the operator's first upload; replacing the key is a new version +// and an edit to this row. + +data cursor_api_key_harness_secret_id: NonEmptyStr = "cursor-api-key-harness" as NonEmptyStr + +data cursor_api_key_harness_secret_ref: SecretRef = secret_ref_pin_version( + ref: fleet_secret_ref(secret_id: cursor_api_key_harness_secret_id), + version: "1", +) + + +// THE LIVENESS PROBE IS THE CLI'S OWN STATUS COMMAND. Observed on cursor-agent 2026.10.01-e373342 +// (2026-10-05) with a deliberately invalid key in CURSOR_API_KEY: `cursor-agent status --format json` +// exits 0 and prints {"status":"unauthenticated","isAuthenticated":false,...}. It asks Cursor whether +// the key is accepted and cannot make a rejected key accepted, so it is ProbeInert and +// with_materialized_secret's independence gate admits it. A --print turn with the same invalid key +// exits 1 with prose on stderr and no JSON, which is why the probe runs before the turn rather than +// the turn's failure standing in for it. +data cursor_status_probe: ReadBackProbe = ReadBackProbe { + label: "cursor-agent status --format json with CURSOR_API_KEY bound", + subject: "the API key cursor-api-key-harness holds is one Cursor currently accepts", + effect_on_subject: ProbeInert, +} + +data cursor_status_is_authenticated_key: NonEmptyStr = "isAuthenticated" as NonEmptyStr +data cursor_status_status_key: NonEmptyStr = "status" as NonEmptyStr + +type CursorStatusReading + = CursorStatusAuthenticated + | CursorStatusUnauthenticated { status: String } + | CursorStatusUnreadable { detail: String } + +fn cursor_status_status_text(doc: JsonValue) -> String { + match json_object_unique_member(v: doc, key: cursor_status_status_key as String) { + JsonMemberFound { value } => + match value { + JsonString { value: s } => s + JsonNull => "" + JsonBool { value: _ } => "" + JsonNumber { lexeme: _ } => "" + JsonArray { elements: _ } => "" + JsonObject { members: _ } => "" + } + JsonMemberAbsent => "" + JsonMemberDuplicated { count: _ } => "" + JsonMemberNotAnObject => "" + } +} + +fn cursor_status_reading(stdout: String) -> CursorStatusReading { + match parse_json_document(s: stdout) { + JsonDocumentUnreadable { gap: _ } => CursorStatusUnreadable { detail: "status output is not a JSON document" } + JsonDocumentParsed { value: doc } => + match json_object_unique_member(v: doc, key: cursor_status_is_authenticated_key as String) { + JsonMemberFound { value } => + match value { + JsonBool { value: authenticated } => + if authenticated { + CursorStatusAuthenticated + } else { + CursorStatusUnauthenticated { status: cursor_status_status_text(doc: doc) } + } + JsonNull => CursorStatusUnreadable { detail: "isAuthenticated is not a boolean" } + JsonNumber { lexeme: _ } => CursorStatusUnreadable { detail: "isAuthenticated is not a boolean" } + JsonString { value: _ } => CursorStatusUnreadable { detail: "isAuthenticated is not a boolean" } + JsonArray { elements: _ } => CursorStatusUnreadable { detail: "isAuthenticated is not a boolean" } + JsonObject { members: _ } => CursorStatusUnreadable { detail: "isAuthenticated is not a boolean" } + } + JsonMemberAbsent => CursorStatusUnreadable { detail: "isAuthenticated absent" } + JsonMemberDuplicated { count: _ } => CursorStatusUnreadable { detail: "isAuthenticated duplicated" } + JsonMemberNotAnObject => CursorStatusUnreadable { detail: "status output is not a JSON object" } + } + } +} + +// Rejected is Drifted (the store holds a value upstream no longer accepts), not Absent: the secret +// exists, the key in it is dead, and the remedy is a new key, not a new secret. +fn cursor_status_liveness(reading: CursorStatusReading) -> ObservationVerdict { + match reading { + CursorStatusAuthenticated => Converged + CursorStatusUnauthenticated { status: _ } => Drifted + CursorStatusUnreadable { detail: _ } => UnknownRefused + } +} + +// THE BRACKET, with the status probe's stdout as the liveness observation. No probe output means no +// observation, and with_materialized_secret refuses that as UnknownRefused. +fn with_cursor_harness_api_key( + existence: SmVersionState, + status_stdout: String?, + use: fn(MaterializedSecret) -> R, +) -> MaterializationOutcome { + let observed = match status_stdout { + Present { value: out } => Present { value: cursor_status_liveness(reading: cursor_status_reading(stdout: out)) } + Absent => none + } + with_materialized_secret( + ref: cursor_api_key_harness_secret_ref, + binding: EnvVar { name: cursor_api_key_env_var }, + existence: existence, + probe: cursor_status_probe, + liveness_observation: observed, + use: use, + ) +} + +// ── The spawn: the custody file becomes CURSOR_API_KEY in the child, never argv ───────────────── +// +// gunbc.host_credential_custody_converge CursorWorkerTurnApiKey places the key at this path on this +// host, read-only to the operator account the dispatch spawns run as. The spawn exports it from the +// file inside the child through the shared extdeps.posix.sh_invocation wrapper, which exits 78 if the +// file cannot be read, so the key is never on an argv the process table shows. + +data cursor_worker_turn_custody_host: HostIdentity = operator_host_srv2 +data cursor_worker_turn_api_key_host_path: NonEmptyStr = "/etc/gunbc-cursor-harness/api-key" as NonEmptyStr + +// THE UNATTENDED RUN: print mode with stream-json events, the default editing mode, --force so the +// agent may run commands without a human to approve them, the sandbox explicitly enabled so --force +// does not also mean unconfined, and --trust because an untrusted workspace prompts and an +// unattended run would hang there (extdeps.llm.cursor_cli). +fn cursor_worker_turn_run(worktree_path: String, prompt: NonEmptyStr) -> CursorRunShape { + CursorRunShape { + model: cursor_model_auto, + mode: CursorDefaultEditMode, + sandbox: CursorSandboxEnabled, + output_format: CursorStreamJson, + force: true, + trust_workspace: true, + workspace: worktree_path as FilePath, + prompt: prompt, + } +} + +// key_path is the attempt's credential snapshot, removed by the shell before exec (the shared +// one-snapshot rule, gunbc.roadmap_dispatch_actuator dispatch_credential_snapshot_path_for_instance). +// The env-var name is admitted as a POSIX name before it reaches the wrapper (extdeps.posix. +// shell_command_language admit_posix_shell_name), and the wrapper's program is emitted from the bash +// grammar, which may refuse; either refusal is one arm carrying its reason. +type CursorWorkerTurnArgv + = CursorWorkerTurnArgvReady { argv: List } + | CursorWorkerTurnRefused { reason: NonEmptyStr } + +fn cursor_worker_turn_argv(key_path: NonEmptyStr, worktree_path: String, prompt: NonEmptyStr) -> CursorWorkerTurnArgv { + match admit_posix_shell_name(spelling: cursor_api_key_env_var as String) { + PosixShellNameAdmitted { name } => + match posix_sh_export_from_file_then_exec_command( + name: name, + path: key_path, + command: cursor_environment_authenticated_argv(run: cursor_worker_turn_run(worktree_path: worktree_path, prompt: prompt)), + disposition: PosixShRemoveFileAfterRead, + ) { + PosixShExportExecReady { command } => CursorWorkerTurnArgvReady { argv: argv_words(command: command) } + PosixShExportExecEmitRefused { name: n } => + CursorWorkerTurnRefused { reason: join(["the sh emitter refused the export-and-exec program for ", n as String], "") as NonEmptyStr } + } + PosixShellNameRefused { spelling } => + CursorWorkerTurnRefused { reason: join(["the cursor credential variable name ", spelling, " is not a POSIX shell name"], "") as NonEmptyStr } + } +} diff --git a/dag/gunbc/deploy_transition_bootstrap.dag b/dag/gunbc/deploy_transition_bootstrap.dag index 954f1777c89..e79f1168a66 100644 --- a/dag/gunbc/deploy_transition_bootstrap.dag +++ b/dag/gunbc/deploy_transition_bootstrap.dag @@ -5,7 +5,10 @@ import std.types { String, Bool, List, NonEmptyStr } import gunbc.live_deploy.spec { DeploymentNames, deployment_belt_service_unit_name, - deployment_belt_timer_unit_name, + deployment_belt_exit_path_unit_name, + deployment_belt_answer_path_unit_name, + deployment_belt_exit_drain_service_unit_name, + deployment_belt_discovery_service_unit_name, } import gunbc.live_deploy.operations { systemctl_mask_now_command, @@ -82,15 +85,19 @@ fn quiescence_regime(awareness: ReleaseMarkerAwareness) -> QuiescenceRegimeDecis } } -// THE QUIESCED SET IS EVERY UNIT THAT CAN ACTUATE A RELEASE, so both belt units are in it, not only -// the timer. Disabling the timer alone leaves the one-shot service startable by anything reaching -// systemd, and the HTTP dispatcher lives in the serve unit -- the deploy convergence design record -// already notes that stopping only the timer leaves the click path admitting against a -// half-published release, which is this defect one layer out. +// THE QUIESCED SET IS EVERY UNIT THAT CAN ACTUATE A RELEASE: the belt's two event path units (a +// recorded exit or a publication answer starts a unit that queues obligations -- there is no belt +// timer since dogfood intervention #4), the belt service that queues the launch, the drain and discovery services those +// paths start (static oneshots: masking only the triggers leaves them startable by any other +// requester), and the serve unit, where the HTTP dispatcher lives -- the deploy convergence design record already notes that stopping +// only the belt's trigger leaves the click path admitting against a half-published release. fn bootstrap_quiesced_units(names: DeploymentNames) -> List { [ - deployment_belt_timer_unit_name(names: names), + deployment_belt_exit_path_unit_name(names: names), + deployment_belt_answer_path_unit_name(names: names), deployment_belt_service_unit_name(names: names), + deployment_belt_exit_drain_service_unit_name(names: names), + deployment_belt_discovery_service_unit_name(names: names), names.unit_name, ] } @@ -102,7 +109,7 @@ fn bootstrap_quiesced_units(names: DeploymentNames) -> List { // asserted a guarantee the operation does not deliver. // // MEASURED, not reasoned: `gunbc.live_deploy.emit` renders the belt SERVICE with -// `install: NotInstallable` -- a STATIC unit, activated by the belt timer rather than an [Install] +// `install: NotInstallable` -- a STATIC unit, activated by another unit rather than an [Install] // section. A static unit has no enablement symlinks, so `systemctl disable` does nothing; only the // `--now` half took effect, which is exactly the non-durable `stop` rejected above. The belt // service's declared durability was supplied by the timer being disabled separately, not by this diff --git a/dag/gunbc/derived_row_roster_seed_growth.dag b/dag/gunbc/derived_row_roster_seed_growth.dag index ecf4e5777e7..844780ac6c6 100644 --- a/dag/gunbc/derived_row_roster_seed_growth.dag +++ b/dag/gunbc/derived_row_roster_seed_growth.dag @@ -4,8 +4,11 @@ import gunbc.roadmap_model { RoadmapNodeId } import gunbc.seed_growth { SeedGrowthJustification } import std.decl_ref { DeclarationRef, WholeDeclaration } -// FORWARD-FREEZE RECEIPT for the host fold that derives gunbc.recurring_failure_mode.roster -// from sibling row files. gunbc.seed_growth_admission seed_growth_forward_freeze_policy_note +// FORWARD-FREEZE RECEIPT for the host fold that derives gunbc.recurring_failure_mode.roster and +// gunbc.rung_drop.roster from sibling row files and contributes them to the compiler's input IN +// MEMORY. The AcquiredDag impl methods (on_disk, path, is_derived, read) are not separately citable +// as DeclarationRefs and are enumerated here instead; they are covered by the AcquiredDag row's +// purpose and retire with it. gunbc.seed_growth_admission seed_growth_forward_freeze_policy_note // makes unenumerated hand growth in src/v1 a stop-line. The PR body pointing at // gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall is the right dissolution // CONTENT and is not a substitute for enumerating the added items (DESIGN §5: a dissolution @@ -31,12 +34,22 @@ data derived_row_roster_seed_growth_justification: SeedGrowthJustification = See DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "ROSTER_MODULE", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "ROW_DIR_REL", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "row_dir_roster", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "ensure_if_row_dir", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "ensure_if_row_dir_or_panic", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "ensure_derived_roster", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "write_atomically", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "row_stems", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "render_roster", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "AcquiredDag", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster", decl_name: "acquire_dir_files", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "scratch_workspace", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "author", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "drop_row", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "tree", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "rung_drop_fixture", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "acquire_rung_drop_dir", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "derived_roster_body", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "acquisition_contributes_the_roster_in_memory_and_writes_nothing", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "adding_and_removing_a_row_file_changes_the_roster", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "a_legacy_physical_roster_refuses_located_and_is_neither_consumed_nor_deleted", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "an_unreadable_row_input_refuses_rather_than_shortening_the_roster", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "acquisition_works_over_read_only_source_roots", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "the_parse_sweep_indexes_the_derived_roster_without_writing_it", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "the_roster_module_is_the_row_module_plus_the_roster_stem", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "reader", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.derived_row_roster.tests", decl_name: "membership_is_selected_by_declared_row_type_not_by_directory", field: WholeDeclaration }, @@ -58,8 +71,8 @@ data derived_row_roster_seed_growth_justification: SeedGrowthJustification = See DeclarationRef { module_path: "v1_compiler.cli_run.rostered_row_join.file_roster_join_tests", decl_name: "roster_member_without_a_file_refuses", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.rostered_row_join.file_roster_join_tests", decl_name: "equal_file_and_roster_sets_are_silent", field: WholeDeclaration } ], - reason: "WHY RUST RATHER THAN .dag: the substrate has no directory enumeration sufficient to BIND each sibling `.dag` as a typed RecurringFailureMode value. Membership discovery exists (`v2.std.decl_index` `data_decl_type_facts`); value binding does not. Until that grounding lands, a checkout with no committed roster has no module for the three importers (`gunbc.design_ledgers`, `gunbc.ledger_row_coherence`, `test.claim.generated_artifact_merge_driver_real_execution_witness`) and the required parse join reports RosterModuleAbsent on the intended steady state. The host fold lists sibling files, writes gitignored roster.dag atomically, and refuses a shortened list. That is a §7 seed realization of a missing capability, not a scaffold: dissolution is grounded in gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall (declaration-value binding), not in a sentence in the Rust module.\n\nThe file-versus-roster join arms are the other half of the same host: a skip in the writer must still refuse. They execute inside run_rostered_row_join on the required parse path (witnesses lane). file_roster_join_tests are the off-path REDs for those arms; they are enumerated because test items are still hand Rust.\n\nWHY ADMITTED AGAINST THE v1 FREEZE: gunbc.v1_maintenance_standing v1_seed_standing admits by PURPOSE. Removing the silent-drop constructor from the failure-mode ledger is an instrument tax every self-host lane was paying (merge hotspot). No new language behavior, no escape hatch, no emitted public surface.\n\nTHE BASE SIDE OF THE DERIVED ROSTER (gunbc#10856, three production items and three test items). is_derived_roster_path, roster_root_prefix and roster_from_path_listing exist because a DERIVED artifact has no base side in a diff and must not be inherited from the head. roster.dag is gitignored and written on the read path, so it can never appear in `git diff --name-status`, and the baseline reconstruction -- carrying forward every head record the diff did not touch -- was therefore standing the HEAD`s generated roster up as the BASE`s. That made an ordinary row append read as a name resolving into a pool rather than an authored reference, because the roster`s two sides were the same bytes. They live HERE rather than beside their caller for the reason this module exists: roster_from_path_listing renders through render_roster, the same function the writer uses, so the roster`s bytes keep ONE authority whether the question is asked of a directory or of a base tree listing. They carry the same trigger as the rest of this population -- with declaration-value binding the derivation has no name-list at all, on either side, and these delete with the writer.\n\nTHE SECOND LEDGER, NOT A SECOND MECHANISM (gunbc.rung_drop cutover). gunbc.rung_drop.roster was a hand-appended list, so a drop file nobody also listed there was absent from docs/design-rung-drops.md. The same fold now derives it: DerivedRowRoster is one table row per ledger (DERIVED_ROW_ROSTERS), and declares_row selects a sibling file only when it declares its own stem as a row of the ledger's type, because the rung-drop directory also holds helper modules (shared_capability, standing). That selector is a line-shape read and not the authority on what was declared; the parse join's DeclaredNotRostered reads the compiled population and refuses any RungDrop row it missed. The base side reads each listed row file at the base revision for the same reason. These carry the same trigger as the rest of the population.\n\nRe-derive this list with tools.seed_growth_change_population rather than maintaining a count.", + reason: "WHY RUST RATHER THAN .dag: the substrate has no directory enumeration sufficient to BIND each sibling `.dag` as a typed row value. Membership discovery exists (`v2.std.decl_index` `data_decl_type_facts`); value binding does not. Until that grounding lands, a checkout carries no roster module for the importers (`gunbc.design_ledgers`, `gunbc.ledger_row_coherence`, `gunbc.rung_drop.standing` and its readers, `test.claim.generated_artifact_merge_driver_real_execution_witness`) unless the host contributes one, and the required parse join reports RosterModuleAbsent on the intended steady state. THE BOUNDARY IS SOURCE ACQUISITION, AND IT WRITES NOTHING: every walk that acquires a source population hands each directory's listing to acquire_dir_files, which returns AcquiredDag sources -- the listed files, plus for a row directory the roster rendered (render_roster) from the sibling contents that same call read -- so compiler_sources = acquired_sources + derive_rosters(acquired_sources) from one read of the rows. AcquiredDag has no Deref to Path, so a consumer cannot reach the disk for a module that has no file. A physical file at a reserved roster identity refuses, located, and is never consumed, overridden or deleted by the loader; an unreadable row refuses rather than shortening the roster. The earlier realization wrote a gitignored roster.dag into the checkout on every walk, which dirtied the checkout each invocation admitted and made deploy candidate admission refuse; that writer (ensure_if_row_dir, ensure_if_row_dir_or_panic, ensure_derived_roster, write_atomically, row_stems) is DELETED, and the deploy-scope exemption it forced is deleted with it. That is a section 7 seed realization of a missing capability, not a scaffold: dissolution is grounded in gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall (declaration-value binding), not in a sentence in the Rust module.\n\nThe file-versus-roster join arms are the other half of the same host: a skip in the derivation must still refuse. They execute inside run_rostered_row_join on the required parse path (witnesses lane). file_roster_join_tests are the off-path REDs for those arms; they are enumerated because test items are still hand Rust.\n\nWHY ADMITTED AGAINST THE v1 FREEZE: gunbc.v1_maintenance_standing v1_seed_standing admits by PURPOSE. Removing the silent-drop constructor from the failure-mode ledger is an instrument tax every self-host lane was paying (merge hotspot). No new language behavior, no escape hatch, no emitted public surface.\n\nTHE BASE SIDE OF THE DERIVED ROSTER (gunbc#10856, three production items and three test items). is_derived_roster_path, roster_root_prefix and roster_from_path_listing exist because a DERIVED artifact has no base side in a diff and must not be inherited from the head. the roster module has no committed file (it is contributed in memory on the read path), so it can never appear in `git diff --name-status`, and the baseline reconstruction -- carrying forward every head record the diff did not touch -- was therefore standing the HEAD`s generated roster up as the BASE`s. That made an ordinary row append read as a name resolving into a pool rather than an authored reference, because the roster`s two sides were the same bytes. They live HERE rather than beside their caller for the reason this module exists: roster_from_path_listing renders through render_roster, the same function head acquisition uses, so the roster`s bytes keep ONE authority whether the question is asked of a directory or of a base tree listing. They carry the same trigger as the rest of this population -- with declaration-value binding the derivation has no name-list at all, on either side, and these delete with the derivation.\n\nTHE SECOND LEDGER, NOT A SECOND MECHANISM (gunbc.rung_drop cutover). gunbc.rung_drop.roster was a hand-appended list, so a drop file nobody also listed there was absent from docs/design-rung-drops.md. The same fold now derives it: DerivedRowRoster is one table row per ledger (DERIVED_ROW_ROSTERS), and declares_row selects a sibling file only when it declares its own stem as a row of the ledger's type, because the rung-drop directory also holds helper modules (shared_capability, standing). That selector is a line-shape read and not the authority on what was declared; the parse join's DeclaredNotRostered reads the compiled population and refuses any RungDrop row it missed. The base side reads each listed row file at the base revision for the same reason. These carry the same trigger as the rest of the population.\n\nRe-derive this list with tools.seed_growth_change_population rather than maintaining a count.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, trigger: "THE SUBJECT IS THESE DECLARATIONS, and they retire with declaration introspection sufficient to BIND a discovered data declaration as a typed value of its declared type — the same capability named by gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall — at which point the host fold and the file-join arms DELETE because there is no authored or derived name-list to omit a row from. A differently named hand writer is not dissolution: it is this obligation under a new key. Registering roster.dag on gunbc.generated_artifact does not discharge this: that would restore heal/drift rewrite of a gitignored file. data_decl_type_facts already exists and does not discharge a trigger that names BINDING.", - current_boundary: "v1_compiler.cli_run.derived_row_roster is reached from run_required_wave_admission (is_derived_roster_path, roster_root_prefix and roster_from_path_listing, which is the phase`s base side and the only consumer of those three), collect_dag_files_result, collect_dag_files_tolerant (write errors ignored), main.rs collect_dag_files, compiler_tests collect_dag_recursive, and run_dag_parse_sweep (the loud required-CI writer). Its DERIVED_ROW_ROSTERS table covers gunbc.recurring_failure_mode.roster and gunbc.rung_drop.roster. join_row_files_to_roster_members is reached only from join_recurring_failure_mode_files_to_roster inside run_rostered_row_join. The derived files are gitignored and are not gunbc.generated_artifact rows. Docs gate main agrees only DesignRungDropsArtifact." + current_boundary: "v1_compiler.cli_run.derived_row_roster is reached from run_required_wave_admission (is_derived_roster_path, roster_root_prefix and roster_from_path_listing, the phase`s base side; namespace_baseline reads a committed base roster as source and synthesizes none beside it), from entry_source_from_index_or_disk (is_derived_roster_path, answering an entry that names a derived roster from the index), and through acquire_dir_files from every source-acquisition walk: collect_dag_files_result, collect_dag_files_tolerant (which now returns a located refusal for a roster it cannot derive instead of swallowing it), main.rs collect_dag_files, compiler_tests collect_dag_recursive, and run_dag_parse_sweep (the required-CI parse phase). Consumers of those walks receive AcquiredDag and read through it, so module indexing, import and reference discovery, entry lookup, for_each_parsed_module_binding, try_build_module_index, try_index_source_root_into_module_index, decl_facts_corpus_walk and decls_parse_only_from_disk see the same population as the parse sweep. Nothing writes under a source root. Its DERIVED_ROW_ROSTERS table covers gunbc.recurring_failure_mode.roster and gunbc.rung_drop.roster. join_row_files_to_roster_members is reached only from join_recurring_failure_mode_files_to_roster inside run_rostered_row_join. Legacy roster files left by older binaries are gunbc.repo_workspace SupersededAuthority rows, retired by gunbc.live_deploy.repository_convergence retire_superseded_workspace_files during deployed-tree convergence. Docs gate main agrees only DesignRungDropsArtifact." } diff --git a/dag/gunbc/design_argument.dag b/dag/gunbc/design_argument.dag index 626165a13f7..0ee8ca45524 100644 --- a/dag/gunbc/design_argument.dag +++ b/dag/gunbc/design_argument.dag @@ -110,7 +110,7 @@ data conformance_domains: List = [ ConformanceDomain { key: "conformance-leasing", label: "leasing / locking / grants / privileged access", - question: "Does the change take exclusive or bounded use of something -- a slot, a head, a host, a file, a budget -- or perform a privileged effect -- a Secret Manager read or write, an IAM binding, a credential mint, a GitHub App token -- without inhabiting the corpus's lease, grant and authorization-pattern model, and if it departs from that model, does it say why? Who may perform an effect and how that permission was granted is one question: the authorization pattern (workload identity federation with a scoped grant, per-effect operator approval through the ntfy loop, or a named human-only step) is SELECTED by gunbc.auth.authorization_pattern_selection from the effect's characteristics, never chosen at the call site, and an operator credential relayed by chat or between sessions is the pattern that selection refuses.", + question: "Does the change take exclusive or bounded use of something -- a slot, a head, a host, a file, a budget -- or perform a privileged effect -- a Secret Manager read or write, an IAM binding, a credential mint, a GitHub App token -- without inhabiting the corpus's lease, grant and authorization-pattern model, and if it departs from that model, does it say why? Who may perform an effect and how that permission was granted is one question: the authorization pattern (workload identity federation with a scoped grant, per-effect operator approval through the ntfy loop, or a named human-only step) is SELECTED by gunbc.auth.authorization_pattern_selection from the effect's characteristics, never chosen at the call site.", homes: ConformanceHomes { first: decl_ref(module_path: "std.temporal_effect", decl_name: "HeldLease"), rest: [ @@ -140,7 +140,7 @@ data conformance_domains: List = [ "a one-off or irreversible effect performed by a dispatched run under the federated identity with no approval request, or a recurring effect gated on a per-run phone tap", "a dashboard-only or checkout-only action performed by prose instruction with no std.human_intervention row naming the step" ], - narrative: "exclusive or bounded use and privileged access are one domain: leases with a declared deadline, compare-and-set expectations, scoped authorizations, grants naming who may perform an effect and how it is released, resource handles -- and, since 2026-09-19, which AUTHORIZATION PATTERN performs a privileged effect. That pattern is a §3d selection (gunbc.auth.authorization_pattern_selection consumes std.decision over the effect's frequency, reversibility, surface, workload-identity binding, minted reach and billing consequence): workload identity federation with a per-secret cell for recurring automated effects, one operator approval through the ntfy loop for one-off or witnessed effects, a named human-only step where no API exists, and a refusal for an operator token relayed by chat or between sessions. The census (gunbc.auth.privileged_effect_census) classifies every current site by executing that selection, so the reviewer's three-valued answer is derived for the sites that exist and asked of the site a change adds. A compare-and-set slot's generation line is kept whole by file_compare_and_set and observe_cas_slot_state (gunbc.durable_cas_file_store), because several slots read it as an audit trail; a slot whose only fact is its head -- the materialization store's occupancy index and family holds are the first -- uses the distinct windowed operations (file_compare_and_set_windowed, observe_cas_slot_state_windowed, and the file_hold_*_windowed family) with a CasGenerationWindow of k >= 2, so a bounded store's own bookkeeping does not grow with its commits. They are two contracts, not one operation with a mode: keep-all callers are untouched, and only a caller that names the windowed operation can delete generations. A window read takes the head from a listing of the slot root and verifies head+1 is absent, re-reading on a race and never guessing; a write reclaims below head-k only after its own commit, and a refused delete is reported, never fatal. Exclusion is unchanged -- losers re-read and retry -- so nothing waits on a hold. docs/plans/fabric-storage.md names the head-probe bound this also lifts and assigns its remedy to compaction -- a head that starts from a snapshot; a window is that head-starting-above-1 shape for a slot whose history needs no snapshot, and the fabric heads are not opted in", + narrative: "exclusive or bounded use and privileged access are one domain: leases with a declared deadline, compare-and-set expectations, scoped authorizations, grants naming who may perform an effect and how it is released, resource handles -- and, since 2026-09-19, which AUTHORIZATION PATTERN performs a privileged effect. That pattern is a §3d selection (gunbc.auth.authorization_pattern_selection consumes std.decision over the effect's frequency, reversibility, surface, workload-identity binding, minted reach and billing consequence): workload identity federation with a per-secret cell for recurring automated effects, one operator approval through the ntfy loop for one-off or witnessed effects, and a named human-only step where no API exists. The census (gunbc.auth.privileged_effect_census) classifies every current site by executing that selection, so the reviewer's three-valued answer is derived for the sites that exist and asked of the site a change adds. A compare-and-set slot's generation line is kept whole by file_compare_and_set and observe_cas_slot_state (gunbc.durable_cas_file_store), because several slots read it as an audit trail; a slot whose only fact is its head -- the materialization store's occupancy index and family holds are the first -- uses the distinct windowed operations (file_compare_and_set_windowed, observe_cas_slot_state_windowed, and the file_hold_*_windowed family) with a CasGenerationWindow of k >= 2, so a bounded store's own bookkeeping does not grow with its commits. They are two contracts, not one operation with a mode: keep-all callers are untouched, and only a caller that names the windowed operation can delete generations. A window read takes the head from a listing of the slot root and verifies head+1 is absent, re-reading on a race and never guessing; a write reclaims below head-k only after its own commit, and a refused delete is reported, never fatal. Exclusion is unchanged -- losers re-read and retry -- so nothing waits on a hold. docs/plans/fabric-storage.md names the head-probe bound this also lifts and assigns its remedy to compaction -- a head that starts from a snapshot; a window is that head-starting-above-1 shape for a slot whose history needs no snapshot, and the fabric heads are not opted in", }, ConformanceDomain { key: "conformance-compute", diff --git a/dag/gunbc/dispatch_selection.dag b/dag/gunbc/dispatch_selection.dag index 42dceb66b2d..59bb6f972bc 100644 --- a/dag/gunbc/dispatch_selection.dag +++ b/dag/gunbc/dispatch_selection.dag @@ -45,6 +45,7 @@ import gunbc.codex_provider_runtime_receipt { codex_process_fingerprint_unobserved_reason, } import extdeps.llm.cursor_cli { CursorModelId, cursor_model_auto } +import gunbc.cursor_harness_credential { cursor_api_key_harness_secret_ref } import extdeps.cloud.gcp.secret_ref { SecretRef } import gunbc.claude_code_credential { claude_code_oauth_harness_secret_ref } import gunbc.roadmap_sizing { @@ -784,6 +785,32 @@ fn inventory_with_observed_standing(inventory: ProviderInventory, kind: Provider } } +// The offer the codex draw is judged on names the custody CODEX_HOME the standing was observed against +// and the spawn binds, never the instance's ambient account state root. This is the root, not the +// standing: the standing join is inventory_with_observed_standing. +fn inventory_with_codex_state_root(inventory: ProviderInventory, account_state_root: FilePath) -> ProviderInventory { + ProviderInventory { + offers: map(inventory.offers, offer => + match offer { + OfferCodex { offer: o } => + OfferCodex { + offer: CodexOffer { + instance: o.instance, + account_state_root: account_state_root, + account_binding: o.account_binding, + standing: o.standing, + default_effort: o.default_effort, + deep: o.deep, + process_fingerprint: o.process_fingerprint, + }, + } + OfferClaude { offer: _ } => offer + OfferCursor { offer: _ } => offer + } + ), + } +} + fn claude_offer_for_instance_with_standing( instance: HostDashboardInstance, standing: ProviderStanding, @@ -945,7 +972,7 @@ fn declared_provider_inventory_for_instance(instance: HostDashboardInstance) -> reason: "inventory row is declared, not observed at selection time" as NonEmptyStr, }, }, - credential: CursorLocalLoginRef, + credential: CursorApiKeySecretRef { secret_ref: cursor_api_key_harness_secret_ref }, standing: provider_standing_inventory_unobserved(), default_model: cursor_model_auto, process_fingerprint: dispatch_cursor_process_fingerprint, @@ -1470,7 +1497,7 @@ fn provider_selection_request_for_kind(kind: ProviderKind) -> ProviderSelectionR CursorCliProvider => SelectCursor { profile: ProfileAutomatic, - credential: CursorLocalLoginRef, + credential: CursorApiKeySecretRef { secret_ref: cursor_api_key_harness_secret_ref }, model: cursor_model_auto, agent_mode: "default-edit" as NonEmptyStr, } diff --git a/dag/gunbc/doc_graph_roots.dag b/dag/gunbc/doc_graph_roots.dag index 3fea9fd2d7b..8e68fde0be8 100644 --- a/dag/gunbc/doc_graph_roots.dag +++ b/dag/gunbc/doc_graph_roots.dag @@ -239,17 +239,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the required floor's in-flight shared-fill OWNERSHIP is changed so this characterization no longer describes the shipped mechanism; the row deletes with the superseded findings document. THE OTHER CONJUNCT OF THIS TRIGGER HAS ALREADY FIRED AND THE ROW IS DELIBERATELY RETAINED, which is stated here because a half-fired trigger read as unfired is how a dead assumption keeps answering questions: the deadline-accounting half died on 2026-09-12 when the claim ceiling moved onto eval_steps and CPU became observed-only for every claim, and changed_witness_cpu_deadline -- the subject this row was registered at -- was deleted with it. The registration moves to claim_cost_basis_standing, the successor decision about which quantity may refuse a claim, and the document carries a superseded-half banner at its head. The shared-fill half is untouched and is the live subject: eval steps are netted of fill exactly as CPU is, so the ownership argument transfers to the new ceiling without restatement, and it is still what required-floor-in-flight-shared-fill-design.md cites.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "fabric-ci-replacement", - primary_work: DeclarationRef { module_path: "gunbc.witness_floor_workflow", decl_name: "witness_floor_job", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "extdeps.github.checks", decl_name: "CheckRun", field: WholeDeclaration }, - DeclarationRef { module_path: "product.fabric.execution", decl_name: "ExecutionGrant", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.ci_runner_target", decl_name: "CiRunnerTarget", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the cutover lands: the fabric issues the grant that runs the required floor, the check verdict is reported through the modeled Checks surface, and .github/workflows/witnesses.yml is deleted along with the emitter that produced it -- at which point this document describes a shipped system rather than a planned one and the row deletes with it. Registered at subject grain: the four systems bound here are the ones whose movement would make this document's purpose false. witness_floor_job is the thing being replaced and is bound deliberately, so that if it is renamed or restructured before the cutover this document reds rather than silently describing a job that no longer exists.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "fabric-recut-program", @@ -360,53 +349,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "each parked item is either promoted to its own design document or retired with a reason, leaving this file with no subjects; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "affected-set-differential-falsifier", - primary_work: DeclarationRef { module_path: "v2.lens.module_graph", decl_name: "entry_affected_by_touched_paths", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject entry_affected_by_touched_paths describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "algebra-grounding-unification-design", - primary_work: DeclarationRef { module_path: "std.algebra", decl_name: "FreeMonoid", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject FreeMonoid describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "body-lowering-design", - primary_work: DeclarationRef { module_path: "std.termination", decl_name: "DescentEvidence", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.cardinality", decl_name: "node_multiplicity", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.cardinality", decl_name: "loop_bound_witness_for_node", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject DescentEvidence describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "build-cache-placement-receipt", - primary_work: DeclarationRef { module_path: "extdeps.cache.sccache", decl_name: "sccache_stats_query_does_not_spawn_note", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject sccache_stats_query_does_not_spawn_note describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "c-linkage-unit-realization-design", - primary_work: DeclarationRef { module_path: "v2.std.compilers.compilation_unit", decl_name: "CompilationUnit", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.compilers.compilation_unit", decl_name: "partition_fold_outcome", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.compilers.compilation_unit", decl_name: "CompilationUnitId", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject CompilationUnit describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "cli-run-reconcile-defork", @@ -416,55 +358,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject import_closure_live describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "compile-wall-endgame", - primary_work: DeclarationRef { module_path: "std.computation_identity", decl_name: "ComputationIdentity", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.termination", decl_name: "DescentEvidence", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ComputationIdentity describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "compiler-algorithm-survey-2026-07-04", - primary_work: DeclarationRef { module_path: "v2.std.node", decl_name: "fold_node", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.algebra", decl_name: "list_snoc_item", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.compilers.lexing", decl_name: "token_stream_first", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject fold_node describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "containment-binding-cross-module-type-references-design", - primary_work: DeclarationRef { module_path: "std.occurrence_binding", decl_name: "OccurrenceBindingResult", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.symbol_index", decl_name: "symbol_index_lookup", field: WholeDeclaration }, - DeclarationRef { module_path: "std.occurrence_binding", decl_name: "occurrence_binding_from_candidates", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject OccurrenceBindingResult describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "cost-risk-benefit-floor-model", - primary_work: DeclarationRef { module_path: "std.realization_schedule", decl_name: "WitnessKind", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.verification", decl_name: "TestClassification", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.live_tree", decl_name: "LiveTreeDisposition", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject WitnessKind describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "cross-entry-typed-module-memo-sketch", - primary_work: DeclarationRef { module_path: "std.computation_identity", decl_name: "ComputationIdentity", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.materialization_ladder", decl_name: "CacheProvider", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.host_effect", decl_name: "HostEffect", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ComputationIdentity describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "dag-scm-design", @@ -474,44 +367,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject GroupMembership describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "decl-emission-defork-design", - primary_work: DeclarationRef { module_path: "v2.std.compilers.target_model", decl_name: "TargetModel", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.compilers.semantic_decl_emission", decl_name: "TargetSemanticDeclEmission", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject TargetModel describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "declared-source-ref-selection-design", - primary_work: DeclarationRef { module_path: "v2.compiler.source_authority", decl_name: "SourceRef", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.source_authority", decl_name: "ModuleStorageIndex", field: WholeDeclaration }, - DeclarationRef { module_path: "std.content_hash", decl_name: "ContentHash", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject SourceRef describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "dependency-fidelity-design", - primary_work: DeclarationRef { module_path: "v2.std.fn_index", decl_name: "FnArrowDecl", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.lens.dependency_fidelity", decl_name: "FidelityVerdict", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject FnArrowDecl describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "design-register-library", - primary_work: DeclarationRef { module_path: "gunbc.design.material", decl_name: "Material", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.design.theme", decl_name: "Theme", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.design.material", decl_name: "AreaClass", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject Material describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "dispatch-maintain-cc", @@ -522,44 +377,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject dispatch_brief_template describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "duplicate-work-graph-lens-design", - primary_work: DeclarationRef { module_path: "v2.std.materialize", decl_name: "materialize", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.realization", decl_name: "Materialization", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.staging", decl_name: "cached_stage", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject materialize describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "emit-host-batch-isolation", - primary_work: DeclarationRef { module_path: "v2.workflow.executor", decl_name: "parallel_executor_plan_from_dependencies", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.realization_schedule", decl_name: "Runnable", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject parallel_executor_plan_from_dependencies describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "emitted-crate-partition-design", - primary_work: DeclarationRef { module_path: "v2.std.compilers.compilation_unit", decl_name: "CompilationUnit", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.compilers.compilation_unit", decl_name: "partition_fold_outcome", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.workflow.rust_crate_partition", decl_name: "PartitionPolicy", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject CompilationUnit describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "enforcement-intent-design", - primary_work: DeclarationRef { module_path: "v2.lens.enforcement.contract", decl_name: "LensContract", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.lens.enforcement.standing_intent", decl_name: "StandingIntent", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject LensContract describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "engram-materialization-placement", @@ -570,43 +387,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "gunbc.fabric.engram_materialization lands and the placement of this component is decided by a consumed select_realization fold whose result the serving subject reads, so its subject deepseek_v4_1_flash_engram is placed by shipped behavior rather than by a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "entry-graph-union-slice2-typecheck-attribution", - primary_work: DeclarationRef { module_path: "std.interface_summary", decl_name: "module_key", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject module_key describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "eval-bind-node-eval-propose", - primary_work: DeclarationRef { module_path: "v2.compiler.eval", decl_name: "eval_bind_node_eval", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.runtime", decl_name: "RuntimeBehaviorInterpreter", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject eval_bind_node_eval describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "execution-spine-design", - primary_work: DeclarationRef { module_path: "v2.std.materialize", decl_name: "materialize", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.dependency", decl_name: "DependencyView", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.spine", decl_name: "spine_receipt", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject materialize describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "extdeps-std-grounding-inventory", - primary_work: DeclarationRef { module_path: "std.measure", decl_name: "Measure", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject Measure describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "emit-type-surface-traversal-cost", @@ -634,23 +414,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the integration/floor-cut cutover merges to main and the old generated-ci / ci_floor_plan authorities named in this plan are deleted per its terminal receipt; the row deletes with the document once the cut is no longer the active migration vehicle. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "floor-memory-pool-parse-regression-diagnosis", - primary_work: DeclarationRef { module_path: "v2.lens.live_read_classification", decl_name: "live_read_classification", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject live_read_classification describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "floor-shared-compute-memoization", - primary_work: DeclarationRef { module_path: "gunbc.ci_layer_roots", decl_name: "witness_layer_roots", field: WholeDeclaration }, - additional_works: [ - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject witness_layer_roots describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "floor-time-namespace-walk-regression-diagnosis", @@ -658,52 +421,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the assembly cost class this document attributes stops being where the floor wall is, measured on a main floor run rather than asserted; the row deletes with the document. REBOUND 2026-10-03: this row named v1.compiler.infer rewire_type_env_import_str_binding_identity, the 58 percent row this document located. That pass is DELETED (calm-pike-525: at a fresh compile 100 percent of its rewrites were the same Rc already present, whole tree and floor subject), so the row now names reconcile_with_census_extra, the reconcile assembly the diagnosis is about; the floor-run measurement the condition asks for decides retirement. REBOUND 2026-08-21: this row named v2.workflow.ci_floor_plan gate_runnable_profile and floor_plan_at_most_one_heavy_gate_resolve_per_batch — the batch-scheduling knobs the diagnosis turned, deleted with their module by the floor cut. The diagnosis is not about scheduling: its own section 7 locates the measured root in reconcile assembly, one row of which is 58 percent of the resolve wall, and that row is the subject bound here. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "for-sugar-over-fold", - primary_work: DeclarationRef { module_path: "v2.compiler.emit_orchestration", decl_name: "orch_emit_step", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.compilers.sugar", decl_name: "SugarKey", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject orch_emit_step describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "formal-concepts-extdeps-grounding", - primary_work: DeclarationRef { module_path: "std.termination", decl_name: "DescentEvidence", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject DescentEvidence describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "func-env-sigs-single-authority", - primary_work: DeclarationRef { module_path: "v2.compiler.resolve", decl_name: "ResolvedTree", field: WholeDeclaration }, - additional_works: [ - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ResolvedTree describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "groupcompletion-pair-construction-design", - primary_work: DeclarationRef { module_path: "std.algebra", decl_name: "GroupCompletion", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.approximate_field", decl_name: "ApproximateField", field: WholeDeclaration }, - DeclarationRef { module_path: "std.rational", decl_name: "Rational", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject GroupCompletion describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "gunb-ai-site-subsumption-design", - primary_work: DeclarationRef { module_path: "extdeps.languages.typescript.program", decl_name: "TsProgram", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.design.material", decl_name: "Material", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.design.material", decl_name: "BuildRule", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject TsProgram describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "gunbc-served-dashboard-design", @@ -714,16 +431,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject belt_dispatch_node describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "hollow-alias-construction-wall", - primary_work: DeclarationRef { module_path: "std.algebra", decl_name: "FieldOfFractions", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.algebra", decl_name: "GroupCompletion", field: WholeDeclaration }, - DeclarationRef { module_path: "std.rational", decl_name: "Rational", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject FieldOfFractions describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "import-strip-witness-discovery-cascade-diagnosis", @@ -733,34 +440,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject DeclarationRef describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ingest-manifest-source-ref-carrier-design", - primary_work: DeclarationRef { module_path: "v2.compiler.source_authority", decl_name: "SourceRef", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.source_authority", decl_name: "SourceRootIngest", field: WholeDeclaration }, - DeclarationRef { module_path: "std.content_hash", decl_name: "ContentHash", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject SourceRef describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "interface-summary-declared-use-arity", - primary_work: DeclarationRef { module_path: "std.interface_summary", decl_name: "InterfaceSummary", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.types", decl_name: "NonEmptyStr", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject InterfaceSummary describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "interpreter-memory-chronicle", - primary_work: DeclarationRef { module_path: "gunbc.fleet_intent", decl_name: "PerformanceReceipt", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject PerformanceReceipt describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "keying-relation-design", @@ -774,15 +453,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject KeyRelation describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "language-support-completion-design", - primary_work: DeclarationRef { module_path: "gunbc.ci_layer_roots", decl_name: "falsifier_substrate_long_lane_rows", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject falsifier_substrate_long_lane_rows describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "layering-imports-reference-repoint-design", @@ -793,34 +463,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject LayerImportFact describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "lens-consolidation-design", - primary_work: DeclarationRef { module_path: "std.materialization_ladder", decl_name: "LadderVerdict", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.lens.enforcement.standing_intent", decl_name: "StandingIntent", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.lens.vacuity", decl_name: "VacuityEvidence", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject LadderVerdict describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "lens-input-authority-design", - primary_work: DeclarationRef { module_path: "extdeps.cache.materialization", decl_name: "provider_from_catalog", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.inferred_tree", decl_name: "InferredTree", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject provider_from_catalog describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "mechanism-inventory-red-controls", - primary_work: DeclarationRef { module_path: "gunbc.ci_gate", decl_name: "Gate", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.process", decl_name: "ProcessExit", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject Gate describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "membership-diff-reconcile-spine-design", @@ -878,53 +520,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject occurrence_binding_from_candidates describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "native-selected-witness-bundle-design", - primary_work: DeclarationRef { module_path: "std.selected_witness_bundle", decl_name: "NativeWitnessBundleIdentity", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject NativeWitnessBundleIdentity describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "node-minimal-representation-sketch", - primary_work: DeclarationRef { module_path: "v2.std.node_minimal", decl_name: "MinimalResolvedNode", field: WholeDeclaration }, - additional_works: [ - - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject MinimalResolvedNode describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "node-subtree-visibility-grants", - primary_work: DeclarationRef { module_path: "std.effect_grant", decl_name: "position_under", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.effect_grant", decl_name: "NamespacePosition", field: WholeDeclaration }, - DeclarationRef { module_path: "std.effect_grant", decl_name: "admit_effect", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject position_under describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "non-fold-irreducible-residue-catalogue", - primary_work: DeclarationRef { module_path: "v2.lens.complexity_linearity_audit", decl_name: "triage_wildcard", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.induction", decl_name: "SubValueRelation", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.lens.cost", decl_name: "SymbolicCost", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject triage_wildcard describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "orchestration-as-intent-design", - primary_work: DeclarationRef { module_path: "v2.std.orchestration", decl_name: "Run", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.orchestration", decl_name: "Pipeline", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject Run describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "post-engine-pr-roadmap", @@ -934,42 +529,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject is_container_type describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "provisioning-window-executor-capability-design", - primary_work: DeclarationRef { module_path: "v2.std.execution_surface", decl_name: "ShellEmissionJustification", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.execution_surface", decl_name: "ExecutorCapability", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.execution_surface", decl_name: "ProvisioningWindow", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ShellEmissionJustification describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "real-debt-syntactic-sample-audit", - primary_work: DeclarationRef { module_path: "v2.lens.complexity_linearity_audit", decl_name: "triage_wildcard", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.verification", decl_name: "ManualAnchorKey", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.compilers.target_model", decl_name: "ConcreteSyntaxToken", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject triage_wildcard describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "resolve-regression-journey", - primary_work: DeclarationRef { module_path: "gunbc.ci_layer_roots", decl_name: "bin_witness_wet_per_row_wall_budget_seconds", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the resolve-regression class this document diagnoses stops recurring and the wet per-row wall budget it now binds to describes settled behavior rather than a live pressure; the row deletes with the document. REBOUND 2026-08-21: the original subject was gunbc.ci_workflow gunbc_ci_floor_step_timeout_discovery_flip_note, which the floor cut deleted along with its module — the citation outlived its subject and the cited-symbol lens refused it. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "resolver-graph-major-design", - primary_work: DeclarationRef { module_path: "v2.std.dependency", decl_name: "DependencyView", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.realization_schedule", decl_name: "RealizationPlan", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject DependencyView describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "resource-aware-scheduler", @@ -1020,14 +579,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject SeedHonestyDischarge describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "seed-shrink-census", - primary_work: DeclarationRef { module_path: "v2.std.node", decl_name: "content_hash", field: WholeDeclaration }, - additional_works: [ - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject content_hash describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "shell-intent-emit-realization-design", @@ -1048,16 +599,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject host_effect_apply describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "space-complexity-design", - primary_work: DeclarationRef { module_path: "v2.lens.cost.expr", decl_name: "CostExpr", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.termination", decl_name: "DescentEvidence", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.lens.cost.summary", decl_name: "ComplexitySummary", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject CostExpr describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "space-lens-minimal-project", @@ -1068,33 +609,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ResourceEnvelope describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "srv3-webui-kvm-virtual-media", - primary_work: DeclarationRef { module_path: "gunbc.boot_artifact_delivery", decl_name: "solve_boot_artifact_delivery", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "extdeps.external_authority", decl_name: "ExternalAuthority", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject solve_boot_artifact_delivery describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "srv4-bmc-onboarding-gap", - primary_work: DeclarationRef { module_path: "gunbc.host_standup", decl_name: "host_standup_spine", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "product.host_identity", decl_name: "HostIdentity", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject host_standup_spine describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "srvn-buildcache-provisioning-design", - primary_work: DeclarationRef { module_path: "gunbc.host_converge", decl_name: "ConvergeTarget", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "extdeps.cache.sccache", decl_name: "sccache_local_facts", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject ConvergeTarget describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "type-env-single-authority-design", @@ -1114,72 +628,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject whole_tree_strict_resolve_exclusion_substrings describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "v2-memory-control-audit", - primary_work: DeclarationRef { module_path: "std.materialization_ladder", decl_name: "CacheProvider", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.std.dependency", decl_name: "DependencyView", field: WholeDeclaration }, - DeclarationRef { module_path: "std.realization_schedule", decl_name: "CostAccount", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject CacheProvider describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "vacuity-lens-design", - primary_work: DeclarationRef { module_path: "v2.lens.vacuity", decl_name: "VacuityEvidence", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.lens.coverage", decl_name: "coverage_defect_vacuous_arm", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.std.verification", decl_name: "TestClaim", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject VacuityEvidence describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "witness-subject-execution-audit", - primary_work: DeclarationRef { module_path: "v2.std.diagnostic", decl_name: "Outcome", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.lens.module_graph", decl_name: "import_closure_live", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the work this document plans lands and its subject Outcome describes shipped behavior rather than a planned change; the row deletes with the document. Registered at subject grain: the systems bound here are the ones whose movement would make this document's purpose false, not every system its prose mentions.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "general-body-producer-design", - primary_work: body_lowering_symbol_index_ref, - additional_works: [ - body_lowering_lexical_lookup_ref, - body_lowering_production_consumer_ref, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the general body producer lands and the document describes a shipped fold rather than a planned one. THIS BIND EXISTS BECAUSE OF A MEASURED FAILURE IN THIS EXACT DOCUMENT: it named v2.std.symbol_index SymbolIndex as an open prerequisite gating Stage A, the prerequisite was discharged, and the sentence stayed -- four copies of that one claim were live across DESIGN.md and two plan documents. Nothing was wrong with the declaration, so no declaration-level check could fire; the sentence was not a reference to anything, so no reference-level check could either. Binding the three systems it discusses puts them inside the cited-symbol population, so a rename or deletion reds without anyone editing the prose. WHAT THIS DOES NOT ESTABLISH, stated so the bind is not over-read: a resolved reference proves the system EXISTS and is uniquely identified, not that the named consumer invokes the named implementation on a production path -- that is a relationship claim, it needs an executing witness, and this row makes none.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ci0-witness-execution-census", - primary_work: DeclarationRef { module_path: "gunbc.witness_execution_class", decl_name: "witness_execution_class_note", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.witness_execution_class", decl_name: "native_blocked_cause_note", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the general witness→TargetModel producer lands (body-lowering keystone) and the enrolled-roster census is re-derived from executed emission attempts rather than the fixture-bound dispatch surface; the census numbers are superseded by that re-derivation and this receipt deletes with the NativeBlocked\{GeneralWitnessTargetModelUnavailable\} population it counts.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ci2-native-cutover-reobservation", - primary_work: DeclarationRef { module_path: "gunbc.ci_materialization", decl_name: "ci_native_cache_root_exempt_note", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.native_witness_transition_receipt", decl_name: "native_at_small_scale_transition_receipt", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the CI2 correctness flip lands — the native-selected population is native-required with the counted interpreter fallback deleted — and a main floor run's [native-selected-bundle] receipt shows verdict accepted with fallback = interpreted = unavailable = 0. The measured-standing content is superseded at that point by the run receipts and the transition-receipt authority; this doc records the pre-flip standing and its two located defects, and deletes with the fallback machinery it diagnosed.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "witness-bridge-reobservation", - primary_work: DeclarationRef { module_path: "v2.compiler.body_producer_forward", decl_name: "body_producer_forward_note", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lowering_fold_note", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the first witness-bridge stage lands (lowered body -> derived TargetModel -> bundle member -> native execution with planted-red control) and the bridge carrier's own notes record the standing this receipt captured; then this doc folds into the bridge design/receipt and deletes.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "review-surface-subject-map", @@ -1191,32 +639,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "every row in the map has been deleted, consumed by a named live edge, or transferred to a typed roadmap obligation with an owner and acceptance. The four bound works are the map's load-bearing subjects and each names its own end state: the completeness roster note's exemption for v2.lens.grounding_ledger cites consumption by v2.lens.grounding that does not exist, and dissolves when the ledger is deleted so the row disappears rather than being reworded; LedgerEntry is that inert module's carrier; first_coincident_target folds to the FIRST non-self match while GroundingWorklistEntry stores a single coincides_with string, which is the silent-pick defect blocking any exclusive-candidate review and dissolves when the candidate population becomes complete, zero/one/many explicit, and identified by DeclarationRef rather than presentation strings; MergeReadinessTally has no producer outside fixtures and a dormant consumer whose kernel has no in-tree driver, and its unread approval_count field dissolves when the two-approval rule becomes a typed obligation with a zero-approval RED. The map is a worksheet, not an authority: it registers here so it cannot sit unreachable, and it deletes outright rather than migrating to DESIGN.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "witness-evidence-lifecycle-design", - primary_work: DeclarationRef { module_path: "v2.workflow.floor_discovery_producer", decl_name: "floor_discovery_process_dag_file", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.ci_layer_roots", decl_name: "long_lane_exclusion_note", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.explicit_witness_admission", decl_name: "known_red_class_note", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "steps 3-5 land or are honestly retired (steps 1-2 are done: the modeling decision was signed by the operator 2026-08-04 and the typed direct-rust-door observation executes green). Remaining: census-before-routing in floor_discovery_process_dag_file, the attempt/evidence algebra retiring QuarantineProbeExpectRed and batch-wide expect_red inversion, and route derived from execution requirement — with test/claim/long DELETED as the terminal state per the operator step-5 ruling (it may persist as migration scaffolding meanwhile, but no new semantic dependence may be added to it, and operator visibility comes from a generated long-route projection rather than the directory name). Then the surviving policy rows migrate to DESIGN or a registered gunbc.plan.Plan row and this bind deletes with the doc.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "production-stage-origin-carrier-design", - primary_work: DeclarationRef { module_path: "v2.compiler.self_host.candidate_generation", decl_name: "mint_provenanced_rust_artifact", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.self_host.fixture_synthetic_emission", decl_name: "fixture_synthetic_emission_note", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "B1-B4 land or are honestly retired: covered-form construction admission, call-occurrence population closure consuming the #7786 successor, production API migration removing raw stage outputs from qualification boundaries, and bypass closure for casts / module identity / fail-open lookup. The ProductionOriginClosureCertificate becomes constructible - constructor and consumer populations closed, fixture reachability zero, unknown-origin zero - at which point the carrier law lives in the substrate and this note deletes. If the census successor proves the class cannot close under the current sole_constructor mechanism, this note is superseded by the recut rather than deleted silently.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "candidate-scoped-gates-atomic-landing", - primary_work: DeclarationRef { module_path: "gunbc.extdeps_scope_frontier", decl_name: "legacy_manifest_freeze_sha", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "gunbc.ci_failure_class", decl_name: "merge_freshness_gating_status", field: WholeDeclaration }, DeclarationRef { module_path: "tools.floor_effect_gate_witness", decl_name: "emit_host_gate_passes", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.merge_admission", decl_name: "merge_admission_required_check_binding_dissolution_trigger", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the four program walls are live: the merge-queue landing boundary enforced (MergeQueueOnly + ALLGREEN, dashboard enqueue-only), the candidate/freeze/manifest observations split with the false law text corrected and the DefaultBranchAudit backstop scheduled, the RunnableProcessExitClaim migration complete with the Boolean gate adapters deleted, and the repository ruleset modeled as desired state with read-back. Then the surviving policy rows migrate to DESIGN or a registered gunbc.plan.Plan row and this bind deletes with the doc.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "generated-file-conflict-policy", @@ -1224,20 +646,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [DeclarationRef { module_path: "tools.generated_artifact_gate", decl_name: "run_generated_artifact_drift_gate", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.commit_workflow", decl_name: "commit_gate_roster", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.stage0_rust_source_lifecycle_scaffold", decl_name: "derived_generated_stage0_repo_paths", field: WholeDeclaration }, DeclarationRef { module_path: "std.keyed_roster", decl_name: "keyed_roster_build", field: WholeDeclaration }], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "all four charter lanes are landed AND bound: commit-writer admission consumed at every writer transport (the heal arm and the dashboard autocommit daemon), the .gitattributes + repo-local git-config projections live with read-back, stage0 membership derived with its host-supply scaffold dissolved or re-triggered, and the keyed-roster construction wall enrolled per-PR. Then the surviving policy rows migrate to DESIGN or a registered gunbc.plan.Plan row and this bind deletes with the doc.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "seamless-deploys-design", - primary_work: DeclarationRef { module_path: "gunbc.live_deploy.service_ready", decl_name: "live_deploy_service_ready_poll_bound_reason", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "all four items the note models are landed or retired: the observation-outcome de-conflation, the systemd F1 readiness assertion, the deploy reconcile (comparable artifact value, service de-fused from the unit file, observed provider), and the handover. Registers as a gunbc.plan.Plan row if the design freezes before they land; deletes outright if they land first. The reasoning and receipts live in the note, not here.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "inner-cost-lanes-scoping", - primary_work: DeclarationRef { module_path: "gunbc.roadmap_authority", decl_name: "roadmap_authority", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "both lanes have landed or been retired: Lane A when process_escapes_loop is migrated onto source_chars with its discriminating non-ASCII receipt, Lane B when the shared-overlap measurement it asks for exists and either opens the program or prices it down. The note is scoping for two roadmap nodes, so it dissolves when those nodes close, not into a gunbc.plan.Plan row.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "five-minute-ci-gate-design", @@ -1251,47 +659,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "every sub-lane row is accepted or honestly retired and the program parent five-minute-ci-gate is accepted on fleet receipts showing a representative leaf .dag PR at ≤5 minutes wall with byte-identical verdicts vs a cold recompute control — or the operator recuts the program and this note is superseded by a registered plan row.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "spark-standup-program-accounting", - primary_work: DeclarationRef { module_path: "gunbc.plans.fleet_subsumption_manual_gaps", decl_name: "fleet_subsumption_manual_gaps_plan", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.spark.dgx_procurement", decl_name: "dgx_spark_arrival_standing", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.spark.dgx_procurement", decl_name: "dgx_spark_router_bindings", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the Spark standup lanes are carried as registered rows rather than by this note — the manual-gap items reach zero and srv5/srv6 are enrolled with their arrival obligations modeled where that arrival lives, at which point the accounting has no facts of its own and this bind deletes with the doc.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "floor-prep-tax-program", - primary_work: DeclarationRef { module_path: "gunbc.executor_schedule_retention", decl_name: "schedule_retention_policy_note", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.roadmap_authority", decl_name: "five_minute_ci_gate_program_note", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.roadmap_authority", decl_name: "roadmap_authority", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.p1_retention_cohort_receipt", decl_name: "p1_retention_cohort_receipt_verdict", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.ci_cost_arc_closeout_receipt", decl_name: "end_to_end_residual", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.ci_cost_arc_closeout_receipt", decl_name: "retention_settle", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "floor receipts show amortized prep (entries 2..N ≪ ~2s setup), selection receipts expose state/ratio/fallback on every affected run, and width>1 / broad native decisions cite those receipts (and the index-share conjunction) — or the operator folds this bridge into the five-minute / materialization authorities and this bind deletes with the doc.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "worker-private-memory-decomposition", - primary_work: DeclarationRef { module_path: "gunbc.executor_schedule_retention", decl_name: "schedule_retention_heads_stay_resident", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.executor_schedule_retention", decl_name: "schedule_retention_policy_note", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.p1_retention_cohort_receipt", decl_name: "p1_retention_cohort_receipt_verdict", field: WholeDeclaration }, - DeclarationRef { module_path: "gunbc.roadmap_authority", decl_name: "five_minute_ci_gate_program_note", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the width-2 crossover decision is taken and cites these terms or a superseding measurement, or the ResolvedGraph-anchored bundle this receipt names becomes shared — at which point the decomposition no longer describes a worker and this bind deletes with the doc.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "accelerator-demo-roundtrip", - primary_work: DeclarationRef { module_path: "extdeps.languages.simd.kernel", decl_name: "simd_kernel_medium", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "gunbc.accelerator_demo_plan", decl_name: "AcceleratorRecognitionOutcome", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding); §6 item 2 execution lane lands on the simd handler (merged from a two-row form 2026-07-31: home+slug is this list's symbolic identity, so two rows gave one doc two independently removable authority rows)") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "machine-shape-orthogonal-scheduling", @@ -1313,27 +680,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [DeclarationRef { module_path: "v2.compiler.self_host", decl_name: "canonical_emitted_bytes_digest", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.bootstrap", decl_name: "CompilerStage", field: WholeDeclaration }], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "Stages 1–9 in the design doc land or are honestly retired: candidate/promoted lifecycle with RecoveryAnchor retention, SuccessorCapabilityReceipt fix-forward gate, declared-change vs regression on the merge path, generation lineage binds ConsumerRead on regen and warm-merge (Promoted bindings only), bridge-generation protocol for incompatible requirement revisions, phased-single-process CI prelude-duplication witness is green on a representative PR, and the doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "fleet-self-converge-enforcement-design", - primary_work: DeclarationRef { module_path: "gunbc.plans.host_effect_orchestration", decl_name: "host_effect_orchestration_plan", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "companion detail for the host-effect plan's Phase E (pull-mode self-converge); dissolves when Phase E's enforcement topology lands in carriers or the doc registers as a gunbc.plan.Plan row") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "holds-base-runtime-error-investigation-receipts", - primary_work: DeclarationRef { module_path: "v2.std.witness", decl_name: "Witness", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the deferred verify-7916-holds-base receipt (CI floor with selection-applied on the reproducing tree, plus cargo test -p v1-compiler --lib) runs against a healthy fleet and its outcome is folded into #7916's history, or the doc is otherwise superseded; either way this bind and the doc delete together") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "e0599-implementation-proposal", - primary_work: DeclarationRef { module_path: "tools.e0599_probe_census", decl_name: "e0599_probe_census_note", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "Phase 0 consumer graph accepted and each named predicate (P-fn, P-derive, P-optional) lands with measured receipt or the doc migrates into a registered gunbc.plan.Plan row") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "rc-ownership-wrap-decision-design", @@ -1347,45 +693,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "arc-store-path-migration-decision", - primary_work: DeclarationRef { module_path: "v1.compiler.languages", decl_name: "sharing_wrap_ctor_for_target", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.wrap_decision", decl_name: "WrapDecisionBundleChildLookup", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the v1-run-stability width>1 resumption trigger in this note fires and the parked alias-flip plus C1 in-memory store land in one motion — or the decision record migrates into a registered gunbc.plan.Plan row") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "module-identity-storage-binding-design", - primary_work: DeclarationRef { module_path: "v2.lens.effect_reach", decl_name: "effect_reach_law", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "v2.workflow.witness_admission", decl_name: "consumer_for_explicit_rosters", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "BOTH conditions gate dissolution (merged from a two-row form 2026-07-31, same identity rule as the guarantee-recovery row): typed SourceRef at host boundaries is ENFORCED and the bare-string file-dependency class is migrated (effect_reach_law's own dissolve condition), and the doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "live-read-witness-classification-design", - primary_work: DeclarationRef { module_path: "v2.lens.live_read_classification", decl_name: "live_read_classification_law", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "doc migrates into a registered gunbc.plan.Plan row (the registration becomes the binding)") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "witness-realization-plan", - primary_work: DeclarationRef { module_path: "std.emit_on_demand", decl_name: "witness_family_build_grain_ruling", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "std.realize_pack", decl_name: "realize_pack_width", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "dissolves into a registered gunbc.plan.Plan row when its P1 lands. REBOUND 2026-08-21: this row named v2.workflow.ci_floor_plan gunbc_ci_floor_plan — the floor plan that would have SCHEDULED the realized witnesses, deleted with its module by the floor cut. The plan itself is live (six documents cite it as their parent authority), and its landed subjects are the two carriers named here: the family build-grain ruling that answers its FLAG B, and the packing law its P4 derives. Scheduling re-enters as a consumer when a floor plan exists again; it was never the subject of this document.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ci-floor-time-45-72-band-attribution", - primary_work: DeclarationRef { module_path: "gunbc.ci_materialization", decl_name: "ci_floor_declared_resolve_count", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "dissolves when realization_measurement_loop Phase-0 Gantt carrier supersedes prose receipts (same trigger as ci-floor-fractal-gantt)") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "ci-floor-child-spawn-attribution", @@ -1393,13 +700,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "dissolves when the child-spawn class is dissolved — run_gunbc_claims_ready pools ClaimRuns into one process or runs them against the executor's warm process_shared_index (the ci_floor_resolve_receipt_note 'resolve once, share by reference' terminal); this receipt's counterfactuals become that PR's before/after") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "progress-observation-design", - primary_work: DeclarationRef { module_path: "std.observation", decl_name: "ObservationEvent", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "P0 lands the observation laws + glyph authority as .dag data rows (roadmap 2d ts-obs-model) — the carriers become the authority and this doc stays as the design record. REBOUND 2026-08-21: this row named v2.workflow.ci_floor_plan gunbc_ci_floor_plan, the floor plan the doc measured its displaced cost against, which the floor cut deleted with its module; the doc is a live roadmap deliverable authority, so the repair is to bind it to the subject it actually designs — std.observation's event model — rather than to a consumer that no longer exists") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "roadmap-workspace-ux-plan", @@ -1424,52 +724,6 @@ data hand_authored_doc_bind_incomings: List = [ ], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the RLM-6 terminal receipt is recorded -- one receipt joining every frozen identity of the serial gate chain (the canary parent Done by explicit acceptance, its child Ready, the parent's relaunch refused AlreadyAccepted); at that point the plan describes shipped behavior and the row deletes with the document") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "recursive-workflow-advancement-design", - primary_work: DeclarationRef { module_path: "gunbc.workflow_reconcile", decl_name: "workflow_recursive_reconcile_note", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the advancement plan, exact-candidate receipt producers through goal audit, and recursive child-workflow realization land as typed carriers and register as a gunbc.plan.Plan row; then this bind deletes while the md remains the design record") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ci-two-tier-placement-redesign", - primary_work: DeclarationRef { module_path: "gunbc.ci_spec", decl_name: "gunbc_ci_spec", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "D3 lands the placement axis (PrTier | Gauntlet) on CiSpec rows with the measured 5s-rule roster — the spec rows become the authority; the D4 audit-step deletion and D0/D1 preconditions are tracked on their carriers") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "test-decomposition-wcf-cut", - primary_work: DeclarationRef { module_path: "gunbc.witness_row_cost", decl_name: "gunbc_ci_fast_lane_rule_note", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "every Class C row on the measured head lands fixture/cadence split (or reclassifies F with a named cost-shape owner); dark long/ files classify only at Gauntlet enrollment — then authority collapses into CiSpec placement rows + enrolled cadence rosters and this doc deletes") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "fallback-arm-census", - primary_work: DeclarationRef { module_path: "v2.lens.fallback_arm_census", decl_name: "fallback_arm_census_law", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "W3 promotes the lens to a compile refusal (zero-false-positive corpus-wide) and/or the doc migrates into a registered gunbc.plan.Plan row — then this bind deletes with the md") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "dag-note-prose-census", - primary_work: DeclarationRef { module_path: "v2.lens.enforcement.standing_intent", decl_name: "StandingIntent", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the typed annotation carriers land (ruling rows on StandingIntent, an event log keyed to the declaration, citation edges) and the annotation-budget lens counts ROWS — at which point a lexical census over prose is superseded by a fold over typed rows, and the doc plus its instrument delete together") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "dag-prose-policy-audit", - primary_work: DeclarationRef { module_path: "v2.std.compilers.lexing", decl_name: "LexRule", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.parse", decl_name: "ParseArtifact", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.extdeps.languages.dag", decl_name: "dag_line_comment_fidelity", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.test.round_trip.dag_comment_wall_test", decl_name: "dag_comment_wall_line_comment_refused", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the source-annotation carrier lands with its executed controls — line comments captured on a THIRD lexical channel (AnnotationRule, never TriviaRule and never an ordinary TokenRule) returning an authored wrapper around an unchanged semantic artifact, consuming no semantic occurrence identity, attached at module-item grain with trailing/body/unattached/block forms refusing, and erasure established by the seven D-C proofs rather than by emitted-byte equality alone (which cannot detect a moved occurrence-identity graph) — and the representation partition over the measured prose population completes; at which point a lexical audit over prose is superseded by a fold over the annotation graph and the typed populations beside it, and this doc plus its uncommitted instrument delete together (same trigger family as dag-note-prose-census)") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "dissolution-census-a-ci-layer-roots", @@ -1477,41 +731,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [DeclarationRef { module_path: "v2.lens.enforcement.standing_intent", decl_name: "StandingIntent", field: WholeDeclaration }], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the typed annotation carriers land (StandingIntent, event log, citation edges) and an annotation-budget lens counts rows — at which point a lexical census over ci_layer_roots prose is superseded by a fold over typed rows, and this doc plus its instrument delete together (same trigger as dag-note-prose-census)") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "extdeps-positioning-restructure", - primary_work: DeclarationRef { module_path: "v2.lens.extdeps_shape_transport_policy", decl_name: "extdeps_shape_transport_policy_clean_holds", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the extdeps positioning restructure PR (pure git mv + import repoint: os/ de-grab-bag + findings 1-4 hub-loose, all under the §3 extdeps discipline this lens enforces) lands and the extdeps directories are §3-congruent — the brief's scope is discharged; delete this row and the doc together") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "v1rt-witness-diagnostic-carrier-decouple-design", - primary_work: DeclarationRef { module_path: "v2.std.witness", decl_name: "witness_from_optional", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "Stage-4 Gate-1 E0308 burn-down probe receipt lands (design note §3 — measurement only); doc archives or registers as a gunbc.plan.Plan row") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "hermetic-tool-provisioning-design", - primary_work: DeclarationRef { module_path: "extdeps.tools", decl_name: "CliTool", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "P1/P2 land the digest-bearing pin and the tool instantiation of gunbc.membership_reconcile — the host-program ensure over the CliTool catalog row lands (program-install lane, successor to the deleted extdeps.tools.resolve) and the four forked resolvers delete; the pin carrier becomes the authority and the plan registers as a gunbc.plan.Plan row") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "ci-invocation-fixed-tax-attribution", - primary_work: DeclarationRef { module_path: "v1.compiler.tokenize", decl_name: "process_escapes_loop", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "REBOUND 2026-08-21: this row named gunbc.ci_workflow gunbc_ci_selection_control_step_note, the demotion rationale this receipt supplied the basis for; the floor cut deleted gunbc.ci_workflow whole and the citation outlived it. The measurement itself is about the invocation fixed tax, whose located root this note names as its lever 1 — the unmigrated frontend construction in v1.compiler.tokenize process_escapes_loop — so the doc is rebound to the system whose movement would make its purpose false. Dissolves when the pre-evaluation whole-corpus passes are persisted across processes or derived from the containment tree (namespace-only resolution lane) — its levers then re-price against the new baseline, and the demotion's outcome-based return trigger (selection control back per-PR once a named receipt shows the whole control step consistently below a seconds-scale ceiling on the CI fleet across a full falsifier window) consumes its measurements") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "srv1-deploy-serialization-and-false-greens", - primary_work: DeclarationRef { module_path: "gunbc.ci_spec", decl_name: "gunbc_ci_deploy_srv1_stage", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "all FOUR findings this diagnosis names are facts on their carriers: (1) srv1 deployment holds a typed HOST-SIDE lease spanning mutation, readiness and identity readback — a runner label or concurrency group is mitigation only, since neither excludes the operator apply path — plus a counted refusal when the candidate is not the authoritative revision under whichever revision authority is ruled, (2) the healthz fingerprint identifies what is DEPLOYED (source closure and binary artifact identity, not build provenance alone) rather than five rendered bodies, (3) the refusal path preserves the decl's located diagnosis instead of substituting a render complaint, with control-byte admission decided per CursorAction and DynamicLineState rather than one global roster, and (4) job_yaml emits Job.concurrency with a discriminating RED. Then the doc deletes with this row. Twice corrected in review: review 44735 caught that the trigger omitted the misattributed-refusal defect entirely, and review 44766 caught that it would have accepted scheduler-only exclusion, source-only identity and a global byte-roster fix") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "compiler-guarantee-recovery-gap-analysis", @@ -1526,51 +745,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [DeclarationRef { module_path: "gunbc.roadmap_component", decl_name: "roadmap_row_archetype_note", field: WholeDeclaration }], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the A2 presentation seam lands on its carriers — the view projection module (WorkRowView/ActivityView/ActionView/DailyWorkspaceView/ObservationImpactView), the row archetype's ActivityRegion placed UnderHead with ActuatorRegion shrunk to actions only, and the attention-rule witnesses including the falsifier specimen (active attempt + seven obligations + session-present/process-exited + long title, coherent at three widths) — and the plan registers as a gunbc.plan.Plan row (extends roadmap-workspace-ux-plan and roadmap-workspace-remodel-plan, whose rows dissolve on their own triggers)") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "p1-retention-vs-drain-cohort-receipt", - primary_work: DeclarationRef { module_path: "gunbc.p1_retention_cohort_receipt", decl_name: "p1_retention_cohort_receipt_reproduction_steps", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "gunbc.p1_retention_cohort_receipt", decl_name: "p1_retention_cohort_receipt_verdict", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the redirected assembly/materialization lane this receipt's REJECT verdict points to is picked up and either supersedes this pinned measurement with a canonical carrier or the parent floor-prep-tax-program plan closes P1 outright — then this receipt and its anchor module delete together, per the same scaffold trigger named beside p1_cohort_receipt_enabled in cli_run.rs.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "p3-classification-cost-ab-receipt", - primary_work: DeclarationRef { module_path: "v2.lens.live_read_classification", decl_name: "live_read_classification_law", field: WholeDeclaration }, - additional_works: [], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "paired fleet receipts for ClassificationCostStanding land on the pinned merge SHA with headSha-verified control/candidate arms and the five-valued verdict is recorded — then surviving measurement facts migrate to the canonical carrier and this bind deletes with the doc.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "push-event-affected-set-baseline-design", - primary_work: DeclarationRef { module_path: "gunbc.diff_baseline", decl_name: "resolve_diff_baseline", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "v2.workflow.floor_diff_observe", decl_name: "floor_observe_ci_diff_event", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.diff_baseline", decl_name: "diff_baseline_fail_closed_law", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the before..head push baseline lands AND the merge-queue candidate gate is enforcing (gunbc.ci_failure_class merge_freshness_gating_status = GatingEnforced) — at which point the exact tree becoming main is verified before it becomes main and this note describes the past. CORRECTED 2026-08-03 (operator ruling): this trigger previously read \"the last-green baseline lands\". Last-green was REJECTED for ordinary selection, so that condition could never be met and this row would have been permanently undissolvable — an unfireable trigger is indistinguishable from an unfired one, so no gate would ever have flagged it.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "provider-control-interface-audit", - primary_work: DeclarationRef { module_path: "gunbc.dispatch_selection", decl_name: "DispatchResolvedExecution", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "extdeps.llm.cli_lifecycle", decl_name: "ProviderReadiness", field: WholeDeclaration }, DeclarationRef { module_path: "gunbc.roadmap_provider_events", decl_name: "provider_execution_state", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the audit's locked decisions are facts on their carriers, not prose. PROVIDER INTERFACE HALF: DispatchResolvedExecution carries ONE nested ProviderInterfaceBinding whose runtime is a CLOSURE identity (package closure + runtime toolchain + adapter artifact + protocol identity — not a single artifact digest), provider identity is split from control interface and both from transport, ProviderReadiness is decomposed into independent installation/auth/entitlement/limit/turn standings with an open native limit token, and provider_execution_state reads typed app-server terminals instead of the exec --json category-discarding arm. PACKAGE DELIVERY HALF (added 2026-08-03 per operator ruling — the audit's §2 gap is a package-management gap, not a provider problem, so closing only the provider carriers would leave the note's load-bearing half undelivered): the generic package-delivery authority exists and is CONSUMED IN PRODUCTION — requirement vs exact selection, lock graph with artifact integrity admitted before unpack, materialization receipt with install scripts forbidden unless explicitly admitted, executable-vs-module consumer binding, runtime-bundle identity, ReleaseBundled posture with no acquisition on the deployed host, and an explicit reviewed lock-refresh path — with the Codex realization delivered through it and srv1's exec/tmux offer removed. The §4 paired Claude entitlement probe must also have RUN, deciding whether ClaudeDirectStreamJson survives as an interface or dissolves; an undecided probe leaves this row live because the audit's retraction of the entitlement claim is what makes the row honest. Then the note becomes the design record and this bind deletes with it.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "sole-modeled-publisher-design", - primary_work: DeclarationRef { module_path: "tools.publication_publisher", decl_name: "PublicationAuthority", field: WholeDeclaration }, - additional_works: [DeclarationRef { module_path: "tools.publication_push_shadow", decl_name: "shadow_replay_public_pushes", field: WholeDeclaration }], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "live slice lands: ordinary sessions lose public-write credentials, git.PublicationTransport dispatch binds to PublicationAuthority, and shadow witness becomes regression control — then register as gunbc.plan.Plan row or delete with carriers") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "c1-cost-expr-authority-design", - primary_work: DeclarationRef { module_path: "v2.lens.cost", decl_name: "cost_lens", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.plans.v2_complexity_capability_parity", decl_name: "v2_complexity_capability_parity_plan", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.lens.enforcement.complexity_contract_subject", decl_name: "complexity_derived_kernel_boundary", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "v2.lens.cost carries grounded SizeExpr/CostExpr with normalize_cost_expr_to_symbolic live, the C1 blocking-wall baseline roster is green post-carrier, bounded-summation and CostRefused witnesses execute green, and parity note C1 acceptance is met — then the carriers state the capability and this bind deletes with the doc.") }, - }, HandAuthoredDocBind { home: ProbeDoc, slug: "leading_minus_continuation_silently_truncates_2026-08-23", @@ -1578,25 +752,6 @@ data hand_authored_doc_bind_incomings: List = [ additional_works: [], dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the reduced parser cases this document carries are held by an EXECUTING REGRESSION CARRIER -- a witness that authors `100 \\n - 1` and its `+` and `*` controls and refuses when the leading-minus continuation is truncated again -- the production citation in extdeps.cpu_attachment.lotes_azifa072 is repointed to that carrier, and no other evidence consumer names this document. Then the receipt is the carrier and this bind deletes with the md. IT IS DELIBERATELY NOT PlanHasNoRetirement: the evidence being load-bearing TODAY is not grounds for making a Markdown file the permanent terminal form of a parser guarantee, which is exactly the shape the three g1 controls were wrongly given before they moved to v2.lens.cited_symbol_resolution.") }, }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "v2-frontend-std-ingestion-frontier", - primary_work: DeclarationRef { module_path: "v2.compiler.normalize", decl_name: "normalize", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "v2.compiler.parse", decl_name: "parse_module", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.compiler.tokenize", decl_name: "lex_walk_artifact", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the measured frontier closes: the v2 frontend ingests the std substrate modules this receipt records as rejecting, at each of the three stages it attributes them to — normalize's retention-then-rejection contract violation first, then the lex and parse gaps — and the per-module stage-and-reason table describes shipped behaviour rather than a standing frontier, at which point the receipt and this bind delete together. Registered at subject grain: the bound declarations are the three frontend stages whose movement would make the table false, not every module its rows name.") }, - }, - HandAuthoredDocBind { - home: PlanDoc, - slug: "four-lane-closeout-and-root", - primary_work: DeclarationRef { module_path: "gunbc.roadmap_authority", decl_name: "five_minute_ci_gate_program_note", field: WholeDeclaration }, - additional_works: [ - DeclarationRef { module_path: "gunbc.witness_floor_workflow", decl_name: "witness_floor_concurrency_group", field: WholeDeclaration }, - ], - dissolution: PlanRetiresWhen { condition: unbound_dissolution(description: "the six findings this note records outlive their archived lanes only until each has a carrier of its own: the rejected_with_pending diagnostic-order defect, the entry-vs-member position divergence, the three-entrypoint admission divergence, the standing section 4c population that makes a full-corpus compile exit 1, the floor-gated deploy, and the concurrency-group key -- that last one now carried by gunbc.witness_floor_workflow witness_floor_concurrency_group, which keys merge_group by sha, heal dispatch by expected_healed_sha, pull_request by number, push by github.ref, and run_id only as the bare-dispatch fallback, with the 2026-08-17 every-main-must-finish unique-run_id reading retired as gunbc.rung_drop main_per_commit_floor_verdict, and by witness_floor_concurrency_policy, which derives the cancel axis from the observed fleet standing; the note is repointed there because the ci.yml-era second authority it used to cite was deleted with its workflow. When each is either fixed or carried as a typed row, the note is a historical account and deletes with this bind. It is registered rather than deleted today because four sessions were archived holding these measurements and nothing else records them — but a cross-lane status narrative is the parallel-ledger shape DESIGN section 6 warns about, so its home is a question for the operator, not a settled one.") }, - }, HandAuthoredDocBind { home: PlanDoc, slug: "self-host-cargo-refusal-root-partition", diff --git a/dag/gunbc/durable_cas_file_store.dag b/dag/gunbc/durable_cas_file_store.dag index 29889384b6d..99b3ec830c2 100644 --- a/dag/gunbc/durable_cas_file_store.dag +++ b/dag/gunbc/durable_cas_file_store.dag @@ -5,7 +5,7 @@ import std.algebra { trim } import std.decimal { decimal_digits_only } import std.list { distinct_by_key } import std.content_hash { ContentHash, content_hash_of_value, compare_content_hash, ContentHashEqual, ContentHashDifferent, ContentHashCrossFamilyIncomparable } -import std.durable_compare_and_set { CasAttempt, cas_attempt, CasOutcome, CasCommitted, CasPreconditionFailed, CasStoreRefused, CasExpectation, ExpectSlotAbsent, ExpectSlotGeneration, CasGeneration, CasSlotVersion, CasReadableAbsent, CasReadablePresent, CasSlotObservation, CasObservedReadable, CasObservedUnreadable, CasUnreadableMalformed, CasUnreadableReadRefused, CasStoreFailure, CasSlotObservationRefused, CasGenerationPublicationRefused, cas_generation_exhausted, cas_generation_first, cas_generation_count, cas_generation_successor, CasSuccessorGeneration, CasSuccessorExhausted } +import std.durable_compare_and_set { CasAttempt, cas_attempt, CasOutcome, CasCommitted, CasPreconditionFailed, CasStoreRefused, CasExpectation, ExpectSlotAbsent, ExpectSlotGeneration, CasGeneration, CasSlotVersion, CasReadableAbsent, CasReadablePresent, CasSlotObservation, CasObservedReadable, CasObservedUnreadable, CasUnreadableMalformed, CasUnreadableReadRefused, CasUnreadableObservationBoundExceeded, CasStoreFailure, CasSlotObservationRefused, CasGenerationPublicationRefused, cas_generation_exhausted, cas_generation_first, cas_generation_count, cas_generation_successor, CasSuccessorGeneration, CasSuccessorExhausted } import std.checked_arithmetic { int_inclusive_max } import v2.std.algebra { skip } import extdeps.access.posix { FileMode, file_mode_bits } @@ -456,9 +456,6 @@ type CasSlotRetention = KeepAllGenerations | KeepGenerationWindow { window: CasGenerationWindow } -// A window read that keeps observing a moving head stops here and refuses with the attempt count. -data cas_window_observation_attempt_bound: Int = 8 - // "" -> the generation it names; none for anything else, for zero, and for a value past the // Int maximum. The multiplication is pre-checked (std.checked_arithmetic: check before, never after). fn cas_generation_of_digits(s: String) -> CasGeneration? { @@ -491,7 +488,7 @@ fn cas_key_generation_of_name(key: NonEmptyStr, name: String) -> CasGeneration? fn cas_window_generations(root: NonEmptyStr, key: NonEmptyStr) -> CasWindowGenerations admit_callers: [ decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_settle_slot"), - decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_observe_window"), + decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_observe_window_once"), decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_reclaim_below"), ] { @@ -516,37 +513,54 @@ fn cas_window_max_generation(generations: List) -> CasGeneration? }) } -fn cas_observe_window(root: NonEmptyStr, key: NonEmptyStr, attempt: Int) -> CasSlotProbe +// THE ONE AUTHORITY FOR THE WINDOWED OBSERVATION BOUND: the roster is the bound. Exhaustion +// reports CasUnreadableObservationBoundExceeded { bound: count(cas_window_observation_attempts) }. +data cas_window_observation_attempts: List = [1, 2, 3, 4, 5, 6, 7, 8] + +// ONE LIST-AND-VERIFY PASS. Recursion here was the unbounded arm: a window whose listed max is +// strictly below a generation that still reads present (stale listing, or a TCO lowering that does +// not increment `attempt`) re-entered cas_observe_window forever, re-reading G and G+1 with no +// further List. A fold over a finite attempt roster cannot fail to stop. +fn cas_observe_window_once(root: NonEmptyStr, key: NonEmptyStr) -> CasSlotProbe? admit_callers: [ - decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_observe_retained_slot"), decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_observe_window"), ] { - if attempt > cas_window_observation_attempt_bound { - ProbedWindowHeadUnsettled { attempts: cas_window_observation_attempt_bound } - } else { - match cas_window_generations(root: root, key: key) { - CasWindowGenerationsUnlisted { cause: c } => ProbedAbsenceUnestablished { cause: c } - CasWindowGenerationsListed { generations: gs } => - match cas_window_max_generation(generations: gs) { - Absent => ProbedAbsent - Present { value: head } => - match cas_read_generation(root: root, key: key, generation: head) { - GenerationRefused { probe: p } => p - GenerationAbsent => cas_observe_window(root: root, key: key, attempt: attempt + 1) - GenerationPresent { content: c } => - match cas_generation_successor(g: head) { - CasSuccessorExhausted { head: _ } => ProbedHead { generation: head, value: c } - CasSuccessorGeneration { generation: next } => - match cas_read_generation(root: root, key: key, generation: next) { - GenerationRefused { probe: p } => p - GenerationAbsent => ProbedHead { generation: head, value: c } - GenerationPresent { content: _ } => cas_observe_window(root: root, key: key, attempt: attempt + 1) - } - } - } - } - } + match cas_window_generations(root: root, key: key) { + CasWindowGenerationsUnlisted { cause: c } => Present { value: ProbedAbsenceUnestablished { cause: c } } + CasWindowGenerationsListed { generations: gs } => + match cas_window_max_generation(generations: gs) { + Absent => Present { value: ProbedAbsent } + Present { value: head } => + match cas_read_generation(root: root, key: key, generation: head) { + GenerationRefused { probe: p } => Present { value: p } + GenerationAbsent => none + GenerationPresent { content: c } => + match cas_generation_successor(g: head) { + CasSuccessorExhausted { head: _ } => Present { value: ProbedHead { generation: head, value: c } } + CasSuccessorGeneration { generation: next } => + match cas_read_generation(root: root, key: key, generation: next) { + GenerationRefused { probe: p } => Present { value: p } + GenerationAbsent => Present { value: ProbedHead { generation: head, value: c } } + GenerationPresent { content: _ } => none + } + } + } + } + } +} + +fn cas_observe_window(root: NonEmptyStr, key: NonEmptyStr) -> CasSlotProbe + admit_callers: [ + decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "cas_observe_retained_slot"), + ] +{ + match fold(cas_window_observation_attempts, init: none, f: (acc, _) => match acc { + Present { value: p } => Present { value: p } + Absent => cas_observe_window_once(root: root, key: key) + }) { + Present { value: p } => p + Absent => ProbedWindowHeadUnsettled { attempts: count(cas_window_observation_attempts) } } } @@ -560,14 +574,12 @@ fn cas_observe_retained_slot(root: NonEmptyStr, key: NonEmptyStr, retention: Cas { match retention { KeepAllGenerations => cas_observe_slot(root: root, key: key) - KeepGenerationWindow { window: _ } => cas_observe_window(root: root, key: key, attempt: 1) + KeepGenerationWindow { window: _ } => cas_observe_window(root: root, key: key) } } fn cas_window_head_unsettled(attempts: Int) -> CasUnreadableSlot { - CasUnreadableReadRefused { - detail: concat(concat("the window head moved during ", attempts as String), " observations and was not settled; a head is never guessed") as NonEmptyStr - } + CasUnreadableObservationBoundExceeded { bound: attempts } } // THE PROJECTION TAKES A HEAD, NOT A PROBE, AND THE DELETED ARM IS THE REPAIR. @@ -677,11 +689,62 @@ fn observe_cas_slot_state_windowed(root: NonEmptyStr, key: NonEmptyStr, window: admit_callers: [ decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_read_index"), decl_ref(module_path: "test.claim.durable_cas_file_store_wet_witness", decl_name: "a_window_slot_holds_at_most_k_plus_one_generations_by_real_execution"), + decl_ref(module_path: "test.claim.durable_cas_file_store_wet_witness", decl_name: "a_window_observation_at_generation_1500_terminates_by_real_execution"), ] { observe_cas_slot_state_retained(root: root, key: key, retention: KeepGenerationWindow { window: window }) } +// A WINDOWED HEAD READ BY CONSTRUCTED NAME, NOT BY LISTING THE SLOT ROOT. +// +// cas_observe_window finds the head by listing every name under the store root and keeping this +// key's generation files. That is the correct discovery when the generation is unknown, because a +// windowed slot is NOT a presence prefix: generations at or below head-k are deleted, so galloping +// from generation 1 would read NotFound and report an empty slot while the live head sat at 100. +// Listing is therefore retained for unknown-head observation (cas_observe_window). +// +// When the caller already names the generation it believes is head -- a hold it just acquired, a +// release at the generation the protocol recorded -- the existing correctness argument does not +// need the listing. The windowed contract's verify step is: the named generation reads present and +// its successor reads absent. Those two reads are Filesystem.Read of cas_file_slot_path, O(1) in +// the store's object count. A successor that is present means the head moved; an absent named +// generation is not this head. The observation is never guessed. +// Realization-private: named verify is not CasSlotProbe and not DurableHoldObservation. G+1 present +// is a moved head, not eight-pass BoundExceeded. Consumers are the windowed hold check and the +// windowed release assessment; they match HeadMoved directly. +type CasNamedWindowVerify + = NamedWindowHeadCurrent { observation: CasSlotObservation } + | NamedWindowHeadMoved { named_generation: CasGeneration } + +fn cas_verify_named_window_head(root: NonEmptyStr, key: NonEmptyStr, generation: CasGeneration) -> CasNamedWindowVerify + admit_callers: [ + decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_hold_check"), + decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_release_assess_windowed_at"), + decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_recovery_assess_windowed"), + ] +{ + if !cas_key_is_slot_addressable(key: key) { + NamedWindowHeadCurrent { observation: CasObservedUnreadable { cause: CasUnreadableReadRefused { detail: cas_key_not_slot_addressable_detail } } } + } else { + match cas_read_generation(root: root, key: key, generation: generation) { + GenerationRefused { probe: p } => NamedWindowHeadCurrent { observation: cas_probe_as_observation(probe: p) } + GenerationAbsent => NamedWindowHeadCurrent { observation: cas_probe_as_observation(probe: ProbedAbsent) } + GenerationPresent { content: c } => + match cas_generation_successor(g: generation) { + CasSuccessorExhausted { head: _ } => + NamedWindowHeadCurrent { observation: cas_probe_as_observation(probe: ProbedHead { generation: generation, value: c }) } + CasSuccessorGeneration { generation: next } => + match cas_read_generation(root: root, key: key, generation: next) { + GenerationRefused { probe: p } => NamedWindowHeadCurrent { observation: cas_probe_as_observation(probe: p) } + GenerationAbsent => + NamedWindowHeadCurrent { observation: cas_probe_as_observation(probe: ProbedHead { generation: generation, value: c }) } + GenerationPresent { content: _ } => NamedWindowHeadMoved { named_generation: generation } + } + } + } + } +} + data cas_key_not_slot_addressable_detail: NonEmptyStr = "key is not slot-addressable: it carries a path separator" @@ -1035,23 +1098,42 @@ type CasFileWrite { reclamation: CasWindowReclamation } +type CasReclaimProgress { + removed: List + failed: List +} + +// ASCENDING, AND STOP AT THE FIRST HOST REFUSAL. Independent deletes of every generation at or +// below the floor could remove G+1 while G survived, which punches a hole: named window verify +// (G present and G+1 absent) would then report G as head while a later generation was live. +// One sort_by of the eligible set, then one walk that leaves every later generation in place when +// a delete fails, keeps that prefix contiguous without a min-and-filter per step. fn cas_reclaim_below(root: NonEmptyStr, key: NonEmptyStr, window: CasGenerationWindow, head: CasGeneration) -> CasWindowReclamation admit_callers: [ decl_ref(module_path: "gunbc.durable_cas_file_store", decl_name: "file_compare_and_set_retained"), decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_settle_slot"), + decl_ref(module_path: "test.claim.durable_cas_file_store_wet_witness", decl_name: "reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution"), ] { match cas_window_generations(root: root, key: key) { CasWindowGenerationsUnlisted { cause: c } => CasReclamationUnlisted { cause: c } CasWindowGenerationsListed { generations: gs } => { let floor = cas_generation_count(g: head) - window.k - let attempts = gs |> filter(g => cas_generation_count(g: g) <= floor) |> map(g => { - let d = Filesystem.Delete(path: cas_file_slot_path(root: root, key: key, generation: g)) - CasGenerationDeleteAttempt { generation: g, success: d.success, error: d.error } - }) - let removed = attempts |> filter(t => t.success) |> map(t => t.generation) - let failed = attempts |> filter(t => !t.success) |> map(t => CasGenerationReclaimFailure { generation: t.generation, host_error: t.error }) - if count(failed) == 0 { CasReclaimed { removed: removed } } else { CasReclamationIncomplete { removed: removed, failed: failed } } + let ordered = gs |> filter(g => cas_generation_count(g: g) <= floor) |> sort_by(g => cas_generation_count(g: g)) + let walk = fold(ordered, init: CasReclaimProgress { removed: [], failed: [] }, f: (acc, g) => + if count(acc.failed) != 0 { acc } else { + let d = Filesystem.Delete(path: cas_file_slot_path(root: root, key: key, generation: g)) + if d.success { + CasReclaimProgress { removed: acc.removed |> list_push(g), failed: acc.failed } + } else { + CasReclaimProgress { + removed: acc.removed, + failed: [CasGenerationReclaimFailure { generation: g, host_error: d.error }], + } + } + } + ) + if count(walk.failed) == 0 { CasReclaimed { removed: walk.removed } } else { CasReclamationIncomplete { removed: walk.removed, failed: walk.failed } } } } } diff --git a/dag/gunbc/durable_exclusive_hold_file_store.dag b/dag/gunbc/durable_exclusive_hold_file_store.dag index fa5efa93c9d..1b1f6f35ba9 100644 --- a/dag/gunbc/durable_exclusive_hold_file_store.dag +++ b/dag/gunbc/durable_exclusive_hold_file_store.dag @@ -37,14 +37,16 @@ import std.durable_exclusive_hold { HoldSlotUndecodable, HoldObservationUnavailable, DurableHoldAcquireRefusal, HoldAcquireEligible, HoldAcquireRefused, HoldAcquireOccupied, HoldAcquireSlotUndecodable, HoldAcquireObservationUnavailable, - HoldReleaseEligible, HoldReleaseRefused, DurableHoldReleaseRefusal, + HoldReleaseEligible, HoldReleaseRefused, DurableHoldReleaseAssessment, DurableHoldReleaseRefusal, durable_hold_acquire_assess, durable_hold_release_assess, - DurableHoldHolderReport, DurableHoldRecoveryRefusal, HoldRecoveryEligible, HoldRecoveryRefused, + DurableHoldHolderReport, DurableHoldRecoveryRefusal, DurableHoldRecoveryAssessment, HoldRecoveryEligible, HoldRecoveryRefused, durable_hold_recovery_assess, } -import gunbc.durable_cas_file_store { KeepAllGenerations, CasSlotRetention, - DefaultAccessCreateOnly, DerivedPayloadAdmitted, DerivedPayloadKeyNotSlotAddressable, - admit_cas_attempt_for_derived_payload, file_compare_and_set, observe_cas_slot_state, +import gunbc.durable_cas_file_store { KeepAllGenerations, KeepGenerationWindow, CasSlotRetention, + CasGenerationWindow, DefaultAccessCreateOnly, DerivedPayloadAdmitted, DerivedPayloadKeyNotSlotAddressable, + admit_cas_attempt_for_derived_payload, file_compare_and_set, file_compare_and_set_retained, + observe_cas_slot_state, observe_cas_slot_state_retained, + cas_verify_named_window_head, CasNamedWindowVerify, NamedWindowHeadCurrent, NamedWindowHeadMoved, } // THE TAGS ARE NAMED ONCE AND THE BODY IS DERIVED FROM THE TAG THAT MATCHED. @@ -142,7 +144,6 @@ fn hold_observation_of_cas(observed: CasSlotObservation) -> Durable fn observe_file_hold_retained(root: NonEmptyStr, key: NonEmptyStr, retention: CasSlotRetention) -> DurableHoldObservation admit_callers: [ decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "observe_file_hold"), - decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "observe_file_hold_windowed"), decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_acquire_prepare_retained"), decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_release_assess_retained"), decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_recovery_assess_retained"), @@ -155,12 +156,54 @@ fn observe_file_hold(root: NonEmptyStr, key: NonEmptyStr) -> DurableHoldObservat observe_file_hold_retained(root: root, key: key, retention: KeepAllGenerations) } -fn observe_file_hold_windowed(root: NonEmptyStr, key: NonEmptyStr, window: CasGenerationWindow) -> DurableHoldObservation +// Named-window head movement is a file/window realization outcome, not a +// protocol refusal. Keep-all consumers of DurableHoldReleaseAssessment never +// see it; only windowed named verify mints these arms. +type WindowedHoldReleaseFromVerify + = WindowedHoldReleaseProtocol { assessment: DurableHoldReleaseAssessment } + | WindowedHoldReleaseNamedHeadMoved { named: CasGeneration } + +type WindowedHoldRecoveryFromVerify + = WindowedHoldRecoveryProtocol { assessment: DurableHoldRecoveryAssessment } + | WindowedHoldRecoveryNamedHeadMoved { named: CasGeneration } + +fn windowed_hold_release_from_verify( + verify: CasNamedWindowVerify, + owner: DurableHoldOwnerRef, + acquired_generation: CasGeneration, +) -> WindowedHoldReleaseFromVerify admit_callers: [ - decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_hold_check"), + decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_release_assess_windowed_at"), + decl_ref(module_path: "test.claim.durable_exclusive_hold_witness", decl_name: "named_window_head_moved_release_does_not_claim_an_observed_current_generation"), + ] +{ + match verify { + NamedWindowHeadMoved { named_generation: n } => + WindowedHoldReleaseNamedHeadMoved { named: n } + NamedWindowHeadCurrent { observation: o } => + WindowedHoldReleaseProtocol { + assessment: durable_hold_release_assess(observed: hold_observation_of_cas(observed: o), owner: owner, acquired_generation: acquired_generation), + } + } +} + +fn windowed_hold_recovery_from_verify( + verify: CasNamedWindowVerify, + report: DurableHoldHolderReport, +) -> WindowedHoldRecoveryFromVerify + admit_callers: [ + decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_recovery_assess_windowed"), + decl_ref(module_path: "test.claim.durable_exclusive_hold_witness", decl_name: "named_window_head_moved_recovery_does_not_claim_an_observed_current_generation"), ] { - observe_file_hold_retained(root: root, key: key, retention: KeepGenerationWindow { window: window }) + match verify { + NamedWindowHeadMoved { named_generation: n } => + WindowedHoldRecoveryNamedHeadMoved { named: n } + NamedWindowHeadCurrent { observation: o } => + WindowedHoldRecoveryProtocol { + assessment: durable_hold_recovery_assess(observed: hold_observation_of_cas(observed: o), report: report), + } + } } // EVERY EFFECTFUL HOLD WRITE IS LIVE READ -> SEALED PLAN -> CAS COMMIT, AND NO @@ -325,6 +368,11 @@ type FileHoldReleaseAssessment = FileHoldReleasePlanned { plan: FileHoldReleasePlan } | FileHoldReleaseNotEligible { slot_key: NonEmptyStr, refusal: DurableHoldReleaseRefusal } +type FileHoldWindowedReleaseAssessment + = FileHoldWindowedReleasePlanned { plan: FileHoldReleasePlan } + | FileHoldWindowedReleaseNotEligible { slot_key: NonEmptyStr, refusal: DurableHoldReleaseRefusal } + | FileHoldWindowedReleaseNamedHeadMoved { slot_key: NonEmptyStr, named: CasGeneration } + // THE ONLY MINT OF A RELEASE PLAN READS THE SLOT ITSELF. There is deliberately // no variant over a supplied observation: a synthetic "held by A at N" handed to // such a mint would produce a plan that frees a real hold of B's at N, and the @@ -340,7 +388,6 @@ fn file_hold_release_assess_retained( ) -> FileHoldReleaseAssessment admit_callers: [ decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_release_assess"), - decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_release_assess_windowed"), ] { match durable_hold_release_assess( @@ -367,12 +414,34 @@ fn file_hold_release_assess(root: NonEmptyStr, slot_key: NonEmptyStr, owner: Dur file_hold_release_assess_retained(root: root, slot_key: slot_key, owner: owner, acquired_generation: acquired_generation, released_by: released_by, retention: KeepAllGenerations) } -fn file_hold_release_assess_windowed(root: NonEmptyStr, slot_key: NonEmptyStr, owner: DurableHoldOwnerRef, acquired_generation: CasGeneration, released_by: DurableHoldReleaseRef, window: CasGenerationWindow) -> FileHoldReleaseAssessment +fn file_hold_release_assess_windowed_at(root: NonEmptyStr, slot_key: NonEmptyStr, owner: DurableHoldOwnerRef, acquired_generation: CasGeneration, released_by: DurableHoldReleaseRef, window: CasGenerationWindow) -> FileHoldWindowedReleaseAssessment admit_callers: [ decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_release_family_hold"), ] { - file_hold_release_assess_retained(root: root, slot_key: slot_key, owner: owner, acquired_generation: acquired_generation, released_by: released_by, retention: KeepGenerationWindow { window: window }) + match windowed_hold_release_from_verify( + verify: cas_verify_named_window_head(root: root, key: slot_key, generation: acquired_generation), + owner: owner, + acquired_generation: acquired_generation, + ) { + WindowedHoldReleaseNamedHeadMoved { named: n } => + FileHoldWindowedReleaseNamedHeadMoved { slot_key: slot_key, named: n } + WindowedHoldReleaseProtocol { assessment: a } => + match a { + HoldReleaseRefused { refusal: r } => FileHoldWindowedReleaseNotEligible { slot_key: slot_key, refusal: r } + HoldReleaseEligible { generation: g } => + FileHoldWindowedReleasePlanned { + plan: FileHoldReleasePlan { + root: root, + slot_key: slot_key, + owner: owner, + generation: g, + free_payload: hold_slot_payload(state: DurableHoldFree { released_by: released_by }), + retention: KeepGenerationWindow { window: window }, + }, + } + } + } } // The commit still tells a lost race from an unavailable store. A release @@ -416,6 +485,11 @@ type FileHoldRecoveryAssessment = FileHoldRecoveryPlanned { plan: FileHoldReleasePlan, dead_holder: DurableHoldOwnerRef, evidence: NonEmptyStr } | FileHoldRecoveryNotEligible { slot_key: NonEmptyStr, refusal: DurableHoldRecoveryRefusal } +type FileHoldWindowedRecoveryAssessment + = FileHoldWindowedRecoveryPlanned { plan: FileHoldReleasePlan, dead_holder: DurableHoldOwnerRef, evidence: NonEmptyStr } + | FileHoldWindowedRecoveryNotEligible { slot_key: NonEmptyStr, refusal: DurableHoldRecoveryRefusal } + | FileHoldWindowedRecoveryNamedHeadMoved { slot_key: NonEmptyStr, named: CasGeneration } + fn file_hold_recovery_assess_retained( root: NonEmptyStr, slot_key: NonEmptyStr, @@ -425,7 +499,6 @@ fn file_hold_recovery_assess_retained( ) -> FileHoldRecoveryAssessment admit_callers: [ decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_recovery_assess"), - decl_ref(module_path: "gunbc.durable_exclusive_hold_file_store", decl_name: "file_hold_recovery_assess_windowed"), ] { match durable_hold_recovery_assess(observed: observe_file_hold_retained(root: root, key: slot_key, retention: retention), report: report) { @@ -454,10 +527,33 @@ fn file_hold_recovery_assess(root: NonEmptyStr, slot_key: NonEmptyStr, report: D file_hold_recovery_assess_retained(root: root, slot_key: slot_key, report: report, released_by: released_by, retention: KeepAllGenerations) } -fn file_hold_recovery_assess_windowed(root: NonEmptyStr, slot_key: NonEmptyStr, report: DurableHoldHolderReport, released_by: DurableHoldReleaseRef, window: CasGenerationWindow) -> FileHoldRecoveryAssessment +fn file_hold_recovery_assess_windowed(root: NonEmptyStr, slot_key: NonEmptyStr, report: DurableHoldHolderReport, released_by: DurableHoldReleaseRef, window: CasGenerationWindow) -> FileHoldWindowedRecoveryAssessment admit_callers: [ decl_ref(module_path: "extdeps.realization.materialization_store_local", decl_name: "local_store_hold_occupied"), ] { - file_hold_recovery_assess_retained(root: root, slot_key: slot_key, report: report, released_by: released_by, retention: KeepGenerationWindow { window: window }) + match windowed_hold_recovery_from_verify( + verify: cas_verify_named_window_head(root: root, key: slot_key, generation: report.generation), + report: report, + ) { + WindowedHoldRecoveryNamedHeadMoved { named: n } => + FileHoldWindowedRecoveryNamedHeadMoved { slot_key: slot_key, named: n } + WindowedHoldRecoveryProtocol { assessment: a } => + match a { + HoldRecoveryRefused { refusal: r } => FileHoldWindowedRecoveryNotEligible { slot_key: slot_key, refusal: r } + HoldRecoveryEligible { generation: g, dead_holder: h, evidence: e } => + FileHoldWindowedRecoveryPlanned { + plan: FileHoldReleasePlan { + root: root, + slot_key: slot_key, + owner: h, + generation: g, + free_payload: hold_slot_payload(state: DurableHoldFree { released_by: released_by }), + retention: KeepGenerationWindow { window: window }, + }, + dead_holder: h, + evidence: e, + } + } + } } diff --git a/dag/gunbc/executor_privileged_operation.dag b/dag/gunbc/executor_privileged_operation.dag index 719f871a0b7..64bb74810fd 100644 --- a/dag/gunbc/executor_privileged_operation.dag +++ b/dag/gunbc/executor_privileged_operation.dag @@ -28,6 +28,7 @@ import v2.std.orchestration { EnvSet } import gunbc.cli_run_workspace_root_scaffold { gunbc_workspace_root_env_name } import gunbc.cli_invoke { gunbc_run_invocation_words_with_functions, invocation_words_bare } import extdeps.posix.shell_command_language { posix_single_quote } +import gunbc.owned_directory { owned_directory_stat_format } import v2.std.algebra { fold_list } import extdeps.github.actions_runner { actions_runner_base_dir, actions_runner_cache_dir } import extdeps.systemd.systemctl { systemctl_enable_verb, systemctl_start_verb, systemctl_set_property_verb, systemctl_reload_verb, systemctl_restart_verb } @@ -42,6 +43,8 @@ import gunbc.fleet_posix_accounts { SudoConntrack, SudoIptablesLegacySave, SudoNfct, + SudoStat, + SudoLs, } import extdeps.tools.conntrack { conntrack_list_expectations_arguments } import extdeps.nftables.ruleset { nft_list_ruleset_arguments, nft_list_table_arguments_wire } @@ -85,6 +88,8 @@ type ExecutorPrivilegedOperation | ReadConntrackExpectations | ReadLegacyXtablesRules | ReadUserspaceConntrackHelpers + | ReadDirectoryIdentity { path: NonEmptyStr } + | ListDirectoryEntries { path: NonEmptyStr } | RunGunbcEntryAs { run_as: NonEmptyStr workspace_root: NonEmptyStr @@ -200,6 +205,8 @@ fn executor_privileged_operation_argv(op: ExecutorPrivilegedOperation) -> List concat([ci_runner_sudo_binary_path(b: SudoConntrack)], conntrack_list_expectations_arguments) ReadLegacyXtablesRules => concat([ci_runner_sudo_binary_path(b: SudoIptablesLegacySave)], iptables_legacy_save_arguments) ReadUserspaceConntrackHelpers => concat([ci_runner_sudo_binary_path(b: SudoNfct)], nfct_helper_list_arguments) + ReadDirectoryIdentity { path: p } => [ci_runner_sudo_binary_path(b: SudoStat), "-c" as NonEmptyStr, owned_directory_stat_format as NonEmptyStr, "--" as NonEmptyStr, p] + ListDirectoryEntries { path: p } => [ci_runner_sudo_binary_path(b: SudoLs), "-1A" as NonEmptyStr, "--" as NonEmptyStr, p] ReadNftTable { family: f, table: t } => concat([ci_runner_sudo_binary_path(b: SudoNft)], nft_list_table_arguments_wire(family_wire: f, name: t)) SystemdDaemonReload => [ ci_runner_sudo_binary_path(b: SudoSystemctl), @@ -246,6 +253,8 @@ fn executor_privileged_operation_principal(op: ExecutorPrivilegedOperation) -> O ReadConntrackExpectations => RunsAsRoot ReadLegacyXtablesRules => RunsAsRoot ReadUserspaceConntrackHelpers => RunsAsRoot + ReadDirectoryIdentity { path: _ } => RunsAsRoot + ListDirectoryEntries { path: _ } => RunsAsRoot } } @@ -478,6 +487,8 @@ fn executor_privileged_operation_precondition(op: ExecutorPrivilegedOperation) - ReadConntrackExpectations => NoPrecondition ReadLegacyXtablesRules => NoPrecondition ReadUserspaceConntrackHelpers => NoPrecondition + ReadDirectoryIdentity { path: _ } => NoPrecondition + ListDirectoryEntries { path: _ } => NoPrecondition RunGunbcEntryAs { run_as: _, workspace_root: _, binary: _, source_roots: _, entry: _, function: _ } => NoPrecondition } } diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index 94f0dcdc79a..d2068be5b8d 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -580,6 +580,10 @@ data scope_carrier_paths: List = [ "dag/extdeps/cloudflare/r2.dag", "dag/extdeps/cloudflare/r2_buckets.dag", "dag/extdeps/deepseek/deepseek_v4_1_flash.dag", + "dag/extdeps/nvidia/api_trial_terms.dag", + "dag/extdeps/nvidia/nemotron_3_ultra.dag", + "dag/extdeps/openrouter/openrouter.dag", + "dag/extdeps/openrouter/nvidia_nemotron_3_ultra_free.dag", "dag/extdeps/zhipu/glm_5_3.dag", "dag/extdeps/radixark/glm_5_3_nvfp4.dag", "dag/extdeps/docker/images/gitcommit90_glm_53_one_spark.dag", diff --git a/dag/gunbc/fabric/fabric_cell_converge.dag b/dag/gunbc/fabric/fabric_cell_converge.dag index f1bb9f67170..b5eb3b66fcd 100644 --- a/dag/gunbc/fabric/fabric_cell_converge.dag +++ b/dag/gunbc/fabric/fabric_cell_converge.dag @@ -534,6 +534,11 @@ type FabricCellAddressEvidence | EvidenceObserved { member: FabricCellMember } | EvidenceConflict +// TWO REFUSALS JOIN TO THE LEAST CAUSE (byte-lexicographic String order), not the first seen, so +// the full value -- AddressUnreadable.cause included -- is a semilattice join and independent of the +// order observations arrive in (test.claim.fabric_cell_evidence_algebra_witness +// evidence_full_value_is_order_independent). Keeping the first cause made the class order-free +// while the payload still moved with caller order. fn fabric_cell_address_evidence( observations: List, ) -> FabricCellAddressEvidence { @@ -541,7 +546,8 @@ fn fabric_cell_address_evidence( match o { AddressObservationRefused { slot: _, address: _, cause: c } => match acc { - EvidenceRefused { cause: first } => EvidenceRefused { cause: first } + EvidenceRefused { cause: seen } => + if c < seen { EvidenceRefused { cause: c } } else { EvidenceRefused { cause: seen } } NoEvidence => EvidenceRefused { cause: c } EvidenceAbsent => EvidenceRefused { cause: c } EvidenceObserved { member: _ } => EvidenceRefused { cause: c } diff --git a/dag/gunbc/fabric/fabric_event_log.dag b/dag/gunbc/fabric/fabric_event_log.dag index bd95ae2f46a..48930300359 100644 --- a/dag/gunbc/fabric/fabric_event_log.dag +++ b/dag/gunbc/fabric/fabric_event_log.dag @@ -25,7 +25,7 @@ import product.capacity.event_chain { import product.capacity.pool_events { PoolEvent, pool_event_wire_text, pool_event_decode, PoolFold, PoolFolded, PoolFoldRefused, pool_fold, SeatRequest, SeatProposal, SeatProposed, SeatRefused, propose_acquire, grant_from_admission, - PoolReleased, + PoolReleased, pool_apply_payload, } import product.capacity.lease { LeasePolicy, LeaseGrant, release_law_eq, release_law_wire } import product.capacity.redemption { @@ -415,6 +415,96 @@ fn pool_event_append_wire(a: PoolEventAppend) -> String { } } +// A POOL TRANSITION THAT IS CHECKED BEFORE IT IS WRITTEN. fabric_pool_event_append appends the event it +// is handed, and a caller that reports success on that append can report success over a transition no +// fold will ever apply: a settlement appended after its lease lapsed, or a hold whose reference +// duplicates an earlier one. The fold refuses such an event on replay, and pool_fold refuses the WHOLE +// partition at the first refused event -- so one unchecked append makes every later read of the pool +// fail while the caller believed it had succeeded. +// +// SO THIS IS SETTLEMENT AND OBSERVATION WITH THE ACQUISITION'S DISCIPLINE: read the partition, fold it, +// apply the transition with the same function replay uses (product.capacity.pool_events +// pool_apply_payload), and only if that advances, compare-and-set append against the head it was +// decided on; a moved head is re-read and re-decided, never written blind. A refused transition is its +// own arm and nothing is appended. The payload is built from the head because a transition that mints +// its own hold needs an identity no other reading shares. +type PoolTransition + = PoolTransitionAppended { id: EventId } + | PoolTransitionRefused { wire: String } + | PoolTransitionContended { attempts: Nat } + | PoolTransitionStoreRefused { step: String, reason: String } + +type PoolTransitionState { + done: PoolTransition? +} + +fn pool_transition_head_word(head: HeadExpectation) -> NonEmptyStr { + match head { + HeadAbsent => "genesis" as NonEmptyStr + HeadAt { id: h } => h as String as NonEmptyStr + } +} + +fn pool_transition_attempt(store: FabricStorageBinding, partition: PartitionId, root: Pool, actor: NonEmptyStr, at: EpochSecs, payload: fn(NonEmptyStr) -> PoolEvent, budget: Nat) -> PoolTransition? { + match event_log_read_partition(store: store, partition: partition, budget: budget) { + PartitionReadRefused { cause: c } => Present { value: PoolTransitionStoreRefused { step: "store", reason: event_log_refusal_wire(cause: c) } } + PartitionReadOk { head: head, walk: walk } => + match walk { + ChainIncomplete { missing: m, newest_first_so_far: _ } => Present { value: PoolTransitionStoreRefused { step: "chain", reason: join(["partition chain is missing event ", m as String], "") } } + ChainBudgetExhausted { at: a } => Present { value: PoolTransitionStoreRefused { step: "chain", reason: join(["partition chain exceeded the read budget at ", a as String], "") } } + ChainWalked { oldest_first: xs } => + match pool_fold(root: root, oldest_first: xs) { + PoolFoldRefused { at_event: e, wire: w } => Present { value: PoolTransitionStoreRefused { step: "fold", reason: join(["event ", e as String, " refused: ", w], "") } } + PoolFolded { pool: p, generation: _ } => { + let identity = join(["pending:", pool_transition_head_word(head: head) as String], "") as NonEmptyStr + let event_payload = payload(identity) + match pool_apply_payload(pool: p, payload: event_payload, at: at, identity: identity) { + PoolRefused { refusal: r } => Present { value: PoolTransitionRefused { wire: pool_refusal_wire(r: r) } } + PoolAdvanced { pool: _ } => + match event_log_append( + store: store, + partition: partition, + event: ChainEvent { + partition: partition, + parent: match head { HeadAbsent => none HeadAt { id: h } => Present { value: h } }, + recorded_at: at, + actor: actor, + payload: event_payload, + }, + expected: head, + ) { + EventAppended { id: h } => Present { value: PoolTransitionAppended { id: h } } + EventAppendStale { expected: _, observed: _ } => none + EventAppendRefused { cause: c } => Present { value: PoolTransitionStoreRefused { step: "store", reason: event_log_refusal_wire(cause: c) } } + } + } + } + } + } + } +} + +fn fabric_pool_transition(store: FabricStorageBinding, partition: PartitionId, root: Pool, actor: NonEmptyStr, at: EpochSecs, payload: fn(NonEmptyStr) -> PoolEvent, attempts: Nat, budget: Nat) -> PoolTransition { + let st = fold(nat_range_inclusive(lo: 1, hi: attempts), init: PoolTransitionState { done: none }, f: (acc, _i) => + match acc.done { + Present { value: _ } => acc + Absent => PoolTransitionState { done: pool_transition_attempt(store: store, partition: partition, root: root, actor: actor, at: at, payload: payload, budget: budget) } + }) + match st.done { + Present { value: d } => d + Absent => PoolTransitionContended { attempts: attempts } + } +} + +fn pool_transition_wire(t: PoolTransition) -> String { + match t { + PoolTransitionAppended { id: h } => join(["appended ", h as String], "") + PoolTransitionRefused { wire: w } => join(["the pool refuses the transition: ", w], "") + PoolTransitionContended { attempts: n } => join(["contended after ", to_string(n), " attempts"], "") + PoolTransitionStoreRefused { step: s, reason: why } => join(["refused at ", s, ": ", why], "") + } +} + // RELEASING A SEAT, AND IT IS CHECKED AGAINST THE FOLDED POOL BEFORE ANYTHING IS APPENDED. // // fabric_pool_event_append is unchecked by construction -- it writes the event a caller hands it -- diff --git a/dag/gunbc/fabric/fabric_quota.dag b/dag/gunbc/fabric/fabric_quota.dag index 870172d2f17..20413667278 100644 --- a/dag/gunbc/fabric/fabric_quota.dag +++ b/dag/gunbc/fabric/fabric_quota.dag @@ -2,17 +2,19 @@ module gunbc.fabric_quota import std.types { String, Bool, Int, NonEmptyStr, List, EpochSecs } import std.nat { Nat } -import std.measure { Measure } +import std.checked_arithmetic { checked_int_to_nat } +import std.measure { Measure, Second, second, second_count } import extdeps.api_rate_limit { UpstreamRateLimit, upstream_rate_limit_key } import product.capacity.quota { quota_partition, quota_root_pool } import product.capacity.event_chain { PartitionId } -import product.capacity.pool_events { PoolSettled, SeatRequest } +import product.capacity.pool_events { PoolSettled, PoolUpstreamObserved, SeatRequest } +import product.capacity.pool { pool_window_at, WindowResolved, WholeLifetime, InstantBeforeAnchor } import product.capacity.lease { LeasePolicy, LeaseGrant, FencedResource } import gunbc.fabric_event_log_host { HostEventLogStore, HostStoreResolved, HostStoreRefused, event_log_store_for_host, now_epoch_seconds } import gunbc.fabric_storage_client { FabricStorageBinding } import gunbc.fabric_event_log { SeatAcquisition, SeatGranted, SeatFull, SeatContended, SeatAcquireRefused, fabric_seat_acquire, seat_acquisition_wire, - PoolEventAppend, PoolEventAppended, PoolEventContended, PoolEventAppendRefused, fabric_pool_event_append, pool_event_append_wire, + PoolTransition, PoolTransitionAppended, fabric_pool_transition, pool_transition_wire, } // A CALLER LEASES UPSTREAM QUOTA BEFORE IT SPENDS IT. The lease holds the calls it is about to @@ -25,7 +27,7 @@ import gunbc.fabric_event_log { // expiry frees is the unspent remainder. type QuotaLease - = QuotaLeased { grant: LeaseGrant, partition: PartitionId, store: FabricStorageBinding } + = QuotaLeased { grant: LeaseGrant, partition: PartitionId, store: FabricStorageBinding, limit: UpstreamRateLimit } | QuotaFull { wire: String } | QuotaLeaseRefused { detail: String } @@ -33,24 +35,116 @@ fn quota_policy(term_seconds: Nat) -> LeasePolicy { LeasePolicy { maximum_duration_seconds: term_seconds, release_law: FencedResource } } +// A TERM IS EITHER THE CALLER'S DURATION OR "TO THE END OF THIS WINDOW". A lapsing pool refuses a lease +// that would outlive its window (product.capacity.pool LeaseCrossesLapse), so a caller whose spend is +// an instant -- a request counted the moment it is sent -- cannot know a fixed term that always fits: +// at second 59 of a minute window only one second is left. The window end is read from the SAME +// clock reading the lease is stamped with, so the term fits by construction rather than by luck. +type QuotaTerm + = QuotaTermSeconds { seconds: Second } + | QuotaTermToWindowEnd + +fn quota_term_to_window_end(limit: UpstreamRateLimit, at: EpochSecs) -> Second? { + match pool_window_at(pool: quota_root_pool(limit: limit), at: at) { + WindowResolved { window: w } => if w.end > at { checked_second(n: w.end - at) } else { none } + WholeLifetime => none + InstantBeforeAnchor { anchor: _, at: _ } => none + } +} + +fn checked_second(n: Int) -> Second? { + match checked_int_to_nat(n: n) { + Absent => none + Present { value: v } => Present { value: second(count: v) } + } +} + +// THE INSTANT THE CURRENT WINDOW BEGAN, read off the same pool the term is: what a caller needs to tell +// which of its outstanding sends were admitted in an earlier window. +fn quota_window_start(limit: UpstreamRateLimit, at: EpochSecs) -> EpochSecs? { + match pool_window_at(pool: quota_root_pool(limit: limit), at: at) { + WindowResolved { window: w } => Present { value: w.start } + WholeLifetime => none + InstantBeforeAnchor { anchor: _, at: _ } => none + } +} + +fn quota_term_seconds(limit: UpstreamRateLimit, term: QuotaTerm, at: EpochSecs) -> Second? { + match term { + QuotaTermSeconds { seconds: n } => Present { value: n } + QuotaTermToWindowEnd => quota_term_to_window_end(limit: limit, at: at) + } +} + fn fabric_quota_lease(short_hostname: String, limit: UpstreamRateLimit, amount: Nat, actor: NonEmptyStr, term_seconds: Nat) -> QuotaLease { + fabric_quota_lease_for(short_hostname: short_hostname, limit: limit, amount: amount, actor: actor, term: QuotaTermSeconds { seconds: second(count: term_seconds) }) +} + +fn fabric_quota_lease_for(short_hostname: String, limit: UpstreamRateLimit, amount: Nat, actor: NonEmptyStr, term: QuotaTerm) -> QuotaLease { match event_log_store_for_host(short_hostname: short_hostname) { HostStoreRefused { detail: d } => QuotaLeaseRefused { detail: d } HostStoreResolved { store: store, executor: _ } => match now_epoch_seconds() { Absent => QuotaLeaseRefused { detail: "the clock could not be read as epoch seconds" } - Present { value: now } => { - let partition = quota_partition(limit: limit) - match fabric_seat_acquire( - store: store, partition: partition, root: quota_root_pool(limit: limit), actor: actor, - request: SeatRequest { reference: join([actor as String, "@", to_string(now)], "") as NonEmptyStr, amount: Measure { count: amount }, at: now, term_seconds: term_seconds }, - policy: quota_policy(term_seconds: term_seconds), attempts: 3, budget: 4096, - ) { - SeatGranted { grant: g, generation: _, attempts_used: _ } => QuotaLeased { grant: g, partition: partition, store: store } - SeatFull { wire: w, attempts_used: _ } => QuotaFull { wire: join([upstream_rate_limit_key(l: limit) as String, ": ", w], "") } - other => QuotaLeaseRefused { detail: seat_acquisition_wire(a: other) } + Present { value: now } => fabric_quota_lease_at(store: store, now: now, limit: limit, amount: amount, actor: actor, term: term) + } + } +} + +// THE LEASE AT A GIVEN STORE AND INSTANT. fabric_quota_lease_for resolves both from the executing host; +// everything after that -- the term derivation, the seat acquisition, the fold -- is this function, so a +// witness that supplies a store in a temporary directory and an instant runs the PRODUCTION lease rather +// than a constructed QuotaLeased. +fn fabric_quota_lease_at(store: FabricStorageBinding, now: EpochSecs, limit: UpstreamRateLimit, amount: Nat, actor: NonEmptyStr, term: QuotaTerm) -> QuotaLease { + match quota_term_seconds(limit: limit, term: term, at: now) { + Absent => QuotaLeaseRefused { detail: join([upstream_rate_limit_key(l: limit) as String, ": no window end is defined at this instant"], "") } + Present { value: term } => { + let term_seconds = second_count(s: term) + let partition = quota_partition(limit: limit) + match fabric_seat_acquire( + store: store, partition: partition, root: quota_root_pool(limit: limit), actor: actor, + request: SeatRequest { reference: join([actor as String, "@", to_string(now)], "") as NonEmptyStr, amount: Measure { count: amount }, at: now, term_seconds: term_seconds }, + policy: quota_policy(term_seconds: term_seconds), attempts: 3, budget: 4096, + ) { + SeatGranted { grant: g, generation: _, attempts_used: _ } => QuotaLeased { grant: g, partition: partition, store: store, limit: limit } + SeatFull { wire: w, attempts_used: _ } => QuotaFull { wire: join([upstream_rate_limit_key(l: limit) as String, ": ", w], "") } + other => QuotaLeaseRefused { detail: seat_acquisition_wire(a: other) } + } + } + } +} + +// WHAT THE UPSTREAM SAYS REMAINS IS RECORDED ON THE PARTITION, NOT KEPT BESIDE IT. A fresh local ledger +// knows nothing of requests spent before it existed, or by another client of the same credential; an +// upstream that reports how many remain is the authority on that. The reading is appended as the +// existing PoolUpstreamObserved event, which the fold turns into a hold of the shortfall for the rest +// of the window (product.capacity.pool observe_upstream_remaining) -- so a reading of zero leaves no +// headroom, a partial one leaves exactly that much, and a later, larger reading never gives back what +// an earlier one held. There is no second balance model: the pool is the balance. +type QuotaObservation + = QuotaUpstreamRecorded { partition: PartitionId, remaining: Nat } + | QuotaObservationRefused { detail: String } + +fn fabric_quota_observe_upstream(short_hostname: String, limit: UpstreamRateLimit, remaining: Nat, actor: NonEmptyStr) -> QuotaObservation { + match event_log_store_for_host(short_hostname: short_hostname) { + HostStoreRefused { detail: d } => QuotaObservationRefused { detail: d } + HostStoreResolved { store: store, executor: _ } => + match now_epoch_seconds() { + Absent => QuotaObservationRefused { detail: "the clock could not be read as epoch seconds" } + Present { value: now } => + match quota_term_to_window_end(limit: limit, at: now) { + Absent => QuotaObservationRefused { detail: join([upstream_rate_limit_key(l: limit) as String, ": no window end is defined at this instant"], "") } + Present { value: term } => { + let partition = quota_partition(limit: limit) + match fabric_pool_transition( + store: store, partition: partition, root: quota_root_pool(limit: limit), actor: actor, at: now, + payload: fn(identity) { PoolUpstreamObserved { remaining: remaining, term_seconds: second_count(s: term) } }, attempts: 3, budget: 4096, + ) { + PoolTransitionAppended { id: _ } => QuotaUpstreamRecorded { partition: partition, remaining: remaining } + other => QuotaObservationRefused { detail: pool_transition_wire(t: other) } + } + } } - } } } } @@ -59,32 +153,39 @@ type QuotaSettle = QuotaSettled { partition: PartitionId, actual: Nat } | QuotaSettleRefused { detail: String } -// SETTLEMENT IS ONE PARTITION APPEND, and the observe/append/retry loop it needs is the carrier's -// (gunbc.fabric_event_log fabric_pool_event_append), not a second copy here. It used to be a copy: -// this module held the only one until a compute reservation needed to release, at which point the -// loop would have existed twice and been free to disagree with itself about a contended head. +// SETTLEMENT IS ONE CHECKED PARTITION TRANSITION (gunbc.fabric_event_log fabric_pool_transition): the +// partition is folded and the settlement applied to it before anything is written, under the same +// compare-and-set the acquisition used. It was an unchecked append, which reported success for a +// settlement whose lease had already lapsed -- an event the fold refuses on replay, leaving the whole +// partition unfoldable while the caller believed it had settled. The lease carries its own limit so the +// settlement folds against the pool the lease was granted from. fn fabric_quota_settle(lease: QuotaLease, actual: Nat) -> QuotaSettle { + match now_epoch_seconds() { + Absent => QuotaSettleRefused { detail: "the clock could not be read as epoch seconds" } + Present { value: now } => fabric_quota_settle_at(lease: lease, actual: actual, now: now) + } +} + +// THE SETTLEMENT AT A GIVEN INSTANT, for the same reason the lease has one: the checked transition and +// everything after the clock read is this function. +fn fabric_quota_settle_at(lease: QuotaLease, actual: Nat, now: EpochSecs) -> QuotaSettle { match lease { QuotaFull { wire: w } => QuotaSettleRefused { detail: join(["nothing to settle: ", w], "") } QuotaLeaseRefused { detail: d } => QuotaSettleRefused { detail: join(["nothing to settle: ", d], "") } - QuotaLeased { grant: g, partition: p, store: store } => - match now_epoch_seconds() { - Absent => QuotaSettleRefused { detail: "the clock could not be read as epoch seconds" } - Present { value: now } => - match fabric_pool_event_append( - store: store, partition: p, actor: g.reference, at: now, - payload: PoolSettled { reference: g.reference, actual: actual }, attempts: 3, - ) { - PoolEventAppended { id: _ } => QuotaSettled { partition: p, actual: actual } - other => QuotaSettleRefused { detail: pool_event_append_wire(a: other) } - } + QuotaLeased { grant: g, partition: p, store: store, limit: limit } => + match fabric_pool_transition( + store: store, partition: p, root: quota_root_pool(limit: limit), actor: g.reference, at: now, + payload: fn(identity) { PoolSettled { reference: g.reference, actual: actual } }, attempts: 3, budget: 4096, + ) { + PoolTransitionAppended { id: _ } => QuotaSettled { partition: p, actual: actual } + other => QuotaSettleRefused { detail: pool_transition_wire(t: other) } } } } fn quota_lease_wire(l: QuotaLease) -> String { match l { - QuotaLeased { grant: g, partition: p, store: _ } => join(["leased ", g.reference as String, " on ", p as String, " fence=", g.fence.grant as String, "@", to_string(g.fence.generation), " expires_at=", to_string(g.expires_at)], "") + QuotaLeased { grant: g, partition: p, store: _, limit: _ } => join(["leased ", g.reference as String, " on ", p as String, " fence=", g.fence.grant as String, "@", to_string(g.fence.generation), " expires_at=", to_string(g.expires_at)], "") QuotaFull { wire: w } => join(["full ", w], "") QuotaLeaseRefused { detail: d } => join(["refused ", d], "") } diff --git a/dag/gunbc/fabric/fabric_storage_placement.dag b/dag/gunbc/fabric/fabric_storage_placement.dag index bebf0d3a505..1d89d886288 100644 --- a/dag/gunbc/fabric/fabric_storage_placement.dag +++ b/dag/gunbc/fabric/fabric_storage_placement.dag @@ -6,12 +6,13 @@ import std.types { String, NonEmptyStr, FilePath, Int, Bool, Port, List } import product.host_identity { HostIdentity } import gunbc.fleet_intent_network { fleet_intent_network } import gunbc.fleet_host_identity { operator_host_srv1 } -import gunbc.roadmap_dashboard_instance { HostDashboardInstance, dashboard_instance_fabric_storage_root, srv1_live_dashboard_instance, dashboard_instance_child } +import gunbc.roadmap_dashboard_instance { HostDashboardInstance, srv1_live_dashboard_instance, dashboard_instance_child } +import gunbc.owned_directory { OwnedDirectoryTarget, OwnedDirectoryManaged } import std.effect_grant { Read, Write, Execute } import extdeps.tailscale.serve { tailscale_serve_unix_proxy_peer } import gunbc.ownership { Ensured } import gunbc.managed_directory { - ManagedDirectory, DirectoryDependent, ManagedDirectoryAdmission, managed_directory_admit, srv1_dashboard_service_principal, + ManagedDirectory, DirectoryDependent, srv1_dashboard_service_principal, EntriesUnmanaged, EntriesReadableByDependents, managed_directory_entry_mode, ManagedEntryModeDerived, ManagedEntryModeRefused, } import gunbc.fleet_posix_accounts { fleet_posix_ci_runner_user } @@ -66,14 +67,21 @@ data fabric_storage_door_proxy_peer: NonEmptyStr = tailscale_serve_unix_proxy_pe -// The store root is the placed instance's fabric DB root -- one authority with the deployment that -// creates it (gunbc.roadmap_dashboard_instance dashboard_instance_fabric_storage_root). +// THE FABRIC DB ROOT: where the fabric DB's files live on the instance that holds the placement +// (gunbc.fabric_storage_file_store). It is this module's fact -- the placement decides where the store +// is -- and the deployment creates exactly the directories fabric_storage_directory_demands declares +// from it. It replaced the per-instance event-log mirror root: no executor keeps a copy of the log any +// more, so only the placement's host holds a root at all. fn fabric_storage_placed_instance() -> HostDashboardInstance { srv1_live_dashboard_instance() } +fn fabric_storage_root_of(instance: HostDashboardInstance) -> FilePath { + dashboard_instance_child(root: instance.instance_root, child: "fabric-storage") +} + fn fabric_storage_store_root() -> NonEmptyStr { - dashboard_instance_fabric_storage_root(instance: fabric_storage_placed_instance()) as String as NonEmptyStr + fabric_storage_root_of(instance: fabric_storage_placed_instance()) as String as NonEmptyStr } // WHETHER AN INSTANCE HOLDS THE PLACEMENT: the deployment owns the store root and the endpoint only @@ -207,20 +215,35 @@ fn fabric_storage_placed_file_root(store_root: NonEmptyStr) -> FabricStoragePlac } } -fn fabric_storage_store_directories() -> List { +fn fabric_storage_store_directories() -> List { let root = fabric_storage_store_root() let areas = fabric_storage_file_root(root: root) [ - managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-root", path: root, writes: false)), - managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-objects", path: areas.objects, writes: true)), - managed_directory_admit(d: fabric_storage_directory(member: "fabric-storage-heads", path: areas.heads, writes: true)), - managed_directory_admit(d: fabric_storage_state_directory(member: "fabric-state-root", path: fabric_storage_state_root(store_root: root))), - managed_directory_admit(d: fabric_storage_state_directory(member: "fabric-state-objects", path: fabric_storage_state_file_root(store_root: root).objects)), - managed_directory_admit(d: fabric_storage_state_directory(member: "fabric-state-heads", path: fabric_storage_state_file_root(store_root: root).heads)), - managed_directory_admit(d: fabric_storage_door_directory()), + fabric_storage_directory(member: "fabric-storage-root", path: root, writes: false), + fabric_storage_directory(member: "fabric-storage-objects", path: areas.objects, writes: true), + fabric_storage_directory(member: "fabric-storage-heads", path: areas.heads, writes: true), + fabric_storage_state_directory(member: "fabric-state-root", path: fabric_storage_state_root(store_root: root)), + fabric_storage_state_directory(member: "fabric-state-objects", path: fabric_storage_state_file_root(store_root: root).objects), + fabric_storage_state_directory(member: "fabric-state-heads", path: fabric_storage_state_file_root(store_root: root).heads), + fabric_storage_door_directory(), ] } +// THE STORE'S DIRECTORIES AS THE PLACED INSTANCE'S DEMAND. The root alone is not a store -- the file +// store writes objects and heads under it, and a create-new under an absent area refuses not_found -- +// so every area is demanded, each a managed directory whose owner, group and mode derive from the +// writers declared above; a writer the owner and group do not cover refuses in the provider +// (gunbc.owned_directory owned_directory_mode) rather than widening. Every other instance demands +// nothing, so a lab twin never creates srv1's store. The store is durable history: nothing the +// deployment does removes it. +fn fabric_storage_directory_demands(instance: HostDashboardInstance) -> List { + if fabric_storage_placed_on(instance: instance) { + map(fabric_storage_store_directories(), d => OwnedDirectoryManaged { directory: d }) + } else { + [] + } +} + // The authenticated state space shares the fabric DB service and CAS implementation, but never // shares its object directory with the legacy CI-writable namespace. A public head alias therefore // cannot expose private state objects. Only the store service writes here, even for local callers. diff --git a/dag/gunbc/fabric/workspace_checkpoint_instrument.dag b/dag/gunbc/fabric/workspace_checkpoint_instrument.dag index 26834bbad79..2d90bd63902 100644 --- a/dag/gunbc/fabric/workspace_checkpoint_instrument.dag +++ b/dag/gunbc/fabric/workspace_checkpoint_instrument.dag @@ -62,7 +62,7 @@ import gunbc.fabric.fabric_object_r2 { put_fabric_object_r2, get_fabric_object_r2, FabricObjectR2PutVerdict, FabricObjectR2Stored, FabricObjectR2PresentVerified, FabricObjectR2OccupiedByOther, FabricObjectR2Refused, FabricObjectR2OutcomeUnknown, } -import gunbc.fabric.workspace_source { fabric_workspace_source_key, workspace_put_words, workspace_put_landed } +import gunbc.fabric.workspace_source { fabric_workspace_source_key, workspace_put_words, workspace_put_landed, workspace_transcript_caveat } import gunbc.fabric_storage_r2_head { R2HeadBinding, R2HeadCredential, r2_head_binding, r2_head_read, r2_head_advance, r2_head_fabric_advance, R2HeadAdvanceResult, R2HeadAdvanceKnown, R2HeadAdvanceOutcomeUnknown, R2HeadStagingCustody, r2_head_staging_of, @@ -204,19 +204,39 @@ fn workspace_part_words(p: WorkspaceMeasuredPart) -> String { } } -fn workspace_bound_words(name: String, parts: List, v: WorkspaceBoundVerdict) -> String { - let inputs = join(map(parts, p => workspace_part_words(p: p)), " + ") - match v { - BoundHolds { total: t, bound: b } => join([name, "=holds total=", workspace_ms_words(n: t), " bound=", workspace_ms_words(n: b), " inputs: ", inputs], "") - BoundExceeded { at_least: t, bound: b, complete: c } => join([name, "=exceeded at_least=", workspace_ms_words(n: t), " bound=", workspace_ms_words(n: b), if c { " complete" } else { " by-measured-parts-alone" }, " inputs: ", inputs], "") - BoundUndecided { measured: t, bound: b, missing: m } => join([name, "=undecided measured=", workspace_ms_words(n: t), " bound=", workspace_ms_words(n: b), " missing: ", join(m, "; "), " inputs: ", inputs], "") - } +// THE BOUND VERDICTS ARE WITHHELD WHILE THE SUBJECT IS A STATIC TREE WITH SYNTHETIC TRANSCRIPTS. +// The source pack (gunbc.fabric.workspace_source) carries transcript content as same-size +// placeholders and the tree does not change while it is measured, so a time over it says nothing +// about checkpointing a workspace whose transcripts are being appended: the 10 s target and the 60 s +// window are not decided by this run, holding or exceeded, and each line says so beside its measured +// inputs. The quota is a size, and the placeholders preserve size, so the 10 GB quota is decided -- +// over the static tree, and labelled static. workspace_bound_verdict above is the fold these lines +// will print once a churn phase measures appends (the later churn child of this lane); until then +// it is exercised by its witness and by nothing that prints. +fn workspace_withheld_bound_words(name: String, parts: List, bound: Nanosecond) -> String { + join([name, "=withheld: churn not measured static_measured=", workspace_ms_words(n: workspace_parts_measured(parts: parts)), " bound=", workspace_ms_words(n: bound), " inputs: ", join(map(parts, p => workspace_part_words(p: p)), " + ")], "") +} + +// THE DRAFT-NUMBER BLOCK OF THE MEASURE RECEIPT: the caveat, the four withheld bound lines, and the +// static quota line (or why it is undecided). +fn workspace_draft_verdict_lines(snapshot_parts: List, scan_parts: List, static_size: ByteSize?, n: WorkspaceDraftNumbers) -> String { + join([ + workspace_transcript_caveat as String, "\n", + workspace_withheld_bound_words(name: "snapshot_path_vs_target", parts: snapshot_parts, bound: n.checkpoint_target), "\n", + workspace_withheld_bound_words(name: "snapshot_path_vs_window", parts: snapshot_parts, bound: n.window), "\n", + workspace_withheld_bound_words(name: "scan_path_vs_target", parts: scan_parts, bound: n.checkpoint_target), "\n", + workspace_withheld_bound_words(name: "scan_path_vs_window", parts: scan_parts, bound: n.window), "\n", + match static_size { + Present { value: b } => workspace_quota_words(v: workspace_quota_verdict(size: b, quota: n.quota)) + Absent => "quota_static=undecided (the upload refused, so no byte total was established)" + }, "\n", + ], "") } fn workspace_quota_words(v: WorkspaceQuotaVerdict) -> String { match v { - QuotaHolds { size: n, quota: q } => join(["quota=holds bytes=", to_string(measure_count(m: n)), " quota_bytes=", to_string(measure_count(m: q))], "") - QuotaExceeded { size: n, quota: q } => join(["quota=exceeded bytes=", to_string(measure_count(m: n)), " quota_bytes=", to_string(measure_count(m: q))], "") + QuotaHolds { size: n, quota: q } => join(["quota_static=holds bytes=", to_string(measure_count(m: n)), " quota_bytes=", to_string(measure_count(m: q))], "") + QuotaExceeded { size: n, quota: q } => join(["quota_static=exceeded bytes=", to_string(measure_count(m: n)), " quota_bytes=", to_string(measure_count(m: q))], "") } } @@ -1008,9 +1028,9 @@ fn workspace_measure_report(workspace: NonEmptyStr, snapshot: NonEmptyStr, scrat join(["files=", to_string(length(es)), " bytes=", to_string(measure_count(m: b)), " chunks_uploaded=", to_string(st), " chunks_already_present=", to_string(oc), " symlinks_not_carried=", to_string(sl), " ", workspace_part_words(p: upload_timed)], "") WorkspaceUploadRefused { detail: d, custody: _ } => join(["upload=refused ", d], "") } - let quota_line = match upload { - WorkspaceUploaded { entries: _, stored: _, occupied: _, bytes: b, symlinks: _, custody: _ } => workspace_quota_words(v: workspace_quota_verdict(size: b, quota: n.quota)) - WorkspaceUploadRefused { detail: _, custody: _ } => "quota=undecided (the upload refused, so no byte total was established)" + let static_size = match upload { + WorkspaceUploaded { entries: _, stored: _, occupied: _, bytes: b, symlinks: _, custody: _ } => Present { value: b } + WorkspaceUploadRefused { detail: _, custody: _ } => none } let cleanup = join([ "cleanup snapshot=", if process_ok(run: workspace_run_local(command: btrfs_subvolume_delete_command(path: snapshot as String))) { "deleted" } else { "retained" }, @@ -1023,11 +1043,7 @@ fn workspace_measure_report(workspace: NonEmptyStr, snapshot: NonEmptyStr, scrat kopia_scan_words(scan: scan), "\n", upload_words, "\n", "commit=", match timed_commit.commit { Present { value: c } => workspace_commit_words(c: c) Absent => "not-attempted" }, " ", workspace_part_words(p: timed_commit.part), "\n", - workspace_bound_words(name: "snapshot_path_vs_target", parts: snapshot_parts, v: workspace_bound_verdict(parts: snapshot_parts, bound: n.checkpoint_target)), "\n", - workspace_bound_words(name: "snapshot_path_vs_window", parts: snapshot_parts, v: workspace_bound_verdict(parts: snapshot_parts, bound: n.window)), "\n", - workspace_bound_words(name: "scan_path_vs_target", parts: scan_parts, v: workspace_bound_verdict(parts: scan_parts, bound: n.checkpoint_target)), "\n", - workspace_bound_words(name: "scan_path_vs_window", parts: scan_parts, v: workspace_bound_verdict(parts: scan_parts, bound: n.window)), "\n", - quota_line, "\n", + workspace_draft_verdict_lines(snapshot_parts: snapshot_parts, scan_parts: scan_parts, static_size: static_size, n: n), cleanup, "\n", workspace_custody_words(c: run_custody), "\n", ], "") diff --git a/dag/gunbc/fabric/workspace_source.dag b/dag/gunbc/fabric/workspace_source.dag index ee02a550ef0..ab8f3a909fb 100644 --- a/dag/gunbc/fabric/workspace_source.dag +++ b/dag/gunbc/fabric/workspace_source.dag @@ -1,12 +1,13 @@ module gunbc.fabric.workspace_source -import std.types { String, NonEmptyStr, Int, Bool, List, FilePath } +import std.types { String, NonEmptyStr, Int, Bool, List, Map, FilePath } +import v2.std.collection { empty_map, map_insert, map_lookup, map_contains_key } import std.optional { Present, Absent } import std.algebra { Cons, Empty } import std.process { ProcessExit, ExitSuccess, exit_failure } import std.measure { ByteSize, byte_size, measure_count, nanosecond, nanosecond_count, ObservationInstant, ProcessMonotonic, WallClock } import std.checkpointed_workspace { WorkspaceAgeOf, WorkspaceAgeUndefined, workspace_age_between } -import std.content_hash { Sha256Digest } +import std.content_hash { Sha256Digest, sha256_hex_digest } import std.fabric_blob { FabricBlobAddress, FabricBlobReading, fabric_blob_object_name, fabric_blob_reading_digest, fabric_blob_reading_size, FabricBlobRead, FabricBlobReadRefused, read_staged_fabric_blob, fabric_blob_fault_text, @@ -16,7 +17,9 @@ import extdeps.filesystem.filesystem_io { Filesystem } import extdeps.shell import extdeps.shell.exec { ProcessRunOutcome, ProcessRan, ProcessNotSpawned } import extdeps.exec.command { CommandTransport, LocalExec } -import extdeps.tools.tar { tar_create_from_list_command, tar_list_command } +import extdeps.tools.tar { tar_create_from_two_lists_command, tar_list_command, tar_list_sizes_command, tar_extract_listed_command } +import extdeps.tools.sha256sum +import extdeps.crypto.seeded_filler { seeded_filler_write, SeededFillerWritten, SeededFillerRefused, seeded_filler_step_words } import gunbc.command_runner { run_shell_command_observe_outcome } import gunbc.auth.access_token_source { AccessTokenSource, AccessTokenSourceSelected, AccessTokenSourceUnavailable, select_access_token_source } import gunbc.cloudflare.r2_origin { FabricWorkspace } @@ -33,8 +36,10 @@ import gunbc.host_effect { LocalShell } // directory. No session holds a key that reaches both, and none is minted for this: the route is two // dispatched jobs through the workspace bucket. This module is the first job. On srv1 it enumerates // the source roots as the runner's own principal, drops every path the credential exclusion names -// and every build output, packs exactly what remains into one tar from that list, reads the tar's -// membership back and refuses if any member is excluded, and puts the tar into the workspace bucket +// and every build output, stands a same-size, same-path synthetic placeholder in for every +// transcript file, packs the real members and the placeholders into one tar, reads the tar's +// membership and sizes back and refuses if any credential is carried or any transcript member is not +// its placeholder at its planned size, and puts the tar into the workspace bucket // under the SHA-256 of its bytes. The srv3 job is dispatched with that address and fetches it. // // A ROOT THE RUNNER CANNOT READ REFUSES, NAMING WHAT IS MISSING. The enumeration runs as the job @@ -53,16 +58,76 @@ type WorkspaceSourceRoot { } // THE SRV1 HOST PATHS, pinned rather than guessed (warm-badger-442, 2026-10-05, read from a session's -// mountinfo on node srv1). Agent state is per auth slot on the host -- /home/briansrls/.claude@, -// mounted at /session-home/.claude inside session containers -- so the slot is part of the declared -// input: @gmail3 is the slot whose state the 2026-10-02 figures measured. The Codex root is its home -// directory; its auth file is excluded as a credential and its sessions and history as transcripts. -data workspace_source_roots: List = [ +// mountinfo on node srv1). The clone and the Codex home are fixed; the Codex auth file is excluded as +// a credential and its sessions and history as transcripts. +data workspace_source_fixed_roots: List = [ WorkspaceSourceRoot { label: "gunbc-clone", path: "/home/briansrls/gunbc", build_output: ["target/"] }, - WorkspaceSourceRoot { label: "claude-agent-state-gmail3", path: "/home/briansrls/.claude@gmail3", build_output: [] }, WorkspaceSourceRoot { label: "codex-state", path: "/home/briansrls/.codex", build_output: [] }, ] +// ── THE AGENT-STATE ROOT: A DECLARED INPUT, ADMITTED AGAINST WHAT EXISTS (warm-badger-442) ─────── +// +// Claude agent state lives per auth slot, /home/briansrls/.claude@ (mounted at +// /session-home/.claude inside session containers), and the operator's credential swaps move WHICH +// slot is active -- so the slot is not a constant of this module. It is the dispatch input +// agent_state_slot (environment WORKSPACE_AGENT_STATE_SLOT), and it is admitted only when it names +// one of the .claude@* directories observed in the home at pack time: a closed choice over what +// exists. There is no default and no fallback to "the active one": an absent or unknown slot refuses, +// naming the observed slots, and the admitted slot and its path are printed in the receipt. +data workspace_agent_state_slot_env: NonEmptyStr = "WORKSPACE_AGENT_STATE_SLOT" + +data workspace_agent_state_home: NonEmptyStr = "/home/briansrls" + +data workspace_agent_state_prefix: NonEmptyStr = ".claude@" + +type WorkspaceAgentStateSlot + = AgentStateSlotAdmitted { slot: NonEmptyStr, root: WorkspaceSourceRoot } + | AgentStateSlotAbsent { observed: List } + | AgentStateSlotUnknown { requested: String, observed: List } + +// THE SLOTS A HOME LISTING HOLDS: each directory whose last segment is .claude@, nonempty. +fn workspace_observed_agent_state_slots(dirs: List) -> List { + flat_map(dirs, d => { + let last = match reverse(split(d, separator: "/")) { Cons { head: h, tail: _ } => h Empty => "" } + let prefix = workspace_agent_state_prefix as String + if starts_with(s: last, prefix: prefix) && length(last) > length(prefix) { [substring(s: last, start: length(prefix), end: length(last))] } else { [] } + }) +} + +fn workspace_agent_state_slot_admission(requested: String?, observed: List) -> WorkspaceAgentStateSlot { + match requested { + Absent => AgentStateSlotAbsent { observed: observed } + Present { value: r } => + if r == "" { + AgentStateSlotAbsent { observed: observed } + } else if any(observed, o => o == r) { + AgentStateSlotAdmitted { + slot: r as NonEmptyStr, + root: WorkspaceSourceRoot { label: concat("claude-agent-state-", r) as NonEmptyStr, path: join([workspace_agent_state_home as String, "/", workspace_agent_state_prefix as String, r], "") as NonEmptyStr, build_output: [] }, + } + } else { + AgentStateSlotUnknown { requested: r, observed: observed } + } + } +} + +fn workspace_agent_state_slot_words(admission: WorkspaceAgentStateSlot) -> String { + match admission { + AgentStateSlotAdmitted { slot: s, root: r } => join(["agent_state_slot=", s as String, " path=", r.path as String], "") + AgentStateSlotAbsent { observed: o } => join(["agent_state_slot=refused-absent observed=", join(o, ","), " (", workspace_agent_state_slot_env as String, " names no slot; there is no default)"], "") + AgentStateSlotUnknown { requested: r, observed: o } => join(["agent_state_slot=refused-unknown requested=", r, " observed=", join(o, ","), " (no ", workspace_agent_state_home as String, "/", workspace_agent_state_prefix as String, r, " exists)"], "") + } +} + +fn workspace_agent_state_slot_observe() -> WorkspaceAgentStateSlot { + let requested = match shell.Env.Get(name: workspace_agent_state_slot_env).value { + Absent => none + Present { value: v } => Present { value: replace(v, "\n", "") } + } + let dirs = shell.Find.ListDirs(path: workspace_agent_state_home as FilePath) + workspace_agent_state_slot_admission(requested: requested, observed: workspace_observed_agent_state_slots(dirs: map(dirs.dirs, d => d as String))) +} + data workspace_source_pack_host: NonEmptyStr = "srv1" // ── THE CREDENTIAL EXCLUSION: ONE ROW, LISTED VERBATIM IN EVERY RECEIPT ──────────────────────── @@ -229,7 +294,7 @@ type WorkspaceSourceMemberKind = SourceFile | SourceSymlink type WorkspaceSourceLine = WorkspaceSourceMember { member: NonEmptyStr, kind: WorkspaceSourceMemberKind } - | WorkspaceSourceExcluded { member: NonEmptyStr, by: WorkspaceExclusion, bytes: ByteSize } + | WorkspaceSourceExcluded { member: NonEmptyStr, kind: WorkspaceSourceMemberKind, by: WorkspaceExclusion, bytes: ByteSize } | WorkspaceSourceBuildOutput { member: NonEmptyStr } | WorkspaceSourceUnparsed { line: String } @@ -256,19 +321,23 @@ fn workspace_source_line(root: WorkspaceSourceRoot, exclusions: List WorkspaceSourceMemberKind? { + if kind == "f" { Present { value: SourceFile } } else if kind == "l" { Present { value: SourceSymlink } } else { none } +} + fn workspace_source_line_sized(root: WorkspaceSourceRoot, exclusions: List, kind: String, relative: String, size: ByteSize, line: String) -> WorkspaceSourceLine { let member = workspace_root_member(root: root, relative: relative) as NonEmptyStr - match workspace_path_exclusion(exclusions: exclusions, path: member as String) { - Present { value: x } => WorkspaceSourceExcluded { member: member, by: x, bytes: size } - Absent => - if workspace_path_is_build_output(root: root, relative: relative) { - WorkspaceSourceBuildOutput { member: member } - } else if kind == "f" { - WorkspaceSourceMember { member: member, kind: SourceFile } - } else if kind == "l" { - WorkspaceSourceMember { member: member, kind: SourceSymlink } - } else { - WorkspaceSourceUnparsed { line: line } + match workspace_source_member_kind(kind: kind) { + Absent => WorkspaceSourceUnparsed { line: line } + Present { value: k } => + match workspace_path_exclusion(exclusions: exclusions, path: member as String) { + Present { value: x } => WorkspaceSourceExcluded { member: member, kind: k, by: x, bytes: size } + Absent => + if workspace_path_is_build_output(root: root, relative: relative) { + WorkspaceSourceBuildOutput { member: member } + } else { + WorkspaceSourceMember { member: member, kind: k } + } } } } @@ -283,6 +352,7 @@ type WorkspaceSourceTally { excluded_credential_bytes: ByteSize excluded_transcripts: Int excluded_transcript_bytes: ByteSize + transcript_placeholders: List build_output: Int unparsed: List } @@ -290,7 +360,7 @@ type WorkspaceSourceTally { // THE EXCLUDED SIZES OF ONE CLASS, partitioned by matching the class itself. fn workspace_excluded_sizes(lines: List, class: WorkspaceExclusionClass) -> List { flat_map(lines, l => match l { - WorkspaceSourceExcluded { member: _, by: x, bytes: b } => + WorkspaceSourceExcluded { member: _, kind: _, by: x, bytes: b } => match class { CredentialExclusion => match x.class { CredentialExclusion => [b] TranscriptExclusion => [] } TranscriptExclusion => match x.class { CredentialExclusion => [] TranscriptExclusion => [b] } @@ -301,6 +371,38 @@ fn workspace_excluded_sizes(lines: List, class: WorkspaceEx }) } +// ── TRANSCRIPT PLACEHOLDERS: THE SIZE AND PATH CARRIED, THE CONTENT NOT (operator, 2026-10-05) ─── +// +// Transcripts are the workspace's largest and highest-churn population, so dropping them would make +// stage 0 describe a cheaper workspace than the operator's. Each excluded transcript FILE is carried +// as a placeholder at the SAME member path with the SAME byte size, its bytes the seeded keystream +// of extdeps.crypto.seeded_filler keyed by the member path: incompressible, so neither kopia nor the +// chunk store's dedup sees less than the real content would cost, and reproducible, so two packs of +// one unchanged tree carry identical placeholders. A placeholder is minted ONLY from a line the +// TranscriptExclusion class excluded, and the match below has no arm that mints one from a +// credential, so a credential's path and size never appear in the archive. A transcript SYMLINK +// carries no content to stand in for and is excluded as before. What a placeholder cannot carry is +// stated, not implied: the content's compressibility and its churn; the receipt says so. +type WorkspaceTranscriptPlaceholder { + member: NonEmptyStr + size: ByteSize +} + +fn workspace_transcript_placeholders(lines: List) -> List { + flat_map(lines, l => match l { + WorkspaceSourceExcluded { member: m, kind: k, by: x, bytes: b } => + match x.class { + CredentialExclusion => [] + TranscriptExclusion => match k { SourceFile => [WorkspaceTranscriptPlaceholder { member: m, size: b }] SourceSymlink => [] } + } + WorkspaceSourceMember { member: _, kind: _ } => [] + WorkspaceSourceBuildOutput { member: _ } => [] + WorkspaceSourceUnparsed { line: _ } => [] + }) +} + +data workspace_transcript_caveat: NonEmptyStr = "transcript content synthetic, sizes preserved, static tree; NOT a 10 s / 60 s verdict" + fn workspace_bytes_total(sizes: List) -> ByteSize { byte_size(count: fold(sizes, init: 0, f: (acc, b) => acc + measure_count(m: b))) } @@ -310,16 +412,16 @@ fn workspace_source_tally(root: WorkspaceSourceRoot, exclusions: List match l { WorkspaceSourceMember { member: m, kind: _ } => [m] - WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => [] + WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => [] WorkspaceSourceBuildOutput { member: _ } => [] WorkspaceSourceUnparsed { line: _ } => [] }), - files: length(filter(lines, l => match l { WorkspaceSourceMember { member: _, kind: SourceFile } => true WorkspaceSourceMember { member: _, kind: SourceSymlink } => false WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), - symlinks: length(filter(lines, l => match l { WorkspaceSourceMember { member: _, kind: SourceSymlink } => true WorkspaceSourceMember { member: _, kind: SourceFile } => false WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), - excluded: length(filter(lines, l => match l { WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => true WorkspaceSourceMember { member: _, kind: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), + files: length(filter(lines, l => match l { WorkspaceSourceMember { member: _, kind: SourceFile } => true WorkspaceSourceMember { member: _, kind: SourceSymlink } => false WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), + symlinks: length(filter(lines, l => match l { WorkspaceSourceMember { member: _, kind: SourceSymlink } => true WorkspaceSourceMember { member: _, kind: SourceFile } => false WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), + excluded: length(filter(lines, l => match l { WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => true WorkspaceSourceMember { member: _, kind: _ } => false WorkspaceSourceBuildOutput { member: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), excluded_credentials: length(workspace_excluded_sizes(lines: lines, class: CredentialExclusion)), excluded_credential_members: flat_map(lines, l => match l { - WorkspaceSourceExcluded { member: m, by: x, bytes: _ } => match x.class { CredentialExclusion => [m] TranscriptExclusion => [] } + WorkspaceSourceExcluded { member: m, kind: _, by: x, bytes: _ } => match x.class { CredentialExclusion => [m] TranscriptExclusion => [] } WorkspaceSourceMember { member: _, kind: _ } => [] WorkspaceSourceBuildOutput { member: _ } => [] WorkspaceSourceUnparsed { line: _ } => [] @@ -327,37 +429,154 @@ fn workspace_source_tally(root: WorkspaceSourceRoot, exclusions: List match l { WorkspaceSourceBuildOutput { member: _ } => true WorkspaceSourceMember { member: _, kind: _ } => false WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), + transcript_placeholders: workspace_transcript_placeholders(lines: lines), + build_output: length(filter(lines, l => match l { WorkspaceSourceBuildOutput { member: _ } => true WorkspaceSourceMember { member: _, kind: _ } => false WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => false WorkspaceSourceUnparsed { line: _ } => false })), unparsed: flat_map(lines, l => match l { WorkspaceSourceUnparsed { line: t } => [t] WorkspaceSourceMember { member: _, kind: _ } => [] - WorkspaceSourceExcluded { member: _, by: _, bytes: _ } => [] + WorkspaceSourceExcluded { member: _, kind: _, by: _, bytes: _ } => [] WorkspaceSourceBuildOutput { member: _ } => [] }), } } -// ── THE READBACK: AN ARCHIVE MEMBER THE EXCLUSION NAMES REFUSES THE UPLOAD ───────────────────── +// ── THE READBACK: WHAT THE ARCHIVE ACTUALLY HOLDS, BEFORE ANY BYTE LEAVES THE HOST ───────────── // -// The pack is built from the filtered list, so an excluded path should never be in it; this reads -// what the archive ACTUALLY holds (tar -tf) and runs every member through the same matcher. It is the -// check that the realization did what the list said -- a tar that recursed, or a list that was not -// the one written -- and its red is the run's refusal, before any byte leaves the host. +// The pack is built from the filtered lists, so a credential should never be in it and a transcript +// should be there only as its placeholder. That is provenance by construction, and it is NOT enough +// at a confidentiality boundary: a name and a size are exactly what a real transcript shares with +// its placeholder. So this reads what the archive ACTUALLY holds and refuses otherwise. Names come +// from `tar -tf` (archive_listing) and run through the same matcher: a credential-class name refuses +// outright, and a transcript-class name refuses unless it is a staged placeholder. Every staged +// placeholder must then be, IN THE ARCHIVE, a regular file (the verbose listing's own type letter, +// size_listing) of its planned size whose bytes -- extracted back out of the archive and hashed, +// archived_digests -- are the SHA-256 the filler produced when it was staged. A real transcript of +// the same size differs in digest and refuses. Each failure is its own typed refusal. +type WorkspaceStagedPlaceholder { + member: NonEmptyStr + size: ByteSize + expected_digest: Sha256Digest +} + type WorkspacePackReadback = WorkspacePackClean { members: Int } | WorkspacePackCarriesExcluded { member: NonEmptyStr, by: WorkspaceExclusion } + | WorkspacePackPlaceholderAbsent { member: NonEmptyStr } + | WorkspacePackPlaceholderNotRegular { member: NonEmptyStr, type_letter: String } + | WorkspacePackPlaceholderSizeDiffers { member: NonEmptyStr, planned: ByteSize, archived: ByteSize } + | WorkspacePackPlaceholderUnread { member: NonEmptyStr } + | WorkspacePackPlaceholderDigestDiffers { member: NonEmptyStr, expected: Sha256Digest, archived: String } -fn workspace_pack_readback(exclusions: List, archive_listing: String) -> WorkspacePackReadback { - workspace_pack_readback_lines(exclusions: exclusions, lines: filter(split(archive_listing, separator: "\n"), l => l != ""), seen: 0) +fn workspace_pack_readback(exclusions: List, placeholders: List, archive_listing: String, size_listing: String, archived_digests: Map) -> WorkspacePackReadback { + let planned = fold(placeholders, init: empty_map(), f: (m, p) => map_insert(m: m, key: p.member as String, value: p.size)) + match workspace_pack_readback_lines(exclusions: exclusions, planned: planned, lines: filter(split(archive_listing, separator: "\n"), l => l != ""), seen: 0) { + WorkspacePackClean { members: n } => { + let archived = fold(flat_map(filter(split(size_listing, separator: "\n"), l => l != ""), l => workspace_archived_entry(line: l)), init: empty_map(), f: (m, a) => map_insert(m: m, key: a.member, value: a)) + workspace_pack_placeholder_checks(placeholders: placeholders, archived: archived, digests: archived_digests, members: n) + } + WorkspacePackCarriesExcluded { member: m, by: x } => WorkspacePackCarriesExcluded { member: m, by: x } + WorkspacePackPlaceholderAbsent { member: m } => WorkspacePackPlaceholderAbsent { member: m } + WorkspacePackPlaceholderNotRegular { member: m, type_letter: t } => WorkspacePackPlaceholderNotRegular { member: m, type_letter: t } + WorkspacePackPlaceholderSizeDiffers { member: m, planned: p, archived: a } => WorkspacePackPlaceholderSizeDiffers { member: m, planned: p, archived: a } + WorkspacePackPlaceholderUnread { member: m } => WorkspacePackPlaceholderUnread { member: m } + WorkspacePackPlaceholderDigestDiffers { member: m, expected: e, archived: a } => WorkspacePackPlaceholderDigestDiffers { member: m, expected: e, archived: a } + } } -fn workspace_pack_readback_lines(exclusions: List, lines: List, seen: Int) -> WorkspacePackReadback { +fn workspace_pack_readback_lines(exclusions: List, planned: Map, lines: List, seen: Int) -> WorkspacePackReadback { match lines { Empty => WorkspacePackClean { members: seen } Cons { head: l, tail: rest } => match workspace_path_exclusion(exclusions: exclusions, path: l) { - Present { value: x } => WorkspacePackCarriesExcluded { member: l as NonEmptyStr, by: x } - Absent => workspace_pack_readback_lines(exclusions: exclusions, lines: rest, seen: seen + 1) + Absent => workspace_pack_readback_lines(exclusions: exclusions, planned: planned, lines: rest, seen: seen + 1) + Present { value: x } => + match x.class { + CredentialExclusion => WorkspacePackCarriesExcluded { member: l as NonEmptyStr, by: x } + TranscriptExclusion => + if map_contains_key(m: planned, key: l) { + workspace_pack_readback_lines(exclusions: exclusions, planned: planned, lines: rest, seen: seen + 1) + } else { + WorkspacePackCarriesExcluded { member: l as NonEmptyStr, by: x } + } + } + } + } +} + +// ONE VERBOSE-LISTING LINE: its type letter (the first character of the mode word: '-' a regular +// file, 'l' a symlink, 'h' a hard link, ...), its size, and its name. Five leading words (mode, +// uid/gid, size, date, time), each possibly padded by several spaces; the name is the rest of the +// line verbatim, so a name with spaces survives. A symlink's or hard link's name carries its target +// after it (" -> t", " link to t"), so such a line never names a placeholder exactly and a planned +// member archived as one reads as absent -- refused either way. +type WorkspaceArchivedEntry { + member: String + type_letter: String + size: ByteSize +} + +fn workspace_archived_entry(line: String) -> List { + match workspace_verbose_fields(words: split(line, separator: " "), fields: [], wanted: 5) { + Absent => [] + Present { value: parsed } => + match parsed.fields { + Cons { head: mode, tail: Cons { head: _, tail: Cons { head: size_text, tail: _ } } } => + match parse_int(s: size_text) { + Absent => [] + Present { value: n } => if n < 0 || parsed.rest == "" { [] } else { [WorkspaceArchivedEntry { member: parsed.rest, type_letter: substring(s: mode, start: 0, end: 1), size: byte_size(count: n) }] } + } + Cons { head: _, tail: Cons { head: _, tail: Empty } } => [] + Cons { head: _, tail: Empty } => [] + Empty => [] + } + } +} + +type WorkspaceVerboseFields { + fields: List + rest: String +} + +fn workspace_verbose_fields(words: List, fields: List, wanted: Int) -> WorkspaceVerboseFields? { + if wanted == 0 { + Present { value: WorkspaceVerboseFields { fields: fields, rest: join(words, " ") } } + } else { + match words { + Empty => none + Cons { head: w, tail: rest } => + if w == "" { + workspace_verbose_fields(words: rest, fields: fields, wanted: wanted) + } else { + workspace_verbose_fields(words: rest, fields: concat(fields, [w]), wanted: wanted - 1) + } + } + } +} + +// Every side is keyed by member name, so the check is one lookup per placeholder rather than a scan +// of the archive per placeholder. +fn workspace_pack_placeholder_checks(placeholders: List, archived: Map, digests: Map, members: Int) -> WorkspacePackReadback { + match placeholders { + Empty => WorkspacePackClean { members: members } + Cons { head: p, tail: rest } => + match map_lookup(m: archived, key: p.member as String) { + Absent => WorkspacePackPlaceholderAbsent { member: p.member } + Present { value: a } => + if a.type_letter != "-" { + WorkspacePackPlaceholderNotRegular { member: p.member, type_letter: a.type_letter } + } else if measure_count(m: a.size) != measure_count(m: p.size) { + WorkspacePackPlaceholderSizeDiffers { member: p.member, planned: p.size, archived: a.size } + } else { + match map_lookup(m: digests, key: p.member as String) { + Absent => WorkspacePackPlaceholderUnread { member: p.member } + Present { value: d } => + if d != (p.expected_digest.hex as String) { + WorkspacePackPlaceholderDigestDiffers { member: p.member, expected: p.expected_digest, archived: d } + } else { + workspace_pack_placeholder_checks(placeholders: rest, archived: archived, digests: digests, members: members) + } + } + } } } } @@ -442,7 +661,7 @@ fn workspace_source_tallies(enumerations: List) -> L fn workspace_tally_line(e: WorkspaceSourceEnumeration) -> String { match e { WorkspaceSourceEnumerated { root: r, tally: t } => - join(["root ", r.label as String, " path=", r.path as String, " files=", to_string(t.files), " symlinks=", to_string(t.symlinks), " excluded=", to_string(t.excluded), " excluded_credentials=", to_string(t.excluded_credentials), " excluded_credential_bytes=", to_string(measure_count(m: t.excluded_credential_bytes)), " excluded_credential_paths=", join(map(t.excluded_credential_members, m => m as String), ","), " excluded_transcripts=", to_string(t.excluded_transcripts), " excluded_transcript_bytes=", to_string(measure_count(m: t.excluded_transcript_bytes)), " build_output=", to_string(t.build_output)], "") + join(["root ", r.label as String, " path=", r.path as String, " files=", to_string(t.files), " symlinks=", to_string(t.symlinks), " excluded=", to_string(t.excluded), " excluded_credentials=", to_string(t.excluded_credentials), " excluded_credential_bytes=", to_string(measure_count(m: t.excluded_credential_bytes)), " excluded_credential_paths=", join(map(t.excluded_credential_members, m => m as String), ","), " excluded_transcripts=", to_string(t.excluded_transcripts), " excluded_transcript_bytes=", to_string(measure_count(m: t.excluded_transcript_bytes)), " transcript_placeholders=", to_string(length(t.transcript_placeholders)), " build_output=", to_string(t.build_output)], "") WorkspaceSourceUnreadable { root: r, detail: _ } => join(["root ", r.label as String, " path=", r.path as String, " UNREADABLE"], "") } } @@ -471,7 +690,7 @@ fn workspace_source_receipt_head(enumerations: List) join([ "workspace_source_pack host=", workspace_source_pack_host as String, "\n", "credential_exclusions=", workspace_exclusion_spellings(exclusions: workspace_credential_exclusions), "\n", - "transcript_exclusions=", workspace_exclusion_spellings(exclusions: workspace_transcript_exclusions), "\n", + "transcript_exclusions=", workspace_exclusion_spellings(exclusions: workspace_transcript_exclusions), " (carried as same-size, same-path placeholders)\n", join(map(enumerations, e => workspace_tally_line(e: e)), "\n"), "\n", ], "") } @@ -500,12 +719,20 @@ fn workspace_source_pack_wet() -> ProcessExit { if host != (workspace_source_pack_host as String) { exit_failure(reason: join(["workspace_source_pack: the source lives on ", workspace_source_pack_host as String, " but this job runs on '", host, "'; dispatch it with host=", workspace_source_pack_host as String], "")) } else { - let enumerations = workspace_source_enumerate_all(roots: workspace_source_roots) - let head = workspace_source_receipt_head(enumerations: enumerations) - match workspace_source_refusals(enumerations: enumerations) { - Cons { head: _, tail: _ } => - workspace_source_finish(receipt: concat(head, "verdict=refused-source-unreadable\n"), outcome: exit_failure(reason: join(concat(["workspace_source_pack: a source root is unreadable by the runner; no permission is widened here:"], workspace_source_refusals(enumerations: enumerations)), "\n"))) - Empty => workspace_source_pack_readable(head: head, tallies: workspace_source_tallies(enumerations: enumerations)) + let slot = workspace_agent_state_slot_observe() + let slot_line = concat(workspace_agent_state_slot_words(admission: slot), "\n") + match slot { + AgentStateSlotAbsent { observed: _ } => workspace_source_finish(receipt: join(["workspace_source_pack host=", host, "\n", slot_line, "verdict=refused-agent-state-slot\n"], ""), outcome: exit_failure(reason: concat("workspace_source_pack: ", slot_line))) + AgentStateSlotUnknown { requested: _, observed: _ } => workspace_source_finish(receipt: join(["workspace_source_pack host=", host, "\n", slot_line, "verdict=refused-agent-state-slot\n"], ""), outcome: exit_failure(reason: concat("workspace_source_pack: ", slot_line))) + AgentStateSlotAdmitted { slot: _, root: agent } => { + let enumerations = workspace_source_enumerate_all(roots: concat(workspace_source_fixed_roots, [agent])) + let head = concat(workspace_source_receipt_head(enumerations: enumerations), slot_line) + match workspace_source_refusals(enumerations: enumerations) { + Cons { head: _, tail: _ } => + workspace_source_finish(receipt: concat(head, "verdict=refused-source-unreadable\n"), outcome: exit_failure(reason: join(concat(["workspace_source_pack: a source root is unreadable by the runner; no permission is widened here:"], workspace_source_refusals(enumerations: enumerations)), "\n"))) + Empty => workspace_source_pack_readable(head: head, tallies: workspace_source_tallies(enumerations: enumerations)) + } + } } } } @@ -515,44 +742,271 @@ fn workspace_source_pack_readable(head: String, tallies: List t.members) + let placeholders = flat_map(tallies, t => t.transcript_placeholders) + let placeholder_root = join([staging.path as String, "/placeholders"], "") + let head2 = concat(head, workspace_placeholder_words(placeholders: placeholders)) + match workspace_stage_placeholders(root: placeholder_root, placeholders: placeholders, staged: []) { + WorkspacePlaceholdersRefused { why: why } => { + let removed = shell.Remove.RecursiveForce(path: staging.path) + workspace_source_finish(receipt: join([head2, "verdict=refused-placeholder-unstaged staging_removed=", if removed.success { "yes" } else { "no" }, "\n"], ""), outcome: exit_failure(reason: concat("workspace_source_pack: a transcript placeholder could not be staged: ", why))) + } + WorkspacePlaceholdersStaged { placeholders: staged } => { + let pack_start = workspace_source_instant(label: "workspace-source-pack-start") + let built = workspace_pack_archive(staging: staging.path as String, members: flat_map(tallies, t => t.members), placeholders: staged, placeholder_root: placeholder_root) + let pack_span = workspace_source_span_words(start: pack_start, end_label: "workspace-source-pack-end") + match built { + WorkspacePackRefused { refusal: v, why: why } => workspace_source_refuse_archive(head: head2, staging: staging.path as String, refusal: v, why: why) + WorkspacePackBuilt { archive: archive, readback: r } => + match workspace_pack_readback_refusal(readback: r) { + Present { value: refusal } => workspace_source_refuse_archive(head: head2, staging: staging.path as String, refusal: refusal.refusal, why: refusal.why) + Absent => workspace_source_publish(head: concat(head2, join(["archive_members=", to_string(workspace_pack_clean_members(readback: r)), " placeholders_digest_checked=", to_string(length(staged)), " pack=", pack_span, "\n"], "")), archive: archive, staging: staging.path as String) + } + } + } + } + } +} + +fn workspace_placeholder_words(placeholders: List) -> String { + join([ + "transcript_placeholders files=", to_string(length(placeholders)), + " bytes=", to_string(measure_count(m: workspace_bytes_total(sizes: map(placeholders, p => p.size)))), + " filler=extdeps.crypto.seeded_filler seed=member-path\n", + workspace_transcript_caveat as String, "\n", + ], "") +} + +type WorkspacePlaceholderStaging + = WorkspacePlaceholdersStaged { placeholders: List } + | WorkspacePlaceholdersRefused { why: String } + +// EACH PLACEHOLDER AT /, its parent directories made first, and its SHA-256 taken from +// the staged bytes: that digest is what the archive readback requires the archived member to hash +// to. The first refusal stops the staging; nothing is packed from a partly staged tree. +fn workspace_stage_placeholders(root: String, placeholders: List, staged: List) -> WorkspacePlaceholderStaging { + match placeholders { + Empty => WorkspacePlaceholdersStaged { placeholders: staged } + Cons { head: p, tail: rest } => { + let path = join([root, "/", p.member as String], "") + let parent = join(workspace_all_but_last(xs: split(path, separator: "/")), "/") + let made = shell.Mkdir.Parents(path: parent as FilePath) + if made.success == false { + WorkspacePlaceholdersRefused { why: concat("mkdir -p failed: ", parent) } + } else { + match seeded_filler_write(seed: p.member, size: p.size, path: path as NonEmptyStr) { + SeededFillerRefused { path: _, step: st, detail: d } => WorkspacePlaceholdersRefused { why: join([p.member as String, ": ", seeded_filler_step_words(step: st), ": ", d], "") } + SeededFillerWritten { path: _, size: _ } => + match workspace_file_digest(path: path) { + Absent => WorkspacePlaceholdersRefused { why: concat("the staged placeholder could not be hashed: ", p.member as String) } + Present { value: d } => workspace_stage_placeholders(root: root, placeholders: rest, staged: concat(staged, [WorkspaceStagedPlaceholder { member: p.member, size: p.size, expected_digest: d }])) + } + } + } + } + } +} + +fn workspace_file_digest(path: String) -> Sha256Digest? { + let d = sha256sum.Sha256.DigestFile(path: path as NonEmptyStr) + if d.success == false { none } else { sha256_hex_digest(hex: substring(s: d.line, start: 0, end: 64)) } +} + +fn workspace_all_but_last(xs: List) -> List { + match xs { + Empty => [] + Cons { head: _, tail: Empty } => [] + Cons { head: x, tail: rest } => concat([x], workspace_all_but_last(xs: rest)) + } +} + +// ── THE ARCHIVE: BUILT FROM BOTH SOURCES, READ BACK, AND ITS PLACEHOLDERS EXTRACTED AND HASHED ── +// +// The real members are read relative to "/" and the placeholders relative to their staging root, +// under the same member names, into one tar. The readback then lists the archive twice (names, then +// types and sizes), extracts exactly the placeholder members into a fresh directory, and hashes each +// extracted file. Every step's failure is a typed refusal; the readback's verdict is returned whole +// so the caller words it. +// +// Two orderings carry the reasons. The placeholder root is made FIRST: the second -C must name a +// directory even when no transcript was planned, so a workspace with no transcripts packs through +// the same two-list route with an empty second list, and the staging directory the lists are written +// into exists whatever was staged. And the LOCATED refusals come before extraction: names, types and +// sizes are judged from the listings alone, with each placeholder's digest assumed, so a missing or +// malformed member is refused by name rather than as a failed extract of the whole list. +// WHY THE ARCHIVE STEP REFUSED, as a closed sum: the receipt's verdict word is rendered from it at the +// edge by workspace_pack_refusal_words, so a refusal kind cannot be misspelled or invented at a +// construction site. The readback arms carry the member they name. +type WorkspacePackRefusal + = PackRefusedPlaceholderRootUnmade + | PackRefusedMemberListUnwritten + | PackRefusedTarFailed + | PackRefusedReadbackFailed + | PackRefusedPlaceholderExtractFailed + | PackRefusedMemberCountMismatch + | PackRefusedExcludedMember { member: NonEmptyStr, rule: String } + | PackRefusedPlaceholderAbsent { member: NonEmptyStr } + | PackRefusedPlaceholderNotRegular { member: NonEmptyStr, type_letter: String } + | PackRefusedPlaceholderSize { member: NonEmptyStr, planned: ByteSize, archived: ByteSize } + | PackRefusedPlaceholderUnread { member: NonEmptyStr } + | PackRefusedPlaceholderDigest { member: NonEmptyStr, expected: String, archived: String } + +fn workspace_pack_refusal_words(refusal: WorkspacePackRefusal) -> String { + match refusal { + PackRefusedPlaceholderRootUnmade => "refused-placeholder-root-unmade" + PackRefusedMemberListUnwritten => "refused-member-list-unwritten" + PackRefusedTarFailed => "refused-pack-failed" + PackRefusedReadbackFailed => "refused-readback-failed" + PackRefusedPlaceholderExtractFailed => "refused-placeholder-extract-failed" + PackRefusedMemberCountMismatch => "refused-member-count-mismatch" + PackRefusedExcludedMember { member: m, rule: x } => join(["refused-excluded-member member=", m as String, " rule=", x], "") + PackRefusedPlaceholderAbsent { member: m } => concat("refused-placeholder-absent member=", m as String) + PackRefusedPlaceholderNotRegular { member: m, type_letter: t } => join(["refused-placeholder-not-regular member=", m as String, " type=", t], "") + PackRefusedPlaceholderSize { member: m, planned: p, archived: a } => join(["refused-placeholder-size member=", m as String, " planned=", to_string(measure_count(m: p)), " archived=", to_string(measure_count(m: a))], "") + PackRefusedPlaceholderUnread { member: m } => concat("refused-placeholder-unread member=", m as String) + PackRefusedPlaceholderDigest { member: m, expected: e, archived: a } => join(["refused-placeholder-digest member=", m as String, " expected=", e, " archived=", a], "") + } +} + +type WorkspacePackArchive + = WorkspacePackBuilt { archive: String, readback: WorkspacePackReadback } + | WorkspacePackRefused { refusal: WorkspacePackRefusal, why: String } + +fn workspace_pack_archive(staging: String, members: List, placeholders: List, placeholder_root: String) -> WorkspacePackArchive { + let list_path = join([staging, "/members"], "") + let placeholder_list_path = join([staging, "/placeholder-members"], "") + let archive = join([staging, "/workspace-source.tar"], "") + let root_made = shell.Mkdir.Parents(path: placeholder_root as FilePath) + if root_made.success == false { + WorkspacePackRefused { refusal: PackRefusedPlaceholderRootUnmade, why: concat("the placeholder staging root could not be made: ", placeholder_root) } + } else { let listed = Filesystem.Write(path: list_path, content: concat(join(map(members, m => m as String), "\n"), "\n")) - if listed.success == false { - workspace_source_finish(receipt: concat(head, "verdict=refused-member-list-unwritten\n"), outcome: exit_failure(reason: concat("workspace_source_pack: the member list could not be written: ", listed.error))) + let placeholder_listed = Filesystem.Write(path: placeholder_list_path, content: join(map(placeholders, p => concat(p.member as String, "\n")), "")) + if listed.success == false || placeholder_listed.success == false { + WorkspacePackRefused { refusal: PackRefusedMemberListUnwritten, why: join(["a member list could not be written: ", listed.error, placeholder_listed.error], "") } } else { - let pack_start = workspace_source_instant(label: "workspace-source-pack-start") - let packed = workspace_run_ok(run: run_shell_command_observe_outcome(command: tar_create_from_list_command(archive: archive, from_directory: "/", member_list: list_path), transport: LocalExec)) - let pack_span = workspace_source_span_words(start: pack_start, end_label: "workspace-source-pack-end") - match packed { - Present { value: why } => workspace_source_finish(receipt: concat(head, "verdict=refused-pack-failed\n"), outcome: exit_failure(reason: concat("workspace_source_pack: tar refused: ", why))) + match workspace_run_ok(run: run_shell_command_observe_outcome(command: tar_create_from_two_lists_command(archive: archive, first_directory: "/", first_list: list_path, second_directory: placeholder_root, second_list: placeholder_list_path), transport: LocalExec)) { + Present { value: why } => WorkspacePackRefused { refusal: PackRefusedTarFailed, why: concat("tar refused: ", why) } Absent => - match run_shell_command_observe_outcome(command: tar_list_command(archive: archive), transport: LocalExec) { - ProcessNotSpawned { program: _, cause: c } => workspace_source_finish(receipt: concat(head, "verdict=refused-readback-unrun\n"), outcome: exit_failure(reason: concat("workspace_source_pack: the archive could not be listed back: ", c as String))) - ProcessRan { observation: o } => - if o.exit_code != 0 { - workspace_source_finish(receipt: concat(head, "verdict=refused-readback-failed\n"), outcome: exit_failure(reason: concat("workspace_source_pack: tar -tf refused: ", o.stderr))) - } else { - match workspace_pack_readback(exclusions: workspace_exclusions(), archive_listing: o.stdout) { - WorkspacePackCarriesExcluded { member: m, by: x } => { - let removed = shell.Remove.RecursiveForce(path: staging.path) - workspace_source_finish(receipt: join([head, "verdict=refused-excluded-member member=", m as String, " rule=", x.spelling as String, " archive_removed=", if removed.success { "yes" } else { "no" }, "\n"], ""), outcome: exit_failure(reason: join(["workspace_source_pack: the archive carries ", m as String, ", which the credential exclusion ", x.spelling as String, " names; nothing was uploaded"], ""))) + match workspace_archive_listings(archive: archive) { + Absent => WorkspacePackRefused { refusal: PackRefusedReadbackFailed, why: "the archive could not be listed back" } + Present { value: listings } => { + let structural = workspace_pack_readback(exclusions: workspace_exclusions(), placeholders: placeholders, archive_listing: listings.names, size_listing: listings.sizes, archived_digests: fold(placeholders, init: empty_map(), f: (m, p) => map_insert(m: m, key: p.member as String, value: p.expected_digest.hex as String))) + match workspace_pack_readback_refusal(readback: structural) { + Present { value: _ } => WorkspacePackBuilt { archive: archive, readback: structural } + Absent => + match workspace_archived_placeholder_digests(staging: staging, archive: archive, placeholder_list: placeholder_list_path, placeholders: placeholders) { + Absent => WorkspacePackRefused { refusal: PackRefusedPlaceholderExtractFailed, why: "the placeholder members could not be extracted back out of the archive" } + Present { value: digests } => { + let r = workspace_pack_readback(exclusions: workspace_exclusions(), placeholders: placeholders, archive_listing: listings.names, size_listing: listings.sizes, archived_digests: digests) + let expected = length(members) + length(placeholders) + match r { + WorkspacePackClean { members: n } => + if n != expected { + WorkspacePackRefused { refusal: PackRefusedMemberCountMismatch, why: join(["the archive holds ", to_string(n), " members but the lists named ", to_string(expected)], "") } + } else { + WorkspacePackBuilt { archive: archive, readback: r } + } + WorkspacePackCarriesExcluded { member: _, by: _ } => WorkspacePackBuilt { archive: archive, readback: r } + WorkspacePackPlaceholderAbsent { member: _ } => WorkspacePackBuilt { archive: archive, readback: r } + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => WorkspacePackBuilt { archive: archive, readback: r } + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => WorkspacePackBuilt { archive: archive, readback: r } + WorkspacePackPlaceholderUnread { member: _ } => WorkspacePackBuilt { archive: archive, readback: r } + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => WorkspacePackBuilt { archive: archive, readback: r } } - WorkspacePackClean { members: n } => - if n != length(members) { - workspace_source_finish(receipt: concat(head, "verdict=refused-member-count-mismatch\n"), outcome: exit_failure(reason: join(["workspace_source_pack: the archive holds ", to_string(n), " members but the list named ", to_string(length(members))], ""))) - } else { - workspace_source_publish(head: concat(head, join(["archive_members=", to_string(n), " pack=", pack_span, "\n"], "")), archive: archive, staging: staging.path as String) - } } } + } + } } } } } } +// THE ARCHIVED PLACEHOLDER BYTES, HASHED: extract exactly the placeholder members into +// /readback and sha256sum each. A member that did not extract or could not be hashed is +// simply absent from the map, which the readback refuses as unread. An empty plan extracts nothing. +fn workspace_archived_placeholder_digests(staging: String, archive: String, placeholder_list: String, placeholders: List) -> Map? { + match placeholders { + Empty => Present { value: empty_map() } + Cons { head: _, tail: _ } => { + let into = join([staging, "/readback"], "") + let made = shell.Mkdir.Parents(path: into as FilePath) + if made.success == false { + none + } else { + match workspace_run_ok(run: run_shell_command_observe_outcome(command: tar_extract_listed_command(archive: archive, into_directory: into, member_list: placeholder_list), transport: LocalExec)) { + Present { value: _ } => none + Absent => Present { value: fold(placeholders, init: empty_map(), f: (m, p) => match workspace_file_digest(path: join([into, "/", p.member as String], "")) { + Present { value: d } => map_insert(m: m, key: p.member as String, value: d.hex as String) + Absent => m + }) } + } + } + } + } +} + +type WorkspaceReadbackRefusal { + refusal: WorkspacePackRefusal + why: String +} + +fn workspace_pack_clean_members(readback: WorkspacePackReadback) -> Int { + match readback { + WorkspacePackClean { members: n } => n + WorkspacePackCarriesExcluded { member: _, by: _ } => 0 + WorkspacePackPlaceholderAbsent { member: _ } => 0 + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => 0 + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => 0 + WorkspacePackPlaceholderUnread { member: _ } => 0 + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => 0 + } +} + +fn workspace_pack_readback_refusal(readback: WorkspacePackReadback) -> WorkspaceReadbackRefusal? { + match readback { + WorkspacePackClean { members: _ } => none + WorkspacePackCarriesExcluded { member: m, by: x } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedExcludedMember { member: m, rule: x.spelling as String }, why: join(["the archive carries ", m as String, ", which the workspace exclusion ", x.spelling as String, " names and no placeholder stands for"], "") } } + WorkspacePackPlaceholderAbsent { member: m } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedPlaceholderAbsent { member: m }, why: concat("the archive holds no member for the transcript placeholder ", m as String) } } + WorkspacePackPlaceholderNotRegular { member: m, type_letter: t } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedPlaceholderNotRegular { member: m, type_letter: t }, why: join(["the transcript placeholder ", m as String, " is archived as type '", t, "', not a regular file"], "") } } + WorkspacePackPlaceholderSizeDiffers { member: m, planned: p, archived: a } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedPlaceholderSize { member: m, planned: p, archived: a }, why: join(["the transcript placeholder ", m as String, " is archived at ", to_string(measure_count(m: a)), " bytes, not its planned ", to_string(measure_count(m: p))], "") } } + WorkspacePackPlaceholderUnread { member: m } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedPlaceholderUnread { member: m }, why: concat("the archived bytes of the transcript placeholder could not be extracted and hashed: ", m as String) } } + WorkspacePackPlaceholderDigestDiffers { member: m, expected: e, archived: a } => + Present { value: WorkspaceReadbackRefusal { refusal: PackRefusedPlaceholderDigest { member: m, expected: e.hex as String, archived: a }, why: join(["the archived bytes of ", m as String, " are not its staged placeholder (sha256 ", a, ", expected ", e.hex as String, "); the archive may carry real transcript content"], "") } } + } +} + +fn workspace_source_refuse_archive(head: String, staging: String, refusal: WorkspacePackRefusal, why: String) -> ProcessExit { + let removed = shell.Remove.RecursiveForce(path: staging as FilePath) + workspace_source_finish(receipt: join([head, "verdict=", workspace_pack_refusal_words(refusal: refusal), " archive_removed=", if removed.success { "yes" } else { "no" }, "\n"], ""), outcome: exit_failure(reason: join(["workspace_source_pack: ", why, "; nothing was uploaded"], ""))) +} + +type WorkspaceArchiveListings { + names: String + sizes: String +} + +fn workspace_archive_listings(archive: String) -> WorkspaceArchiveListings? { + match run_shell_command_observe_outcome(command: tar_list_command(archive: archive), transport: LocalExec) { + ProcessNotSpawned { program: _, cause: _ } => none + ProcessRan { observation: names } => + if names.exit_code != 0 { + none + } else { + match run_shell_command_observe_outcome(command: tar_list_sizes_command(archive: archive), transport: LocalExec) { + ProcessNotSpawned { program: _, cause: _ } => none + ProcessRan { observation: sizes } => if sizes.exit_code != 0 { none } else { Present { value: WorkspaceArchiveListings { names: names.stdout, sizes: sizes.stdout } } } + } + } + } +} + fn workspace_source_publish(head: String, archive: String, staging: String) -> ProcessExit { match read_staged_fabric_blob(path: archive as NonEmptyStr) { FabricBlobReadRefused { fault: f } => workspace_source_finish(receipt: concat(head, "verdict=refused-archive-unread\n"), outcome: exit_failure(reason: concat("workspace_source_pack: ", fabric_blob_fault_text(fault: f)))) diff --git a/dag/gunbc/fabric/workspace_transcript_rate.dag b/dag/gunbc/fabric/workspace_transcript_rate.dag new file mode 100644 index 00000000000..7a3d0da91df --- /dev/null +++ b/dag/gunbc/fabric/workspace_transcript_rate.dag @@ -0,0 +1,581 @@ +module gunbc.fabric.workspace_transcript_rate + +import std.types { String, NonEmptyStr, Int, Bool, List, Map } +import v2.std.collection { empty_map, map_insert, map_lookup } +import std.optional { Present, Absent } +import std.algebra { Cons, Empty } +import std.process { ProcessExit, ExitSuccess, exit_failure } +import std.measure { ByteSize, byte_size, measure_count, Nanosecond, nanosecond, nanosecond_count, Second, second, second_count, Bandwidth, bandwidth, bandwidth_count, bits_per_byte, ObservationInstant, ProcessMonotonic, WallClock, InstantOrder, InstantAfter, InstantIncomparable, InstantBefore, InstantSame } +import std.checkpointed_workspace { WorkspaceAgeOf, WorkspaceAgeUndefined, workspace_age_between } +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.shell.exec { ProcessRunOutcome, ProcessRan, ProcessNotSpawned } +import extdeps.exec.command { ArgvCommand, LocalExec } +import extdeps.tools.findutils { find_regular_file_size_mtime_command } +import extdeps.tools.sleep { sleep_delay_seconds_second_carrier_projection } +import gunbc.command_runner { run_shell_command_observe_outcome } +import gunbc.floor_cost_distribution { permille_percentile } +import gunbc.hostname_read { hostname_short_read, HostnameShortCaptured, HostnameShortCaptureRefused } +import gunbc.host_effect { LocalShell } +import gunbc.fabric.workspace_source { + WorkspaceSourceRoot, workspace_source_fixed_roots, workspace_source_pack_host, + WorkspaceAgentStateSlot, AgentStateSlotAdmitted, AgentStateSlotAbsent, AgentStateSlotUnknown, + workspace_agent_state_slot_observe, workspace_agent_state_slot_words, workspace_root_member, + workspace_exclusions, workspace_path_exclusion, workspace_exclusion_spellings, workspace_transcript_exclusions, + CredentialExclusion, TranscriptExclusion, +} + +// HOW FAST THE OPERATOR'S AGENT TRANSCRIPTS ACTUALLY GROW, OBSERVED ON THE HOST THAT HOLDS THEM, FROM +// SIZES AND MODIFICATION TIMES ALONE (step 3 of the workspace stage-0 sequence, operator decision +// via warm-badger-442, 2026-10-05). The stage-0 pack (gunbc.fabric.workspace_source) carries every +// transcript as a same-size synthetic placeholder of a static tree, so its checkpoint-lag figures +// say nothing about a tree being written to; the 10 s / 60 s verdicts stay withheld until a churn +// phase appends to those placeholders at a realistic rate. This module produces that rate. On srv1 +// it samples, once per declared interval over a declared window, the byte size and mtime of every +// file the pack's own TranscriptExclusion rows match under the pinned agent-state roots, and +// records per interval what grew, what appeared, what shrank and how many bytes were appended. +// +// CONTENT IS NEVER READ. The only reader is extdeps.tools.findutils +// find_regular_file_size_mtime_command, whose -printf directives are answered from the inode and +// whose expression runs no other program; the witness checks the second fact on the command value. +// Transcript content is exactly what the operator ruled is not carried, and a rate instrument has no +// need of it. +// +// WHAT ONE RUN IS: one window, on one host, during whatever agent activity happened to be live. It +// is ONE SAMPLE OF THE WORKLOAD, NOT THE WORKLOAD, and every receipt says so (DESIGN 4d: the figure +// is a typed reading of one occasion, and a consumer that quotes it as the operator's rate is the +// defect). + +// ── WHAT IS OBSERVED, AND FOR HOW LONG ───────────────────────────────────────────────────────── + +// THE ROOTS ARE THE PACK'S OWN: the Claude agent-state root is the one the pack's slot admission +// (gunbc.fabric.workspace_source workspace_agent_state_slot_observe) admits from the same +// agent_state_slot input -- the active slot moves with the operator's credential swaps, so it is a +// declared input with no default, never a pinned path -- and the Codex home is the pack's fixed row, +// selected by its label. The gunbc clone is not observed: no transcript lives in it, and walking a +// build tree each interval would cost the window its resolution. +data workspace_transcript_rate_fixed_root_labels: List = ["codex-state"] + +// TEN MINUTES AT TEN SECONDS, declared and printed in every receipt. The interval matches the +// stage-0 checkpoint-lag target, so one row is one target-length of appends; the window spans +// sixty of them, which is the smallest population whose 95th percentile is not its maximum. +data workspace_transcript_rate_window: Second = second(count: 600) +data workspace_transcript_rate_sample_interval: Second = second(count: 10) + +data workspace_transcript_rate_receipt_path: String = "target/workspace-transcript-rate-receipt.txt" + +data workspace_transcript_rate_caveat: NonEmptyStr = "ONE SAMPLE OF WORKLOAD, NOT THE WORKLOAD: one window on one host during whatever agent activity was live; bytes_appended is a LOWER BOUND (stat sees net size change per interval, never writes undone or overwritten within it)" + +type TranscriptRateRootSelection + = TranscriptRateRootsSelected { roots: List } + | TranscriptRateRootUndeclared { label: NonEmptyStr } + | TranscriptRateSlotRefused { slot: WorkspaceAgentStateSlot } + +// The admitted slot's root first, then the fixed rows; a refused slot refuses the selection, carrying +// the admission so the receipt prints the pack's own words for it. +fn workspace_transcript_rate_roots(slot: WorkspaceAgentStateSlot, fixed: TranscriptRateRootSelection) -> TranscriptRateRootSelection { + match slot { + AgentStateSlotAbsent { observed: _ } => TranscriptRateSlotRefused { slot: slot } + AgentStateSlotUnknown { requested: _, observed: _ } => TranscriptRateSlotRefused { slot: slot } + AgentStateSlotAdmitted { slot: _, root: agent } => + match fixed { + TranscriptRateRootsSelected { roots: rs } => TranscriptRateRootsSelected { roots: Cons { head: agent, tail: rs } } + TranscriptRateRootUndeclared { label: l } => TranscriptRateRootUndeclared { label: l } + TranscriptRateSlotRefused { slot: s2 } => TranscriptRateSlotRefused { slot: s2 } + } + } +} + +fn workspace_transcript_rate_select_roots(labels: List, declared: List) -> TranscriptRateRootSelection { + match labels { + Empty => TranscriptRateRootsSelected { roots: [] } + Cons { head: l, tail: rest } => + match filter(declared, r => (r.label as String) == (l as String)) { + Empty => TranscriptRateRootUndeclared { label: l } + Cons { head: r, tail: _ } => + match workspace_transcript_rate_select_roots(labels: rest, declared: declared) { + TranscriptRateRootUndeclared { label: u } => TranscriptRateRootUndeclared { label: u } + TranscriptRateSlotRefused { slot: s } => TranscriptRateSlotRefused { slot: s } + TranscriptRateRootsSelected { roots: rs } => TranscriptRateRootsSelected { roots: Cons { head: r, tail: rs } } + } + } + } +} + +// ── ONE READING OF ONE ROOT: SIZE AND MTIME, NEVER CONTENT ───────────────────────────────────── + +// A FILE'S MODIFICATION TIME IS THE FILESYSTEM'S CLOCK, NOT THIS PROCESS'S: st_mtime counted from +// the epoch, as find's %T@ prints it. It has its own carrier so it can never stand where a sample +// instant (an ObservationInstant on this process's monotonic clock) is expected, and the two are +// never subtracted from each other; an mtime is only ever compared with another mtime. +type TranscriptFileMtime { + since_epoch: Nanosecond +} + +type TranscriptFileStat { + member: NonEmptyStr + size: ByteSize + mtime: TranscriptFileMtime +} + +// A ROOT THAT COULD NOT BE READ REFUSES, and so does a line that is not a record: a partial listing +// is not a smaller transcript population, it is a reading that did not happen, and an interval +// computed against it would report every unlisted file as removed. +type TranscriptRootReading + = TranscriptRootRead { files: List } + | TranscriptRootRefused { root: WorkspaceSourceRoot, detail: String } + +fn workspace_transcript_rate_root_command(root: WorkspaceSourceRoot) -> ArgvCommand { + find_regular_file_size_mtime_command(root: root.path as String) +} + +fn workspace_transcript_rate_root_reading(root: WorkspaceSourceRoot, run: ProcessRunOutcome) -> TranscriptRootReading { + match run { + ProcessNotSpawned { program: p, cause: c } => TranscriptRootRefused { root: root, detail: join([p as String, " did not run: ", c as String], "") } + ProcessRan { observation: o } => + if o.exit_code != 0 { + TranscriptRootRefused { root: root, detail: join(["find exited ", to_string(o.exit_code), " reading ", root.path as String, ": ", o.stderr], "") } + } else { + workspace_transcript_rate_lines(root: root, lines: filter(split(o.stdout, separator: "\n"), l => l != ""), files: []) + } + } +} + +// Each line is classified by the pack's own matcher over its member path, so this module restates +// no rule: a path the pack excludes as a transcript is observed, a credential-class match (counted +// first, as the pack counts it) or an unmatched path is not. Kept files accumulate in reverse and +// the order is irrelevant to every consumer, which keys them by member. +fn workspace_transcript_rate_lines(root: WorkspaceSourceRoot, lines: List, files: List) -> TranscriptRootReading { + match lines { + Empty => TranscriptRootRead { files: files } + Cons { head: l, tail: rest } => + match workspace_transcript_rate_line(root: root, line: l) { + TranscriptLineUnparsed { line: bad } => TranscriptRootRefused { root: root, detail: concat("find printed a line that is not a size, mtime and path record: ", bad) } + TranscriptLineOther => workspace_transcript_rate_lines(root: root, lines: rest, files: files) + TranscriptLineFile { stat: s } => workspace_transcript_rate_lines(root: root, lines: rest, files: Cons { head: s, tail: files }) + } + } +} + +type TranscriptLine + = TranscriptLineFile { stat: TranscriptFileStat } + | TranscriptLineOther + | TranscriptLineUnparsed { line: String } + +fn workspace_transcript_rate_line(root: WorkspaceSourceRoot, line: String) -> TranscriptLine { + match split(line, separator: "\t") { + Cons { head: size_text, tail: Cons { head: mtime_text, tail: Cons { head: first, tail: more } } } => { + let relative = join(Cons { head: first, tail: more }, "\t") + match parse_int(s: size_text) { + Absent => TranscriptLineUnparsed { line: line } + Present { value: n } => + match workspace_transcript_rate_mtime(text: mtime_text) { + Absent => TranscriptLineUnparsed { line: line } + Present { value: t } => + if n < 0 || relative == "" { + TranscriptLineUnparsed { line: line } + } else { + workspace_transcript_rate_classify(stat: TranscriptFileStat { member: workspace_root_member(root: root, relative: relative) as NonEmptyStr, size: byte_size(count: n), mtime: t }) + } + } + } + } + Cons { head: _, tail: Cons { head: _, tail: Empty } } => TranscriptLineUnparsed { line: line } + Cons { head: _, tail: Empty } => TranscriptLineUnparsed { line: line } + Empty => TranscriptLineUnparsed { line: line } + } +} + +fn workspace_transcript_rate_classify(stat: TranscriptFileStat) -> TranscriptLine { + match workspace_path_exclusion(exclusions: workspace_exclusions(), path: stat.member as String) { + Absent => TranscriptLineOther + Present { value: x } => + match x.class { + TranscriptExclusion => TranscriptLineFile { stat: stat } + CredentialExclusion => TranscriptLineOther + } + } +} + +// %T@ PRINTS SECONDS, A POINT AND A FRACTION (GNU find prints ten fractional digits; a filesystem +// with coarser timestamps prints zeros there). Nanoseconds are the first nine fractional digits, +// right-padded, so a shorter fraction reads as the value it spells rather than a smaller one. +fn workspace_transcript_rate_mtime(text: String) -> TranscriptFileMtime? { + match split(text, separator: ".") { + Cons { head: whole, tail: Empty } => + match parse_int(s: whole) { + Absent => none + Present { value: s } => if s < 0 { none } else { Present { value: TranscriptFileMtime { since_epoch: nanosecond(count: s * 1000000000) } } } + } + Cons { head: whole, tail: Cons { head: frac, tail: Empty } } => { + let padded = substring(s: concat(frac, "000000000"), start: 0, end: 9) + match parse_int(s: whole) { + Absent => none + Present { value: s } => + match parse_int(s: padded) { + Absent => none + Present { value: f } => if s < 0 || f < 0 || frac == "" { none } else { Present { value: TranscriptFileMtime { since_epoch: nanosecond(count: s * 1000000000 + f) } } } + } + } + } + Cons { head: _, tail: Cons { head: _, tail: Cons { head: _, tail: _ } } } => none + Empty => none + } +} + +// ── ONE SAMPLE: EVERY ROOT, AT ONE INSTANT ───────────────────────────────────────────────────── +// +// The instant is read on this process's monotonic clock immediately before the first root is +// walked, so an interval's span is the time between the starts of two walks; a walk's own duration +// is inside the span it opens, never subtracted from it. +type TranscriptRateSample + = TranscriptRateSampled { at: ObservationInstant, files: List } + | TranscriptRateSampleRefused { refusals: List } + +fn workspace_transcript_rate_sample_of(at: ObservationInstant, readings: List) -> TranscriptRateSample { + let refusals = flat_map(readings, r => match r { + TranscriptRootRefused { root: root, detail: d } => [join([root.label as String, " (", root.path as String, "): ", d], "")] + TranscriptRootRead { files: _ } => [] + }) + match refusals { + Cons { head: _, tail: _ } => TranscriptRateSampleRefused { refusals: refusals } + Empty => TranscriptRateSampled { at: at, files: flat_map(readings, r => match r { + TranscriptRootRead { files: fs } => fs + TranscriptRootRefused { root: _, detail: _ } => [] + }) } + } +} + +// ONE CLOCK DOMAIN FOR EVERY SAMPLE OF A WINDOW: this process's monotonic clock, wall basis -- the +// shape gunbc.fabric.workspace_source workspace_source_instant gives the pack's spans. +fn workspace_transcript_rate_instant(label: String) -> ObservationInstant { + ObservationInstant { domain: ProcessMonotonic { process: "workspace-transcript-rate" }, basis: WallClock, since_origin: nanosecond(count: observed_monotonic_nanos(label)) } +} + +fn workspace_transcript_rate_sample_wet(roots: List, label: String) -> TranscriptRateSample { + let at = workspace_transcript_rate_instant(label: label) + workspace_transcript_rate_sample_of(at: at, readings: map(roots, r => workspace_transcript_rate_root_reading(root: r, run: run_shell_command_observe_outcome(command: workspace_transcript_rate_root_command(root: r), transport: LocalExec)))) +} + +// ── ONE INTERVAL: THE DIFFERENCE OF TWO SAMPLES ──────────────────────────────────────────────── +// +// A FILE THAT SHRANK IS A TRUNCATION, REPORTED AS ONE, never a negative append: its delta is not +// subtracted from the bytes appended, because a rewrite that shortens a file appends nothing and +// would otherwise cancel real appends elsewhere in the interval. A CREATED file contributes its +// whole size, all of it written since the previous sample. A file whose mtime advanced at the same +// size was rewritten in place and appended nothing. The ACTIVE-WRITER COUNT is the number of +// distinct files written in the interval -- created, grown, shrunk or mtime-advanced. It counts +// FILES, not processes: one agent session appends to its own transcript and a subagent to its own, +// so it tracks live writers without being a process count, and stat cannot see the processes. +type TranscriptTruncation { + member: NonEmptyStr + from: ByteSize + to: ByteSize +} + +type TranscriptAppendInterval { + span: Nanosecond + files_grown: Int + files_created: Int + files_removed: Int + files_rewritten_same_size: Int + truncations: List + bytes_appended: ByteSize + active_writers: Int +} + +type TranscriptFileChange + = TranscriptFileCreated { size: ByteSize } + | TranscriptFileGrown { by: ByteSize } + | TranscriptFileShrank { truncation: TranscriptTruncation } + | TranscriptFileRewritten + | TranscriptFileUnchanged + +fn workspace_transcript_file_change(before: TranscriptFileStat?, after: TranscriptFileStat) -> TranscriptFileChange { + match before { + Absent => TranscriptFileCreated { size: after.size } + Present { value: b } => { + let delta = measure_count(m: after.size) - measure_count(m: b.size) + if delta > 0 { + TranscriptFileGrown { by: byte_size(count: delta) } + } else if delta < 0 { + TranscriptFileShrank { truncation: TranscriptTruncation { member: after.member, from: b.size, to: after.size } } + } else if nanosecond_count(n: after.mtime.since_epoch) != nanosecond_count(n: b.mtime.since_epoch) { + TranscriptFileRewritten + } else { + TranscriptFileUnchanged + } + } + } +} + +fn workspace_transcript_rate_index(files: List) -> Map { + fold(files, init: empty_map(), f: (m, s) => map_insert(m: m, key: s.member as String, value: s)) +} + +// Every file of the later sample is looked up once in the earlier one, so an interval costs one +// pass over each sample; a removed file is counted as the earlier files the later one did not keep. +fn workspace_transcript_rate_interval(before: List, after: List, span: Nanosecond) -> TranscriptAppendInterval { + let index = workspace_transcript_rate_index(files: before) + let changes = map(after, s => workspace_transcript_file_change(before: map_lookup(m: index, key: s.member as String), after: s)) + let created = length(filter(changes, c => match c { + TranscriptFileCreated { size: _ } => true + TranscriptFileGrown { by: _ } => false + TranscriptFileShrank { truncation: _ } => false + TranscriptFileRewritten => false + TranscriptFileUnchanged => false + })) + let grown = length(filter(changes, c => match c { + TranscriptFileCreated { size: _ } => false + TranscriptFileGrown { by: _ } => true + TranscriptFileShrank { truncation: _ } => false + TranscriptFileRewritten => false + TranscriptFileUnchanged => false + })) + let rewritten = length(filter(changes, c => match c { + TranscriptFileCreated { size: _ } => false + TranscriptFileGrown { by: _ } => false + TranscriptFileShrank { truncation: _ } => false + TranscriptFileRewritten => true + TranscriptFileUnchanged => false + })) + let truncations = flat_map(changes, c => match c { + TranscriptFileCreated { size: _ } => [] + TranscriptFileGrown { by: _ } => [] + TranscriptFileShrank { truncation: t } => [t] + TranscriptFileRewritten => [] + TranscriptFileUnchanged => [] + }) + let appended = fold(changes, init: 0, f: (acc, c) => match c { + TranscriptFileCreated { size: s } => acc + measure_count(m: s) + TranscriptFileGrown { by: d } => acc + measure_count(m: d) + TranscriptFileShrank { truncation: _ } => acc + TranscriptFileRewritten => acc + TranscriptFileUnchanged => acc + }) + TranscriptAppendInterval { + span: span, + files_grown: grown, + files_created: created, + files_removed: length(before) - (length(after) - created), + files_rewritten_same_size: rewritten, + truncations: truncations, + bytes_appended: byte_size(count: appended), + active_writers: created + grown + length(truncations) + rewritten, + } +} + +// ── THE OBSERVED RATE: THE CARRIER THE CHURN PHASE CONSUMES ──────────────────────────────────── +// +// THE RATE IS CARRIED AS ITS EXACT QUOTIENT PAIR -- bytes appended over a measured span, per +// interval -- because std.measure has no bytes-per-time type to carry it as one figure: its only +// rate of information is Bandwidth (DataRate, bits per second, a Nat count at One scale), which +// would floor every interval to whole bits per second and lose a quiet interval to zero. That gap +// is stated, not filled here: the receipt's rates are DERIVED from these pairs as Bandwidth (see +// workspace_transcript_rate_bandwidth), and a consumer derives what it needs from the pairs. +// +// THE CONSUMER IS A DECLARED FRONTIER (DESIGN 3c): the stage-0 churn phase (step 2 of the +// workspace stage-0 sequence, a later change under still-dove-673) appends to the synthetic +// transcript placeholders of gunbc.fabric.workspace_source at this observed rate, and the 10 s / +// 60 s checkpoint-lag verdicts are quotable only over a run that carries that churn. Its trigger is +// that change landing; until it does, the receipt renderer below is this carrier's executing +// consumer, and the receipt is the dispatch input the churn phase is parameterized from. +type TranscriptAppendRate { + host: NonEmptyStr + window: Second + interval: Second + rows: List + caveat: NonEmptyStr +} + +// A WINDOW EITHER COMPLETES OR REFUSES. A root that becomes unreadable mid-window, or a sleep that +// fails, ends the observation; the rows already taken are printed in the receipt beside the +// refusal, never summarized as if the window had finished. +type TranscriptRateObservation + = TranscriptRateObserved { rate: TranscriptAppendRate } + | TranscriptRateRefused { reason: String, rows: List } + | TranscriptRateClockUndefined { from: ObservationInstant, to: ObservationInstant, order: InstantOrder, rows: List } + +// THE SPAN IS DERIVED, NEVER SUBTRACTED: std.checkpointed_workspace workspace_age_between orders two +// consecutive sample instants first, so a later sample that does not follow the earlier one +// (InstantAfter) or one on another clock (InstantIncomparable) ends the window as its own typed +// refusal, TranscriptRateClockUndefined, instead of standing in a span. +// +// THE PURE WINDOW: a baseline sample and one sample per interval, each interval the difference of +// consecutive samples. The sampler and the pause are supplied, so the wet entry passes the real +// walk and the real sleep and a witness passes recorded samples; the fold itself is one function. +fn workspace_transcript_rate_window_of(baseline: TranscriptRateSample, remaining: Int, next: fn(Int) -> TranscriptRateSample, pause: fn(Int) -> Bool, rows: List) -> TranscriptRateObservation { + match baseline { + TranscriptRateSampleRefused { refusals: rs } => TranscriptRateRefused { reason: concat("a transcript root could not be read; no permission is widened here: ", join(rs, "; ")), rows: reverse(rows) } + TranscriptRateSampled { at: at, files: files } => + if remaining <= 0 { + TranscriptRateObserved { rate: TranscriptAppendRate { host: workspace_source_pack_host, window: workspace_transcript_rate_window, interval: workspace_transcript_rate_sample_interval, rows: reverse(rows), caveat: workspace_transcript_rate_caveat } } + } else if pause(remaining) == false { + TranscriptRateRefused { reason: "the pause between samples failed; the interval is not what the window declares", rows: reverse(rows) } + } else { + match next(remaining) { + TranscriptRateSampleRefused { refusals: rs } => TranscriptRateRefused { reason: concat("a transcript root became unreadable mid-window: ", join(rs, "; ")), rows: reverse(rows) } + TranscriptRateSampled { at: at2, files: files2 } => + match workspace_age_between(from: at, to: at2) { + WorkspaceAgeUndefined { order: o } => TranscriptRateClockUndefined { from: at, to: at2, order: o, rows: reverse(rows) } + WorkspaceAgeOf { age: span } => + workspace_transcript_rate_window_of( + baseline: TranscriptRateSampled { at: at2, files: files2 }, + remaining: remaining - 1, + next: next, + pause: pause, + rows: Cons { head: workspace_transcript_rate_interval(before: files, after: files2, span: span), tail: rows }, + ) + } + } + } + } +} + +fn workspace_transcript_rate_interval_count() -> Int { + second_count(s: workspace_transcript_rate_window) / second_count(s: workspace_transcript_rate_sample_interval) +} + +// ── THE SUMMARY, DERIVED FOR THE RECEIPT ─────────────────────────────────────────────────────── + +// A RATE IS A std.measure Bandwidth -- DataRate, bits per second -- derived from the exact quotient +// pair, never a bare number. Bits rather than bytes because Bandwidth is the one information-rate +// carrier std.measure has; eight bits to the octet (bits_per_byte) makes the conversion exact, and the +// floor is taken once, on bits. The receipt prints the carrier's own bits/s beside the bytes/s it +// implies (bits / 8, floored), which is the figure the churn phase is quoted in. +fn workspace_transcript_rate_bandwidth(bytes: ByteSize, span: Nanosecond) -> Bandwidth? { + let ns = nanosecond_count(n: span) + if ns <= 0 { none } else { Present { value: bandwidth(count: (measure_count(m: bytes) * bits_per_byte() * 1000000000) / ns) } } +} + +fn workspace_transcript_rate_row_rates(rows: List) -> List { + flat_map(rows, r => match workspace_transcript_rate_bandwidth(bytes: r.bytes_appended, span: r.span) { + Absent => [] + Present { value: v } => [v] + }) +} + +fn workspace_transcript_rate_percentile(rates: List, percent: Int) -> Bandwidth? { + match permille_percentile(values: map(rates, b => bandwidth_count(b: b)), percent: percent) { + Absent => none + Present { value: v } => Present { value: bandwidth(count: v) } + } +} + +fn workspace_transcript_rate_rate_words(name: String, rate: Bandwidth?, absent: String) -> String { + match rate { + Absent => join([" ", name, "_bits_per_s=", absent, " ", name, "_bytes_per_s=", absent], "") + Present { value: b } => join([" ", name, "_bits_per_s=", to_string(bandwidth_count(b: b)), " ", name, "_bytes_per_s=", to_string(bandwidth_count(b: b) / bits_per_byte())], "") + } +} + +fn workspace_transcript_rate_row_line(index: Int, r: TranscriptAppendInterval) -> String { + join([ + "interval ", to_string(index), + " span_ms=", to_string(nanosecond_count(n: r.span) / 1000000), + " bytes_appended=", to_string(measure_count(m: r.bytes_appended)), + " files_grown=", to_string(r.files_grown), + " files_created=", to_string(r.files_created), + " files_removed=", to_string(r.files_removed), + " files_rewritten_same_size=", to_string(r.files_rewritten_same_size), + " truncations=", to_string(length(r.truncations)), + join(map(r.truncations, t => join([" truncated=", t.member as String, ":", to_string(measure_count(m: t.from)), "->", to_string(measure_count(m: t.to))], "")), ""), + " active_writers=", to_string(r.active_writers), + ], "") +} + +fn workspace_transcript_rate_rows_text(rows: List, index: Int, acc: List) -> String { + match rows { + Empty => join(reverse(acc), "") + Cons { head: r, tail: rest } => workspace_transcript_rate_rows_text(rows: rest, index: index + 1, acc: Cons { head: concat(workspace_transcript_rate_row_line(index: index, r: r), "\n"), tail: acc }) + } +} + +fn workspace_transcript_rate_summary(rate: TranscriptAppendRate) -> String { + let total_bytes = byte_size(count: fold(rate.rows, init: 0, f: (acc, r) => acc + measure_count(m: r.bytes_appended))) + let total_span = nanosecond(count: fold(rate.rows, init: 0, f: (acc, r) => acc + nanosecond_count(n: r.span))) + let touched = fold(rate.rows, init: 0, f: (acc, r) => acc + r.active_writers) + let n = length(rate.rows) + let rates = workspace_transcript_rate_row_rates(rows: rate.rows) + join([ + "summary intervals=", to_string(n), + " measured_window_ms=", to_string(nanosecond_count(n: total_span) / 1000000), + " bytes_appended_total=", to_string(measure_count(m: total_bytes)), + workspace_transcript_rate_rate_words(name: "mean", rate: workspace_transcript_rate_bandwidth(bytes: total_bytes, span: total_span), absent: "unmeasured(no-span)"), + workspace_transcript_rate_rate_words(name: "p95", rate: workspace_transcript_rate_percentile(rates: rates, percent: 95), absent: "unmeasured(no-interval)"), + workspace_transcript_rate_rate_words(name: "max", rate: workspace_transcript_rate_percentile(rates: rates, percent: 100), absent: "unmeasured(no-interval)"), + " files_touched_per_interval_mean=", if n == 0 { "unmeasured(no interval)" } else { join([to_string(touched / n), " (", to_string(touched), "/", to_string(n), ")"], "") }, + " truncations_total=", to_string(fold(rate.rows, init: 0, f: (acc, r) => acc + length(r.truncations))), + "\n", + ], "") +} + +fn workspace_transcript_rate_receipt_head(roots: List) -> String { + join([ + "workspace_transcript_rate_observe host=", workspace_source_pack_host as String, "\n", + "window_s=", to_string(second_count(s: workspace_transcript_rate_window)), " interval_s=", to_string(second_count(s: workspace_transcript_rate_sample_interval)), "\n", + "observed=", workspace_exclusion_spellings(exclusions: workspace_transcript_exclusions), " (the pack's TranscriptExclusion rows; size and mtime only, content never read)\n", + join(map(roots, r => join(["root ", r.label as String, " path=", r.path as String], "")), "\n"), "\n", + workspace_transcript_rate_caveat as String, "\n", + ], "") +} + +fn workspace_transcript_rate_order_words(order: InstantOrder) -> String { + match order { + InstantAfter => "a later sample instant did not follow the earlier one on the same clock; no span is derived" + InstantIncomparable { left: _, right: _ } => "two sample instants are on different clock domains or bases; no span is derived" + InstantBefore => "the instants ordered, yet no span was derived" + InstantSame => "the instants were the same, yet no span was derived" + } +} + +fn workspace_transcript_rate_receipt(head: String, observation: TranscriptRateObservation) -> String { + match observation { + TranscriptRateObserved { rate: r } => join([head, workspace_transcript_rate_rows_text(rows: r.rows, index: 1, acc: []), workspace_transcript_rate_summary(rate: r), "verdict=observed\n"], "") + TranscriptRateRefused { reason: why, rows: rows } => join([head, workspace_transcript_rate_rows_text(rows: rows, index: 1, acc: []), "refused=", why, "\nverdict=refused\n"], "") + TranscriptRateClockUndefined { from: _, to: _, order: o, rows: rows } => join([head, workspace_transcript_rate_rows_text(rows: rows, index: 1, acc: []), "refused=", workspace_transcript_rate_order_words(order: o), "\nverdict=refused-clock\n"], "") + } +} + +// ── THE OBSERVATION, ON THE HOST THAT HOLDS THE TRANSCRIPTS ──────────────────────────────────── + +fn workspace_transcript_rate_finish(receipt: String, outcome: ProcessExit) -> ProcessExit { + let w = Filesystem.Write(path: workspace_transcript_rate_receipt_path, content: receipt) + if w.success == false { + exit_failure(reason: join(["workspace_transcript_rate_observe: receipt write failed: ", w.error, "\n", receipt], "")) + } else { + outcome + } +} + +fn workspace_transcript_rate_observe_wet() -> ProcessExit { + let host = match hostname_short_read(transport: LocalShell) { + HostnameShortCaptured { value: v } => v + HostnameShortCaptureRefused { reason: _ } => "" + } + if host != (workspace_source_pack_host as String) { + exit_failure(reason: join(["workspace_transcript_rate_observe: the transcripts live on ", workspace_source_pack_host as String, " but this job runs on '", host, "'; dispatch it with host=", workspace_source_pack_host as String], "")) + } else { + let slot = workspace_agent_state_slot_observe() + let slot_line = concat(workspace_agent_state_slot_words(admission: slot), "\n") + match workspace_transcript_rate_roots(slot: slot, fixed: workspace_transcript_rate_select_roots(labels: workspace_transcript_rate_fixed_root_labels, declared: workspace_source_fixed_roots)) { + TranscriptRateRootUndeclared { label: l } => exit_failure(reason: join(["workspace_transcript_rate_observe: no fixed workspace source root is labelled '", l as String, "'"], "")) + TranscriptRateSlotRefused { slot: _ } => workspace_transcript_rate_finish(receipt: join(["workspace_transcript_rate_observe host=", host, "\n", slot_line, "verdict=refused-agent-state-slot\n"], ""), outcome: exit_failure(reason: concat("workspace_transcript_rate_observe: ", slot_line))) + TranscriptRateRootsSelected { roots: roots } => { + let head = concat(workspace_transcript_rate_receipt_head(roots: roots), slot_line) + let observation = workspace_transcript_rate_window_of( + baseline: workspace_transcript_rate_sample_wet(roots: roots, label: "workspace-transcript-rate-baseline"), + remaining: workspace_transcript_rate_interval_count(), + next: k => workspace_transcript_rate_sample_wet(roots: roots, label: concat("workspace-transcript-rate-sample-", to_string(k))), + pause: _ => sleep_delay_seconds_second_carrier_projection(duration: workspace_transcript_rate_sample_interval), + rows: [], + ) + let receipt = workspace_transcript_rate_receipt(head: head, observation: observation) + match observation { + TranscriptRateObserved { rate: _ } => workspace_transcript_rate_finish(receipt: receipt, outcome: ExitSuccess) + TranscriptRateRefused { reason: why, rows: _ } => workspace_transcript_rate_finish(receipt: receipt, outcome: exit_failure(reason: concat("workspace_transcript_rate_observe: ", why))) + TranscriptRateClockUndefined { from: _, to: _, order: o, rows: _ } => workspace_transcript_rate_finish(receipt: receipt, outcome: exit_failure(reason: concat("workspace_transcript_rate_observe: ", workspace_transcript_rate_order_words(order: o)))) + } + } + } + } +} diff --git a/dag/gunbc/fleet/altra_firmware_library.dag b/dag/gunbc/fleet/altra_firmware_library.dag index 4a4b50987cf..0b33478171f 100644 --- a/dag/gunbc/fleet/altra_firmware_library.dag +++ b/dag/gunbc/fleet/altra_firmware_library.dag @@ -13,9 +13,12 @@ import gunbc.fleet_bmc_observation { srv3_bmc_firmware_observation, srv4_bmc_fir import extdeps.boards.asrock_rack { asrock_altrad8ud_bios_3_10_instant_flash_release, asrock_altrad8ud_bmc_3_22_00_release, asrock_altrad8ud_download_authority, + asrock_altrad8ud_bmc_3_22_00_push_artifact_name, asrock_altrad8ud_bmc_3_22_00_push_artifact_bytes, + asrock_altrad8ud_bmc_3_22_00_push_artifact_digest, } import extdeps.boards.asrock_altrad8ud_beta_firmware { AsrockAltrad8udBetaFirmwareListing, asrock_altrad8ud_beta_firmware_listings, + asrock_altrad8ud_beta_bios_4_01_listing, asrock_altrad8ud_beta_scp_2_10_20260210_listing, asrock_altrad8ud_beta_disclaimer, AsrockBetaFirmwareVersion, SemanticFirmwareVersion, ScpBuildVersion, } @@ -29,8 +32,11 @@ import extdeps.ampere.altra_ssif_patch { // The BIOS ZIP matched the existing vendor-download digest. BMC ZIP contains // altrad8ud-1l2t_3.22.00.static.mtd.tar; ROM ZIP contains ALTRAD8UD-1L2T_3.10.ROM. // A Drive /view link is for human retrieval, not an unauthenticated raw download. +// A beta build is a listing on ASRock's beta table, not a cited release row, so +// its archive names the listing it was downloaded against. type AltraLibraryArchiveSubject = FirmwareDistribution { firmware_release: FirmwareReleaseRow } + | BetaFirmwareDistribution { listing: AsrockAltrad8udBetaFirmwareListing } | SsifKernelPatchBundle type AltraLibraryArchive { @@ -63,10 +69,102 @@ data altra_firmware_drive_archives: List = [ archive_bytes: byte_size(count: 5697), archive_digest: Present { value: sha256_digest(hex: "74ca2343e20b682f419f3240006dfc55ed391978e57c86bdce698e8b72ee0531") }, }, + AltraLibraryArchive { + subject: BetaFirmwareDistribution { listing: asrock_altrad8ud_beta_bios_4_01_listing }, + archive_name: "ALTRAD8UD-1L2T(4.01)TAR.zip", + human_retrieval: uri_https(locator: "drive.google.com/file/d/1HNtIH6UOkuBHSXgr5SWH6ERYR7Gev8wz/view"), + archive_bytes: byte_size(count: 5359843), + archive_digest: Present { value: sha256_digest(hex: "5753a13b92c5f3c9cfeb5dc5fc2f06c6f1ef34e8e594a7ed9598a2ae3177d5b0") }, + }, + AltraLibraryArchive { + subject: BetaFirmwareDistribution { listing: asrock_altrad8ud_beta_bios_4_01_listing }, + archive_name: "ALTRAD8UD-1L2T(4.01)ROM.zip", + human_retrieval: uri_https(locator: "drive.google.com/file/d/1gMuW0TU0bDwCSC60jtJbC3a8OfRtcPnb/view"), + archive_bytes: byte_size(count: 5359521), + archive_digest: Present { value: sha256_digest(hex: "8adc6439518a95b5aa1d472ae37d53f2814e43008eba524d2526c027a8f0dc51") }, + }, + AltraLibraryArchive { + subject: BetaFirmwareDistribution { listing: asrock_altrad8ud_beta_scp_2_10_20260210_listing }, + archive_name: "ALTRAD8UD-1L2T(2.10.20260210)SCP.zip", + human_retrieval: uri_https(locator: "drive.google.com/file/d/1nK_8RYtqWTWcTul5SL93MjlLNWB8JUvl/view"), + archive_bytes: byte_size(count: 169220), + archive_digest: Present { value: sha256_digest(hex: "2b4de6a81e7f386af0e01ad709159dd71ccef7585d85674b4647f1b361778a76") }, + }, + AltraLibraryArchive { + subject: BetaFirmwareDistribution { listing: asrock_altrad8ud_beta_scp_2_10_20260210_listing }, + archive_name: "ALTRAD8UD-1L2T(2.10.20260210)SLIM.zip", + human_retrieval: uri_https(locator: "drive.google.com/file/d/1ruwky8Ye7WWLndhcWIxVr1pBRzol6Ctb/view"), + archive_bytes: byte_size(count: 168971), + archive_digest: Present { value: sha256_digest(hex: "8eac1c95fd35e36d52b2ee0ef3c896e75708aff763a591524cb50fe7cb8f610b") }, + }, +] + +// THE FLASHABLE MEMBER INSIDE EACH FIRMWARE ARCHIVE. An archive digest identifies +// the download; a flash consumes the member, a different byte sequence. Where the +// member is an OpenBMC push tar its MANIFEST version is recorded as read, because it +// need not equal the archive's name: the SCP archive named 2.10.20260210 carries +// obmc-scp-altrad8ud-1l2t-4.01.tar whose MANIFEST reads version=4.01, Type=scp, so a +// BMC reading back that push reports 4.01. Measured 2026-10-08. +type AltraLibraryMemberManifest + = OpenBmcManifestVersion { version: NonEmptyStr } + | NoOpenBmcManifest + +type AltraLibraryMember { + archive_name: NonEmptyStr + member_name: NonEmptyStr + member_bytes: ByteSize + member_digest: Digest + manifest: AltraLibraryMemberManifest +} + +data altra_firmware_archive_members: List = [ + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(3.10)ROM.zip", + member_name: "ALTRAD8UD-1L2T_3.10.ROM", + member_bytes: byte_size(count: 33554432), + member_digest: sha256_digest(hex: "1c8b1a9375db36bd3aba3192f0f535808addc72fd7d6a673030bd98e8a100009"), + manifest: NoOpenBmcManifest, + }, + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(03.22.00)BMC.zip", + member_name: asrock_altrad8ud_bmc_3_22_00_push_artifact_name, + member_bytes: asrock_altrad8ud_bmc_3_22_00_push_artifact_bytes, + member_digest: asrock_altrad8ud_bmc_3_22_00_push_artifact_digest, + manifest: OpenBmcManifestVersion { version: "3.22.00" }, + }, + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(4.01)TAR.zip", + member_name: "obmc-bios-altrad8ud-1l2t-4.01.all.tar", + member_bytes: byte_size(count: 33566720), + member_digest: sha256_digest(hex: "33937359ef707cf1a2bf2c55bf6c944011ed28c41cd928ba1785e5a14193c7a6"), + manifest: OpenBmcManifestVersion { version: "4.01" }, + }, + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(4.01)ROM.zip", + member_name: "ALTRAD8UD-1L2T_4.01.ROM", + member_bytes: byte_size(count: 33554432), + member_digest: sha256_digest(hex: "85d549ad93bc54ed2cce9aa6ec883880bb90253286f1f0dfb4cc8d3ac4288efa"), + manifest: NoOpenBmcManifest, + }, + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(2.10.20260210)SCP.zip", + member_name: "obmc-scp-altrad8ud-1l2t-4.01.tar", + member_bytes: byte_size(count: 266240), + member_digest: sha256_digest(hex: "c3f6b8903c93c8bb8d51d865ff71c6c77a5dd0c0d400e180674bdd0b8b8ac248"), + manifest: OpenBmcManifestVersion { version: "4.01" }, + }, + AltraLibraryMember { + archive_name: "ALTRAD8UD-1L2T(2.10.20260210)SLIM.zip", + member_name: "altra_scp_signed_2.10.20260210.slim", + member_bytes: byte_size(count: 261120), + member_digest: sha256_digest(hex: "d7f748c6f29fbf02f9079bd19f9b4959e645425e20996c058481d6c282cca57e"), + manifest: NoOpenBmcManifest, + }, ] type AltraFirmwareReferenceReport { drive_archives: List + archive_members: List ssif_patch_bundle: AltraSsifPatchBundle beta_reference: Uri beta_disclaimer: NonEmptyStr @@ -109,6 +207,7 @@ fn beta_bmc_version_comparisons( fn reference_report() -> AltraFirmwareReferenceReport { AltraFirmwareReferenceReport { drive_archives: altra_firmware_drive_archives, + archive_members: altra_firmware_archive_members, ssif_patch_bundle: altra_ssif_v6, beta_reference: asrock_altrad8ud_download_authority.uri, beta_disclaimer: asrock_altrad8ud_beta_disclaimer, diff --git a/dag/gunbc/fleet/convergence_fold.dag b/dag/gunbc/fleet/convergence_fold.dag index 432da9faf74..ae8065babc0 100644 --- a/dag/gunbc/fleet/convergence_fold.dag +++ b/dag/gunbc/fleet/convergence_fold.dag @@ -42,12 +42,18 @@ import gunbc.host_convergence_census { // independent readback does not ground. Incomplete is neither refused (the domain decided not to act, // or acting was refused before any effect) nor converged, and it never satisfies a precondition. // +// WHAT THIS HOME CARRIES SINCE O1c-3, with BmcSecure as first gated consumer +// (gunbc.machine_intake_arrival_converge): a step is Ungated or GatedOnAuthorization; a gated +// instance whose receipt declares that authorization was required (the Apply arm) refuses without a +// discharge for that (run, step, subject); Noop does not require one (parent D4). A discharge's +// principal is NonEmptyStr at the sole constructor, so the fold does not re-check emptiness. +// The pattern itself is selected before the fold (DESIGN §3d); the fold only checks discharge. +// // WHAT IT STILL DOES NOT CARRY, because no consumer exercises it yet and a designed fixture is not an -// inhabitant (DESIGN §3 pairing obligation): the authorization gate and its discharge, principals, -// mutation lanes, pending with re-attach across runs, subject quantifiers (all-of / any-of over a -// subject set), the domain-declared cheap readback, and order derived from preconditions alone where -// no order authority exists. Each lands with its first real consumer: O1c-3 (BmcSecure and -// ManagedHostAdmission), the Spark arm. +// inhabitant (DESIGN §3 pairing obligation): pending with re-attach across runs, subject quantifiers +// (all-of / any-of over a subject set), the domain-declared cheap readback, and order derived from +// preconditions alone where no order authority exists. Each lands with its first real consumer: the +// Spark arm. // A RUN AND A SUBJECT ARE THE CALLER'S IDENTITIES, BRANDED SO NEITHER IS READ AS THE OTHER. The // subject key is the domain's canonical spelling of what one step instance converges: for the arrival @@ -96,10 +102,26 @@ fn precondition_key(p: StepPrecondition, key: StepInstanceKey) -> StepInstanceKe // A STEP: its census identity, the ONE member of the order authority it is bound to, and its // preconditions. The member type is the authority's own (for the arrival prefix // gunbc.machine_intake_phase IntakePhase); the fold reads it only through the authority's rank. +// A STEP IS UNGATED, OR GATED ON AN AUTHORIZATION DISCHARGED FOR THAT INSTANCE. Selected before the +// fold: the caller names which pattern discharged it. The fold never selects a pattern. +type StepGate + = StepUngated + | StepGatedOnAuthorization + type ConvergenceStep { identity: DeclarationRef order_member: M preconditions: List + gate: StepGate +} + +fn ungated_readonly_step(identity: DeclarationRef, order_member: M, preconditions: List) -> ConvergenceStep { + ConvergenceStep { + identity: identity, + order_member: order_member, + preconditions: preconditions, + gate: StepUngated, + } } // THE ORDER AUTHORITY: which declaration owns the order, and its ranking. Absent is an unranked @@ -309,17 +331,48 @@ fn step_outcome_incomplete(outcome: StepOutcome) -> I? { type ConvergenceReceiptProjection sole_constructor { key_of: fn(R) -> StepInstanceKey consumed_of: fn(R) -> List + authorization_required_of: fn(R) -> Bool } -fn convergence_receipt_projection(key_of: fn(R) -> StepInstanceKey, consumed_of: fn(R) -> List) -> ConvergenceReceiptProjection +fn convergence_receipt_projection(key_of: fn(R) -> StepInstanceKey, consumed_of: fn(R) -> List, authorization_required_of: fn(R) -> Bool) -> ConvergenceReceiptProjection admit_callers: [ decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "arrival_receipt_projection"), decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_a_precondition_is_not_satisfied_by_a_receipt_for_another_subject"), decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_a_precondition_is_not_satisfied_by_a_receipt_from_another_run"), decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_an_incomplete_instance_satisfies_no_dependent_and_the_run_is_incomplete"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_noop_needs_no_discharge"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_without_discharge_refuses"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_with_the_mtjade1_standing_grant_converges_over_the_dry_world"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "apply_fold_undischarged"), + ] +{ + ConvergenceReceiptProjection { key_of: key_of, consumed_of: consumed_of, authorization_required_of: authorization_required_of } +} + +// A DISCHARGE FOR ONE GATED INSTANCE, sealed: the domain verifies the selected pattern (operator +// capability, standing grant coverage, or federated workload principal) and mints this row; the fold +// only checks that a gated Apply has one for this key. Noop never requires it. +type InstanceAuthorization sole_constructor { + key: StepInstanceKey + principal: NonEmptyStr +} + +fn instance_authorization(key: StepInstanceKey, principal: NonEmptyStr) -> InstanceAuthorization + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "discharge_bmc_secure_apply"), + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "capability_discharge"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_without_discharge_refuses"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_noop_needs_no_discharge"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "apply_fold_undischarged"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_discharge_for_another_run_refuses"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_discharge_for_another_unit_refuses"), ] { - ConvergenceReceiptProjection { key_of: key_of, consumed_of: consumed_of } + InstanceAuthorization { key: key, principal: principal } +} + +fn instance_authorization_for(discharges: List, key: StepInstanceKey) -> InstanceAuthorization? { + discharges |> filter(d => step_instance_key_eq(a: d.key, b: key)) |> first() } type StepInstanceRefusal @@ -327,6 +380,7 @@ type StepInstanceRefusal | StepOutcomeAbsent | PreconditionUnmet { precondition: StepInstanceKey, consumed: List } | ConsumedReceiptNotAPrecondition { consumed: StepInstanceKey } + | AuthorizationUndischarged type StepInstanceStanding = InstanceConverged { key: StepInstanceKey, receipt: R } @@ -472,6 +526,7 @@ fn walk_instance( subject: StepSubjectKey, outcomes: List>, projection: ConvergenceReceiptProjection, + discharges: List, ) -> LedgerWalk { let key = step_instance_key(run: run, step: ranked.step.identity, subject: subject) match acc.line { @@ -483,11 +538,31 @@ fn walk_instance( Present { value: o } => match o { StepRefused { key: _, cause: c } => InstanceRefused { key: key, cause: DomainRefused { cause: c } } - StepIncomplete { key: _, cause: i } => InstanceIncomplete { key: key, cause: i } + StepIncomplete { key: _, cause: i } => + match ranked.step.gate { + StepUngated => InstanceIncomplete { key: key, cause: i } + StepGatedOnAuthorization => + match instance_authorization_for(discharges: discharges, key: key) { + Absent => InstanceRefused { key: key, cause: AuthorizationUndischarged } + Present { value: _ } => InstanceIncomplete { key: key, cause: i } + } + } StepEstablished { receipt: r } => match precondition_refusal(step: ranked.step, key: key, consumed: projection.consumed_of(r), ledger: acc.instances) { Present { value: why } => InstanceRefused { key: key, cause: why } - Absent => InstanceConverged { key: key, receipt: r } + Absent => + match ranked.step.gate { + StepUngated => InstanceConverged { key: key, receipt: r } + StepGatedOnAuthorization => + if projection.authorization_required_of(r) { + match instance_authorization_for(discharges: discharges, key: key) { + Absent => InstanceRefused { key: key, cause: AuthorizationUndischarged } + Present { value: _ } => InstanceConverged { key: key, receipt: r } + } + } else { + InstanceConverged { key: key, receipt: r } + } + } } } } @@ -496,20 +571,19 @@ fn walk_instance( } } -// THE RUN FOLD: one ledger per run, each instance converged, refused, incomplete or not reached, in the admitted -// order. The verdict is derived from the ledger and names the instance the line stopped at. fn fold_convergence_run( plan: AdmittedConvergencePlan, run: ConvergenceRunId, subject: StepSubjectKey, outcomes: List>, projection: ConvergenceReceiptProjection, + discharges: List, ) -> ConvergenceRunVerdict { match first_outcome_refusal(run: run, subject: subject, plan: plan, keys: map(outcomes, o => outcome_key(outcome: o, projection: projection))) { Present { value: r } => RunOutcomesRefused { cause: r } Absent => { let walked = fold(plan.steps, init: LedgerWalk { line: LineOpen, instances: [] }, f: (acc, s) => - walk_instance(acc: acc, ranked: s, run: run, subject: subject, outcomes: outcomes, projection: projection)) + walk_instance(acc: acc, ranked: s, run: run, subject: subject, outcomes: outcomes, projection: projection, discharges: discharges)) let ledger = ConvergenceRunLedger { run: run, subject: subject, order_authority: plan.authority, instances: walked.instances } match walked.line { LineOpen => RunConverged { run: ConvergedRun { ledger: ledger } } diff --git a/dag/gunbc/fleet/fleet_converge_workflow.dag b/dag/gunbc/fleet/fleet_converge_workflow.dag index a5898bdb942..bdbd9448754 100644 --- a/dag/gunbc/fleet/fleet_converge_workflow.dag +++ b/dag/gunbc/fleet/fleet_converge_workflow.dag @@ -16,7 +16,11 @@ import gunbc.namecheap.observation_artifact { namecheap_observation_receipt_path import gunbc.fleet.printer_federation { printer_environment, printer_provider_resource, printer_service_account,printer_lan_host_identity } import gunbc.ci_spec { gunbc_ci_printer_workflow_invoke } import gunbc.namecheap.federation { namecheap_environment, namecheap_provider_resource, namecheap_service_account } -import gunbc.ci_spec { gunbc_ci_namecheap_observe_invoke, gunbc_ci_sm_max_time } +import gunbc.auth.mtjade1_arrival_pair { + mtjade1_arrival_environment, mtjade1_arrival_provider_resource, mtjade1_arrival_service_account, +} +import gunbc.machine_intake_arrival_subject { arrival_converge_subject_options } +import gunbc.ci_spec { gunbc_ci_namecheap_observe_invoke, gunbc_ci_arrival_admit_invoke, gunbc_ci_arrival_converge_invoke, gunbc_ci_sm_max_time } import gunbc.runner_microvm_slot_unit { microvm_slot_unit_timeout_stop, microvm_slot_start_receipt_path, microvm_slot_reserve_receipt_path, microvm_slot_reserve_wait_bound } import std.measure { Second, minute_count, second_count } @@ -46,9 +50,9 @@ import gunbc.cloudflare.r2_conditional_put_race_probe { race_probe_receipt_path import gunbc.machine_intake_mtcollins1_fan_observe { mtcollins1_fan_observation_receipt_path } import gunbc.machine_intake_mtcollins1_kvm_observer_observe { mtcollins1_kvm_observer_probe_bound, mtcollins1_kvm_observer_probe_artifact_glob } import gunbc.machine_intake_megarac_kvm_still { megarac_kvm_observer_release_allowance } -import gunbc.machine_intake_mtcollins1_ui_bundle_observe { mtcollins1_ui_bundle_artifact_glob, mtcollins1_ui_bundle_read_bound } import gunbc.fabric_writer_identity_observe { fabric_writer_identity_receipt_path } -import gunbc.fabric.workspace_source { workspace_source_receipt_path } +import gunbc.fabric.workspace_source { workspace_source_receipt_path, workspace_agent_state_slot_env } +import gunbc.fabric.workspace_transcript_rate { workspace_transcript_rate_receipt_path } import gunbc.fabric.workspace_checkpoint_instrument { workspace_measure_receipt_path, workspace_restore_receipt_path, workspace_source_object_env } import gunbc.machine_intake_mtcollins1_census_image_publish { mtcollins1_census_image_receipt_path } import gunbc.machine_intake_mtcollins1_census_member_readback { census_member_readback_receipt_path } @@ -129,14 +133,14 @@ import gunbc.managed_host_unit_hold { UnitHoldHostAdmission, UnitHoldHostAdmitted, UnitHoldHostNotManaged, UnitHoldHostNotRequired, unit_hold_admission_for, unit_hold_host_admission_refusal, } -import gunbc.fleet_host_identity { operator_host_mtcollins1 } +import gunbc.fleet_host_identity { operator_host_mtcollins1, operator_host_srv1 } import gunbc.auth.gcp_iam_converge_federation { gcp_iam_converge_environment, gcp_iam_observe_wif_provider_resource } import gunbc.auth.gcp_iam_converge { iam_observe_service_account, iam_converge_service_account, iam_converge_token_lifetime } import gunbc.auth.gcp_iam_converge_run { gcp_iam_converge_step_timeout_minutes, iam_converge_token_env, gcp_iam_converge_approved_output } import gunbc.auth.approval_broker_endpoint { approval_broker_host_identity } import gunbc.auth.managed_host_boot_federation { - ManagedHostBootFederationStanding, BootFederationProvisioned, BootFederationUnprovisioned, - boot_federation_for, boot_federation_provider_resource, + ManagedHostBootFederationStanding, BootFederationProvisioned, BootFederationProvisionedUnderDeclaredDrop, + BootFederationUnprovisioned, boot_federation_for, boot_federation_provider_resource, } import gunbc.fleet_host_identity { operator_host_mtcollins1 } import gunbc.auth.ci_app_key_rotation { @@ -159,7 +163,6 @@ import gunbc.ci_spec { gunbc_ci_app_key_version_verify_mint_script, gunbc_ci_app_key_version_verify_verdict_invoke, gunbc_ci_mtcollins1_fan_observe_invoke, - gunbc_ci_mtcollins1_ui_bundle_observe_invoke, gunbc_ci_mtcollins1_kvm_observer_observe_invoke, gunbc_ci_mtcollins1_kvm_observer_release_invoke, gunbc_ci_fabric_writer_identity_observe_invoke, @@ -179,6 +182,7 @@ import gunbc.ci_spec { gunbc_ci_workspace_source_pack_invoke, gunbc_ci_workspace_checkpoint_measure_invoke, gunbc_ci_workspace_checkpoint_restore_invoke, + gunbc_ci_workspace_transcript_rate_observe_invoke, gunbc_ci_approval_keyring_converge_invoke, gunbc_ci_approval_device_enrolment_code_issue_invoke, gunbc_ci_mtcollins1_boot_invoke, @@ -235,7 +239,7 @@ import gunbc.ci_spec { gunbc_rlm_launch_deployment_receipt_artifact_dir, } import gunbc.fleet_converge_receipt { fleet_converge_plan_run_id_env_name } -import gunbc.actions_run_binding { rlm_expected_revision_env_name, fleet_converge_expected_host_env_name } +import gunbc.actions_run_binding { rlm_expected_revision_env_name, fleet_converge_expected_host_env_name, fleet_converge_arrival_subject_env_name } import gunbc.host_credential_custody_converge { host_custody_credential_options, host_credential_custody_env_name } import gunbc.spark.credential_workflow { spark_administrator_credential_roster_hosts } import gunbc.fleet_runner_connectivity { fleet_hosts } @@ -257,7 +261,7 @@ import gunbc.host_reset_return { host_reset_return_receipt_path, host_reset_observer_label_env, } -import gunbc.host_reset_subject_roster { reset_observer_peer_roster, reset_subject_roster } +import gunbc.host_reset_subject_roster { reset_observer_peer_roster, reset_roster_hosts, reset_subject_roster } import gunbc.host_reset_return { ResetObserverPairing } import gunbc.live_deploy.apply_receipt { rlm_plan_run_id_env_name, rlm_apply_run_id_env_name, rlm_plan_artifact_hash_env_name } import gunbc.live_deploy.apply_receipt { rlm_dashboard_run_id_env_name } @@ -363,12 +367,12 @@ type FleetConvergeWorkflowMode | WorkspaceSourcePack | WorkspaceCheckpointMeasure | WorkspaceCheckpointRestore + | WorkspaceTranscriptRateObserve | ApprovalKeyringConverge | ApprovalDeviceEnrolmentCodeIssue | MtCollins1Boot | PairServingD0 | MtCollins1FanObserve - | MtCollins1UiBundleObserve | MtCollins1KvmObserverObserve | FabricWriterIdentityObserve | MtCollins1CensusImagePublish @@ -379,6 +383,7 @@ type FleetConvergeWorkflowMode | OrgRunnerRosterObserve | GcpIamConverge | NamecheapObserve + | ArrivalConverge | PrinterWorkflow fn fleet_converge_workflow_mode_wire(mode: FleetConvergeWorkflowMode) -> String { @@ -442,15 +447,16 @@ fn fleet_converge_workflow_mode_wire(mode: FleetConvergeWorkflowMode) -> String WorkspaceSourcePack => "workspace_source_pack" WorkspaceCheckpointMeasure => "workspace_checkpoint_measure" WorkspaceCheckpointRestore => "workspace_checkpoint_restore" + WorkspaceTranscriptRateObserve => "workspace_transcript_rate_observe" ApprovalKeyringConverge => "approval_keyring_converge" ApprovalDeviceEnrolmentCodeIssue => "approval_device_enrolment_code_issue" MtCollins1Boot => "mtcollins1_boot" GcpIamConverge => "gcp_iam_converge" NamecheapObserve => "namecheap_observe" + ArrivalConverge => "arrival_converge" PrinterWorkflow => "printer" PairServingD0 => "pair_serving_d0" MtCollins1FanObserve => "mtcollins1_fan_observe" - MtCollins1UiBundleObserve => "mtcollins1_ui_bundle_observe" MtCollins1KvmObserverObserve => "mtcollins1_kvm_observer_observe" FabricWriterIdentityObserve => "fabric_writer_identity_observe" MtCollins1CensusImagePublish => "mtcollins1_census_image_publish" @@ -483,7 +489,7 @@ fn fleet_converge_spark_target_description() -> String { ], "") } -data fleet_converge_workflow_modes: List = [FullHostPlan, LaunchEnvironmentPlan, AllocationStorePlan, WorkspaceCommissioningPlan, FleetApply, OrgActionsObserve, AppControlPlaneObserve, MicrovmHostConverge, MicrovmNetworkObserve, MicrovmNetworkApply, GuestImageObserve, GuestImageConverge, MicrovmBootProbe, SparkGrants, SparkBootstrap, SparkServingApply, SparkNativeServingApply, SparkRuntimeImageProbe, SparkWirelessLinkConverge, SparkGrantsObserve, SparkV41CheckpointMaterialize, SparkV41RowStoreEncode, SparkV41RowStoreReadback, SparkV41EngramDifferential, SparkRuntimeImageProduce, SparkRuntimeImageDistribute, SparkArmGroupLoad, SparkArmCheckpointMaterialize, SparkArmGroupObserve, SparkArmGroupLaunchPlan, SparkArmGroupLaunch, SparkV41ServingLoad, DashboardDeploy, ApprovalBrokerDarkInstall, MicrovmControllerInstall, MicrovmSlotReserve, MicrovmSlotStart, RlmLaunchDeploymentReceipt, HostResetReturn, RunnerHostFileObserve, RunnerHostFileConverge, SitePxeEdgeObserve, SitePxeEdgeConverge, RunnerPasswordSessionToolConverge, RunnerBrowserToolchainConverge, R2MintPreflight, R2ObjectWriteMint, OrgRunnerRosterObserve, ApprovalKeyringConverge, ApprovalDeviceEnrolmentCodeIssue, MtCollins1Boot, MtCollins1FanObserve, MtCollins1UiBundleObserve, MtCollins1KvmObserverObserve, MtCollins1CensusImagePublish, MtCollins1CensusMemberReadback, MtCollins1CensusQemuHostObserve, MtCollins1CensusQemuToolchainConverge, HostCredentialCustodyConverge, AppKeyVersionVerify, R2BucketEnsure, R2BucketAdminMint, R2WorkspaceObjectReadMint, R2WorkspaceObjectWriteMint, R2CacheObjectReadMint, R2CacheObjectWriteMint, R2ConditionalPutRaceProbe, WorkspaceSourcePack, WorkspaceCheckpointMeasure, WorkspaceCheckpointRestore, FabricWriterIdentityObserve, PairServingD0, MicrovmRunnerGroupEnsure, GcpIamConverge, NamecheapObserve, PrinterWorkflow] +data fleet_converge_workflow_modes: List = [FullHostPlan, LaunchEnvironmentPlan, AllocationStorePlan, WorkspaceCommissioningPlan, FleetApply, OrgActionsObserve, AppControlPlaneObserve, MicrovmHostConverge, MicrovmNetworkObserve, MicrovmNetworkApply, GuestImageObserve, GuestImageConverge, MicrovmBootProbe, SparkGrants, SparkBootstrap, SparkServingApply, SparkNativeServingApply, SparkRuntimeImageProbe, SparkWirelessLinkConverge, SparkGrantsObserve, SparkV41CheckpointMaterialize, SparkV41RowStoreEncode, SparkV41RowStoreReadback, SparkV41EngramDifferential, SparkRuntimeImageProduce, SparkRuntimeImageDistribute, SparkArmGroupLoad, SparkArmCheckpointMaterialize, SparkArmGroupObserve, SparkArmGroupLaunchPlan, SparkArmGroupLaunch, SparkV41ServingLoad, DashboardDeploy, ApprovalBrokerDarkInstall, MicrovmControllerInstall, MicrovmSlotReserve, MicrovmSlotStart, RlmLaunchDeploymentReceipt, HostResetReturn, RunnerHostFileObserve, RunnerHostFileConverge, SitePxeEdgeObserve, SitePxeEdgeConverge, RunnerPasswordSessionToolConverge, RunnerBrowserToolchainConverge, R2MintPreflight, R2ObjectWriteMint, OrgRunnerRosterObserve, ApprovalKeyringConverge, ApprovalDeviceEnrolmentCodeIssue, MtCollins1Boot, MtCollins1FanObserve, MtCollins1UiBundleObserve, MtCollins1KvmObserverObserve, MtCollins1CensusImagePublish, MtCollins1CensusMemberReadback, MtCollins1CensusQemuHostObserve, MtCollins1CensusQemuToolchainConverge, HostCredentialCustodyConverge, AppKeyVersionVerify, R2BucketEnsure, R2BucketAdminMint, R2WorkspaceObjectReadMint, R2WorkspaceObjectWriteMint, R2CacheObjectReadMint, R2CacheObjectWriteMint, R2ConditionalPutRaceProbe, WorkspaceSourcePack, WorkspaceCheckpointMeasure, WorkspaceCheckpointRestore, WorkspaceTranscriptRateObserve, FabricWriterIdentityObserve, PairServingD0, MicrovmRunnerGroupEnsure, GcpIamConverge, NamecheapObserve, PrinterWorkflow, ArrivalConverge] // WHICH RELEASE PRODUCT A MODE'S JOB RUNS, NAMED PER MODE WITH NO WILDCARD (as the scope and fleet-key // matches above name theirs). A mode whose steps run only gunbc and claim_executor declares the // compiler pair (gunbc.fleet_release_bins_key compiler_pair_release_product): its build job then @@ -549,12 +555,12 @@ fn fleet_converge_mode_release_product(mode: FleetConvergeWorkflowMode) -> Fleet WorkspaceSourcePack => CompilerPairOnly WorkspaceCheckpointMeasure => CompilerPairOnly WorkspaceCheckpointRestore => CompilerPairOnly + WorkspaceTranscriptRateObserve => CompilerPairOnly OrgRunnerRosterObserve => SixteenBinaryPack ApprovalKeyringConverge => SixteenBinaryPack ApprovalDeviceEnrolmentCodeIssue => CompilerPairOnly MtCollins1Boot => SixteenBinaryPack MtCollins1FanObserve => SixteenBinaryPack - MtCollins1UiBundleObserve => SixteenBinaryPack MtCollins1KvmObserverObserve => SixteenBinaryPack MtCollins1CensusImagePublish => SixteenBinaryPack MtCollins1CensusMemberReadback => SixteenBinaryPack @@ -569,6 +575,7 @@ fn fleet_converge_mode_release_product(mode: FleetConvergeWorkflowMode) -> Fleet MicrovmRunnerGroupEnsure => SixteenBinaryPack GcpIamConverge => SixteenBinaryPack NamecheapObserve => SixteenBinaryPack + ArrivalConverge => SixteenBinaryPack PrinterWorkflow => SixteenBinaryPack } } @@ -664,15 +671,16 @@ fn fleet_converge_mode_scope(mode: FleetConvergeWorkflowMode) -> FleetConvergeSc WorkspaceSourcePack => none WorkspaceCheckpointMeasure => none WorkspaceCheckpointRestore => none + WorkspaceTranscriptRateObserve => none ApprovalKeyringConverge => none ApprovalDeviceEnrolmentCodeIssue => none MtCollins1Boot => none GcpIamConverge => none NamecheapObserve => none + ArrivalConverge => none PrinterWorkflow => none PairServingD0 => none MtCollins1FanObserve => none - MtCollins1UiBundleObserve => none MtCollins1KvmObserverObserve => none FabricWriterIdentityObserve => none MtCollins1CensusImagePublish => none @@ -768,6 +776,7 @@ fn fleet_converge_mode_fleet_ssh_key_demand(mode: FleetConvergeWorkflowMode) -> WorkspaceSourcePack => FleetSshKeyNotConsumed WorkspaceCheckpointMeasure => FleetSshKeyNotConsumed WorkspaceCheckpointRestore => FleetSshKeyNotConsumed + WorkspaceTranscriptRateObserve => FleetSshKeyNotConsumed FullHostPlan => FleetSshKeyConsumed LaunchEnvironmentPlan => FleetSshKeyConsumed AllocationStorePlan => FleetSshKeyConsumed @@ -820,7 +829,6 @@ fn fleet_converge_mode_fleet_ssh_key_demand(mode: FleetConvergeWorkflowMode) -> MtCollins1Boot => FleetSshKeyConsumed PairServingD0 => FleetSshKeyConsumed MtCollins1FanObserve => FleetSshKeyNotConsumed - MtCollins1UiBundleObserve => FleetSshKeyNotConsumed MtCollins1KvmObserverObserve => FleetSshKeyNotConsumed FabricWriterIdentityObserve => FleetSshKeyNotConsumed MtCollins1CensusImagePublish => FleetSshKeyConsumed @@ -830,6 +838,7 @@ fn fleet_converge_mode_fleet_ssh_key_demand(mode: FleetConvergeWorkflowMode) -> MicrovmRunnerGroupEnsure => FleetSshKeyNotConsumed GcpIamConverge => FleetSshKeyNotConsumed NamecheapObserve => FleetSshKeyNotConsumed + ArrivalConverge => FleetSshKeyNotConsumed PrinterWorkflow => FleetSshKeyNotConsumed } } @@ -876,7 +885,7 @@ data fleet_converge_mode_options: List = map(fleet_converge_workflow_mod // the one a later worker would most plausibly act on is the one that disarms the corroboration. // gunbc.host_reset_return carries the full argument, above reset_return_verdict_of_mint. data fleet_converge_reset_assignment: ResetDispatchAssignment = reset_dispatch_pairings( - subject_roster: reset_subject_roster(), + subject_roster: reset_roster_hosts(entries: reset_subject_roster()), peer_roster: reset_observer_peer_roster(), ) @@ -1490,12 +1499,12 @@ fn fleet_converge_mode_mutation_domain(mode: FleetConvergeWorkflowMode) -> Fleet WorkspaceSourcePack => ExecutorDomain WorkspaceCheckpointMeasure => ExecutorDomain WorkspaceCheckpointRestore => ExecutorDomain + WorkspaceTranscriptRateObserve => ExecutorDomain ApprovalKeyringConverge => ExecutorDomain ApprovalDeviceEnrolmentCodeIssue => SubjectDomain { group: fleet_subject_mutation_group_approval_device_store } MtCollins1Boot => ExecutorDomain PairServingD0 => SubjectDomain { group: fleet_arm_mutation_group_deepseek_group_a } MtCollins1FanObserve => ExecutorDomain - MtCollins1UiBundleObserve => ExecutorDomain MtCollins1KvmObserverObserve => ExecutorDomain FabricWriterIdentityObserve => ExecutorDomain MtCollins1CensusImagePublish => ExecutorDomain @@ -1506,6 +1515,7 @@ fn fleet_converge_mode_mutation_domain(mode: FleetConvergeWorkflowMode) -> Fleet OrgRunnerRosterObserve => ExecutorDomain GcpIamConverge => ExecutorDomain NamecheapObserve => ExecutorDomain + ArrivalConverge => ExecutorDomain PrinterWorkflow => ExecutorDomain } } @@ -1538,6 +1548,16 @@ data fleet_converge_mutation_concurrency_group_expression: String = join([ "format('", fleet_host_mutation_group_prefix, "{0}', ", fleet_host_input_expression, ") }}", ], "") +fn fleet_converge_exclusive_job_concurrency(group: String) -> ConcurrencySpec? { + Present { + value: ConcurrencyMappingQueueNotMax { + group: group, + cancel_in_progress: Present { value: CancelInProgressBool { value: false } }, + explicit_single: none + } + } +} + data fleet_converge_spark_runtime_image_probe_step_if: String = fleet_converge_mode_step_if(mode: SparkRuntimeImageProbe) data fleet_converge_spark_wireless_link_converge_step_if: String = fleet_converge_mode_step_if(mode: SparkWirelessLinkConverge) data fleet_converge_spark_grants_observe_step_if: String = fleet_converge_mode_step_if(mode: SparkGrantsObserve) @@ -2402,51 +2422,18 @@ fn fleet_converge_mtcollins1_fan_observe_step() -> Step { } } -// MT. COLLINS' OWN UI BUNDLE AND ITS DIGEST (gunbc.machine_intake_mtcollins1_ui_bundle_observe): the -// firmware revision over IPMI, then the served source.min.js bytes, only under revision 0.32. Reads -// only, no web session. The receipt and the bytes are uploaded whatever the outcome, because a refused -// run's receipt is the one that says why it holds no bytes; a step condition without a status -// function is implicitly success() and would drop exactly that receipt. // A READ-ONLY MT. COLLINS PROBE STEP'S BUDGET IS A FOLD, NEVER A LITERAL, over the step's phases in // order, each counted once and rounded once: gunbc run's load of the probe's closure, whose authority is // the measured resolve budget (gunbc.fleet_workflow_steps gunbc_ci_gunbc_run_resolve_step_timeout_minutes), // then the step's own phases -- for a probe, the credential prelude's one bounded Secret Manager read // (gunbc.ci_spec gunbc_ci_sm_max_time; the prelude's other lines are local file writes) and the probe's // declared bound; for the always() release, the owned release alone. The flat aux budget -// is what killed the UI bundle's first wet run (36759368077) while it was still typechecking. +// is what killed a probe's first wet run while it was still typechecking. fn fleet_converge_mtcollins1_probe_step_timeout_minutes(phases: List) -> Int { let secs = fold(phases, gunbc_ci_gunbc_run_resolve_step_timeout_minutes * 60, (acc, p) => acc + (second_count(s: p) as Int)) (secs + 59) / 60 } -data fleet_converge_mtcollins1_ui_bundle_observe_step_if: String = fleet_converge_mode_step_if(mode: MtCollins1UiBundleObserve) - -fn fleet_converge_mtcollins1_ui_bundle_observe_step() -> Step { - RunStep { - name: Present { value: "Mt. Collins served UI: firmware revision, then source.min.js, viewer.html and every script they load, each with its sha256 (reads only)" }, - id: Present { value: "mtcollins1_ui_bundle_observe" }, - run: gunbc_ci_mtcollins1_ui_bundle_observe_invoke(), - shell: none, - env: Present { value: [ - kv(key: "WIF_ACCESS_TOKEN", value: yaml_string(s: "${{ steps.wif_auth.outputs.access_token }}")), - ] }, - working_directory: none, - if_condition: Present { value: fleet_converge_mtcollins1_ui_bundle_observe_step_if }, - continue_on_error: none, - timeout_minutes: Present { value: fleet_converge_mtcollins1_probe_step_timeout_minutes(phases: [gunbc_ci_sm_max_time, mtcollins1_ui_bundle_read_bound()]) } - } -} - -fn fleet_converge_mtcollins1_ui_bundle_observe_upload_step() -> Step { - fleet_converge_receipt_upload_step( - id: "mtcollins1_ui_bundle_observe_upload", - name: "Upload Mt. Collins served UI files and receipt", - artifact: "mtcollins1-ui-bundle", - path: mtcollins1_ui_bundle_artifact_glob, - if_condition: join(["always() && ", fleet_converge_mtcollins1_ui_bundle_observe_step_if], "") - ) -} - // THE REAL KVM OBSERVER AGAINST THE REAL BMC, NO BOOT (gunbc.machine_intake_mtcollins1_kvm_observer_observe): // under the unit hold, log in, establish the viewer, take one still, release the web session and the // browser. Nothing is written to media, power, boot device or SEL. The journal, its pre-teardown @@ -2522,6 +2509,7 @@ data fleet_converge_fabric_writer_identity_observe_receipt_if: String = join(["a data fleet_converge_workspace_source_pack_step_if: String = fleet_converge_mode_step_if(mode: WorkspaceSourcePack) data fleet_converge_workspace_checkpoint_measure_step_if: String = fleet_converge_mode_step_if(mode: WorkspaceCheckpointMeasure) data fleet_converge_workspace_checkpoint_restore_step_if: String = fleet_converge_mode_step_if(mode: WorkspaceCheckpointRestore) +data fleet_converge_workspace_transcript_rate_observe_step_if: String = fleet_converge_mode_step_if(mode: WorkspaceTranscriptRateObserve) // THE INSTRUMENT STEPS' ENVELOPE, STATED AS AN ENVELOPE, NOT A MEASUREMENT: the time these steps take // IS what stage 0 measures (a twenty-thousand-file workspace hashed and put one chunk at a time), so @@ -2544,7 +2532,9 @@ fn fleet_converge_workspace_instrument_step(name: String, id: String, run: Strin } fn fleet_converge_workspace_source_pack_step() -> Step { - fleet_converge_workspace_instrument_step(name: "Workspace source pack: the operator workspace minus credentials and build output, into the workspace bucket by its SHA-256", id: "workspace_source_pack", run: gunbc_ci_workspace_source_pack_invoke(), if_condition: fleet_converge_workspace_source_pack_step_if, extra_env: []) + fleet_converge_workspace_instrument_step(name: "Workspace source pack: the operator workspace minus credentials and build output, into the workspace bucket by its SHA-256", id: "workspace_source_pack", run: gunbc_ci_workspace_source_pack_invoke(), if_condition: fleet_converge_workspace_source_pack_step_if, extra_env: [ + kv(key: workspace_agent_state_slot_env as String, value: yaml_string(s: "${{ github.event.inputs.agent_state_slot }}")), + ]) } fn fleet_converge_workspace_source_pack_receipt_upload_step() -> Step { @@ -2587,6 +2577,37 @@ fn fleet_converge_workspace_checkpoint_restore_receipt_upload_step() -> Step { ) } +// THE TRANSCRIPT APPEND-RATE OBSERVATION (gunbc.fabric.workspace_transcript_rate) reads sizes and +// mtimes on srv1 and touches no bucket, so it carries no WIF token: it is the instrument envelope +// without the credential. Its timeout is an ENVELOPE around a declared ten-minute window plus one +// walk of the agent-state roots per interval, three times the window so a slow walk still reaches +// its receipt; the window, not this bound, is what the receipt reports. +data fleet_converge_workspace_transcript_rate_step_timeout_minutes: Duration = 30 + +fn fleet_converge_workspace_transcript_rate_observe_step() -> Step { + RunStep { + name: Present { value: "Workspace transcript append rate: size and mtime of every transcript file on srv1, sampled per interval over a declared window, content never read" }, + id: Present { value: "workspace_transcript_rate_observe" }, + run: gunbc_ci_workspace_transcript_rate_observe_invoke(), + shell: none, + env: Present { value: [kv(key: workspace_agent_state_slot_env as String, value: yaml_string(s: "${{ github.event.inputs.agent_state_slot }}"))] }, + working_directory: none, + if_condition: Present { value: fleet_converge_workspace_transcript_rate_observe_step_if }, + continue_on_error: none, + timeout_minutes: Present { value: fleet_converge_workspace_transcript_rate_step_timeout_minutes } + } +} + +fn fleet_converge_workspace_transcript_rate_observe_receipt_upload_step() -> Step { + fleet_converge_receipt_upload_step( + id: "workspace_transcript_rate_observe_receipt_upload", + name: "Upload workspace transcript append-rate receipt", + artifact: "workspace-transcript-rate-observe", + path: workspace_transcript_rate_receipt_path, + if_condition: join(["always() && ", fleet_converge_workspace_transcript_rate_observe_step_if], "") + ) +} + fn fleet_converge_fabric_writer_identity_observe_step() -> Step { RunStep { name: Present { value: "Fabric writer identity: the login this host presents at the fabric DB's served door (reads only)" }, @@ -4180,7 +4201,10 @@ data fleet_converge_checkout_ref: Expression = LogicalOr { left: LogicalAnd { left: LogicalOr { left: LogicalOr { left: fleet_converge_dispatch_mode_is(mode: MtCollins1Boot), right: fleet_converge_dispatch_mode_is(mode: GcpIamConverge) }, - right: LogicalOr { left: fleet_converge_dispatch_mode_is(mode: NamecheapObserve), right: fleet_converge_dispatch_mode_is(mode: PrinterWorkflow) }, + right: LogicalOr { + left: fleet_converge_dispatch_mode_is(mode: NamecheapObserve), + right: LogicalOr { left: fleet_converge_dispatch_mode_is(mode: ArrivalConverge), right: fleet_converge_dispatch_mode_is(mode: PrinterWorkflow) }, + }, }, right: fleet_converge_event_sha, }, @@ -4248,6 +4272,7 @@ data fleet_converge_shared_job_if: String = join([ "github.event.inputs.mode != '", fleet_converge_workflow_mode_wire(mode: MtCollins1Boot), "'", " && github.event.inputs.mode != '", fleet_converge_workflow_mode_wire(mode: GcpIamConverge), "'", " && github.event.inputs.mode != '", fleet_converge_workflow_mode_wire(mode: NamecheapObserve), "'", + " && github.event.inputs.mode != '", fleet_converge_workflow_mode_wire(mode: ArrivalConverge), "'", " && github.event.inputs.mode != '", fleet_converge_workflow_mode_wire(mode: PrinterWorkflow), "'", ], "") @@ -4263,6 +4288,8 @@ data fleet_converge_boot_federation_unprovisioned_refusal: String = "REFUSED gun fn fleet_converge_mtcollins1_boot_provider_resource() -> String { match fleet_converge_mtcollins1_boot_federation { BootFederationProvisioned { federation: f } => boot_federation_provider_resource(f: f) + BootFederationProvisionedUnderDeclaredDrop { federation: f, drop: _, standing_subject: _, current_subject: _ } => + boot_federation_provider_resource(f: f) BootFederationUnprovisioned { host: _, cause: _ } => fleet_converge_boot_federation_unprovisioned_refusal } } @@ -4270,6 +4297,8 @@ fn fleet_converge_mtcollins1_boot_provider_resource() -> String { fn fleet_converge_mtcollins1_boot_service_account() -> String { match fleet_converge_mtcollins1_boot_federation { BootFederationProvisioned { federation: f } => f.names.service_account as String + BootFederationProvisionedUnderDeclaredDrop { federation: f, drop: _, standing_subject: _, current_subject: _ } => + f.names.service_account as String BootFederationUnprovisioned { host: _, cause: _ } => fleet_converge_boot_federation_unprovisioned_refusal } } @@ -4277,6 +4306,8 @@ fn fleet_converge_mtcollins1_boot_service_account() -> String { fn fleet_converge_mtcollins1_boot_environment() -> String { match fleet_converge_mtcollins1_boot_federation { BootFederationProvisioned { federation: f } => f.names.environment as String + BootFederationProvisionedUnderDeclaredDrop { federation: f, drop: _, standing_subject: _, current_subject: _ } => + f.names.environment as String BootFederationUnprovisioned { host: _, cause: _ } => fleet_converge_boot_federation_unprovisioned_refusal } } @@ -4284,6 +4315,7 @@ fn fleet_converge_mtcollins1_boot_environment() -> String { fn fleet_converge_boot_federation_admitted() -> Bool { match fleet_converge_mtcollins1_boot_federation { BootFederationProvisioned { federation: _ } => true + BootFederationProvisionedUnderDeclaredDrop { federation: _, drop: _, standing_subject: _, current_subject: _ } => true BootFederationUnprovisioned { host: _, cause: _ } => false } } @@ -4462,8 +4494,6 @@ fn fleet_converge_job() -> Job { fleet_converge_app_key_version_verify_receipt_upload_step(), fleet_converge_mtcollins1_fan_observe_step(), fleet_converge_mtcollins1_fan_observe_receipt_upload_step(), - fleet_converge_mtcollins1_ui_bundle_observe_step(), - fleet_converge_mtcollins1_ui_bundle_observe_upload_step(), fleet_converge_mtcollins1_kvm_observer_observe_step(), fleet_converge_mtcollins1_kvm_observer_release_step(), fleet_converge_mtcollins1_kvm_observer_observe_upload_step(), @@ -4502,6 +4532,8 @@ fn fleet_converge_job() -> Job { fleet_converge_workspace_checkpoint_measure_receipt_upload_step(), fleet_converge_workspace_checkpoint_restore_step(), fleet_converge_workspace_checkpoint_restore_receipt_upload_step(), + fleet_converge_workspace_transcript_rate_observe_step(), + fleet_converge_workspace_transcript_rate_observe_receipt_upload_step(), fleet_converge_approval_keyring_converge_step(), fleet_converge_approval_device_enrolment_code_issue_step(), fleet_converge_microvm_runner_group_ensure_step(), @@ -4746,7 +4778,6 @@ fn fleet_converge_mode_job_id(mode: FleetConvergeWorkflowMode) -> NonEmptyStr { ApprovalDeviceEnrolmentCodeIssue => fleet_converge_shared_job_id MtCollins1Boot => "mtcollins1-boot" as NonEmptyStr MtCollins1FanObserve => fleet_converge_shared_job_id - MtCollins1UiBundleObserve => fleet_converge_shared_job_id MtCollins1KvmObserverObserve => fleet_converge_shared_job_id MtCollins1CensusImagePublish => fleet_converge_shared_job_id MtCollins1CensusMemberReadback => fleet_converge_shared_job_id @@ -4764,11 +4795,13 @@ fn fleet_converge_mode_job_id(mode: FleetConvergeWorkflowMode) -> NonEmptyStr { WorkspaceSourcePack => fleet_converge_shared_job_id WorkspaceCheckpointMeasure => fleet_converge_shared_job_id WorkspaceCheckpointRestore => fleet_converge_shared_job_id + WorkspaceTranscriptRateObserve => fleet_converge_shared_job_id FabricWriterIdentityObserve => fleet_converge_shared_job_id PairServingD0 => fleet_converge_shared_job_id MicrovmRunnerGroupEnsure => fleet_converge_shared_job_id GcpIamConverge => "gcp-iam-converge" as NonEmptyStr NamecheapObserve => "namecheap-observe" as NonEmptyStr + ArrivalConverge => "arrival-converge" as NonEmptyStr PrinterWorkflow => "printer" as NonEmptyStr } } @@ -4816,6 +4849,7 @@ data fleet_converge_job_edges: List = [ FleetConvergeJobEdge { id: fleet_converge_mode_job_id(mode: PrinterWorkflow), needs: [fleet_converge_release_bins_job_id as String] }, FleetConvergeJobEdge { id: fleet_converge_mode_job_id(mode: NamecheapObserve), needs: [fleet_converge_release_bins_job_id as String] }, FleetConvergeJobEdge { id: fleet_converge_mode_job_id(mode: GcpIamConverge), needs: [fleet_converge_release_bins_job_id as String] }, + FleetConvergeJobEdge { id: fleet_converge_mode_job_id(mode: ArrivalConverge), needs: [fleet_converge_release_bins_job_id as String] }, ] // A BUILDER ASKS THE ROSTER FOR ITS OWN EDGE BY ID AND THE LOOKUP REFUSES RATHER THAN INVENTS. An @@ -4853,6 +4887,7 @@ fn fleet_converge_jobs() -> List { fleet_converge_rlm_launch_deployment_receipt_job(), fleet_converge_gcp_iam_converge_job(), fleet_converge_namecheap_observe_job(), + fleet_converge_arrival_converge_job(), fleet_converge_printer_job(), ] } @@ -4983,9 +5018,16 @@ data fleet_converge_dispatch_inputs: List = [ default: none, type: InputChoice { options: fleet_converge_enrolled_host_options }, }, + DispatchInput { + name: "arrival_subject", + description: Present { value: "arrival_converge only: the arrival-population unit (not executor host, not managed_host). Options are gunbc.machine_intake_arrival_subject arrival_converge_subject_options. Names signed off msg_f03558d1" }, + required: false, + default: none, + type: InputChoice { options: arrival_converge_subject_options() }, + }, DispatchInput { name: "mode", - description: Present { value: "plan shows membership hunks; apply executes a prior plan artifact; org_actions_observe validates the org credential and refuses on runner-group drift; org_runner_roster_observe reads the organization's self-hosted runner registrations to exhaustion under the same token and refuses unless the roster is complete; app_control_plane_observe reads the gunbai-ci App registration and webhook config with a short-lived App JWT and refuses on hook-config drift; app_key_version_verify reads the gunbai-ci App private key at the EXACT Secret Manager version named by app_key_version, mints an installation token with it, and refuses unless GitHub accepts it and the key's rotation deadline has not passed -- no add, disable or destroy; runner_browser_toolchain_converge installs the declared Playwright/Chromium toolchain (apt host libraries as the administrator, digest-pinned node, Playwright and Chromium archives into the job user's root) on the selected host, which must be in the pool that runs the floor job, and refuses unless every digest, version and host library reads back and headless Chromium renders a local page; microvm_host_converge installs the cited Firecracker release on the selected host and reads the kvm grant back, refusing by name when the grant has not landed (the grant itself is applied by the full-host apply spine); microvm_network_apply stages the slot and host network files the model renders at the named expected_revision as root:root 0600 in a root-only directory over the fleet SSH edge as the host's ADMINISTRATOR, installs them with the modeled operations, reloads networkd, systemd-sysctl and the nft loader unit, and reads the ruleset back -- the job user is granted none of it, because install plus systemctl over content that principal can write is arbitrary root for any pull request; guest_image_observe reports the micro-VM base artifacts and their measured digests on the selected host and builds nothing; guest_image_converge builds the runner guest image there; microvm_boot_probe boots that image and reads its serial console; spark_grants and spark_bootstrap prepare the selected Spark; spark_serving_apply applies the promoted fabric groups' pair vLLM units over the password session, workers before heads; spark_native_serving_apply relaunches group B's native four-rank arm as ONE BOUNDED TRANSACTION -- an all-host preflight that mutates nothing and refuses on any unresolved artifact from an earlier run, the incumbent UNIT preserved (plus a diagnostic inspect of the incumbent container -- the container itself cannot outlive its unit, because these run --rm in the foreground under systemd), the head applied BEFORE the workers so every rank joins the head's new rendezvous store, then a readback of the COMPLETE realization through each container's own inspect -- every field the create spec states, the effective --node-rank off that container's argv, and NCCL's typed transport verdict on that incarnation -- plus the head's front door answering with this arm's served model, and then commit or a rollback of the whole arm -- and it is a separate mode because it reloads a 328 GB arm and nobody should reach it by asking for the pair units; spark_runtime_image_probe pulls the pinned runtime image on the selected Spark and reads its capabilities from inside its own digest, changing nothing; spark_wireless_link_converge reads the selected Spark's Wi-Fi power save (runtime via iw, persisted via the NetworkManager profile), link and kernel disconnect count, sets whichever realization differs from the declared intent, and refuses unless the readback decides Noop; spark_grants_observe reads every procured Spark's sudo grant listing as gunbc-automation and the bootstrap principal, and its sudoers drop-in shape, and writes nothing; spark_v41_checkpoint_materialize fetches the admitted published DeepSeek V4.1 files onto the selected Group A Spark (about 510 GB; spark_v41_row_store_encode encodes the eight Engram row stores from the verified shards on the selected Group A Spark and reads each store's sha256; spark_v41_row_store_readback reads those stores back at their header, first and last record and every rank seam, with the published source rows at the same rows, and writes nothing; spark_v41_engram_differential compares upstream's Engram lookup kernel with the design-B file-backed lookup over sampled real rows of every row store, byte for byte, and writes nothing; it states the requirement and refuses before fetching when the disk cannot hold it), publishes each only after its sha256 matches the manifest, leaves a present file with the right digest alone and refuses one with the wrong digest, and reads the storage-backed Engram spans from the verified shards; a transfer runs detached and a rerun reattaches; spark_v41_runtime_image_build PRODUCES the DeepSeek V4.1 image on the selected Spark -- it verifies the candidate's three FlashInfer wheels against the digests the candidate keys, converges the patched source tree, builds from it, reads the produced configuration digest back from inside the image through the probe route, and admits that digest against the candidate's own recipe, refusing a digest that does not recompute from it -- and it is a separate mode from the probe because it occupies one host for hours where the probe occupies it for minutes; spark_v41_runtime_image_distribute moves that produced image, named by the configuration digest its production receipt read back, from the host that receipt names to the selected Group A Spark -- save into its fabric blob root, pulled by the target straight over the fabric rail, load -- after stating its size against every filesystem a copy lands on, leaves a target already holding the digest untouched, refuses a target holding a different image under the tag, and refuses unless the target's image inspect Id reads back as that digest; spark_v41_group_a_launch reads the production image back under its tag on every Group A host, reads its registry inside its digest and every host's occupancy, and only when Group A is suspended for this candidate with every host held and vacant stages the Engram manifest and applies the V4.1 four-rank arm as one transaction at the capacity measurement's shape -- the target names only the session host; spark_v41_serving_load runs the shared serving-load runner against the V4.1 head (target must be srv6): one vllm bench serve step per capacity-measurement concurrency, metrics scraped around each, host pressure read on every Group A rank, and the staircase stop rules applied over the worst rank; it changes no unit and writes only its receipt; host_reset_return drives a rostered reset subject through its controller and measures the return FROM A PEER, and takes reset_observer rather than host because the observer must not be the subject; runner_host_file_observe reads the runner teardown drop-in, the needrestart deferral and the loaded teardown on the selected host as its administrator and writes nothing; runner_host_file_converge writes whichever of those two files differs, reloads systemd only if the drop-in changed, and refuses unless the manager then reports the declared teardown; microvm_controller_install writes the microVM slot controller's root-owned release locus (gunbc + sources + Firecracker + jailer) and the gunbc-microvm-slot@ template unit and the gunbc-microvm-slot-reserve broker unit on srv1 and starts neither; microvm_slot_reserve starts that broker unit once on srv1, which reserves the shakedown cell through the fabric broker route from inside the root process (slot, demand and offer derived from the model, never inputs), and uploads that invocation's reservation receipt, failing unless the reservation committed; microvm_slot_start starts the shakedown slot's controller unit once on srv1 (the instance is derived from the model, never an input), waits for it bounded by the unit's own stop timeout, and uploads that invocation's controller receipt; microvm_runner_group_ensure (srv1 only) reads the organization's runner groups and, only when the microvm-shakedown group is absent, files ONE operator approval, creates it restricted to the shakedown workflow on the default branch, and refuses unless the readback holds that restriction; mtcollins1_census_qemu_host_observe reads the selected host's KVM device and, under the job user's census QEMU root, the qemu-system-aarch64 build, its ldd libraries and the AAVMF images against their pins, and writes nothing; mtcollins1_census_qemu_toolchain_converge places that root as the job user (digest-pinned noble .debs unpacked with dpkg-deb -x, no apt, no Recommends) and refuses unless the same observe reads it ready; workspace_source_pack (host=srv1) enumerates the operator workspace as the job user, drops every path the credential exclusion row names and every build output, tars exactly the remainder, reads the archive back and refuses if any member is excluded, and puts it into the workspace bucket under its SHA-256, refusing by name when the runner cannot read a source root; workspace_checkpoint_measure (host=srv3, workspace_source_object = that SHA-256) refuses unless the job user's home is on btrfs, then times a read-only subvolume snapshot, a pinned kopia scan of it, the content-addressed chunk upload and the gated revision commit, and receipts every time with the 10 s / 60 s / 10 GB draft verdicts; workspace_checkpoint_restore (host=srv3) commits a small authored workspace, puts one chunk with no head advance, destroys the directory, restores it from the head closure and requires byte-equality with the uncommitted chunk absent -- both srv3 modes refuse their commit while the R2 head's CAS ground is uncited" }, + description: Present { value: "plan shows membership hunks; apply executes a prior plan artifact; org_actions_observe validates the org credential and refuses on runner-group drift; org_runner_roster_observe reads the organization's self-hosted runner registrations to exhaustion under the same token and refuses unless the roster is complete; app_control_plane_observe reads the gunbai-ci App registration and webhook config with a short-lived App JWT and refuses on hook-config drift; app_key_version_verify reads the gunbai-ci App private key at the EXACT Secret Manager version named by app_key_version, mints an installation token with it, and refuses unless GitHub accepts it and the key's rotation deadline has not passed -- no add, disable or destroy; runner_browser_toolchain_converge installs the declared Playwright/Chromium toolchain (apt host libraries as the administrator, digest-pinned node, Playwright and Chromium archives into the job user's root) on the selected host, which must be in the pool that runs the floor job, and refuses unless every digest, version and host library reads back and headless Chromium renders a local page; microvm_host_converge installs the cited Firecracker release on the selected host and reads the kvm grant back, refusing by name when the grant has not landed (the grant itself is applied by the full-host apply spine); microvm_network_apply stages the slot and host network files the model renders at the named expected_revision as root:root 0600 in a root-only directory over the fleet SSH edge as the host's ADMINISTRATOR, installs them with the modeled operations, reloads networkd, systemd-sysctl and the nft loader unit, and reads the ruleset back -- the job user is granted none of it, because install plus systemctl over content that principal can write is arbitrary root for any pull request; guest_image_observe reports the micro-VM base artifacts and their measured digests on the selected host and builds nothing; guest_image_converge builds the runner guest image there; microvm_boot_probe boots that image and reads its serial console; spark_grants and spark_bootstrap prepare the selected Spark; spark_serving_apply applies the promoted fabric groups' pair vLLM units over the password session, workers before heads; spark_native_serving_apply relaunches group B's native four-rank arm as ONE BOUNDED TRANSACTION -- an all-host preflight that mutates nothing and refuses on any unresolved artifact from an earlier run, the incumbent UNIT preserved (plus a diagnostic inspect of the incumbent container -- the container itself cannot outlive its unit, because these run --rm in the foreground under systemd), the head applied BEFORE the workers so every rank joins the head's new rendezvous store, then a readback of the COMPLETE realization through each container's own inspect -- every field the create spec states, the effective --node-rank off that container's argv, and NCCL's typed transport verdict on that incarnation -- plus the head's front door answering with this arm's served model, and then commit or a rollback of the whole arm -- and it is a separate mode because it reloads a 328 GB arm and nobody should reach it by asking for the pair units; spark_runtime_image_probe pulls the pinned runtime image on the selected Spark and reads its capabilities from inside its own digest, changing nothing; spark_wireless_link_converge reads the selected Spark's Wi-Fi power save (runtime via iw, persisted via the NetworkManager profile), link and kernel disconnect count, sets whichever realization differs from the declared intent, and refuses unless the readback decides Noop; spark_grants_observe reads every procured Spark's sudo grant listing as gunbc-automation and the bootstrap principal, and its sudoers drop-in shape, and writes nothing; spark_v41_checkpoint_materialize fetches the admitted published DeepSeek V4.1 files onto the selected Group A Spark (about 510 GB; spark_v41_row_store_encode encodes the eight Engram row stores from the verified shards on the selected Group A Spark and reads each store's sha256; spark_v41_row_store_readback reads those stores back at their header, first and last record and every rank seam, with the published source rows at the same rows, and writes nothing; spark_v41_engram_differential compares upstream's Engram lookup kernel with the design-B file-backed lookup over sampled real rows of every row store, byte for byte, and writes nothing; it states the requirement and refuses before fetching when the disk cannot hold it), publishes each only after its sha256 matches the manifest, leaves a present file with the right digest alone and refuses one with the wrong digest, and reads the storage-backed Engram spans from the verified shards; a transfer runs detached and a rerun reattaches; spark_v41_runtime_image_build PRODUCES the DeepSeek V4.1 image on the selected Spark -- it verifies the candidate's three FlashInfer wheels against the digests the candidate keys, converges the patched source tree, builds from it, reads the produced configuration digest back from inside the image through the probe route, and admits that digest against the candidate's own recipe, refusing a digest that does not recompute from it -- and it is a separate mode from the probe because it occupies one host for hours where the probe occupies it for minutes; spark_v41_runtime_image_distribute moves that produced image, named by the configuration digest its production receipt read back, from the host that receipt names to the selected Group A Spark -- save into its fabric blob root, pulled by the target straight over the fabric rail, load -- after stating its size against every filesystem a copy lands on, leaves a target already holding the digest untouched, refuses a target holding a different image under the tag, and refuses unless the target's image inspect Id reads back as that digest; spark_v41_group_a_launch reads the production image back under its tag on every Group A host, reads its registry inside its digest and every host's occupancy, and only when Group A is suspended for this candidate with every host held and vacant stages the Engram manifest and applies the V4.1 four-rank arm as one transaction at the capacity measurement's shape -- the target names only the session host; spark_v41_serving_load runs the shared serving-load runner against the V4.1 head (target must be srv6): one vllm bench serve step per capacity-measurement concurrency, metrics scraped around each, host pressure read on every Group A rank, and the staircase stop rules applied over the worst rank; it changes no unit and writes only its receipt; host_reset_return drives a rostered reset subject through its controller and measures the return FROM A PEER, and takes reset_observer rather than host because the observer must not be the subject; runner_host_file_observe reads the runner teardown drop-in, the needrestart deferral and the loaded teardown on the selected host as its administrator and writes nothing; runner_host_file_converge writes whichever of those two files differs, reloads systemd only if the drop-in changed, and refuses unless the manager then reports the declared teardown; microvm_controller_install writes the microVM slot controller's root-owned release locus (gunbc + sources + Firecracker + jailer) and the gunbc-microvm-slot@ template unit and the gunbc-microvm-slot-reserve broker unit on srv1 and starts neither; microvm_slot_reserve starts that broker unit once on srv1, which reserves the shakedown cell through the fabric broker route from inside the root process (slot, demand and offer derived from the model, never inputs), and uploads that invocation's reservation receipt, failing unless the reservation committed; microvm_slot_start starts the shakedown slot's controller unit once on srv1 (the instance is derived from the model, never an input), waits for it bounded by the unit's own stop timeout, and uploads that invocation's controller receipt; microvm_runner_group_ensure (srv1 only) reads the organization's runner groups and, only when the microvm-shakedown group is absent, files ONE operator approval, creates it restricted to the shakedown workflow on the default branch, and refuses unless the readback holds that restriction; mtcollins1_census_qemu_host_observe reads the selected host's KVM device and, under the job user's census QEMU root, the qemu-system-aarch64 build, its ldd libraries and the AAVMF images against their pins, and writes nothing; mtcollins1_census_qemu_toolchain_converge places that root as the job user (digest-pinned noble .debs unpacked with dpkg-deb -x, no apt, no Recommends) and refuses unless the same observe reads it ready; workspace_source_pack (host=srv1, agent_state_slot = an existing .claude@, no default) enumerates the operator workspace as the job user, drops every path the credential exclusion row names and every build output, stands same-size seeded placeholders in for transcript files, tars the remainder and the placeholders, reads the archive back and refuses if any credential is carried or any placeholder's archived bytes are not its staged digest, and puts it into the workspace bucket under its SHA-256, refusing by name when the runner cannot read a source root; workspace_checkpoint_measure (host=srv3, workspace_source_object = that SHA-256) refuses unless the job user's home is on btrfs, then times a read-only subvolume snapshot, a pinned kopia scan of it, the content-addressed chunk upload and the gated revision commit, and receipts every time, withholding the 10 s / 60 s draft verdicts while churn is unmeasured and deciding the 10 GB quota over the static size; workspace_checkpoint_restore (host=srv3) commits a small authored workspace, puts one chunk with no head advance, destroys the directory, restores it from the head closure and requires byte-equality with the uncommitted chunk absent -- both srv3 modes refuse their commit while the R2 head's CAS ground is uncited; workspace_transcript_rate_observe (host=srv1, agent_state_slot as for workspace_source_pack, admitted by the same rule) samples the size and mtime of every transcript file the pack's transcript exclusion rows match under that slot and the Codex home, once per interval over a ten-minute window, never opening content, and receipts per-interval growth, creations, truncations and active writers with mean and p95 bytes/s as ONE sample of workload" }, required: true, default: none, type: InputChoice { options: fleet_converge_mode_options }, @@ -5115,6 +5157,13 @@ data fleet_converge_dispatch_inputs: List = [ DispatchInput { name: "printer_project_sha256", description: Present { value: "SHA-256 of the reviewed sliced project" }, required: false, default: none, type: InputString }, DispatchInput { name: "printer_credential_version", description: Present { value: "Numeric Secret Manager version returned by observation" }, required: false, default: none, type: InputString }, DispatchInput { name: "printer_bed_clear", description: Present { value: "Operator reports this printer bed is clear for this dispatch" }, required: false, default: none, type: InputChoice { options: ["false", "true"] } }, + DispatchInput { + name: "agent_state_slot", + description: Present { value: "workspace_source_pack and workspace_transcript_rate_observe: the Claude auth slot whose agent state is packed, as in /home/briansrls/.claude@ on srv1; admitted only if that directory exists, refused naming the observed slots otherwise; no default" }, + required: false, + default: none, + type: InputString, + }, DispatchInput { name: "workspace_source_object", description: Present { value: "workspace_checkpoint_measure only: the lower-hex SHA-256 the workspace_source_pack receipt printed as address= (the archive's own digest; the fetched bytes must hash to it)" }, @@ -5260,6 +5309,70 @@ fn fleet_converge_yml_after_unit_hold_admission() -> FleetConvergeYamlGeneration } +fn fleet_converge_arrival_converge_job() -> Job { + Job { + id: fleet_converge_mode_job_id(mode: ArrivalConverge) as String, + name: none, + runner: SelfHosted { labels: ["self-hosted", "linux", "arm64", operator_host_srv1 as String] }, + environment: Present { value: mtjade1_arrival_environment as String }, + steps: [ + fleet_converge_event_sha_checkout_step(), + ci_release_bins_download_step(), + ci_release_bins_unpack_verify_step(), + RunStep { + name: Present { value: "Arrival admission (credential-free): store host srv1 and a population arrival_subject" }, + id: Present { value: "arrival_admit" }, + run: gunbc_ci_arrival_admit_invoke(), + shell: none, + env: Present { value: [ + kv(key: fleet_converge_expected_host_env_name, value: yaml_string(s: "${{ github.event.inputs.host }}")), + kv(key: fleet_converge_arrival_subject_env_name, value: yaml_string(s: "${{ github.event.inputs.arrival_subject }}")), + ] }, + working_directory: none, + if_condition: none, + continue_on_error: none, + timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes }, + }, + ci_wif_auth_step_for( + name: "WIF auth (OIDC -> dedicated mtjade1-arrival pool -> mtjade1-arrival SA, access token only)", + provider_resource: mtjade1_arrival_provider_resource(), + service_account: mtjade1_arrival_service_account as String, + ), + RunStep { + name: Present { value: "arrival_converge under the standing grant and arrival interlock" }, + id: Present { value: "arrival_converge" }, + run: gunbc_ci_arrival_converge_invoke(), + shell: none, + env: Present { value: [ + kv(key: "WIF_ACCESS_TOKEN", value: yaml_string(s: "${{ steps.wif_auth.outputs.access_token }}")), + kv(key: fleet_converge_expected_host_env_name, value: yaml_string(s: "${{ github.event.inputs.host }}")), + kv(key: fleet_converge_arrival_subject_env_name, value: yaml_string(s: "${{ github.event.inputs.arrival_subject }}")), + ] }, + working_directory: none, + if_condition: none, + continue_on_error: none, + timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes }, + }, + ], + needs: fleet_converge_job_needs(id: fleet_converge_mode_job_id(mode: ArrivalConverge)), + env: none, + outputs: none, + if_condition: Present { value: fleet_converge_mode_step_if(mode: ArrivalConverge) }, + timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes + 20 }, + continue_on_error: none, + concurrency: fleet_converge_exclusive_job_concurrency(group: join([fleet_host_mutation_group_prefix, operator_host_srv1 as String], "")), + permissions: Present { + value: WorkflowPermissions { + contents: Present { value: PermRead }, + pull_requests: none, + issues: none, + actions: Present { value: PermRead }, + id_token: Present { value: PermWrite }, + } + }, + } +} + fn fleet_converge_namecheap_observe_job() -> Job { Job { id: fleet_converge_mode_job_id(mode: NamecheapObserve) as String, name: none, @@ -5337,9 +5450,7 @@ fn fleet_converge_printer_job() -> Job { if_condition: Present { value: fleet_converge_mode_step_if(mode: PrinterWorkflow) }, timeout_minutes: Present { value: gunbc_ci_gunbc_run_resolve_step_timeout_minutes + 20 }, continue_on_error: none, - concurrency: Present { value: ConcurrencyMappingQueueNotMax { - group: "printer-${{ inputs.printer }}", cancel_in_progress: Present { value: CancelInProgressBool { value: false } }, explicit_single: none, - } }, + concurrency: fleet_converge_exclusive_job_concurrency(group: "printer-${{ inputs.printer }}"), permissions: Present { value: WorkflowPermissions { contents: Present { value: PermRead }, pull_requests: none, issues: none, actions: Present { value: PermRead }, id_token: Present { value: PermWrite }, diff --git a/dag/gunbc/fleet/fleet_desired_admission_workflow.dag b/dag/gunbc/fleet/fleet_desired_admission_workflow.dag index 48edde80c97..02e5e66a5ad 100644 --- a/dag/gunbc/fleet/fleet_desired_admission_workflow.dag +++ b/dag/gunbc/fleet/fleet_desired_admission_workflow.dag @@ -19,7 +19,7 @@ import gunbc.action_use_admission { import gunbc.ci_deploy_target_host { gunbc_ci_deploy_srv1_runner_spec } import gunbc.repo_identity { gunbc_default_branch_name } import gunbc.repo_self_build { repo_self_build_command } -import gunbc.fleet_desired_candidate { fleet_desired_candidate_fetch_script } +import gunbc.fleet_desired_candidate { fleet_desired_candidate_fetch_script, fleet_desired_candidate_fetch_script_row, candidate_scripts_refusal } import gunbc.fleet_release_bins_key { compiler_pair_release_product, release_bins_canonical_consumer_script_for } import gunbc.merge_admission_produce { ci_repo_root_shell } import gunbc.witness_floor_workflow { witness_floor_toolchain_step } @@ -37,7 +37,7 @@ import gunbc.fleet_desired_admission { fleet_desired_admission_event_payload_path, fleet_desired_admission_authorized_action_path, } -import std.optional { Present } +import std.optional { Present, Absent } import std.types { String, List } // THIS WORKFLOW IS THE ADMISSION GATE'S TRIGGER, AND IT IS SEPARATE FROM THE DEPLOY WORKFLOW @@ -498,10 +498,14 @@ fn expected_fleet_desired_yml() -> FleetDesiredGenerationOutcome { WorkflowCheckoutRefsAdmitted => match admit_workflow_toolchain_homes(workflow: fleet_desired_admission_workflow) { WorkflowToolchainHomesAdmitted => + match candidate_scripts_refusal(rows: [fleet_desired_candidate_fetch_script_row]) { + Present { value: refusal } => FleetDesiredGenerationRefused { reason: refusal } + Absent => match emit_yaml(v: project_workflow_to_yaml(workflow: fleet_desired_admission_workflow)) { EmittedYaml { text: t } => FleetDesiredGenerated { content: t } YamlEmitRefused { path: p, reason: r } => FleetDesiredGenerationRefused { reason: yaml_emit_refusal_text(module_path: "gunbc.fleet_desired_admission_workflow", path: p, reason: r) } } + } WorkflowToolchainHomeRefused { job_id: j, refusal: r } => FleetDesiredGenerationRefused { reason: toolchain_home_refusal_reason(job_id: j, refusal: r) } } diff --git a/dag/gunbc/fleet/fleet_desired_candidate.dag b/dag/gunbc/fleet/fleet_desired_candidate.dag index 1e6b8803b6b..c0974ca2875 100644 --- a/dag/gunbc/fleet/fleet_desired_candidate.dag +++ b/dag/gunbc/fleet/fleet_desired_candidate.dag @@ -2,7 +2,7 @@ module gunbc.fleet_desired_candidate import std.types { String, List } import v2.std.node { Node } -import v2.std.diagnostic { Outcome, Accepted, Rejected } +import v2.std.diagnostic { Outcome, Accepted, Rejected, NonEmptyDiagnostics, diagnostics_reason_lexemes } import v2.workflow.bash_emit { bash_emit_stmts } import v2.extdeps.languages.bash_build { bash_build_word_lit, bash_build_word_var, bash_build_word_concat_from_nodes, bash_build_word_cmdsubst, @@ -11,7 +11,7 @@ import v2.extdeps.languages.bash_build { bash_build_or_else, bash_build_subshell_from_stmts, bash_build_with_redir_to_file, bash_build_exit, bash_build_pipe, } -import gunbc.fleet_release_bins_key { compiler_pair_candidate_artifact_name, release_bins_pack_object_name } +import gunbc.fleet_release_bins_key { compiler_pair_candidate_artifact_name, release_bins_pack_object_name, script_emission_refused_text } // WHERE fleet-desired FINDS THE CANDIDATE THE MERGE QUEUE BUILT FOR THE REVISION THAT JUST LANDED. // @@ -158,6 +158,6 @@ fn fleet_desired_candidate_fetch_statements() -> List { fn fleet_desired_candidate_fetch_script() -> String { match bash_emit_stmts(statements: fleet_desired_candidate_fetch_statements(), set_e: true) { Accepted { value: v, diagnostics: _ } => v - Rejected { diagnostics: _ } => "" + Rejected { diagnostics: ds } => script_emission_refused_text(script: "gunbc.fleet_desired_candidate fleet_desired_candidate_fetch_script", diagnostics: ds) } } diff --git a/dag/gunbc/fleet/fleet_host_identity.dag b/dag/gunbc/fleet/fleet_host_identity.dag index 42deced7e0d..38c5aa454f1 100644 --- a/dag/gunbc/fleet/fleet_host_identity.dag +++ b/dag/gunbc/fleet/fleet_host_identity.dag @@ -1,5 +1,7 @@ module gunbc.fleet_host_identity +import std.types { List, NonEmptyStr, String } +import v2.std.algebra { filter } import product.host_identity { HostIdentity } // THE FLEET'S HOST IDENTITIES, as a leaf. These rows were authored in gunbc.fleet_intent_network, @@ -80,3 +82,39 @@ data operator_host_srv11: HostIdentity = "srv11" // lease table the day the fabric switch was cabled and landed on switch lane qsfp56-dd-2-3. srv12 // is its identity slot, assigned by the same ascending-address rule. data operator_host_srv12: HostIdentity = "srv12" + +// ALTRA-BATCH-1 (operator, 2026-10-07). Four ASRock ALTRAD8UD hosts, not yet racked, named srv13-srv16 +// by the operator. Naming is not enrollment: no address, BMC endpoint, MAC or hardware row exists +// for them until first contact observes one, and every procedure refuses a host it cannot name a +// fact for. +data operator_host_srv13: HostIdentity = "srv13" +data operator_host_srv14: HostIdentity = "srv14" +data operator_host_srv15: HostIdentity = "srv15" +data operator_host_srv16: HostIdentity = "srv16" + +// THE NAMED ROSTER, ONE LIST BESIDE THE ROWS. A step subject or other label resolves to a host only +// by joining this membership; a spelling that is not a row here is not a fleet host. +data operator_hosts: List = [ + operator_host_srv1, + operator_host_srv2, + operator_host_srv3, + operator_host_srv4, + operator_host_mtcollins1, + operator_host_mtjade1, + operator_host_srv5, + operator_host_srv6, + operator_host_srv7, + operator_host_srv8, + operator_host_srv9, + operator_host_srv10, + operator_host_srv11, + operator_host_srv12, + operator_host_srv13, + operator_host_srv14, + operator_host_srv15, + operator_host_srv16, +] + +fn operator_host_named(label: NonEmptyStr) -> HostIdentity? { + operator_hosts |> filter(h => (h as String) == (label as String)) |> first() +} diff --git a/dag/gunbc/fleet/fleet_intent.dag b/dag/gunbc/fleet/fleet_intent.dag index 81d96dd99bc..ad502f85619 100644 --- a/dag/gunbc/fleet/fleet_intent.dag +++ b/dag/gunbc/fleet/fleet_intent.dag @@ -282,6 +282,14 @@ data srv4_baseboard: ServerBaseboard = ServerBaseboard { catalog: asrock_altrad8ud_1l2t_catalog, bmc: BmcEndpoint { host: "192.168.1.195", protocol: Redfish }, } +// srv13's board and BMC endpoint as first contact observed them (2026-10-07): Redfish +// Systems/system reported Model ALTRAD8UD-1L2T, and the controller answered at the router's +// bmc-altrad8ud-1l2t-5 reservation, MAC 9c:6b:00:e7:c9:83. No host row yet: CPU, memory and +// storage are not observed. +data srv13_baseboard: ServerBaseboard = ServerBaseboard { + catalog: asrock_altrad8ud_1l2t_catalog, + bmc: BmcEndpoint { host: "192.168.1.247", protocol: Redfish }, +} data srv1_nvme0: StorageDevice = nvme_storage_device( catalog: wd_black_sn850x_2tb_catalog, diff --git a/dag/gunbc/fleet/fleet_posix_accounts.dag b/dag/gunbc/fleet/fleet_posix_accounts.dag index c6d60bf5abc..bb982fc680c 100644 --- a/dag/gunbc/fleet/fleet_posix_accounts.dag +++ b/dag/gunbc/fleet/fleet_posix_accounts.dag @@ -238,6 +238,15 @@ data fleet_cp_binary_path: NonEmptyStr = "/usr/bin/cp" data fleet_mv_binary_path: NonEmptyStr = "/usr/bin/mv" +// THE TWO READ-ONLY METADATA BINARIES THE DEPLOY PLAN OBSERVES DIRECTORIES THROUGH. They are NOT rows +// of ci_runner_sudo_binaries (no path-unrestricted grant): each is granted only as a pinned-argv +// ExecutorPrivilegedOperation over one named directory (gunbc.executor_privileged_operation +// ReadDirectoryIdentity, ListDirectoryEntries), rendered into the runner host's sudoers by +// gunbc.runner_host_grants. A managed directory whose mode admits only its owner cannot be traversed by +// the job user, and its standing must still be observable before and after the apply. +data fleet_stat_binary_path: NonEmptyStr = "/usr/bin/stat" +data fleet_ls_binary_path: NonEmptyStr = "/usr/bin/ls" + // nft LIVES IN sbin AND IS PRIVILEGED EVEN TO READ: listing a table needs CAP_NET_ADMIN, so a // readback of the installed ruleset is a granted command like any mutation. data fleet_nft_binary_path: NonEmptyStr = nft_installed_path() as NonEmptyStr @@ -289,6 +298,8 @@ type CiRunnerSudoBinary | SudoNfct | SudoCp | SudoMv + | SudoStat + | SudoLs fn ci_runner_sudo_binary_path(b: CiRunnerSudoBinary) -> NonEmptyStr { match b { @@ -306,6 +317,8 @@ fn ci_runner_sudo_binary_path(b: CiRunnerSudoBinary) -> NonEmptyStr { SudoNfct => fleet_nfct_binary_path SudoCp => fleet_cp_binary_path SudoMv => fleet_mv_binary_path + SudoStat => fleet_stat_binary_path + SudoLs => fleet_ls_binary_path } } @@ -325,6 +338,8 @@ fn ci_runner_sudo_binary_probe(b: CiRunnerSudoBinary) -> SudoNopasswdProbeKind { SudoNfct => NopasswdExecuteCheck { check_argv: "--version" } SudoCp => NopasswdExecuteCheck { check_argv: "--version" } SudoMv => NopasswdExecuteCheck { check_argv: "--version" } + SudoStat => NopasswdExecuteCheck { check_argv: "--version" } + SudoLs => NopasswdExecuteCheck { check_argv: "--version" } } } diff --git a/dag/gunbc/fleet/fleet_release_bins_key.dag b/dag/gunbc/fleet/fleet_release_bins_key.dag index a85e69d4457..132c226b2fb 100644 --- a/dag/gunbc/fleet/fleet_release_bins_key.dag +++ b/dag/gunbc/fleet/fleet_release_bins_key.dag @@ -3,7 +3,7 @@ module gunbc.fleet_release_bins_key import std.types { String, List, Bool, Int, list_length } import std.measure { Second, second, second_count, seconds_per_day } import v2.std.node { Node } -import v2.std.diagnostic { Outcome, Accepted, Rejected, outcome_accepted } +import v2.std.diagnostic { Outcome, Accepted, Rejected, NonEmptyDiagnostics, outcome_accepted, diagnostics_reason_lexemes } import v2.workflow.bash_emit { bash_emit_stmts } import extdeps.shell.exec { TransportScript, transport_script_seal } import std.optional { Present, Absent } @@ -337,9 +337,11 @@ type ReleaseBinsRefusal | BuildDiagnosticsMissing { file: String } | SidecarMissing { file: String } | ConsumerProductKindMismatch { kind: String } + | ScriptEmissionRefused { script: String, reasons: String } fn release_bins_refusal_name(refusal: ReleaseBinsRefusal) -> String { match refusal { + ScriptEmissionRefused { script: _, reasons: _ } => "ScriptEmissionRefused" AddressFamilyNotAdmitted => "AddressFamilyNotAdmitted" SourceTreeDirty => "SourceTreeDirty" AxisValueUnread { variable: _ } => "AxisValueUnread" @@ -373,6 +375,7 @@ fn release_bins_refusal_name(refusal: ReleaseBinsRefusal) -> String { fn release_bins_refusal_message(refusal: ReleaseBinsRefusal) -> String { match refusal { + ScriptEmissionRefused { script: sc, reasons: rs } => join(["REFUSED ", sc, ": v2.workflow.bash_emit refused its statements (", rs, ")"], "") AddressFamilyNotAdmitted => "std.materialization_provider no longer admits sha256 at CrossRunScope; the release-bins address has no admitted family" AxisValueUnread { variable: v } => join(["the key input ", v, " read back empty, so the key would not name the build; refusing to key"], "") SourceTreeDirty => "tracked files differ from HEAD, so HEAD^\{tree} does not name the build input; refusing to key" @@ -1415,10 +1418,23 @@ fn release_bins_joined(parts: List>) -> Outcome { }) } +// A REFUSED RUNNER IS A REFUSING STEP, NEVER AN EMPTY ONE. Its text is the ScriptEmissionRefused +// refusal built from nodes and emitted through bash_emit like every other statement. The last arm is +// reached only if the emitter refuses that fixed refusal statement too: its text is a marker that is +// not shell, so the step cannot run and the committed-workflow drift gate goes red. +data script_emission_refusal_unemittable_marker: String = "__GUNBC_SCRIPT_EMISSION_REFUSAL_UNEMITTABLE__" + +fn script_emission_refused_text(script: String, diagnostics: NonEmptyDiagnostics) -> String { + match release_bins_emitted(statements: [refuse(r: ScriptEmissionRefused { script: script, reasons: diagnostics_reason_lexemes(diagnostics: diagnostics) })]) { + Accepted { value: v, diagnostics: _ } => v + Rejected { diagnostics: _ } => script_emission_refusal_unemittable_marker + } +} + fn release_bins_script_text(outcome: Outcome) -> String { match outcome { Accepted { value: v, diagnostics: _ } => v - Rejected { diagnostics: _ } => "" + Rejected { diagnostics: ds } => script_emission_refused_text(script: "gunbc.fleet_release_bins_key release_bins_script_text", diagnostics: ds) } } diff --git a/dag/gunbc/fleet/host_credential_custody_converge.dag b/dag/gunbc/fleet/host_credential_custody_converge.dag index fdcc603ac5b..ad446bee0c4 100644 --- a/dag/gunbc/fleet/host_credential_custody_converge.dag +++ b/dag/gunbc/fleet/host_credential_custody_converge.dag @@ -18,7 +18,32 @@ import gunbc.clock_read { clock_now_probed_at, probed_at_word } import gunbc.actions_run_binding { actions_variable_read, ActionsVariablePresent, ActionsVariableAbsent, fleet_converge_expected_host_env_name } import gunbc.fleet_ssh_locus { prepare_fleet_ssh_agent_context, FleetSshContextReady, FleetSshContextRefused } import gunbc.fleet_known_hosts_anchor { FleetSshExecutionContext, SshTarget } -import gunbc.fleet_host_identity { operator_host_srv1 } +import gunbc.fleet_host_identity { operator_host_srv1, operator_host_srv2 } +import gunbc.cursor_harness_credential { cursor_api_key_harness_secret_ref, cursor_worker_turn_api_key_host_path, cursor_worker_turn_custody_host } +import gunbc.codex_harness_credential { + codex_auth_harness_secret_ref, + codex_worker_turn_auth_host_path, + codex_worker_turn_custody_host, + codex_harness_auth_read, + CodexHarnessAuthAdmitted, + CodexHarnessAuthRefused, + codex_harness_write_back_decision, + CodexHarnessWriteBackDecision, + CodexHarnessWriteBackNotOwed, + CodexHarnessWriteBackOwed, + CodexHarnessWriteBackRefused, + codex_harness_write_back_refusal_wire, + codex_harness_add_version, + CodexHarnessWriteBackReceipt, + CodexHarnessVersionAdded, + CodexHarnessStoreUnchanged, + CodexHarnessVersionAddRefused, + CodexHarnessVersionAddAmbiguous, + CodexHarnessWriteBackWithheld, + codex_harness_write_back_receipt_wire, +} +import std.types { Secret } +import extdeps.filesystem.filesystem_io { FilesystemFileRead, FilesystemFileIndeterminate, FilesystemFileAbsent, FilesystemFileObservationsDisagree, FilesystemFileSubjectRefused } import gunbc.fleet_posix_accounts { fleet_posix_root_user } import gunbc.file_access { FileTreeScope, path_within_file_tree_scope } import gunbc.auth.github_apps { gunbai_ci_app_pem_secret } @@ -29,6 +54,7 @@ import gunbc.managed_directory { DeclaredDirectoryMode, DirectoryOwnerOnly, DirectoryOwnerAllGroupTraverse, + DirectoryOwnerAllEveryoneTraverse, declared_directory_mode_octal, declared_directory_grants_group_traverse, } @@ -50,6 +76,7 @@ import gunbc.typed_remote_file_write { RemoteFileMode, remote_file_mode_octal, RemoteFileModeOwnerRead, + RemoteFileModeOwnerReadWrite, RemoteFileModeOwnerReadWriteGroupRead, RemoteFileViaNonInteractiveSudo, TypedRemoteFileWrite, @@ -62,12 +89,15 @@ import gunbc.typed_remote_file_write { TypedRemoteFileStepLegRefused, typed_remote_file_write_seal, typed_remote_file_staging_path, + typed_remote_file_stage_argv, + typed_remote_file_write_stdin_payload, typed_remote_file_compare_argv, typed_remote_file_step, classify_typed_remote_file_comparison, remote_file_privileged_argv, converge_typed_remote_file, } +import extdeps.tools.chmod { chmod_path_resolved_program } import gunbc.runner_host_deploy { runner_host_spec_for, RunnerHostSpecFound, RunnerHostSpecAbsent } import gunbc.runner_host_grants { bootstrap_principal_is_not_the_job_user } import gunbc.fleet_bootstrap_principal { executor_bootstrap_principal } @@ -168,21 +198,49 @@ type HostCustodyCredential = ControllerAppKey | ApprovalNtfyPublisherToken | FabricStateWriterKey + | CursorWorkerTurnApiKey + | CodexWorkerTurnAuth | OracleOciApiSigningKey | ClaudeCodeOauthHarnessToken -data host_custody_credential_roster: List = [ControllerAppKey, ApprovalNtfyPublisherToken, FabricStateWriterKey, OracleOciApiSigningKey, ClaudeCodeOauthHarnessToken] +data host_custody_credential_roster: List = [ControllerAppKey, ApprovalNtfyPublisherToken, FabricStateWriterKey, CursorWorkerTurnApiKey, CodexWorkerTurnAuth, OracleOciApiSigningKey, ClaudeCodeOauthHarnessToken] fn host_custody_credential_wire(c: HostCustodyCredential) -> String { match c { ControllerAppKey => "controller_app_key" ApprovalNtfyPublisherToken => "approval_ntfy_publisher_token" FabricStateWriterKey => "fabric_state_writer_key" + CursorWorkerTurnApiKey => "cursor_worker_turn_api_key" + CodexWorkerTurnAuth => "codex_worker_turn_auth" OracleOciApiSigningKey => "oracle_oci_api_signing_key" ClaudeCodeOauthHarnessToken => "claude_code_oauth_harness_token" } } +// WHO HOLDS THE CURRENT VALUE AFTER PLACEMENT. Every row but one is store-authoritative: the secret +// is the truth and the host copy is converged to it. The codex subscription auth.json is not: codex +// rotates its refresh token on the host while it runs (extdeps.llm.codex_auth +// codex_source_refresh_persistence), so after placement the HOST holds the live token and the store +// lags until this converge reads the host file back and adds it as a new version. Overwriting that +// host file from the store would put a spent token back, and so would placing the store's copy on a +// second host. That is why a host-authoritative row must name exactly one host (placement refuses +// otherwise) and why its converge never writes over a file that is already there. +type CustodyAuthority + = CustodyStoreAuthoritative + | CustodyHostAuthoritativeAfterPlacement + +fn host_custody_authority(c: HostCustodyCredential) -> CustodyAuthority { + match c { + ControllerAppKey => CustodyStoreAuthoritative + ApprovalNtfyPublisherToken => CustodyStoreAuthoritative + FabricStateWriterKey => CustodyStoreAuthoritative + CursorWorkerTurnApiKey => CustodyStoreAuthoritative + CodexWorkerTurnAuth => CustodyHostAuthoritativeAfterPlacement + OracleOciApiSigningKey => CustodyStoreAuthoritative + ClaudeCodeOauthHarnessToken => CustodyStoreAuthoritative + } +} + data host_custody_credential_options: List = map(host_custody_credential_roster, c => host_custody_credential_wire(c: c)) type HostCustodyCredentialSelection @@ -204,11 +262,13 @@ fn select_host_custody_credential(supplied: String) -> HostCustodyCredentialSele // and whether it grants the group read -- four facts, one match each, no octal parameter anywhere. type CustodyFileMode = CustodyFileOwnerRead + | CustodyFileOwnerReadWrite | CustodyFileOwnerReadWriteGroupRead fn custody_file_remote_mode(m: CustodyFileMode) -> RemoteFileMode { match m { CustodyFileOwnerRead => RemoteFileModeOwnerRead + CustodyFileOwnerReadWrite => RemoteFileModeOwnerReadWrite CustodyFileOwnerReadWriteGroupRead => RemoteFileModeOwnerReadWriteGroupRead } } @@ -226,6 +286,7 @@ fn custody_file_exact_perm(m: CustodyFileMode) -> String { fn custody_file_forbidden_perm(m: CustodyFileMode) -> String { match m { CustodyFileOwnerRead => "/077" + CustodyFileOwnerReadWrite => "/077" CustodyFileOwnerReadWriteGroupRead => "/037" } } @@ -233,6 +294,7 @@ fn custody_file_forbidden_perm(m: CustodyFileMode) -> String { fn custody_file_grants_group(m: CustodyFileMode) -> Bool { match m { CustodyFileOwnerRead => false + CustodyFileOwnerReadWrite => false CustodyFileOwnerReadWriteGroupRead => true } } @@ -257,6 +319,7 @@ fn custody_dir_forbidden_perm(m: DeclaredDirectoryMode) -> String { match m { DirectoryOwnerOnly => "/077" DirectoryOwnerAllGroupTraverse => "/027" + DirectoryOwnerAllEveryoneTraverse => "/022" } } @@ -306,6 +369,16 @@ data custody_root_owned_ntfy_group: PosixOwnerNames = PosixOwnerNames { group: approval_ntfy_principal_name, } +data custody_operator_only: PosixOwnerNames = PosixOwnerNames { + user: fleet_posix_operator_user.name, + group: fleet_posix_operator_user.name, +} + +// THE WORKER-TURN CREDENTIALS live on srv2, the host the declared Cursor offer and the codex harness +// turns dispatch on, owned by the operator account the dispatch spawns run as. Cursor's key is +// read-only on the host. The codex directory IS the turn's CODEX_HOME: codex writes auth.json, +// sessions and logs beneath it, so the directory and the file are owner read-write. + // OracleOciApiSigningKey signs every OCI request gunbc.oracle_oci.compartment_ensure makes, through // the signer's own key reference, never re-spelled. It is delivered to srv1 only, root-owned and // owner-read, because srv1 is where the OCI converge runs and nothing else signs with it. @@ -330,6 +403,26 @@ fn host_credential_custody_row(c: HostCustodyCredential) -> HostCredentialCustod dir_ownership: custody_root_only, dir_mode: DirectoryOwnerOnly, } + CursorWorkerTurnApiKey => HostCredentialCustodyRow { + credential: c, + secret: cursor_api_key_harness_secret_ref, + path: cursor_worker_turn_api_key_host_path, + host_scope: CustodyOnlyOnHost { host: cursor_worker_turn_custody_host }, + file_ownership: custody_operator_only, + file_mode: CustodyFileOwnerRead, + dir_ownership: custody_operator_only, + dir_mode: DirectoryOwnerOnly, + } + CodexWorkerTurnAuth => HostCredentialCustodyRow { + credential: c, + secret: codex_auth_harness_secret_ref, + path: codex_worker_turn_auth_host_path, + host_scope: CustodyOnlyOnHost { host: codex_worker_turn_custody_host }, + file_ownership: custody_operator_only, + file_mode: CustodyFileOwnerReadWrite, + dir_ownership: custody_operator_only, + dir_mode: DirectoryOwnerOnly, + } OracleOciApiSigningKey => HostCredentialCustodyRow { credential: c, secret: oci_api_signing_key_secret_ref, @@ -373,6 +466,7 @@ type CustodyPlacementRefusal | CustodyPathNoDedicatedDirectory { path: NonEmptyStr } | CustodyGroupCannotTraverse { path: NonEmptyStr, dir: NonEmptyStr, group: NonEmptyStr, file_perm: String, dir_perm: String } | CustodyDirectoryGroupDiffers { path: NonEmptyStr, file_group: NonEmptyStr, dir_group: NonEmptyStr } + | CustodyHostAuthoritativeNotSingleHost { path: NonEmptyStr } type CustodyPlacement = CustodyPlaced { row: HostCredentialCustodyRow, key_dir: NonEmptyStr, upper_ancestors: List } @@ -390,6 +484,8 @@ fn custody_placement_refusal_wire(r: CustodyPlacementRefusal) -> String { join(["custody row for ", p as String, " grants group ", g as String, " read (", fp, ") but its directory ", d as String, " is declared ", dp, ", which denies that group traversal -- the reader could not open the file the readback would report as held"], "") CustodyDirectoryGroupDiffers { path: p, file_group: fg, dir_group: dg } => join(["custody row for ", p as String, " grants group ", fg as String, " read but its directory is group ", dg as String, ", so the file's group has no traversal through it"], "") + CustodyHostAuthoritativeNotSingleHost { path: p } => + join(["custody row for ", p as String, " is host-authoritative after placement and must name exactly one host; a second host would receive the store's lagging, possibly spent, copy"], "") } } @@ -404,7 +500,17 @@ fn custody_row_placement(row: HostCredentialCustodyRow) -> CustodyPlacement { let key_path = row.path let ancestors = runner_host_path_ancestors(path: key_path as String) let dir = last_or_empty(xs: ancestors) - if path_within_file_tree_scope(scope: custody_forbidden_tree, path: key_path) { + let host_authoritative_on_many = match host_custody_authority(c: row.credential) { + CustodyStoreAuthoritative => false + CustodyHostAuthoritativeAfterPlacement => + match row.host_scope { + CustodyAnyRunnerHost => true + CustodyOnlyOnHost { host: _ } => false + } + } + if host_authoritative_on_many { + CustodyPlacementRefused { cause: CustodyHostAuthoritativeNotSingleHost { path: key_path } } + } else if path_within_file_tree_scope(scope: custody_forbidden_tree, path: key_path) { CustodyPlacementRefused { cause: CustodyPathInForbiddenTree { path: key_path } } } else if !path_within_file_tree_scope(scope: runner_host_file_scope, path: key_path) { CustodyPlacementRefused { cause: CustodyPathOutsideScope { path: key_path } } @@ -668,12 +774,78 @@ fn host_credential_custody(row: HostCredentialCustodyRow, readback: CustodyReadb type CustodyAction = CustodyLeaveInPlace | CustodyWrite + | CustodyWriteBackToStore { payload: Secret } + | CustodyRepairMetadataInPlace + | CustodyCreateOnly | CustodyActionRefused { reason: String } -// The directory and the ancestors are decided first because a write cannot repair them and writing -// into a directory that is not as declared could publish the credential where an undeclared -// principal holds bits on it. An unsafe FILE is repaired by the write: the staged sibling is -// created by root, moded from the arm, renamed over it, and then owned from the row. +// THE HOST-AUTHORITATIVE DECISION, layered on the store-authoritative one rather than beside it: the +// directory, ancestor and metadata decisions are the same, and only a file that is already there and +// DIFFERS from the store is decided differently. It is never overwritten. If its refresh token moved +// on, it is owed to the store; if not, the host copy is merely fresher in its short-lived tokens and +// is left alone; if it now names another account or cannot be read, the converge refuses. +fn host_authoritative_differing_action(decision: CodexHarnessWriteBackDecision?) -> CustodyAction { + match decision { + Absent => CustodyActionRefused { reason: "host-authoritative file differs from the store and was not read back" } + Present { value: d } => + match d { + CodexHarnessWriteBackNotOwed => CustodyLeaveInPlace + CodexHarnessWriteBackOwed { payload } => CustodyWriteBackToStore { payload: payload } + CodexHarnessWriteBackRefused { cause } => + CustodyActionRefused { reason: concat("host file not written back: ", codex_harness_write_back_refusal_wire(cause: cause)) } + } + } +} + +fn host_authoritative_custody_action(base: CustodyAction, content: RunnerHostFileStanding, decision: CodexHarnessWriteBackDecision?) -> CustodyAction { + match base { + CustodyActionRefused { reason: _ } => base + CustodyWriteBackToStore { payload: _ } => base + CustodyRepairMetadataInPlace => base + CustodyCreateOnly => base + CustodyLeaveInPlace => host_authoritative_settled_action(base: base, content: content, decision: decision) + CustodyWrite => host_authoritative_settled_action(base: base, content: content, decision: decision) + } +} + +fn host_authoritative_settled_action(base: CustodyAction, content: RunnerHostFileStanding, decision: CodexHarnessWriteBackDecision?) -> CustodyAction { + match content { + HostFileDiffers => host_authoritative_differing_action(decision: decision) + HostFileIdentical => host_authoritative_metadata_action(base: base) + HostFileAbsent => host_authoritative_first_placement_action(base: base) + HostFileUnobservable { reason: _ } => base + } +} + +// A HOST-AUTHORITATIVE FILE IS NEVER REPLACED FROM THE STORE, whatever the reason the ordinary +// decision gave. Identical content with wrong owner or mode is repaired in place (chown and chmod on +// the path, no content written): the store-to-host content write stages the STORE bytes and renames +// them over the live file, so a refresh that moved the host file on between observation and rename +// would be lost -- the rename is outside the codex turn lock. +fn host_authoritative_metadata_action(base: CustodyAction) -> CustodyAction { + match base { + CustodyWrite => CustodyRepairMetadataInPlace + CustodyLeaveInPlace => base + CustodyRepairMetadataInPlace => base + CustodyCreateOnly => base + CustodyWriteBackToStore { payload: _ } => base + CustodyActionRefused { reason: _ } => base + } +} + +// FIRST PLACEMENT PUBLISHES CREATE-ONLY: a file that appears after the absence was observed (codex +// logged in by hand, or a racing converge) makes the publication fail and is left as found. +fn host_authoritative_first_placement_action(base: CustodyAction) -> CustodyAction { + match base { + CustodyWrite => CustodyCreateOnly + CustodyLeaveInPlace => base + CustodyRepairMetadataInPlace => base + CustodyCreateOnly => base + CustodyWriteBackToStore { payload: _ } => base + CustodyActionRefused { reason: _ } => base + } +} + fn custody_action( dir: CustodyPathMetadata, upper_ancestors: RunnerHostPathMetadata, @@ -714,6 +886,9 @@ fn custody_action_wire(a: CustodyAction) -> String { match a { CustodyLeaveInPlace => "left-in-place" CustodyWrite => "written" + CustodyWriteBackToStore { payload: _ } => "written-back-to-store" + CustodyRepairMetadataInPlace => "metadata-repaired-in-place" + CustodyCreateOnly => "created-only" CustodyActionRefused { reason: r } => concat("REFUSED ", r) } } @@ -735,6 +910,14 @@ fn custody_file_chown_argv(row: HostCredentialCustodyRow) -> List { chown_argv(owner: ChownOwnerNames { names: row.file_ownership }, path: row.path as String) } +fn custody_chown_argv_at(row: HostCredentialCustodyRow, path: String) -> List { + chown_argv(owner: ChownOwnerNames { names: row.file_ownership }, path: path) +} + +fn custody_chmod_argv_at(row: HostCredentialCustodyRow, path: String) -> List { + [chmod_path_resolved_program as String, remote_file_mode_octal(mode: custody_file_remote_mode(m: row.file_mode)), path] +} + fn staging_remove_argv(staging: String) -> List { ["rm", "-f", "--", staging] } @@ -958,6 +1141,7 @@ type CustodyRun { sm_version: SmResolvedVersionIdentity action: CustodyAction write_refusal: String? + store_write_back: CodexHarnessWriteBackReceipt? readback: CustodyReadback } @@ -1005,9 +1189,134 @@ fn write_and_own(subject: CustodyHost, context: FleetSshExecutionContext, row: H } } +type CustodyLeg { + argv: List + stdin_payload: String +} + +fn run_legs_until_refused(subject: CustodyHost, context: FleetSshExecutionContext, legs: List, step: String) -> String? { + fold(legs, init: none, f: (acc, leg) => + match acc { + Present { value: _ } => acc + Absent => + match run_elevated_leg(subject: subject, context: context, argv: leg.argv, stdin_payload: leg.stdin_payload) { + ElevatedLegRefused { reason: r } => Present { value: join(["step=", step, " not run: ", r], "") } + ElevatedLegRan { exit_code: c, stdout: _, stderr: e } => + if c != 0 { Present { value: join(["step=", step, " exit=", to_string(c), " ", trim(s: e)], "") } } else { none } + } + } + ) +} + +// METADATA ONLY: owner then mode on the existing path; the content is never touched. +fn repair_metadata_in_place(subject: CustodyHost, context: FleetSshExecutionContext, row: HostCredentialCustodyRow) -> String? { + run_legs_until_refused( + subject: subject, context: context, step: "metadata", + legs: [ + CustodyLeg { argv: custody_chown_argv_at(row: row, path: row.path as String), stdin_payload: "" }, + CustodyLeg { argv: custody_chmod_argv_at(row: row, path: row.path as String), stdin_payload: "" }, + ], + ) +} + +// CREATE-ONLY PUBLICATION: stage the store bytes beside the path, give the STAGED file the row's final +// owner and mode, then publish with a hard link, which fails when the destination exists rather +// than replacing it. A file that appeared after the absence observation therefore survives and the +// converge refuses; the staging sibling is removed afterwards by the shared staging cleanup. +fn create_only_publish(subject: CustodyHost, context: FleetSshExecutionContext, row: HostCredentialCustodyRow, write: TypedRemoteFileWrite) -> String? { + let staging = typed_remote_file_staging_path(write: write) + run_legs_until_refused( + subject: subject, context: context, step: "create-only", + legs: [ + CustodyLeg { argv: typed_remote_file_stage_argv(write: write), stdin_payload: typed_remote_file_write_stdin_payload(write: write) }, + CustodyLeg { argv: custody_chown_argv_at(row: row, path: staging), stdin_payload: "" }, + CustodyLeg { argv: custody_chmod_argv_at(row: row, path: staging), stdin_payload: "" }, + CustodyLeg { argv: ["ln", "-T", staging, row.path as String], stdin_payload: "" }, + ], + ) +} + // THE ANCESTORS ARE OBSERVED BEFORE ANY MUTATION (gunbc#11902): ensure_custody_dir creates, so it // runs only once the ancestors read root-only; otherwise the directory is only observed and the same // pure custody_action decides. +// THE HOST FILE AS THE HOST HOLDS IT, read only for a host-authoritative row whose content already +// differs from the store. A failed read is an indeterminate observation, never an empty file. +fn read_host_custody_file(subject: CustodyHost, context: FleetSshExecutionContext, path: String) -> extdeps.filesystem.filesystem_io.FilesystemFileObservation +{ + match run_elevated_leg(subject: subject, context: context, argv: ["cat", path], stdin_payload: "") { + ElevatedLegRefused { reason: r } => FilesystemFileIndeterminate { cause: concat("host file read not run: ", r) } + ElevatedLegRan { exit_code: c, stdout: o, stderr: e } => + if c != 0 { + FilesystemFileIndeterminate { cause: join(["host file read exit=", to_string(c), " ", trim(s: e)], "") } + } else { + FilesystemFileRead { path: path, content: o } + } + } +} + +type HostAuthoritativeReading { + decision: CodexHarnessWriteBackDecision? + host_content: String? +} + +fn read_host_authoritative( + subject: CustodyHost, + context: FleetSshExecutionContext, + row: HostCredentialCustodyRow, + store_payload: Secret, + content: RunnerHostFileStanding, +) -> HostAuthoritativeReading +{ + match content { + HostFileDiffers => + match codex_harness_auth_read(content: store_payload as String) { + CodexHarnessAuthRefused { cause: _ } => HostAuthoritativeReading { decision: none, host_content: none } + CodexHarnessAuthAdmitted { identity } => { + let observed = read_host_custody_file(subject: subject, context: context, path: row.path as String) + HostAuthoritativeReading { + decision: Present { value: codex_harness_write_back_decision(materialized: identity, read_back: observed) }, + host_content: host_custody_file_content(observed: observed), + } + } + } + HostFileIdentical => HostAuthoritativeReading { decision: none, host_content: none } + HostFileAbsent => HostAuthoritativeReading { decision: none, host_content: none } + HostFileUnobservable { reason: _ } => HostAuthoritativeReading { decision: none, host_content: none } + } +} + +fn host_custody_file_content(observed: extdeps.filesystem.filesystem_io.FilesystemFileObservation) -> String? { + match observed { + FilesystemFileRead { path: _, content: c } => Present { value: c } + FilesystemFileAbsent(_) => none + FilesystemFileIndeterminate { cause: _ } => none + FilesystemFileObservationsDisagree { path: _, cause: _ } => none + FilesystemFileSubjectRefused { directory: _, name: _, cause: _ } => none + } +} + +// The content readback against the bytes the action left authoritative: the store's for a write or a +// leave-in-place on identical content; the host file's own bytes, read again, for a host-authoritative +// copy that was left or written back. The second is a re-read of the host, not an assumption about it. +fn observe_custody_content_against( + subject: CustodyHost, + context: FleetSshExecutionContext, + row: HostCredentialCustodyRow, + key_dir: NonEmptyStr, + store_write: TypedRemoteFileWrite, + authoritative_content: String?, +) -> RunnerHostFileStanding +{ + match authoritative_content { + Absent => observe_custody_content(subject: subject, context: context, write: store_write) + Present { value: c } => + match typed_remote_file_write_seal(path: row.path as FilePath, content: c, allowed_scope: FileTreeScope { root_path: key_dir }) { + TypedRemoteFileWriteAdmissionRefused { cause: why } => HostFileUnobservable { reason: concat("host-authoritative readback not sealed: ", why as String) } + TypedRemoteFileWriteAdmitted { write: w } => observe_custody_content(subject: subject, context: context, write: w) + } + } +} + fn converge_host_credential_custody( subject: CustodyHost, context: FleetSshExecutionContext, @@ -1015,6 +1324,7 @@ fn converge_host_credential_custody( key_dir: NonEmptyStr, upper_ancestors: List, write: TypedRemoteFileWrite, + store_payload: Secret, sm_version: SmResolvedVersionIdentity, ) -> CustodyRun { @@ -1029,10 +1339,39 @@ fn converge_host_credential_custody( } let content_before = observe_custody_content(subject: subject, context: context, write: write) let file_before = observe_custody_path(subject: subject, context: context, path: row.path as String, legs: custody_file_legs(row: row)) - let action = custody_action(dir: dir_before, upper_ancestors: ancestors_before, content: content_before, file: file_before) + let base_action = custody_action(dir: dir_before, upper_ancestors: ancestors_before, content: content_before, file: file_before) + let host_reading = match host_custody_authority(c: row.credential) { + CustodyStoreAuthoritative => HostAuthoritativeReading { decision: none, host_content: none } + CustodyHostAuthoritativeAfterPlacement => + read_host_authoritative(subject: subject, context: context, row: row, store_payload: store_payload, content: content_before) + } + let action = match host_custody_authority(c: row.credential) { + CustodyStoreAuthoritative => base_action + CustodyHostAuthoritativeAfterPlacement => + host_authoritative_custody_action(base: base_action, content: content_before, decision: host_reading.decision) + } let write_refusal = match action { CustodyWrite => write_and_own(subject: subject, context: context, row: row, write: write) + CustodyRepairMetadataInPlace => repair_metadata_in_place(subject: subject, context: context, row: row) + CustodyCreateOnly => create_only_publish(subject: subject, context: context, row: row, write: write) CustodyLeaveInPlace => none + CustodyWriteBackToStore { payload: _ } => none + CustodyActionRefused { reason: _ } => none + } + let store_write_back = match action { + CustodyWriteBackToStore { payload } => Present { value: codex_harness_add_version(token_source: WorkloadIdentityToken, payload: payload) } + CustodyLeaveInPlace => none + CustodyWrite => none + CustodyRepairMetadataInPlace => none + CustodyCreateOnly => none + CustodyActionRefused { reason: _ } => none + } + let authoritative_content = match action { + CustodyWriteBackToStore { payload: _ } => host_reading.host_content + CustodyLeaveInPlace => host_reading.host_content + CustodyRepairMetadataInPlace => host_reading.host_content + CustodyCreateOnly => none + CustodyWrite => none CustodyActionRefused { reason: _ } => none } let staging_after = match admit_staging_cleanup( @@ -1048,7 +1387,10 @@ fn converge_host_credential_custody( let file_after = observe_custody_path(subject: subject, context: context, path: row.path as String, legs: custody_file_legs(row: row)) let dir_after = observe_custody_path(subject: subject, context: context, path: key_dir as String, legs: custody_dir_legs(row: row)) let upper_ancestors_after = observe_upper_ancestors(subject: subject, context: context, paths: upper_ancestors) - let content_after = observe_custody_content(subject: subject, context: context, write: write) + let content_after = observe_custody_content_against( + subject: subject, context: context, row: row, key_dir: key_dir, + store_write: write, authoritative_content: authoritative_content, + ) let readback = CustodyReadback { file: file_after, dir: dir_after, @@ -1062,6 +1404,7 @@ fn converge_host_credential_custody( sm_version: sm_version, action: action, write_refusal: write_refusal, + store_write_back: store_write_back, readback: readback, } } @@ -1071,16 +1414,30 @@ fn custody_run_settled(run: CustodyRun) -> Bool { CustodyActionRefused { reason: _ } => true CustodyLeaveInPlace => false CustodyWrite => false + CustodyRepairMetadataInPlace => false + CustodyCreateOnly => false + CustodyWriteBackToStore { payload: _ } => false } let write_failed = match run.write_refusal { Present { value: _ } => true Absent => false } + let store_write_failed = match run.store_write_back { + Absent => false + Present { value: r } => + match r { + CodexHarnessVersionAdded { version_name: _ } => false + CodexHarnessStoreUnchanged => true + CodexHarnessVersionAddRefused { detail: _ } => true + CodexHarnessVersionAddAmbiguous { detail: _ } => true + CodexHarnessWriteBackWithheld { cause: _ } => true + } + } let held = match host_credential_custody(row: run.row, readback: run.readback) { CustodyHeld => true CustodyNotHeld { causes: _ } => false } - !refused_before && !write_failed && held + !refused_before && !write_failed && !store_write_failed && held } // NEVER THE BYTES. Every field below is metadata or a judgment; the content is named only by the @@ -1097,6 +1454,10 @@ fn custody_receipt(run: CustodyRun) -> String { Present { value: w } => concat(" write=REFUSED ", w) Absent => "" }, + match run.store_write_back { + Present { value: r } => concat(" store=", codex_harness_write_back_receipt_wire(r: r)) + Absent => "" + }, "\n file=", custody_path_metadata_wire(m: run.readback.file, legs: custody_file_legs(row: run.row)), "\n dir=", custody_path_metadata_wire(m: run.readback.dir, legs: custody_dir_legs(row: run.row)), "\n ancestors=", runner_host_path_metadata_wire(m: run.readback.upper_ancestors), @@ -1207,7 +1568,7 @@ fn host_credential_custody_converge_ci_wet() -> ProcessExit FleetSshContextReady { context: context, receipt: context_receipt } => { let run = converge_host_credential_custody( subject: subject, context: context, placement_row: placed, key_dir: kd, - upper_ancestors: ua, write: w, sm_version: version, + upper_ancestors: ua, write: w, store_payload: material, sm_version: version, ) write_receipt_then_exit( body: join([stamp, "\n", context_receipt, custody_receipt(run: run)], ""), diff --git a/dag/gunbc/fleet/mtcollins_firmware_baseline.dag b/dag/gunbc/fleet/mtcollins_firmware_baseline.dag index 5f409ba68fa..90ecfeed87e 100644 --- a/dag/gunbc/fleet/mtcollins_firmware_baseline.dag +++ b/dag/gunbc/fleet/mtcollins_firmware_baseline.dag @@ -1,5 +1,6 @@ module gunbc.fleet.mtcollins_firmware_baseline +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import std.types { Bool, Int, List, NonEmptyStr, String } import std.decl_ref { DeclarationRef, decl_ref, declaration_ref_eq } import std.interval { degenerate_interval } @@ -262,7 +263,7 @@ fn baseline_assessment() -> DecisionAssessment { constraint: c.identity, evidence: "a gate at 1 applied to every candidate before dominance" as NonEmptyStr, }), - parameters: [ParameterBound { parameter: id_param_board, evidence: "AmpereMtCollins (mtcollins1)" as NonEmptyStr }], + parameters: [ParameterBound { parameter: id_param_board, evidence: "Mt. Collins board (mtcollins1)" as NonEmptyStr }], selected_candidate: Absent, term_drops: [], physical_model: PhysicalModelEstablished { terms: [id_term] }, diff --git a/dag/gunbc/github_effect_perform.dag b/dag/gunbc/github_effect_perform.dag index 565bc4b9f86..7c78e17218e 100644 --- a/dag/gunbc/github_effect_perform.dag +++ b/dag/gunbc/github_effect_perform.dag @@ -1,7 +1,8 @@ module gunbc.github_effect_perform -import std.types { String, NonEmptyStr, Int, List, Secret, EpochSecs } +import std.types { String, NonEmptyStr, Int, List, Secret, EpochSecs, Bytes } import std.resources { Network } +import std.decl_ref { decl_ref } import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.clock { clock_unix_millis_read, ClockUnixMillisObserved, ClockUnixMillisRefused, epoch_secs_of_millis, @@ -29,6 +30,16 @@ import extdeps.github.app import extdeps.github.actions_jit_runner import extdeps.github.actions_self_hosted_runners import extdeps.github.actions_self_hosted_runners { OrganizationRunnerListWire, OrganizationRunnerDeletedBody } +import extdeps.github.workflows +import extdeps.github.workflows { WorkflowDispatchReceipt } +import extdeps.github.workflow_runs +import extdeps.github.git_database +import extdeps.github.git_database { GitRefWire, GitRefDeletedBody } +import extdeps.github.org_actions +import extdeps.github.org_actions { OrgRunnerGroupWire } +import extdeps.github.workflow_runs { WorkflowRun, WorkflowJobRunList } +import extdeps.github.actions_artifacts +import extdeps.github.actions_artifacts { ActionsArtifactList } import gunbc.auth.github_apps { DeclaredGitHubApp, gunbai_ci_declared } import extdeps.github.app { GitHubAppInstallationId } import gunbc.runner.runner_jit_perform { @@ -138,10 +149,17 @@ type JitMintAppJwt // same jit_config_mint_outcome every status goes through, so an empty blob under 201 is still // refused there; a non-2xx the remote DECIDED is a JitConfigMintOutcome refusal; an answer that // never arrived is commit-ambiguous and is NOT reported as a refusal. +// +// ADMITTED ONLY FROM THE PERFORM FUNCTION AND A CLAIM. This fold takes the dispatch and the answer +// separately, so an open call would mint a delivery for dispatch A from any 201 body; the perform +// function is the one caller whose answer is GitHub's response to dispatch.request. fn jit_mint_performance_of_generate( dispatch: JitMintDispatch, outcome: RestResult, -) -> JitMintPerformance { +) -> JitMintPerformance admit_callers: [ + decl_ref(module_path: "gunbc.github_effect_perform", decl_name: "perform_organization_jit_mint"), + decl_ref(module_path: "test.claim.github_effect_perform_witness", decl_name: "witness_generate_step_delivers_refuses_or_reports_ambiguity_by_what_github_answered"), +] { match outcome { RestAnswered { answer: created } => JitMintReceived { @@ -317,3 +335,157 @@ fn delete_runner_with(token: ControllerInstallationToken, organization: NonEmpty RestRefused { refusal } => RestRefused { refusal: refusal } } } + +// THE QUALIFICATION DISPATCH AND ITS READBACK, under the same controller installation token +// (gunbc.runner.runner_qualification_dispatch). Each answer is GitHub's RestResult, sealed so the +// decision module receives only what GitHub returned; the decision classifies a refusal as a read's +// or a mutation's (extdeps.transports.rest classify_rest_refusal), so for the dispatch a 5xx, a lost +// response or an undecodable 200 is commit-ambiguous rather than refused. +type WorkflowDispatchAnswer sole_constructor { + dispatch: RestResult +} + +fn dispatch_workflow_with( + token: ControllerInstallationToken, + owner: NonEmptyStr, + repo: NonEmptyStr, + workflow_id: NonEmptyStr, + git_ref: NonEmptyStr, + inputs: Map, +) -> WorkflowDispatchAnswer uses net: Network { + let answered = github.Workflows.CreateDispatch( + auth_token: token.token, + owner: owner as String, + repo: repo as String, + workflow_id: workflow_id as String, + ref: git_ref as String, + inputs: inputs, + ) + WorkflowDispatchAnswer { + dispatch: match answered { + RestAnswered { answer } => RestAnswered { answer: answer.result } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +type WorkflowRunRead sole_constructor { + run: RestResult +} + +fn read_run_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> WorkflowRunRead uses net: Network { + let read = github.WorkflowRuns.GetRun(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String) + WorkflowRunRead { + run: match read { + RestAnswered { answer } => RestAnswered { answer: answer.run } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +type WorkflowAttemptJobsRead sole_constructor { + jobs: RestResult +} + +fn read_attempt_jobs_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int, attempt: Int) -> WorkflowAttemptJobsRead uses net: Network { + let read = github.WorkflowRuns.ListAttemptJobs(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String, attempt_number: attempt) + WorkflowAttemptJobsRead { + jobs: match read { + RestAnswered { answer } => RestAnswered { answer: answer.result } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +// THE QUALIFICATION RUN'S OUTPUTS, READ UNDER THE SAME CONTROLLER TOKEN AS THE RUN ITSELF. One job's +// log (actions/download-job-logs-for-workflow-run, followed through its redirect by the bound REST +// handler), the run's artifact listing, and one artifact's archive. Each answer is sealed beside its +// RestResult, like the run and job reads above, so a consumer cannot pair a body with a result it +// did not come with. The archive is Bytes and today's handler reads bodies as text, +// so a real download answers RestRefused with RestBodyUndecodable (extdeps.github.actions_artifacts). +type WorkflowJobLogRead sole_constructor { + job_id: Int + log: RestResult +} + +fn read_job_log_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, job_id: Int) -> WorkflowJobLogRead uses net: Network { + let read = github.WorkflowRuns.DownloadJobLogsForWorkflowRun(auth_token: token.token, owner: owner as String, repo: repo as String, job_id: job_id as String) + WorkflowJobLogRead { + job_id: job_id, + log: match read { + RestAnswered { answer } => RestAnswered { answer: answer.log } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +type WorkflowRunArtifactsRead sole_constructor { + listed: RestResult +} + +fn list_run_artifacts_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, run_id: Int) -> WorkflowRunArtifactsRead uses net: Network { + let read = github.Artifacts.ListWorkflowRunArtifacts(auth_token: token.token, owner: owner as String, repo: repo as String, run_id: run_id as String) + WorkflowRunArtifactsRead { + listed: match read { + RestAnswered { answer } => RestAnswered { answer: answer.result } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +type ArtifactArchiveRead sole_constructor { + artifact_id: Int + archive: RestResult +} + +fn download_artifact_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, artifact_id: Int) -> ArtifactArchiveRead uses net: Network { + let read = github.Artifacts.DownloadArtifact(auth_token: token.token, owner: owner as String, repo: repo as String, artifact_id: artifact_id as String) + ArtifactArchiveRead { + artifact_id: artifact_id, + archive: match read { + RestAnswered { answer } => RestAnswered { answer: answer.archive } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +// THE QUALIFICATION REF PIN'S EXCHANGES (gunbc.runner.runner_qualification_ref_pin), under the same +// controller installation token. Ref and group reads are reads; the create, delete and group write +// are mutations, so a lost answer is commit-ambiguous. Each answer is sealed for the same reason as +// the dispatch's: the decision module judges only what GitHub returned. +type GitRefRead sole_constructor { + reference: RestResult +} + +fn read_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, ref: NonEmptyStr) -> GitRefRead uses net: Network { + let read = github.GitDatabase.GetRef(auth_token: token.token, owner: owner as String, repo: repo as String, ref: ref as String) + GitRefRead { + reference: match read { + RestAnswered { answer } => RestAnswered { answer: answer.reference } + RestRefused { refusal } => RestRefused { refusal: refusal } + }, + } +} + +fn create_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, full_ref: NonEmptyStr, sha: String) -> RestResult uses net: Network { + match github.GitDatabase.CreateRef(auth_token: token.token, owner: owner as String, repo: repo as String, full_ref: full_ref as String, sha: sha) { + RestAnswered { answer } => RestAnswered { answer: answer.reference } + RestRefused { refusal } => RestRefused { refusal: refusal } + } +} + +fn delete_git_ref_with(token: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, ref: NonEmptyStr) -> RestResult uses net: Network { + match github.GitDatabase.DeleteRef(auth_token: token.token, owner: owner as String, repo: repo as String, ref: ref as String) { + RestAnswered { answer } => RestAnswered { answer: answer.result } + RestRefused { refusal } => RestRefused { refusal: refusal } + } +} + +fn update_runner_group_workflows_with(token: ControllerInstallationToken, organization: NonEmptyStr, runner_group_id: Int, selected_workflows: List) -> RestResult uses net: Network { + match github.OrgRunnerGroupsRest.UpdateOrganizationRunnerGroupWorkflows( + installation_token: token.token, org: organization, runner_group_id: runner_group_id, selected_workflows: selected_workflows, + ) { + RestAnswered { answer } => RestAnswered { answer: answer.result } + RestRefused { refusal } => RestRefused { refusal: refusal } + } +} diff --git a/dag/gunbc/guarantee_stall/authored_record_field_list_standing_for_type_population_stall.dag b/dag/gunbc/guarantee_stall/authored_record_field_list_standing_for_type_population_stall.dag new file mode 100644 index 00000000000..0b65eff47d5 --- /dev/null +++ b/dag/gunbc/guarantee_stall/authored_record_field_list_standing_for_type_population_stall.dag @@ -0,0 +1,39 @@ +module gunbc.guarantee_stall.authored_record_field_list_standing_for_type_population_stall + +import gunbc.guarantee_rung { Mitigatable, StructurallyImpossible } +import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, BoundedPopulation, climbs_when } + +// WHY THIS IS A STALL AND NOT A SECTION 4b(3) DROP. Nothing was lowered. Before the BmcSecure +// observation join grew past four fields, the join already bound an authored subset; extending it +// to the rest of BmcAccountStateObservation except controller_clock did not drop a rung that had +// been held. DESIGN section 4b(2) requires a class below its ceiling to name its next-rung trigger. +// +// HOMES CHECKED BEFORE MINTING, so this is not a nickname of an existing class: +// gunbc.guarantee_stall.axis_list_re_enumerates_its_variant_type_stall -- authored list mirroring a +// declared COPRODUCT constructor population; waits on constructor enumeration. Record field +// identity-and-type is a different primitive. +// gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall -- authored roster of DATA +// declarations; waits on declaration VALUE BINDING. A List is not a record field set. +// gunbc.recurring_failure_mode.optional_equality_answers_by_representation -- Optional == answering +// by representation (interpreter `first` vs Present). That is one ARM of this row's OR trigger, not +// the authored-field-list class. +// gunbc.recurring_failure_mode.handwritten_population_wrong_by_gain_and_by_loss, +// gunbc.recurring_failure_mode.roster_is_its_own_denominator -- waiter/report lists, not type fields. +// gunbc.recurring_failure_mode.kind_carried_but_its_fields_stay_optional -- Node payload optionality. +// gunbc.recurring_failure_mode.a_pattern_binding_a_subset_of_fields_is_an_implicit_wildcard -- match +// wildcards, not equality adapters. +data authored_record_field_list_standing_for_type_population_stall: GuaranteeStall = GuaranteeStall { + subject: "an authored field list stands for a record type's declared population, so a new field or a new Optional can fall out of an equality join silently", + current: Mitigatable, + ceiling: StructurallyImpossible, + blocker: AwaitsOneGrounding { + grounding: "a value-layer read of a record's declared fields with their types, sufficient to refuse an unhandled field or Optional at a join; or structural == that is value-exact over Optionals (gunbc#13488), after which clock blanking and the hand match delete", + }, + population: BoundedPopulation { + members: [ + "gunbc.machine_intake_bmc_secure bmc_account_state_observation_same", + "gunbc.machine_intake_bmc_secure controller_clock_reading_same", + ], + }, + next_rung_trigger: climbs_when(capability: "a value-layer read of a record's declared fields with their types, sufficient to refuse an unhandled field or an unhandled Optional at this join, checked by field identity and declared type never by count; OR gunbc#13488 making structural == value-exact over Optionals, after which observation_with_controller_clock blanking and controller_clock_reading_same are deleted. A count of fields, a comment claiming completeness, or a second authored list is not this capability"), +} diff --git a/dag/gunbc/guarantee_stall/grammar_change_parses_no_real_corpus_file_stall.dag b/dag/gunbc/guarantee_stall/grammar_change_parses_no_real_corpus_file_stall.dag new file mode 100644 index 00000000000..f0323a9925a --- /dev/null +++ b/dag/gunbc/guarantee_stall/grammar_change_parses_no_real_corpus_file_stall.dag @@ -0,0 +1,32 @@ +module gunbc.guarantee_stall.grammar_change_parses_no_real_corpus_file_stall + +import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } +import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, UncountedNotEnumerable, climbs_when } + +// A §4b(2) STALL, NOT A §4b(3) DROP: no control has ever parsed real corpus files on a grammar +// change, so nothing fell and there is no previous rung to name (gunbc.rung_drop's own header +// routes a never-held class here). Declared under sharp-raven-357's ruling of 2026-10-03 on +// gunbc#13126, after gunbc#13056 broke src/v2/std/node.dag, dag/gunbc/scm/object_store.dag and +// src/v2/workflow/realization_runner.dag with every merge-gating check green. +// +// WHAT HOLDS TODAY. The floor carries route controls on supplied source: one per production the +// zero-overlap grammar program changed, plus the verbatim broken shapes, in the admitted +// test.claim.parse_test family (test.claim.parse_test_grammar_program_routes and its siblings). They +// catch a regression of those shapes and no other: a grammar change that breaks a real file through a +// shape no control spells still merges. +// +// THE BLOCKER IS COST ON THE INTERPRETED ROUTE. Its instrument: one real corpus file parsed through +// the v2 front end by v2.lens.enforcement.grammar_coverage grammar_coverage_ingest_path, executed as a +// claim under claim_batch, which reports that claim's eval steps and CPU. On the interpreted route a +// whole file costs orders of magnitude more than the floor's new-witness budget per claim; the +// native-built parser does not. So the climb is the native route gating, not an interpreter ceiling +// lane (the operator declined a long-test CI lane, so nothing gating would run interpreted whole-file +// claims). +data grammar_change_parses_no_real_corpus_file_stall: GuaranteeStall = GuaranteeStall { + subject: "A grammar-authority change merges without any control parsing the real corpus files it can affect: today no control parses real corpus files on a grammar change, which is how gunbc#13056 broke src/v2/std/node.dag unseen. Held instead: supplied-source route controls, one per changed production, plus the verbatim broken shapes", + current: Mitigatable, + ceiling: MechanicallyPreventable, + blocker: AwaitsOneGrounding { grounding: "the native-built parser on the merge path: a whole-file parse on the interpreted route exceeds the floor's per-claim budget by orders of magnitude (re-derived by v2.lens.enforcement.grammar_coverage grammar_coverage_ingest_path under claim_batch)" }, + population: UncountedNotEnumerable { reason: "the corpus .dag files a grammar-authority change can affect" }, + next_rung_trigger: climbs_when(capability: "every corpus file is parsed by the native-built parser on every pull request that changes a grammar authority, and a parse refusal blocks the merge.") +} diff --git a/dag/gunbc/guarantee_stall/member_kind_door_binding_executed_by_no_claim_stall.dag b/dag/gunbc/guarantee_stall/member_kind_door_binding_executed_by_no_claim_stall.dag new file mode 100644 index 00000000000..15a0f4cb8f4 --- /dev/null +++ b/dag/gunbc/guarantee_stall/member_kind_door_binding_executed_by_no_claim_stall.dag @@ -0,0 +1,25 @@ +module gunbc.guarantee_stall.member_kind_door_binding_executed_by_no_claim_stall + +import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } +import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, BoundedPopulation, climbs_when } + +// A NEW LINK THAT HAS NEVER EXECUTED, RECORDED AS A STALL -- NOT A DROP: no rung stood here and none is claimed to have +// fallen. gunbc#13513 carries the authored data-vs-fn kind from normalize to the mandatory_tag gate +// (NormalizedTree.member_declarations -> member_kind_channel_of_tree -> ResolvedTree.member_kinds). The legs +// normalize -> member_kind_channel_of_tree -> gate execute in test.long.mandatory_tag_gate_witness. The leg from +// v2.compiler.name_resolve resolve_in_context (subject_member_kind_channel) through ResolvedTree.member_kinds to +// v2.compiler.compile validate_then_compile's mandatory_tag_root_lens_with_provenance binding is compiled and +// typechecked and executed by no claim: a door-grain control through compile_source_root_ingest_with_admission was +// refused with member_not_a_binder by a roster lens ahead of mandatory_tag on even a trivial single-module extdeps.* +// subject, a refusal independent of #13513 and filed as its own item. Each removed door-grain witness measured roughly +// 470-560k eval steps against the 72300 new-witness budget, so it is a long-lane witness once runnable. +data member_kind_door_binding_executed_by_no_claim_stall: GuaranteeStall = GuaranteeStall { + subject: "door-grain mandatory_tag provenance: the resolve_in_context -> ResolvedTree.member_kinds -> validate_then_compile mandatory_tag_root_lens_with_provenance link is executed by no claim", + current: Mitigatable, + ceiling: MechanicallyPreventable, + blocker: AwaitsOneGrounding { grounding: "the door accepting a minimal extdeps.* subject past every roster lens (the member_not_a_binder door refusal, its own item), which makes the door-grain controls runnable" }, + population: BoundedPopulation { members: [ + "v2.compiler.name_resolve resolve_in_context subject_member_kind_channel -> v2.compiler.resolve ResolvedTree.member_kinds -> v2.compiler.compile validate_then_compile mandatory_tag_root_lens_with_provenance, for every extdeps.* subject compiled through compile_source_root_ingest_with_admission" + ] }, + next_rung_trigger: climbs_when(capability: "a minimal extdeps.* subject is accepted through compile_source_root_ingest_with_admission past every roster lens, and the door-grain mandatory_tag clean and zero-arg-fn impersonation controls execute on the long lane. SUFFICIENT FOR: deleting the ResolvedTree.member_kinds binding (or resolve_in_context's subject_member_kind_channel) turns a door-grain control red. A control that supplies the channel, runs a lens directly, or reaches the gate below the door retires nothing") +} diff --git a/dag/gunbc/guarantee_stall/roster.dag b/dag/gunbc/guarantee_stall/roster.dag index 2240a5c4741..6e440e5fbad 100644 --- a/dag/gunbc/guarantee_stall/roster.dag +++ b/dag/gunbc/guarantee_stall/roster.dag @@ -30,6 +30,7 @@ import gunbc.guarantee_stall.doc_graph_dangling_link_population_stall { doc_grap import gunbc.guarantee_stall.enforcement_live_closure_gate_stall { enforcement_live_closure_gate_stall } import gunbc.guarantee_stall.mandatory_tag_live_corpus_stall { mandatory_tag_live_corpus_stall } import gunbc.guarantee_stall.test_reference_ledger_typed_census_stall { test_reference_ledger_typed_census_stall } +import gunbc.guarantee_stall.grammar_change_parses_no_real_corpus_file_stall { grammar_change_parses_no_real_corpus_file_stall } import gunbc.guarantee_stall.parse_ingest_grammar_relation_stall { parse_ingest_grammar_relation_stall } import gunbc.guarantee_stall.required_regen_host_derivation_stall { required_regen_host_derivation_stall } import gunbc.guarantee_stall.self_host_hand_authorized_cargo_toml_stall { self_host_hand_authorized_cargo_toml_stall } @@ -76,7 +77,9 @@ import gunbc.guarantee_stall.text_carrier_render_not_keyed_on_identity_stall { t import gunbc.guarantee_stall.free_semigroup_text_crossing_decided_by_spelling_stall { free_semigroup_text_crossing_decided_by_spelling_stall } import gunbc.guarantee_stall.emitted_inline_crate_path_edges_unreturned_stall { emitted_inline_crate_path_edges_unreturned_stall } import gunbc.guarantee_stall.compile_pool_provision_wired_but_unreachable_stall { compile_pool_provision_wired_but_unreachable_stall } +import gunbc.guarantee_stall.member_kind_door_binding_executed_by_no_claim_stall { member_kind_door_binding_executed_by_no_claim_stall } import gunbc.guarantee_stall.filesystem_read_outcome_node_grain_census_stall { filesystem_read_outcome_node_grain_census_stall } +import gunbc.guarantee_stall.authored_record_field_list_standing_for_type_population_stall { authored_record_field_list_standing_for_type_population_stall } // THE COHORT PROVENANCE NOTES BELOW WERE AUTHORED WHEN EVERY ROW SAT IN ONE FILE, and they are // carried here verbatim rather than split across the row modules they describe. Their membership @@ -245,10 +248,13 @@ data all_guarantee_stalls: List = [ emitted_inline_crate_path_edges_unreturned_stall, test_reference_ledger_typed_census_stall, compile_pool_provision_wired_but_unreachable_stall, + grammar_change_parses_no_real_corpus_file_stall, browser_ready_labels_fleet_route_stall, accumulator_copy_untyped_combiner_refusals_stall, keyed_apply_patch_rebuilds_rows_per_hunk_stall, + member_kind_door_binding_executed_by_no_claim_stall, filesystem_read_outcome_node_grain_census_stall, + authored_record_field_list_standing_for_type_population_stall, ] // THE FOUR SUBJECTS RESTORED TO THE ROSTER, PINNED BY IDENTITY SO THEIR LOSS REFUSES. diff --git a/dag/gunbc/harness/harness_cli.dag b/dag/gunbc/harness/harness_cli.dag index b4860965697..defa14d04d7 100644 --- a/dag/gunbc/harness/harness_cli.dag +++ b/dag/gunbc/harness/harness_cli.dag @@ -14,7 +14,7 @@ import std.process { ProcessExit, ExitSuccess, ExitFailure } import gunbc.harness.harness_tool { HarnessToolEnvironment } import gunbc.harness.harness_turn { HarnessTurnConfig, HarnessTurnOutcome, TurnProviderStopped, TurnCompletedByArtifact, TurnAlignmentCheckpointDue, harness_tool_contract_wire, - CompletesOnProviderStop, CompletesWhenEntryPresent, harness_run_turn, harness_outcome_label, + CompletesOnProviderStop, CompletesWhenEntryPresent, harness_run_turn, harness_outcome_label, TurnHostedQuotaFull, TurnHostedQuotaUnleasable, TurnHostedRateLimited, harness_outcome_detail, harness_event_placement_waiting, } import gunbc.harness.harness_guidance { @@ -29,15 +29,16 @@ import gunbc.harness.harness_supervisor_guidance { supervisor_system_prompt_at } import gunbc.harness.harness_goal_audit_guidance { harness_auditor_guidance_at, audit_guidance_prose } import gunbc.roadmap.roadmap_review_role { review_verdict_shape_text } import gunbc.harness.harness_wire { harness_context_budget_for } -import gunbc.harness.harness_reasoning_wire { HarnessServingUnit } +import gunbc.harness.harness_reasoning_wire { HarnessServingUnit, NemotronUltraFreeOnOpenRouter } +import gunbc.harness.harness_hosted_route { HarnessBackendAccess, BackendAdmittedBySeat, harness_hosted_backend_access, harness_hosted_access_wire } import gunbc.serving.turn_admission { ServingCoTenancyClass, InteractiveQualitySensitive, BatchQualityTolerant, serving_co_tenancy_class_wire, } import gunbc.harness.harness_seat_pointer { harness_seat_pointer_path, harness_seat_pointer_json } import gunbc.serving.caller_retry_contract { caller_backlog_wire, serving_route_caller_backlog } import gunbc.harness.harness_seat { harness_turn_request, harness_seat_pointer_of, - HarnessSeatBinding, HarnessSeatBound, HarnessSeatPending, HarnessSeatRefused, harness_bind_seat, - HarnessSeatRelease, HarnessSeatReleased, HarnessSeatReleaseRefused, harness_release_seat, + HarnessSeatBinding, HarnessSeatBound, HarnessSeatPending, HarnessSeatRefused, HarnessHostedBound, harness_bind_seat, harness_bind_hosted, + HarnessSeatRelease, HarnessSeatReleased, HarnessHostedCredentialRemoved, HarnessSeatReleaseRefused, harness_release_seat, harness_seat_release_wire, harness_release_held, harness_release_capability, HarnessReleaseCapability, harness_release_capability_wire, ServingModelRequirement, AnyAdmittedModel, @@ -152,7 +153,9 @@ data harness_default_wire_shape: HarnessWireShape = OpenAiChatCompletionsShape // One longer round would have ended the run through the truncation refusal, correctly. At the // declared floor 16,384 tokens is about eleven minutes of decode, and the deadline below follows // by derivation rather than being re-guessed beside it. -data harness_default_max_tokens: Int = token_count_value(t: token_count(count: 16384)) +data harness_default_max_token_count: TokenCount = token_count(count: 16384) + +data harness_default_max_tokens: Int = token_count_value(t: harness_default_max_token_count) // THE SLOWEST DECODE THIS DEPLOYMENT IS EXPECTED TO SUSTAIN, in tokens per second. It is a declared // policy floor rather than a measurement of any one run: observed rates here span 2.4 tok/s under @@ -206,6 +209,7 @@ fn harness_executor_short_hostname() -> String { fn harness_turn_exit_released(outcome: HarnessTurnOutcome, seat: HarnessSeatRelease) -> ProcessExit { match seat { HarnessSeatReleased { partition: _ } => harness_turn_exit(outcome: outcome) + HarnessHostedCredentialRemoved => harness_turn_exit(outcome: outcome) HarnessSeatReleaseRefused { detail: d } => ExitFailure { code: 1, reason: join([harness_outcome_label(outcome: outcome), "; seat ", harness_seat_release_wire(r: seat)], "") } } @@ -236,22 +240,23 @@ fn harness_macbook_environment() -> HarnessToolEnvironment { // field the way it takes the worktree, the timeout program and its two paths -- so the same loop // runs against a different serving surface by being handed a different number, not by importing a // different authority. -fn harness_macbook_config(backend_url: String, model: NonEmptyStr, unit: HarnessServingUnit) -> HarnessTurnConfig { +fn harness_macbook_config(backend: HarnessBoundBackend) -> HarnessTurnConfig { HarnessTurnConfig { - backend_url: backend_url, - model: model as String, + backend_url: backend.url, + model: backend.model as String, max_tokens: harness_default_max_tokens, - serving_unit: unit, - reasoning_mode: harness_reasoning_mode, + serving_unit: backend.unit, + reasoning_mode: backend.reasoning, wire_shape: harness_default_wire_shape, system_prompt: harness_default_system_prompt, alignment_chain_fingerprint: "", request_scratch_path: harness_request_scratch_path, events_path: harness_probe_events_path, - context_budget: harness_context_budget_for(unit: unit), + context_budget: harness_context_budget_for(unit: backend.unit), request_deadline: harness_request_deadline, environment: harness_macbook_environment(), completion: CompletesOnProviderStop, + backend_access: backend.access, } } @@ -262,9 +267,9 @@ fn harness_macbook_config(backend_url: String, model: NonEmptyStr, unit: Harness // posted, received a tool_use, executed it, posted the result back, and received a terminal. data harness_probe_prompt: String = "How many files in the dag/gunbc/harness directory of this repository end in .dag? Use run_command to find out, then state the number." -fn harness_probe_outcome(backend_url: String, model: NonEmptyStr, unit: HarnessServingUnit) -> HarnessTurnOutcome { +fn harness_probe_outcome(backend: HarnessBoundBackend) -> HarnessTurnOutcome { harness_run_turn( - config: harness_macbook_config(backend_url: backend_url, model: model, unit: unit), + config: harness_macbook_config(backend: backend), prompt: harness_probe_prompt, step_budget: harness_default_step_budget, ) @@ -291,6 +296,9 @@ fn harness_turn_exit(outcome: HarnessTurnOutcome) -> ProcessExit { TurnThinkingFormatInvalid { cause: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) TurnUsageUnavailable { cause: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) TurnSubmissionUndecodable { reason: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) + TurnHostedQuotaFull { wire: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) + TurnHostedQuotaUnleasable { detail: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) + TurnHostedRateLimited { refusal: _, steps_taken: _ } => harness_turn_refusal_exit(outcome: outcome) TurnAlignmentCheckpointDue { steps_taken: _ } => ExitFailure { code: 3, reason: harness_outcome_label(outcome: outcome) } } @@ -367,6 +375,7 @@ fn harness_bind_seat_waiting( } HarnessSeatRefused { detail: _ } => acc HarnessSeatBound { url: _, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: _, unit: _, class: _, seat: _ } => acc + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: _ } => acc }, ) waited.binding @@ -500,6 +509,8 @@ fn harness_place_for(request: String, attempt: String, receipt: String) -> Proce SeatPermitMinted { permit: permit } => harness_place_receipt(binding: binding, url: url, group_wire: group_wire, bound_model: bound_model, launch: launch, capability: capability, grant: grant, permit: permit, receipt: receipt) } } + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: _ } => + harness_place_receipt_failed(binding: binding, cause: "an any-model placement answered with a hosted binding, which holds no seat a router can be handed") } } @@ -588,6 +599,7 @@ fn harness_release_admitted(partition: String, reference: String, grant: String, } match harness_release_held(short_hostname: harness_executor_short_hostname(), capability: capability, reason: reason as NonEmptyStr) { HarnessSeatReleased { partition: _ } => ExitSuccess + HarnessHostedCredentialRemoved => ExitFailure { code: 1, reason: join(["harness seat release for ", harness_release_capability_wire(c: capability), " answered as a hosted credential removal; the seat may still be held"], "") } HarnessSeatReleaseRefused { detail: d } => ExitFailure { code: 1, reason: join(["harness seat release refused for ", harness_release_capability_wire(c: capability), ": ", d], "") } } @@ -602,11 +614,11 @@ fn harness_release_admitted(partition: String, reference: String, grant: String, // while something else is in flight. fn harness_probe_cli() -> ProcessExit { let binding = harness_bind_seat(short_hostname: harness_executor_short_hostname(), class: BatchQualityTolerant, requirement: AnyAdmittedModel, shape: harness_default_wire_shape, inputs: harness_model_request_inputs(role: "probe", shape: harness_default_wire_shape, system_prompt: harness_default_system_prompt, user_input: harness_probe_prompt), actor: "probe" as NonEmptyStr, attempt: harness_probe_attempt(label: "probe"), policy: harness_seat_policy) - match binding { - HarnessSeatRefused { detail } => harness_backend_refusal_exit(detail: detail) - HarnessSeatPending { detail } => harness_placement_pending_exit(detail: detail) - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { - let outcome = harness_probe_outcome(backend_url: url, model: bound_model, unit: bound_unit) + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { + let outcome = harness_probe_outcome(backend: backend) let seat_release = harness_release_seat(binding: binding, reason: harness_outcome_label(outcome: outcome) as NonEmptyStr) harness_turn_exit_released(outcome: outcome, seat: seat_release) } @@ -642,24 +654,22 @@ fn harness_worker_config( timeout_program: FilePath, request_scratch_path: FilePath, events_path: FilePath, - backend_url: String, - model: NonEmptyStr, - unit: HarnessServingUnit, + backend: HarnessBoundBackend, system_prompt: String, alignment_chain_fingerprint: String, ) -> HarnessTurnConfig { HarnessTurnConfig { - backend_url: backend_url, - model: model as String, + backend_url: backend.url, + model: backend.model as String, max_tokens: harness_default_max_tokens, - serving_unit: unit, - reasoning_mode: harness_reasoning_mode, + serving_unit: backend.unit, + reasoning_mode: backend.reasoning, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, alignment_chain_fingerprint: alignment_chain_fingerprint, request_scratch_path: request_scratch_path, events_path: events_path, - context_budget: harness_context_budget_for(unit: unit), + context_budget: harness_context_budget_for(unit: backend.unit), request_deadline: harness_request_deadline, environment: HarnessToolEnvironment { working_directory: worktree, @@ -667,6 +677,7 @@ fn harness_worker_config( duration_limit_seconds: harness_default_duration_limit_seconds, }, completion: CompletesOnProviderStop, + backend_access: backend.access, } } @@ -732,10 +743,10 @@ fn harness_worker_cli( ) -> ProcessExit { let system_prompt = harness_worker_system_prompt_at(worktree: worktree, repo_root: repo_root, gunbc_program: gunbc_program) let binding = harness_bind_seat_waiting(role: "worker", system_prompt: system_prompt, brief: brief, actor: join(["worker:", worktree], "") as NonEmptyStr, attempt: attempt_identity as NonEmptyStr, events_path: events_path) - match binding { - HarnessSeatRefused { detail } => harness_backend_refusal_exit(detail: detail) - HarnessSeatPending { detail } => harness_placement_pending_exit(detail: detail) - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { match harness_seat_pointer_persist(events_path: events_path, binding: binding) { HarnessSeatPointerUnwritten { detail: pointer_detail } => harness_seat_pointer_refused_exit(binding: binding, detail: pointer_detail) HarnessSeatPointerWritten => { @@ -745,9 +756,7 @@ fn harness_worker_cli( timeout_program: timeout_program as FilePath, request_scratch_path: request_scratch_path as FilePath, events_path: events_path as FilePath, - backend_url: url, - model: bound_model, - unit: bound_unit, + backend: backend, system_prompt: system_prompt, alignment_chain_fingerprint: alignment_chain_fingerprint, ), @@ -787,23 +796,21 @@ fn harness_reviewer_config( events_path: FilePath, verdict_dir: String, verdict_name: String, - backend_url: String, - model: NonEmptyStr, - unit: HarnessServingUnit, + backend: HarnessBoundBackend, system_prompt: String, ) -> HarnessTurnConfig { HarnessTurnConfig { - backend_url: backend_url, - model: model as String, + backend_url: backend.url, + model: backend.model as String, max_tokens: harness_default_max_tokens, - serving_unit: unit, - reasoning_mode: harness_reasoning_mode, + serving_unit: backend.unit, + reasoning_mode: backend.reasoning, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, - context_budget: harness_context_budget_for(unit: unit), + context_budget: harness_context_budget_for(unit: backend.unit), request_deadline: harness_request_deadline, environment: HarnessToolEnvironment { working_directory: worktree, @@ -811,6 +818,7 @@ fn harness_reviewer_config( duration_limit_seconds: harness_default_duration_limit_seconds, }, completion: CompletesWhenEntryPresent { directory: verdict_dir, name: verdict_name }, + backend_access: backend.access, } } @@ -831,10 +839,10 @@ fn harness_reviewer_cli( let verdict_path = join([verdict_dir, "/", verdict_name], "") let system_prompt = harness_reviewer_system_prompt_at(worktree: worktree, verdict_path: verdict_path) let binding = harness_bind_seat_waiting(role: "reviewer", system_prompt: system_prompt, brief: brief, actor: join(["reviewer:", verdict_path], "") as NonEmptyStr, attempt: attempt_identity as NonEmptyStr, events_path: events_path) - match binding { - HarnessSeatRefused { detail } => harness_backend_refusal_exit(detail: detail) - HarnessSeatPending { detail } => harness_placement_pending_exit(detail: detail) - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { match harness_seat_pointer_persist(events_path: events_path, binding: binding) { HarnessSeatPointerUnwritten { detail: pointer_detail } => harness_seat_pointer_refused_exit(binding: binding, detail: pointer_detail) HarnessSeatPointerWritten => { @@ -846,9 +854,7 @@ fn harness_reviewer_cli( events_path: events_path as FilePath, verdict_dir: verdict_dir, verdict_name: verdict_name, - backend_url: url, - model: bound_model, - unit: bound_unit, + backend: backend, system_prompt: system_prompt, ), prompt: brief, @@ -874,23 +880,21 @@ fn harness_auditor_config( events_path: FilePath, verdict_dir: String, verdict_name: String, - backend_url: String, - model: NonEmptyStr, - unit: HarnessServingUnit, + backend: HarnessBoundBackend, system_prompt: String, ) -> HarnessTurnConfig { HarnessTurnConfig { - backend_url: backend_url, - model: model as String, + backend_url: backend.url, + model: backend.model as String, max_tokens: harness_default_max_tokens, - serving_unit: unit, - reasoning_mode: harness_reasoning_mode, + serving_unit: backend.unit, + reasoning_mode: backend.reasoning, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, - context_budget: harness_context_budget_for(unit: unit), + context_budget: harness_context_budget_for(unit: backend.unit), request_deadline: harness_request_deadline, environment: HarnessToolEnvironment { working_directory: worktree, @@ -898,6 +902,7 @@ fn harness_auditor_config( duration_limit_seconds: harness_default_duration_limit_seconds, }, completion: CompletesWhenEntryPresent { directory: verdict_dir, name: verdict_name }, + backend_access: backend.access, } } @@ -917,10 +922,10 @@ fn harness_auditor_cli( let verdict_path = join([verdict_dir, "/", verdict_name], "") let system_prompt = harness_auditor_system_prompt_at(worktree: worktree, verdict_path: verdict_path) let binding = harness_bind_seat_waiting(role: "auditor", system_prompt: system_prompt, brief: brief, actor: join(["auditor:", verdict_path], "") as NonEmptyStr, attempt: attempt_identity as NonEmptyStr, events_path: events_path) - match binding { - HarnessSeatRefused { detail } => harness_backend_refusal_exit(detail: detail) - HarnessSeatPending { detail } => harness_placement_pending_exit(detail: detail) - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { match harness_seat_pointer_persist(events_path: events_path, binding: binding) { HarnessSeatPointerUnwritten { detail: pointer_detail } => harness_seat_pointer_refused_exit(binding: binding, detail: pointer_detail) HarnessSeatPointerWritten => { @@ -932,9 +937,7 @@ fn harness_auditor_cli( events_path: events_path as FilePath, verdict_dir: verdict_dir, verdict_name: verdict_name, - backend_url: url, - model: bound_model, - unit: bound_unit, + backend: backend, system_prompt: system_prompt, ), prompt: brief, @@ -970,23 +973,21 @@ fn harness_supervisor_config( events_path: FilePath, verdict_dir: String, verdict_name: String, - backend_url: String, - model: NonEmptyStr, - unit: HarnessServingUnit, + backend: HarnessBoundBackend, system_prompt: String, ) -> HarnessTurnConfig { HarnessTurnConfig { - backend_url: backend_url, - model: model as String, + backend_url: backend.url, + model: backend.model as String, max_tokens: harness_default_max_tokens, - serving_unit: unit, - reasoning_mode: harness_reasoning_mode, + serving_unit: backend.unit, + reasoning_mode: backend.reasoning, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, - context_budget: harness_context_budget_for(unit: unit), + context_budget: harness_context_budget_for(unit: backend.unit), request_deadline: harness_request_deadline, environment: HarnessToolEnvironment { working_directory: worktree, @@ -994,6 +995,7 @@ fn harness_supervisor_config( duration_limit_seconds: harness_default_duration_limit_seconds, }, completion: CompletesWhenEntryPresent { directory: verdict_dir, name: verdict_name }, + backend_access: backend.access, } } @@ -1010,10 +1012,10 @@ fn harness_supervisor_cli( let verdict_path = join([verdict_dir, "/", verdict_name], "") let system_prompt = supervisor_system_prompt_at(worktree: worktree, verdict_path: verdict_path) let binding = harness_bind_seat_waiting(role: "supervisor", system_prompt: system_prompt, brief: brief, actor: join(["supervisor:", verdict_path], "") as NonEmptyStr, attempt: attempt_identity as NonEmptyStr, events_path: events_path) - match binding { - HarnessSeatRefused { detail } => harness_backend_refusal_exit(detail: detail) - HarnessSeatPending { detail } => harness_placement_pending_exit(detail: detail) - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { match harness_seat_pointer_persist(events_path: events_path, binding: binding) { HarnessSeatPointerUnwritten { detail: pointer_detail } => harness_seat_pointer_refused_exit(binding: binding, detail: pointer_detail) HarnessSeatPointerWritten => { @@ -1025,9 +1027,7 @@ fn harness_supervisor_cli( events_path: events_path as FilePath, verdict_dir: verdict_dir, verdict_name: verdict_name, - backend_url: url, - model: bound_model, - unit: bound_unit, + backend: backend, system_prompt: system_prompt, ), prompt: brief, @@ -1045,11 +1045,11 @@ fn harness_supervisor_cli( // with a door beside it. fn harness_probe_report() -> String { let binding = harness_bind_seat(short_hostname: harness_executor_short_hostname(), class: BatchQualityTolerant, requirement: AnyAdmittedModel, shape: harness_default_wire_shape, inputs: harness_model_request_inputs(role: "probe", shape: harness_default_wire_shape, system_prompt: harness_default_system_prompt, user_input: harness_probe_prompt), actor: "probe-report" as NonEmptyStr, attempt: harness_probe_attempt(label: "probe-report"), policy: harness_seat_policy) - match binding { - HarnessSeatRefused { detail } => join(["harness probe refused before the turn: ", detail, "\n"], "") - HarnessSeatPending { detail } => join(["harness probe is pending placement: ", detail, "\n"], "") - HarnessSeatBound { url, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: bound_model, unit: bound_unit, class: _, seat: _ } => { - let outcome = harness_probe_outcome(backend_url: url, model: bound_model, unit: bound_unit) + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => join(["harness probe refused before the turn: ", detail, "\n"], "") + BindingPending { detail } => join(["harness probe is pending placement: ", detail, "\n"], "") + BindingReady { backend } => { + let outcome = harness_probe_outcome(backend: backend) let seat_release = harness_release_seat(binding: binding, reason: harness_outcome_label(outcome: outcome) as NonEmptyStr) join([ "harness probe outcome: ", harness_outcome_label(outcome: outcome), "\n", @@ -1059,3 +1059,90 @@ fn harness_probe_report() -> String { } } } + +// ── ONE BACKEND PER BINDING, WHICHEVER WAY IT WAS ADMITTED ──────────────────────────────────── +// +// Every entry runs the same turn against what its binding names: where requests go, which model and +// unit answer them, at which reasoning level, and how each request is admitted. A fleet seat runs at +// the operator's declared level and posts unauthenticated over the fleet network; a hosted binding runs +// at the level its route SELECTED and leases quota before each request. Deriving that once here is what +// keeps the hosted arm from being a second copy of every entry body. +type HarnessBoundBackend { + url: String + model: NonEmptyStr + unit: HarnessServingUnit + reasoning: HarnessReasoningLevel + access: HarnessBackendAccess +} + +type HarnessBindingAnswer + = BindingRefused { detail: String } + | BindingPending { detail: String } + | BindingReady { backend: HarnessBoundBackend } + +fn harness_binding_answer(binding: HarnessSeatBinding) -> HarnessBindingAnswer { + match binding { + HarnessSeatRefused { detail: d } => BindingRefused { detail: d } + HarnessSeatPending { detail: d } => BindingPending { detail: d } + HarnessSeatBound { url: u, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: m, unit: un, class: _, seat: _ } => + BindingReady { backend: HarnessBoundBackend { url: u, model: m, unit: un, reasoning: harness_reasoning_mode, access: BackendAdmittedBySeat } } + HarnessHostedBound { url: u, model: m, unit: un, level: l, access: a } => + BindingReady { backend: HarnessBoundBackend { url: u, model: m, unit: un, reasoning: l, access: harness_hosted_backend_access(access: a) } } + } +} + +// THE HOSTED PROBE: the probe turn, placed on the hosted Nemotron 3 Ultra free listing by NAME. It is the +// one entry that reaches a hosted unit, because an unnamed requirement never does (gunbc.harness.harness_seat +// harness_requirement_placement): the listing is governed by NVIDIA's trial terms, so reaching it is an +// operator's explicit act, not a placement default. It takes its tool environment as arguments, as the +// worker does, so it runs on whatever host invokes it rather than assuming the MacBook's. +// THE ROUTE'S STANDING IS WRITTEN BESIDE THE EVENTS BEFORE THE HOSTED PROBE'S TURN RUNS: the tier and daily +// counters the key read reported, the quota rows each request is charged to, and the NVIDIA trial terms +// that govern the endpoint. A run whose receipt cannot be written does not run, and gives its credential back. +fn harness_hosted_route_receipt(binding: HarnessSeatBinding) -> String? { + match binding { + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: a } => Present { value: harness_hosted_access_wire(access: a) } + HarnessSeatBound { url: _, grant: _, partition: _, ceiling: _, group_wire: _, launch: _, store: _, model: _, unit: _, class: _, seat: _ } => none + HarnessSeatPending { detail: _ } => none + HarnessSeatRefused { detail: _ } => none + } +} + +fn harness_hosted_probe_cli(worktree: String, timeout_program: String, request_scratch_path: String, events_path: String) -> ProcessExit { + let binding = harness_bind_hosted(short_hostname: harness_executor_short_hostname(), unit: NemotronUltraFreeOnOpenRouter, actor: join(["hosted-probe:", worktree], "") as NonEmptyStr, max_tokens: harness_default_max_token_count) + match harness_binding_answer(binding: binding) { + BindingRefused { detail } => harness_backend_refusal_exit(detail: detail) + BindingPending { detail } => harness_placement_pending_exit(detail: detail) + BindingReady { backend } => { + match harness_hosted_route_receipt(binding: binding) { + Absent => { + let seat_release = harness_release_seat(binding: binding, reason: "the hosted route receipt could not be derived" as NonEmptyStr) + ExitFailure { code: 1, reason: join(["hosted probe: the binding carries no hosted access to describe; ", harness_seat_release_wire(r: seat_release)], "") } + } + Present { value: receipt } => { + let written = Filesystem.Write(path: join([events_path, ".route"], ""), content: concat(receipt, "\n")) + if !written.success { + let seat_release = harness_release_seat(binding: binding, reason: "the hosted route receipt could not be written" as NonEmptyStr) + ExitFailure { code: 1, reason: join(["hosted probe: the route receipt could not be written to ", events_path, ".route: ", written.error, "; ", harness_seat_release_wire(r: seat_release)], "") } + } else { + let outcome = harness_run_turn( + config: harness_worker_config( + worktree: worktree as FilePath, + timeout_program: timeout_program as FilePath, + request_scratch_path: request_scratch_path as FilePath, + events_path: events_path as FilePath, + backend: backend, + system_prompt: harness_default_system_prompt, + alignment_chain_fingerprint: "", + ), + prompt: harness_probe_prompt, + step_budget: harness_default_step_budget, + ) + let seat_release = harness_release_seat(binding: binding, reason: harness_outcome_label(outcome: outcome) as NonEmptyStr) + harness_turn_exit_released(outcome: outcome, seat: seat_release) + } + } + } + } + } +} diff --git a/dag/gunbc/harness/harness_guidance.dag b/dag/gunbc/harness/harness_guidance.dag index 7d2d5bf52ef..13d1c4fec11 100644 --- a/dag/gunbc/harness/harness_guidance.dag +++ b/dag/gunbc/harness/harness_guidance.dag @@ -211,7 +211,7 @@ fn harness_worker_guidance_at(worktree: String, test_command: String, test_recei grounded(premise: IterationSelectsByPattern { grain: "//pkg:name" }, module_path: "gunbc.compute.test_selection", decl_name: "select_witness_files"), grounded(premise: RunRebuildsOnTreeChange, - module_path: "gunbc.compute.work_request", decl_name: "work_identity"), + module_path: "gunbc.compute.work_provider_local", decl_name: "compute_work_identity"), grounded(premise: AcceptanceIsTheContractValidation, module_path: "gunbc.roadmap_execution_contract", decl_name: "validation_operation_command"), grounded(premise: CapabilityWithheld { capability: BuildOrTestOutsideTheProcess }, diff --git a/dag/gunbc/harness/harness_hosted_route.dag b/dag/gunbc/harness/harness_hosted_route.dag new file mode 100644 index 00000000000..84a1d39210e --- /dev/null +++ b/dag/gunbc/harness/harness_hosted_route.dag @@ -0,0 +1,670 @@ +module gunbc.harness.harness_hosted_route + +import std.types { String, Bool, Int, List, NonEmptyStr, FilePath, EpochSecs } +import std.algebra { trim } +import std.nat { Nat } +import std.measure { TokenCount, token_count_value, Second, second, second_count, MoneyAmountMicro, money_amount_micro_count } +import std.checked_arithmetic { checked_int_to_nat } +import std.decl_ref { DeclarationRef, decl_ref, declaration_ref_eq } +import std.interval { degenerate_interval } +import std.pareto { AxisGap, AxisReading, HigherIsBetter, LowerIsBetter, ParetoEntry, SelectionAxis } +import std.decision { + ChoiceDomain, ConstraintSatisfied, CausalModelEstablished, DecisionAssessment, DecisionConstraint, + DecisionRegion, DecisionSubject, DecisionVariable, ContextParameter, HardConstraint, ObjectiveMeasure, + ObjectiveTerm, ParameterBound, PhysicalModelEstablished, RealizationSelectionResult, MonotoneIncreasing, + UniqueSurvivorRequired, VariesAlong, VendorDocumentedCoupling, select_realization, + SelectedWithin, CandidateFieldUnestablished, NoFeasibleRealization, SelectionNeedsEvidence, + SelectionNeedsPolicy, RealizationSelectionRefused, +} +import std.upsert_decision { ObservationVerdict, Converged, Inaccessible } +import extdeps.external_authority { CitedToAuthority } +import extdeps.api_rate_limit { UpstreamRateLimit } +import extdeps.cloud.gcp.secret_ref { SecretRef } +import extdeps.cloud.gcp.secret_manager { SmEnabled } +import extdeps.http.client +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.shell +import extdeps.openrouter.openrouter { + OpenRouterReasoningEffort, openrouter_chat_completions_url, openrouter_key_url, openrouter_authorization_header_line, + openrouter_free_models_per_minute, openrouter_free_models_per_day, openrouter_reasoning_authority, + CreditsTierObserved, CreditsTierUnrecognized, openrouter_credits_from_daily_limit, + OpenRouterKeyRead, OpenRouterKeyDecoded, OpenRouterKeyUndecodable, openrouter_decode_key_read, + KeySpendUncapped, KeySpendCappedAtZero, KeySpendCappedAbove, openrouter_key_spend_limit_wire, + OpenRouterErrorDecoded, OpenRouterErrorUndecodable, openrouter_decode_error_body, openrouter_error_wire, + openrouter_effort_eq, +} +import extdeps.openrouter.nvidia_nemotron_3_ultra_free { + nemotron_ultra_free_max_completion_tokens, nemotron_ultra_free_supported_efforts, + nemotron_ultra_free_price_per_token, nemotron_ultra_free_terms, +} +import extdeps.nvidia.api_trial_terms { NvidiaApiTrialTerms, nvidia_api_trial_terms_wire } +import gunbc.secret_provision { openrouter_free_tier_key_secret_ref } +import gunbc.auth.access_token_source { AccessTokenReady, AccessTokenUpsertRequired, AccessTokenEnsureRefused, resolve_access_token, access_token_upsert_required_reason } +import gunbc.auth.secret_ref_credential { + SecretCredentialReady, SecretCredentialAccessUpsertRequired, SecretCredentialWireDecodeRefused, + SecretCredentialFetchRefused, SecretCredentialResolvedVersionMismatch, fetch_secret_ref_credential_with_token, +} +import gunbc.auth.materialized_secret { + BearerHeaderFile, with_materialized_secret, MaterializationCompleted, MaterializationRefused, + SecretNotLive, SecretVersionUnusable, LivenessProbeNotIndependent, +} +import gunbc.readback_independence { ReadBackProbe, ProbeInert } +import gunbc.fabric_quota { + QuotaLease, QuotaLeased, QuotaFull, QuotaLeaseRefused, fabric_quota_lease_for, QuotaTermToWindowEnd, + fabric_quota_settle, QuotaSettle, QuotaSettled, QuotaSettleRefused, quota_settle_wire, + fabric_quota_observe_upstream, QuotaUpstreamRecorded, QuotaObservationRefused, +} +import gunbc.harness.harness_reasoning_wire { + HarnessServingUnit, DeepseekV4FlashOnVllm, Glm53FlashOnVllm, NemotronUltraFreeOnOpenRouter, + HarnessReasoningLevel, ReasoningOff, ReasoningStandard, ReasoningMaximum, harness_reasoning_level_wire, + harness_openrouter_level_effort, harness_reasoning_kwargs_for, ReasoningKwargsAdmitted, ReasoningLevelUnsupported, + harness_unit_model_id, harness_serving_unit_wire, +} + +// A HOSTED HARNESS ROUTE: a unit the harness reaches through an upstream it does not operate, admitted by +// leasing that upstream's quota rather than by taking a seat on a serving group. The seat model +// (gunbc.harness.harness_seat) is about co-tenancy on an engine with loaded state; a hosted upstream +// meters REQUESTS against a credential, so the honest capacity fact is the per-request quota and its +// carrier is gunbc.fabric_quota over the upstream's published UpstreamRateLimit rows. +// +// FOUR THINGS HAPPEN HERE AND NOWHERE ELSE: the route's facts are joined from their upstream authorities; +// the reasoning level a turn on the route runs at is SELECTED, not written down; the credential is +// materialized and its liveness read before any turn spends it; and each request leases both of the +// upstream's pools before it goes out and settles them after. + +// ── THE ROUTE, JOINED FROM ITS AUTHORITIES ──────────────────────────────────────────────────── + +type HarnessHostedRoute { + unit: HarnessServingUnit + url: NonEmptyStr + key_url: NonEmptyStr + secret: SecretRef + per_minute: UpstreamRateLimit + max_completion: TokenCount + supported_efforts: List + price_per_token: MoneyAmountMicro + terms: NvidiaApiTrialTerms +} + +// A FLEET UNIT HAS NO HOSTED ROUTE, and the answer is absent rather than a default route: a caller that +// reached this function with a fleet unit has mis-dispatched, and the seat binder refuses it by name. +fn harness_hosted_route_for(unit: HarnessServingUnit) -> HarnessHostedRoute? { + match unit { + DeepseekV4FlashOnVllm => none + Glm53FlashOnVllm => none + NemotronUltraFreeOnOpenRouter => Present { value: HarnessHostedRoute { + unit: unit, + url: openrouter_chat_completions_url, + key_url: openrouter_key_url, + secret: openrouter_free_tier_key_secret_ref, + per_minute: openrouter_free_models_per_minute, + max_completion: nemotron_ultra_free_max_completion_tokens, + supported_efforts: nemotron_ultra_free_supported_efforts, + price_per_token: nemotron_ultra_free_price_per_token, + terms: nemotron_ultra_free_terms, + } } + } +} + +// ── THE REASONING LEVEL IS SELECTED, NOT WRITTEN DOWN ───────────────────────────────────────── +// +// The fleet harness runs at a declared level (gunbc.harness.harness_cli harness_reasoning_mode), chosen by +// the operator for an engine whose compute is the fleet's. On a hosted free listing the trade is +// different and stated by the listing itself: the price is zero per token and the limit is per request, +// so a deeper effort costs neither money nor quota -- only latency and the completion budget its thinking +// shares with the answer. That is a selection over a funded field (DESIGN section 3d), so it is asked of +// std.decision rather than answered by a constant. +// +// THE FIELD is the harness's three levels. THE AXES are reasoning depth (higher is better; the rank is the +// effort's position in the listing's supported_efforts, which the reasoning-tokens guide says are +// "returned in descending effort order", with disabled below every effort), money per request and quota +// per request (lower is better; equal across the field by the listing's price and the limits page's +// per-request metering), and admissibility. THE HARD CONSTRAINT is admissibility: a level the unit cannot +// express, or a request whose max_tokens exceeds the listing's completion ceiling, reads 0 and is excluded +// before Pareto, never ranked. +// +// LATENCY IS NOT A FUNDED AXIS, and that is the stated reason the answer is the deepest admitted level: +// nothing in this harness measures per-effort latency on this listing, and a turn's deadline already +// bounds it (gunbc.harness.harness_cli harness_request_deadline). Funding latency without a reading +// would answer SelectionNeedsEvidence, which is the honest arm the day a latency budget binds; until then +// depth dominates on a field where money and quota are equal. +data harness_hosted_selection_module: String = "gunbc.harness.harness_hosted_route" + +fn hosted_ref(name: String) -> DeclarationRef { + decl_ref(module_path: harness_hosted_selection_module, decl_name: name) +} + +data ax_hosted_reasoning_depth: DeclarationRef = hosted_ref(name: "ax_hosted_reasoning_depth") +data ax_hosted_money_per_request: DeclarationRef = hosted_ref(name: "ax_hosted_money_per_request") +data ax_hosted_quota_per_request: DeclarationRef = hosted_ref(name: "ax_hosted_quota_per_request") +data ax_hosted_level_admitted: DeclarationRef = hosted_ref(name: "ax_hosted_level_admitted") + +data harness_hosted_level_axes: List = [ + SelectionAxis { identity: ax_hosted_reasoning_depth, goal: HigherIsBetter }, + SelectionAxis { identity: ax_hosted_money_per_request, goal: LowerIsBetter }, + SelectionAxis { identity: ax_hosted_quota_per_request, goal: LowerIsBetter }, + SelectionAxis { identity: ax_hosted_level_admitted, goal: HigherIsBetter }, +] + +data hosted_level_admitted_constraint: DeclarationRef = hosted_ref(name: "hosted_level_admitted_constraint") + +data harness_hosted_level_constraints: List = [ + HardConstraint { identity: hosted_level_admitted_constraint, axis: ax_hosted_level_admitted, bound_micro: 1000000 }, +] + +fn hosted_level_identity(level: HarnessReasoningLevel) -> DeclarationRef { + hosted_ref(name: join(["hosted_level_", harness_reasoning_level_wire(level: level) as String], "")) +} + +// THE DEPTH RANK, from the listing's own order. Disabled is 0; an effort at index i of an n-long +// descending list ranks n - i; an effort the listing does not carry has no rank, and the level that maps +// to it is inadmissible anyway, so its reading is 0 and the hard constraint is what excludes it. +type HostedRankAcc { seen: Bool, rank: Int } + +fn hosted_effort_rank(efforts: List, effort: OpenRouterReasoningEffort) -> Int { + let acc = fold(efforts, init: HostedRankAcc { seen: false, rank: 0 }, f: (a, e) => { + let seen = a.seen || openrouter_effort_eq(a: e, b: effort) + HostedRankAcc { seen: seen, rank: if seen { a.rank + 1 } else { 0 } } + }) + acc.rank +} + +fn hosted_level_depth_rank(route: HarnessHostedRoute, level: HarnessReasoningLevel) -> Int { + match harness_openrouter_level_effort(level: level) { + Absent => 0 + Present { value: effort } => hosted_effort_rank(efforts: route.supported_efforts, effort: effort) + } +} + +fn hosted_level_admitted(route: HarnessHostedRoute, level: HarnessReasoningLevel, max_tokens: TokenCount) -> Bool { + token_count_value(t: max_tokens) <= token_count_value(t: route.max_completion) + && (match harness_reasoning_kwargs_for(unit: route.unit, level: level) { + ReasoningKwargsAdmitted { kwargs: _ } => true + ReasoningLevelUnsupported { unit: _, level: _, cause: _ } => false + }) +} + +fn hosted_level_entry(route: HarnessHostedRoute, level: HarnessReasoningLevel, max_tokens: TokenCount) -> ParetoEntry { + let no_gaps: List = [] + ParetoEntry { + identity: hosted_level_identity(level: level), + display_label: harness_reasoning_level_wire(level: level), + readings: [ + AxisReading { axis: ax_hosted_reasoning_depth, interval_micro: degenerate_interval(point: hosted_level_depth_rank(route: route, level: level) * 1000000) }, + AxisReading { axis: ax_hosted_money_per_request, interval_micro: degenerate_interval(point: (money_amount_micro_count(m: route.price_per_token) as Int) * token_count_value(t: max_tokens)) }, + AxisReading { axis: ax_hosted_quota_per_request, interval_micro: degenerate_interval(point: 1000000) }, + AxisReading { axis: ax_hosted_level_admitted, interval_micro: degenerate_interval(point: if hosted_level_admitted(route: route, level: level, max_tokens: max_tokens) { 1000000 } else { 0 }) }, + ], + missing_inputs: no_gaps, + } +} + +data harness_hosted_levels: List = [ReasoningOff, ReasoningStandard, ReasoningMaximum] + +fn harness_hosted_level_field(route: HarnessHostedRoute, max_tokens: TokenCount) -> List { + map(harness_hosted_levels, l => hosted_level_entry(route: route, level: l, max_tokens: max_tokens)) +} + +fn harness_hosted_level_assessment() -> DecisionAssessment { + let subject_id = hosted_ref(name: "harness_hosted_level_subject") + let var_id = hosted_ref(name: "harness_hosted_level_variable") + let term_id = hosted_ref(name: "harness_hosted_reasoning_depth_term") + let param_id = hosted_ref(name: "harness_hosted_route_parameter") + DecisionAssessment { + subject: DecisionSubject { + identity: subject_id, + question: "which reasoning level a harness turn on a hosted free listing runs at" as NonEmptyStr, + parameters: [ContextParameter { identity: param_id }], + constraints: [DecisionConstraint { identity: hosted_level_admitted_constraint }], + objective: ObjectiveMeasure { + identity: hosted_ref(name: "harness_hosted_level_objective"), + goal: HigherIsBetter, + terms: [ObjectiveTerm { identity: term_id, ground: CitedToAuthority { authority: openrouter_reasoning_authority } }], + }, + choice_domain: ChoiceDomain { + identity: hosted_ref(name: "harness_hosted_level_choice_domain"), + variables: [DecisionVariable { identity: var_id }], + }, + }, + sensitivities: [ + VariesAlong { + term: term_id, + variable: var_id, + region: DecisionRegion { identity: hosted_ref(name: "harness_hosted_level_region") }, + relation: MonotoneIncreasing, + coupling: CitedToAuthority { authority: openrouter_reasoning_authority }, + }, + ], + constraints: [ + ConstraintSatisfied { constraint: hosted_level_admitted_constraint, evidence: "admissibility applied to the field as the hard constraint: the unit's reasoning seam and the listing's completion ceiling" as NonEmptyStr }, + ], + parameters: [ + ParameterBound { parameter: param_id, evidence: "the hosted route joined from its listing" as NonEmptyStr }, + ], + selected_candidate: Absent, + term_drops: [], + physical_model: PhysicalModelEstablished { terms: [term_id] }, + causal_model: CausalModelEstablished { + couplings: [ + VendorDocumentedCoupling { authority: openrouter_reasoning_authority, term: term_id, variable: var_id, subject: subject_id }, + ], + }, + } +} + +fn harness_hosted_level_selection(route: HarnessHostedRoute, max_tokens: TokenCount) -> RealizationSelectionResult { + select_realization( + assessment: harness_hosted_level_assessment(), + candidate_field_identity: hosted_ref(name: "harness_hosted_level_field"), + field: harness_hosted_level_field(route: route, max_tokens: max_tokens), + axes: harness_hosted_level_axes, + constraints: harness_hosted_level_constraints, + constraint_authority: hosted_ref(name: "harness_hosted_level_constraints"), + policy: UniqueSurvivorRequired { identity: hosted_ref(name: "harness_hosted_level_policy") }, + evidence_snapshot: hosted_ref(name: "harness_hosted_route_for"), + ) +} + +type HostedLevelChoice + = HostedLevelSelected { level: HarnessReasoningLevel } + | HostedLevelUnselected { cause: String } + +fn hosted_level_of_identity(identity: DeclarationRef) -> HarnessReasoningLevel? { + match filter(harness_hosted_levels, l => declaration_ref_eq(a: hosted_level_identity(level: l), b: identity)).first() { + Absent => none + Present { value: l } => Present { value: l } + } +} + +// ANY ARM BUT A SELECTION IS A REFUSAL, carried with the arm's name. A turn does not run at a level the +// selection did not choose: there is no fallback level, because a fallback is exactly the literal this +// selection replaced. +fn harness_hosted_level_choice(route: HarnessHostedRoute, max_tokens: TokenCount) -> HostedLevelChoice { + match harness_hosted_level_selection(route: route, max_tokens: max_tokens) { + SelectedWithin { candidate: c, receipt: _ } => + match hosted_level_of_identity(identity: c.identity) { + Present { value: l } => HostedLevelSelected { level: l } + Absent => HostedLevelUnselected { cause: "the selection returned a candidate that is not one of the harness's levels" } + } + CandidateFieldUnestablished { causes: cs } => HostedLevelUnselected { cause: join(["the level field is unestablished: ", join(map(cs, c => c as String), "; ")], "") } + NoFeasibleRealization { causes: cs, scope: _ } => HostedLevelUnselected { cause: join(["no level is admissible on this route: ", join(map(cs, c => c as String), "; ")], "") } + SelectionNeedsEvidence { needs: _ } => HostedLevelUnselected { cause: "the level selection needs evidence it does not have" } + SelectionNeedsPolicy { survivors: _ } => HostedLevelUnselected { cause: "more than one level survives and no policy chooses between them" } + RealizationSelectionRefused { defects: ds } => HostedLevelUnselected { cause: join(["the level selection refused: ", join(map(ds, d => d as String), "; ")], "") } + } +} + +// ── THE CREDENTIAL: MATERIALIZED, LIVENESS-READ, AND SCOPED TO THE BINDING ────────────────────── +// +// The key is fetched from Secret Manager, written as one header line into a 0600 file in a fresh +// directory, and handed to curl by path (-H @file), so it is never an argv word. Liveness is read by the +// key endpoint itself: GET /api/v1/key with the same header file asks OpenRouter whether this key is +// accepted and cannot make a rejected key accepted, so the probe is inert (gunbc.readback_independence) +// and gunbc.auth.materialized_secret with_materialized_secret is the gate that refuses a key the upstream +// does not accept. The directory lives as long as the binding and is removed by its release. +data openrouter_key_read_probe: ReadBackProbe = ReadBackProbe { + label: "GET https://openrouter.ai/api/v1/key with the materialized bearer header file", + subject: "the key this SecretRef holds is one OpenRouter currently accepts", + effect_on_subject: ProbeInert, +} + +data hosted_key_read_deadline: Second = second(count: 20) + +type HarnessHostedQuota { + short_hostname: String + actor: NonEmptyStr + per_minute: UpstreamRateLimit + per_day: UpstreamRateLimit +} + +// WHAT A HOSTED BINDING HOLDS IN PLACE OF A SEAT: the header file and the directory it lives in, the two +// quota rows each request leases, the selected level, what the key read reported, and the terms that +// govern the route. The terms ride the binding so every turn placed on it renders the standing it ran +// under; they are not a gate this code can evaluate, because what the operator sends is not visible to it. +type HarnessHostedAccess { + header_dir: NonEmptyStr + header_file: NonEmptyStr + quota: HarnessHostedQuota + key: OpenRouterKeyRead + terms: NvidiaApiTrialTerms +} + +type HostedBindOutcome + = HostedBound { url: NonEmptyStr, model: NonEmptyStr, unit: HarnessServingUnit, level: HarnessReasoningLevel, access: HarnessHostedAccess } + | HostedBindRefused { detail: String } + +// EACH WAY THE KEY CAN FAIL TO ARRIVE IS ITS OWN CAUSE, because they have different remedies: no Secret +// Manager access token to fetch with (a federation or upsert problem, nothing to do with the secret), a +// refused read (the secret or its accessor cell), a response that did not decode, and a response that +// answered about another version. Collapsing them to "could not be read" would send the operator to the +// secret when the fault is the token. +type HostedKeyFetch + = HostedKeyFetched { text: String } + | HostedKeyFetchRefused { cause: String } + +fn hosted_secret_text(route: HarnessHostedRoute) -> HostedKeyFetch { + match resolve_access_token() { + AccessTokenReady { token: t } => + match fetch_secret_ref_credential_with_token(secret_ref: route.secret, access_token: t) { + SecretCredentialReady { credential: c, resolved_version: _ } => HostedKeyFetched { text: trim(s: c as String) } + SecretCredentialAccessUpsertRequired { plan: p } => + HostedKeyFetchRefused { cause: join(["the secret read needs a Secret Manager access upsert first: ", access_token_upsert_required_reason(plan: p)], "") } + SecretCredentialWireDecodeRefused { identity_cause: i, payload_cause: pc } => + HostedKeyFetchRefused { cause: join([ + "the Secret Manager access response did not decode (version identity ", match i { Present { value: _ } => "refused" Absent => "decoded" }, + ", payload ", match pc { Present { value: _ } => "refused" Absent => "decoded" }, ")", + ], "") } + SecretCredentialFetchRefused { reason: r } => HostedKeyFetchRefused { cause: join(["the Secret Manager read was refused: ", r as String], "") } + SecretCredentialResolvedVersionMismatch { requested: q, resolved: r } => + HostedKeyFetchRefused { cause: join(["the Secret Manager read asked for version ", q, " and was answered about ", r], "") } + } + AccessTokenUpsertRequired { plan: p } => + HostedKeyFetchRefused { cause: join(["no Secret Manager access token is available and an access upsert is required: ", access_token_upsert_required_reason(plan: p)], "") } + AccessTokenEnsureRefused { reason: r } => + HostedKeyFetchRefused { cause: join(["no Secret Manager access token could be ensured: ", r as String], "") } + } +} + +// THE KEY READ, AS AN OBSERVATION VERDICT FOR THE MATERIALIZATION GATE, plus the decoded body for the +// binding. A transport failure is Inaccessible, never Converged: an unread key is not a live one. +type HostedKeyObservation + = HostedKeyObserved { key: OpenRouterKeyRead } + | HostedKeyUnobserved { cause: String } + +fn hosted_read_key(route: HarnessHostedRoute, header_file: NonEmptyStr) -> HostedKeyObservation { + let read = http.Client.GetFollowRedirectsBoundedWithHeaderFile( + url: route.key_url, + max_seconds: to_string(second_count(s: hosted_key_read_deadline)) as NonEmptyStr, + accept: "Accept: application/json" as NonEmptyStr, + header_file: header_file, + ) + if !read.success { + HostedKeyUnobserved { cause: join(["the key read failed: ", read.body], "") } + } else { + match openrouter_decode_key_read(body: read.body) { + OpenRouterKeyUndecodable { cause: c } => HostedKeyUnobserved { cause: c } + OpenRouterKeyDecoded { key: k } => HostedKeyObserved { key: k } + } + } +} + +fn hosted_key_liveness(observation: HostedKeyObservation) -> ObservationVerdict? { + match observation { + HostedKeyObserved { key: _ } => Present { value: Converged } + HostedKeyUnobserved { cause: _ } => Present { value: Inaccessible } + } +} + +// THE KEY MUST BE ONE THAT CANNOT SPEND MONEY, and the binding refuses one that can. The operator capped +// this key at zero credits so a request that strays onto a priced model answers 402 instead of charging; +// a key read showing no cap, or a positive one, means that guard is gone, and a free-only route that ran +// anyway would be the externalized degradation DESIGN section 5 names -- the same name, a different +// burden. The daily tier is read from the same response: it is per ACCOUNT and the key cap does not +// change it. +fn hosted_access_from_key(route: HarnessHostedRoute, key: OpenRouterKeyRead, level: HarnessReasoningLevel, header_dir: NonEmptyStr, header_file: NonEmptyStr, short_hostname: String, actor: NonEmptyStr) -> HostedBindOutcome? { + match key.spend_limit { + KeySpendUncapped => none + KeySpendCappedAbove { lexeme: _ } => none + KeySpendCappedAtZero => + match openrouter_credits_from_daily_limit(daily_limit: key.free_daily_limit) { + CreditsTierUnrecognized { daily_limit: _ } => none + CreditsTierObserved { credits: credits } => Present { value: HostedBound { + url: route.url, + model: harness_unit_model_id(unit: route.unit), + unit: route.unit, + level: level, + access: HarnessHostedAccess { + header_dir: header_dir, + header_file: header_file, + quota: HarnessHostedQuota { short_hostname: short_hostname, actor: actor, per_minute: route.per_minute, per_day: openrouter_free_models_per_day(credits: credits) }, + key: key, + terms: route.terms, + }, + } } + } + } +} + +// THE DAY POOL STARTS FROM WHAT THE UPSTREAM SAYS REMAINS, NOT FROM ITS CEILING. The tier sets the +// ceiling; free_model_daily_requests.remaining says how much of it is left today, including requests +// another client of this key spent and requests made before this ledger existed. The reading is +// recorded on the day partition (gunbc.fabric_quota fabric_quota_observe_upstream) before the binding is +// handed out, so the first lease already sees it. A reading that cannot be recorded refuses the bind: +// leasing against the bare ceiling would admit requests the upstream has already refused. +fn hosted_day_remaining(key: OpenRouterKeyRead) -> Nat? { + checked_int_to_nat(n: key.free_daily_remaining) +} + +fn hosted_record_day_remaining(bound: HostedBindOutcome, key: OpenRouterKeyRead) -> HostedBindOutcome { + match bound { + HostedBindRefused { detail: d } => HostedBindRefused { detail: d } + HostedBound { url: u, model: m, unit: un, level: l, access: a } => + match hosted_day_remaining(key: key) { + Absent => HostedBindRefused { detail: join(["the key read reports a negative free-model remaining count (", to_string(key.free_daily_remaining), ")"], "") } + Present { value: remaining } => + match fabric_quota_observe_upstream(short_hostname: a.quota.short_hostname, limit: a.quota.per_day, remaining: remaining, actor: a.quota.actor) { + QuotaObservationRefused { detail: d } => HostedBindRefused { detail: join(["the upstream's remaining free-model requests could not be recorded on the day pool: ", d], "") } + QuotaUpstreamRecorded { partition: _, remaining: _ } => HostedBound { url: u, model: m, unit: un, level: l, access: a } + } + } + } +} + +fn hosted_key_refusal(key: OpenRouterKeyRead) -> String { + match key.spend_limit { + KeySpendCappedAtZero => + join(["the key reports a free-model daily limit of ", to_string(key.free_daily_limit), ", which is neither published tier (50 or 1000); refusing to lease against an unpublished ceiling"], "") + KeySpendUncapped => "the key reports no credit limit: it can spend money on a priced model, and this route admits only a key capped at zero credits" + KeySpendCappedAbove { lexeme: lx } => join(["the key reports a credit limit of ", lx, ": it can spend money on a priced model, and this route admits only a key capped at zero credits"], "") + } +} + +fn hosted_bind_with_header(route: HarnessHostedRoute, header_dir: NonEmptyStr, header_file: NonEmptyStr, short_hostname: String, actor: NonEmptyStr, max_tokens: TokenCount) -> HostedBindOutcome { + let observation = hosted_read_key(route: route, header_file: header_file) + match with_materialized_secret( + ref: route.secret, + binding: BearerHeaderFile, + existence: SmEnabled, + probe: openrouter_key_read_probe, + liveness_observation: hosted_key_liveness(observation: observation), + use: m => observation, + ) { + MaterializationRefused { cause: SecretNotLive { verdict: _ } } => + HostedBindRefused { detail: match observation { HostedKeyUnobserved { cause: c } => join(["the OpenRouter key is not live: ", c], "") HostedKeyObserved { key: _ } => "the OpenRouter key is not live" } } + MaterializationRefused { cause: SecretVersionUnusable { state: _ } } => HostedBindRefused { detail: "the OpenRouter key's secret version is not usable" } + MaterializationRefused { cause: LivenessProbeNotIndependent { probe_label: p } } => HostedBindRefused { detail: join(["the key liveness probe is not independent: ", p as String], "") } + MaterializationCompleted { value: HostedKeyUnobserved { cause: c } } => HostedBindRefused { detail: c } + MaterializationCompleted { value: HostedKeyObserved { key: key } } => + match harness_hosted_level_choice(route: route, max_tokens: max_tokens) { + HostedLevelUnselected { cause: c } => HostedBindRefused { detail: c } + HostedLevelSelected { level: l } => + match hosted_access_from_key(route: route, key: key, level: l, header_dir: header_dir, header_file: header_file, short_hostname: short_hostname, actor: actor) { + Absent => HostedBindRefused { detail: hosted_key_refusal(key: key) } + Present { value: bound } => hosted_record_day_remaining(bound: bound, key: key) + } + } + } +} + +// THE BIND. A fetched secret that could not be written leaves nothing behind; any refusal after the +// header file exists removes its directory before answering, so a refused bind holds no credential on +// disk. +fn harness_hosted_bind(unit: HarnessServingUnit, short_hostname: String, actor: NonEmptyStr, max_tokens: TokenCount) -> HostedBindOutcome { + match harness_hosted_route_for(unit: unit) { + Absent => HostedBindRefused { detail: join(["the unit ", harness_serving_unit_wire(unit: unit) as String, " is served on a fleet seat and has no hosted route"], "") } + Present { value: route } => + match hosted_secret_text(route: route) { + HostedKeyFetchRefused { cause: c } => HostedBindRefused { detail: join(["the OpenRouter key (", route.secret.secret as String, ") could not be fetched: ", c], "") } + HostedKeyFetched { text: key_text } => + if key_text == "" { + HostedBindRefused { detail: "the OpenRouter key secret is empty" } + } else { + let dir = shell.Mktemp.Dir() + if !dir.success { + HostedBindRefused { detail: "mktemp -d failed for the bearer header file" } + } else { + let header_file = join([dir.path as String, "/authorization"], "") as NonEmptyStr + let wrote = Filesystem.WriteOwnerOnly(path: header_file as String, content: openrouter_authorization_header_line(key: key_text as NonEmptyStr)) + if !wrote.success { + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + HostedBindRefused { detail: "could not write the bearer header file" } + } else { + match hosted_bind_with_header(route: route, header_dir: dir.path as String as NonEmptyStr, header_file: header_file, short_hostname: short_hostname, actor: actor, max_tokens: max_tokens) { + HostedBindRefused { detail: d } => { + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + HostedBindRefused { detail: d } + } + HostedBound { url: u, model: m, unit: un, level: l, access: a } => HostedBound { url: u, model: m, unit: un, level: l, access: a } + } + } + } + } + } + } +} + +type HostedRelease + = HostedReleased + | HostedReleaseRefused { detail: String } + +fn harness_hosted_release(access: HarnessHostedAccess) -> HostedRelease { + let removed = shell.Remove.RecursiveForce(path: access.header_dir as String as FilePath) + if removed.success { HostedReleased } else { HostedReleaseRefused { detail: join(["the bearer header directory ", access.header_dir as String, " could not be removed"], "") } } +} + +fn harness_hosted_access_wire(access: HarnessHostedAccess) -> String { + join([ + "hosted quota ", to_string(access.quota.per_minute.requests), "/minute and ", to_string(access.quota.per_day.requests), "/day; ", + openrouter_key_spend_limit_wire(l: access.key.spend_limit), "; free-model requests today ", to_string(access.key.free_daily_used), "/", to_string(access.key.free_daily_limit), + "; governed by ", nvidia_api_trial_terms_wire(t: access.terms) as String, + ], "") +} + +// ── EACH REQUEST IS CHARGED TO BOTH POOLS BEFORE IT IS SENT ─────────────────────────────────── +// +// THE LOCAL QUOTA IS A CLOSE, CONSERVATIVE PRE-CHECK, NOT AN EXACT GATE, AND THE UPSTREAM'S 429 IS THE +// BACKSTOP. This route deliberately revises the 2026-09-06 ruling that a caller refuses before the upstream +// would have answered 429 (gunbc.fabric_quota): the pools below make an over-spend rare and keep the +// ledger honest, but nothing here can make the local count equal the upstream's at a window boundary -- +// the upstream counts a request when it RECEIVES it, and nothing on this side bounds when a process's first +// request byte leaves. A 429 that gets through is a typed refusal carrying the upstream's own retry hints +// (extdeps.openrouter.openrouter OpenRouterRateLimitRefusal), reported in the turn outcome and never +// retried (operator decision, escalation msg_46a417a3). Machinery whose only job was to make the count +// exact -- an admission lock, charging every window a request might span -- is deliberately absent. +// +// THE CHARGE AND THE RESPONSE ARE TWO LIFETIMES. The upstream counts a request in the window it ARRIVES +// in, whatever happens to its answer afterwards; the answer may take minutes. An earlier revision held +// the lease across the response and settled it afterwards, with the response deadline as the term -- +// and a 2,348-second term can never fit a 60-second lapsing window, so the minute pool refused +// LeaseCrossesLapse on every request and nothing was ever sent. Shortening the term would not have +// fixed it: a lease held until the answer arrives still outlives a window the request was counted in. +// +// SO A REQUEST IS CHARGED, NOT HELD. Each pool is leased for the remainder of its current window +// (gunbc.fabric_quota QuotaTermToWindowEnd, read off the same clock reading the lease is stamped with), +// and both leases are settled at one BEFORE the request goes out. The charge then stands for the rest of +// the window whatever the response does, which is also the conservative accounting for a request whose +// fate is unknown: a crash after the send, or a transport that failed mid-flight, still counted upstream +// as far as anyone here can tell, so it is never refunded. +// +// BOTH POOLS OR NEITHER. The minute pool is leased first because it refills soonest; if the day pool then +// refuses, the minute lease is settled at zero so its slot returns at once. +type HostedRoundLease + = HostedRoundCharged + | HostedRoundQuotaFull { wire: String } + | HostedRoundUnleasable { detail: String } + +// THE DECISION IS PURE AND THE LEASING IS NOT. What a pair of lease answers MEANS is decided from +// supplied answers; the day pool is asked only when the minute pool leased, so its answer is absent +// otherwise, and a refusal after the minute pool leased names that lease as owed back. +type HostedRoundDecision + = RoundBothLeased { minute: QuotaLease, day: QuotaLease } + | RoundRefusedFull { wire: String, minute_to_return: QuotaLease? } + | RoundRefusedUnleasable { detail: String, minute_to_return: QuotaLease? } + +fn hosted_round_decision(minute: QuotaLease, day: QuotaLease?) -> HostedRoundDecision { + match minute { + QuotaFull { wire: w } => RoundRefusedFull { wire: w, minute_to_return: none } + QuotaLeaseRefused { detail: d } => RoundRefusedUnleasable { detail: d, minute_to_return: none } + QuotaLeased { grant: _, partition: _, store: _, limit: _ } => + match day { + Absent => RoundRefusedUnleasable { detail: "the minute pool leased but the day pool was never asked", minute_to_return: Present { value: minute } } + Present { value: QuotaLeased { grant: g, partition: p, store: st, limit: l } } => RoundBothLeased { minute: minute, day: QuotaLeased { grant: g, partition: p, store: st, limit: l } } + Present { value: QuotaFull { wire: w } } => RoundRefusedFull { wire: w, minute_to_return: Present { value: minute } } + Present { value: QuotaLeaseRefused { detail: d } } => RoundRefusedUnleasable { detail: d, minute_to_return: Present { value: minute } } + } + } +} + +fn hosted_return_minute(minute: QuotaLease?) -> String { + match minute { + Absent => "" + Present { value: m } => join(["; minute lease returned: ", quota_settle_wire(s: fabric_quota_settle(lease: m, actual: 0))], "") + } +} + +fn hosted_settle_refusal(s: QuotaSettle, pool: String) -> List { + match s { + QuotaSettled { partition: _, actual: _ } => [] + QuotaSettleRefused { detail: d } => [join([pool, ": ", d], "")] + } +} + +// A CHARGE THAT DID NOT RECORD IS NOT SENT. The request has not gone out yet, so refusing costs nothing +// upstream; sending it would spend quota the ledger does not show, which is the over-admission the +// lease exists to prevent. A lease left unsettled by the refusal still charges until its window ends, +// so the refusal errs on the side of spending less. +fn hosted_round_charge_both(minute: QuotaLease, day: QuotaLease, minute_amount: Nat, day_amount: Nat) -> HostedRoundLease { + let refused = concat( + hosted_settle_refusal(s: fabric_quota_settle(lease: minute, actual: minute_amount), pool: "minute"), + hosted_settle_refusal(s: fabric_quota_settle(lease: day, actual: day_amount), pool: "day"), + ) + if count(refused) == 0 { + HostedRoundCharged + } else { + HostedRoundUnleasable { detail: join(["the request's charge did not record, so it was not sent: ", join(refused, "; ")], "") } + } +} + +// THE CHARGE COVERS THIS SEND AND EVERY SEND STILL IN FLIGHT FROM AN EARLIER WINDOW +// (gunbc.harness.harness_seat hosted_inflight_carried): each may arrive in this window, and none was +// charged to it. A refusal carries the held seats' release hint, so a pool starved by seats whose holders +// died says which seats and how to release them. +fn harness_hosted_round_charge(quota: HarnessHostedQuota, carried_minute: Nat, carried_day: Nat, release_hint: String) -> HostedRoundLease { + let minute_amount = carried_minute + 1 + let day_amount = carried_day + 1 + let minute = fabric_quota_lease_for(short_hostname: quota.short_hostname, limit: quota.per_minute, amount: minute_amount, actor: quota.actor, term: QuotaTermToWindowEnd) + let day = match minute { + QuotaLeased { grant: _, partition: _, store: _, limit: _ } => + Present { value: fabric_quota_lease_for(short_hostname: quota.short_hostname, limit: quota.per_day, amount: day_amount, actor: quota.actor, term: QuotaTermToWindowEnd) } + QuotaFull { wire: _ } => none + QuotaLeaseRefused { detail: _ } => none + } + match hosted_round_decision(minute: minute, day: day) { + RoundBothLeased { minute: m, day: d } => hosted_round_charge_both(minute: m, day: d, minute_amount: minute_amount, day_amount: day_amount) + RoundRefusedFull { wire: w, minute_to_return: r } => HostedRoundQuotaFull { wire: join([w, hosted_return_minute(minute: r), release_hint], "") } + RoundRefusedUnleasable { detail: d, minute_to_return: r } => HostedRoundUnleasable { detail: join([d, hosted_return_minute(minute: r)], "") } + } +} + +// THE UPSTREAM'S REFUSAL, DECODED. An error body is OpenRouter's typed error when it decodes; a body that +// does not decode is reported as itself, because a transport failure has no error object to read. +fn harness_hosted_refusal_cause(body: String) -> String { + match openrouter_decode_error_body(body: body) { + OpenRouterErrorDecoded { error: e } => openrouter_error_wire(e: e) + OpenRouterErrorUndecodable { cause: _ } => body + } +} + +// ── WHAT A TURN CARRIES ABOUT HOW ITS REQUESTS ARE ADMITTED ─────────────────────────────────── +// +// A fleet turn's requests are admitted by the seat it already holds, unauthenticated over the fleet's +// network. A hosted turn's requests each lease quota and carry the bearer header file. The turn config +// carries which, so the POST is chosen by the binding the turn was placed on, never re-derived from the +// unit inside the loop. +type HarnessBackendAccess + = BackendAdmittedBySeat + | BackendAdmittedByHostedQuota { header_file: NonEmptyStr, quota: HarnessHostedQuota } + +fn harness_hosted_backend_access(access: HarnessHostedAccess) -> HarnessBackendAccess { + BackendAdmittedByHostedQuota { header_file: access.header_file, quota: access.quota } +} diff --git a/dag/gunbc/harness/harness_reasoning_wire.dag b/dag/gunbc/harness/harness_reasoning_wire.dag index f414986029a..d033b0b52dc 100644 --- a/dag/gunbc/harness/harness_reasoning_wire.dag +++ b/dag/gunbc/harness/harness_reasoning_wire.dag @@ -3,6 +3,15 @@ module gunbc.harness.harness_reasoning_wire import std.types { String, Bool, List, NonEmptyStr } import std.measure { TokenCount } import extdeps.languages.json.emit { JsonKeyValue, JsonBool, json_kv, json_string, json_object } +import extdeps.openrouter.openrouter { + OpenRouterReasoningEffort, EffortHigh, EffortMedium, OpenRouterEffortAdmission, EffortAdmitted, EffortNotSupported, + openrouter_effort_admitted, openrouter_reasoning_effort_wire, openrouter_message_reasoning_field, +} +import extdeps.openrouter.nvidia_nemotron_3_ultra_free { + nemotron_ultra_free_model_id, nemotron_ultra_free_context_length, nemotron_ultra_free_supported_efforts, + nemotron_ultra_free_reasoning_mandatory, +} +import extdeps.nvidia.nemotron_3_ultra { nemotron_ultra_thinking_end_token } import gunbc.spark.serving_arm { ReasoningResponseProjection, ParsedReasoningField, InlineReasoning, gitcommit90_sm121_exl3_row, gunbc_sparkrun_ds4_row, @@ -38,14 +47,38 @@ import extdeps.zhipu.glm_5_3_flash { // for that family -- vLLM ships glm45 for GLM and, on this fleet's build, nothing for DeepSeek V4. // Attributing that to the model would be authority substitution; attributing it to the unit is // exactly right. +// +// A HOSTED UNIT IS A MODEL PLUS THE ROUTER IT IS REACHED THROUGH, by the same reasoning: whether reasoning +// comes back split, under which member, and which efforts are accepted are the router's facts about its +// listing (extdeps.openrouter.nvidia_nemotron_3_ultra_free), not the model's. How the harness PLACES a turn +// on each unit -- a fleet seat or a hosted quota lease -- is asked of harness_unit_placement below. type HarnessServingUnit = DeepseekV4FlashOnVllm | Glm53FlashOnVllm + | NemotronUltraFreeOnOpenRouter fn harness_serving_unit_wire(unit: HarnessServingUnit) -> NonEmptyStr { match unit { DeepseekV4FlashOnVllm => "deepseek-v4-flash on vllm" as NonEmptyStr Glm53FlashOnVllm => "glm-5.3-flash on vllm" as NonEmptyStr + NemotronUltraFreeOnOpenRouter => "nemotron-3-ultra:free on openrouter" as NonEmptyStr + } +} + +// HOW A TURN ON THIS UNIT IS ADMITTED. A fleet unit is served by an engine this repository launches, so a +// turn takes a seat on a serving group (gunbc.harness.harness_seat harness_bind_seat). A hosted unit has no +// group, no replica and no loaded state to share: what bounds it is the upstream's per-request quota, so a +// turn leases that quota (gunbc.fabric_quota) before each request instead. The two are different capacity +// facts, which is why this is a closed choice and not a flag. +type HarnessUnitPlacement + = PlacedOnFleetSeat + | LeasedFromHostedQuota + +fn harness_unit_placement(unit: HarnessServingUnit) -> HarnessUnitPlacement { + match unit { + DeepseekV4FlashOnVllm => PlacedOnFleetSeat + Glm53FlashOnVllm => PlacedOnFleetSeat + NemotronUltraFreeOnOpenRouter => LeasedFromHostedQuota } } @@ -133,10 +166,62 @@ fn harness_glm_reasoning_kwargs(level: HarnessReasoningLevel) -> HarnessReasonin } } +// THE OPENROUTER ARM, AND ITS WIRE IS THE ROUTER'S, NOT THE MODEL'S. The request carries OpenRouter's +// top-level `reasoning` object, never Nemotron's chat_template_kwargs: the router owns the translation to +// the provider, and a client spelling the model's template keys past it would be addressing a template +// it cannot see. +// +// EACH LEVEL IS MAPPED TO AN EFFORT AND THE EFFORT IS ADMITTED AGAINST THE LISTING'S PUBLISHED SET, so +// what this arm may send is decided by the listing's row and never by this function. Standard is medium +// and maximum is high, because those are the two efforts the listing accepts and that is their order; +// off is `enabled: false`, which the listing admits because its reasoning is not mandatory. A listing +// whose set lacks the mapped effort, or whose reasoning is mandatory, refuses the level with both names. +fn harness_openrouter_level_effort(level: HarnessReasoningLevel) -> OpenRouterReasoningEffort? { + match level { + ReasoningOff => none + ReasoningStandard => Present { value: EffortMedium } + ReasoningMaximum => Present { value: EffortHigh } + } +} + +fn harness_openrouter_reasoning_kwargs(unit: HarnessServingUnit, level: HarnessReasoningLevel, supported: List, mandatory: Bool) -> HarnessReasoningKwargs { + match harness_openrouter_level_effort(level: level) { + Absent => + if mandatory { + ReasoningLevelUnsupported { + unit: harness_serving_unit_wire(unit: unit), + level: harness_reasoning_level_wire(level: level), + cause: "the listing marks reasoning mandatory, so a request with reasoning disabled is not one it admits" as NonEmptyStr, + } + } else { + ReasoningKwargsAdmitted { kwargs: [json_kv(key: "reasoning", value: json_object([ + json_kv(key: "enabled", value: JsonBool { value: false }), + ]))] } + } + Present { value: effort } => + match openrouter_effort_admitted(effort: effort, supported: supported) { + EffortNotSupported { effort: e, supported: _ } => ReasoningLevelUnsupported { + unit: harness_serving_unit_wire(unit: unit), + level: harness_reasoning_level_wire(level: level), + cause: join([ + "the level maps to reasoning effort ", openrouter_reasoning_effort_wire(e: e) as String, + ", which the listing's supported_efforts do not include; an unsupported effort is translated by the router to a setting the caller never sees, so it is refused rather than sent", + ], "") as NonEmptyStr, + } + EffortAdmitted { effort: e } => + ReasoningKwargsAdmitted { kwargs: [json_kv(key: "reasoning", value: json_object([ + json_kv(key: "effort", value: json_string(s: openrouter_reasoning_effort_wire(e: e) as String)), + ]))] } + } + } +} + fn harness_reasoning_kwargs_for(unit: HarnessServingUnit, level: HarnessReasoningLevel) -> HarnessReasoningKwargs { match unit { DeepseekV4FlashOnVllm => harness_deepseek_reasoning_kwargs(level: level) Glm53FlashOnVllm => harness_glm_reasoning_kwargs(level: level) + NemotronUltraFreeOnOpenRouter => + harness_openrouter_reasoning_kwargs(unit: unit, level: level, supported: nemotron_ultra_free_supported_efforts, mandatory: nemotron_ultra_free_reasoning_mandatory) } } @@ -154,6 +239,12 @@ fn harness_unit_thinking_requested(unit: HarnessServingUnit, level: HarnessReaso ReasoningStandard => true ReasoningMaximum => true } + NemotronUltraFreeOnOpenRouter => + match level { + ReasoningOff => false + ReasoningStandard => true + ReasoningMaximum => true + } } } @@ -163,16 +254,21 @@ fn harness_unit_model_id(unit: HarnessServingUnit) -> NonEmptyStr { match unit { DeepseekV4FlashOnVllm => "deepseek-v4-flash:iq3s-split" as NonEmptyStr Glm53FlashOnVllm => glm_5_3_flash_model_id + NemotronUltraFreeOnOpenRouter => nemotron_ultra_free_model_id } } // THE ENGINE'S TOOL-CALL PARSER FOR THIS UNIT, delegated for the same reason. The DeepSeek arm keeps // the string because extdeps.deepseek.deepseek_v4 carries no parser row; that asymmetry is the // DeepSeek module's gap, named here rather than papered over with a second GLM-shaped row. -fn harness_unit_tool_call_parser(unit: HarnessServingUnit) -> NonEmptyStr { +// A HOSTED UNIT HAS NO ENGINE THIS REPOSITORY LAUNCHES, so there is no parser for it to name: the +// router's provider parses tool calls and returns them already structured. The answer is absent rather +// than a borrowed name, because a launch reading a name here would launch a parser nobody chose. +fn harness_unit_tool_call_parser(unit: HarnessServingUnit) -> NonEmptyStr? { match unit { - DeepseekV4FlashOnVllm => "deepseek_v4" as NonEmptyStr - Glm53FlashOnVllm => glm_5_3_flash_vllm_tool_call_parser + DeepseekV4FlashOnVllm => Present { value: "deepseek_v4" as NonEmptyStr } + Glm53FlashOnVllm => Present { value: glm_5_3_flash_vllm_tool_call_parser } + NemotronUltraFreeOnOpenRouter => none } } @@ -206,6 +302,7 @@ fn harness_unit_reasoning_projection(unit: HarnessServingUnit) -> ReasoningRespo match unit { DeepseekV4FlashOnVllm => gunbc_sparkrun_ds4_row.reasoning_projection Glm53FlashOnVllm => gitcommit90_sm121_exl3_row.reasoning_projection + NemotronUltraFreeOnOpenRouter => Present { value: ParsedReasoningField { field: openrouter_message_reasoning_field } } } } @@ -234,6 +331,7 @@ fn harness_unit_model_reasoning_field(unit: HarnessServingUnit) -> NonEmptyStr { match unit { DeepseekV4FlashOnVllm => deepseek_v4_chat_reasoning_field Glm53FlashOnVllm => glm_5_3_flash_chat_reasoning_field + NemotronUltraFreeOnOpenRouter => openrouter_message_reasoning_field } } @@ -308,6 +406,7 @@ fn harness_unit_server_parses_reasoning(unit: HarnessServingUnit) -> Bool { match unit { DeepseekV4FlashOnVllm => false Glm53FlashOnVllm => true + NemotronUltraFreeOnOpenRouter => true } } @@ -318,6 +417,7 @@ fn harness_unit_thinking_end_token(unit: HarnessServingUnit) -> NonEmptyStr { match unit { DeepseekV4FlashOnVllm => deepseek_v4_thinking_end_token Glm53FlashOnVllm => glm_5_3_flash_thinking_end_token + NemotronUltraFreeOnOpenRouter => nemotron_ultra_thinking_end_token } } @@ -327,5 +427,6 @@ fn harness_unit_declared_context_ceiling(unit: HarnessServingUnit) -> TokenCount match unit { DeepseekV4FlashOnVllm => deepseek_v4_context_length Glm53FlashOnVllm => glm_5_3_flash_context_length + NemotronUltraFreeOnOpenRouter => nemotron_ultra_free_context_length } } diff --git a/dag/gunbc/harness/harness_seat.dag b/dag/gunbc/harness/harness_seat.dag index 19d77dcfa3a..bfd0ffb2d5a 100644 --- a/dag/gunbc/harness/harness_seat.dag +++ b/dag/gunbc/harness/harness_seat.dag @@ -3,7 +3,9 @@ module gunbc.harness.harness_seat import product.host_identity { HostIdentity } import std.types { String, Bool, Int, NonEmptyStr, List, EpochSecs } import std.nat { Nat, nat_range_inclusive, nat_min } -import std.measure { Measure, Count, One, money_amount_micro, second, measure_count } +import std.measure { Measure, Count, One, money_amount_micro, second, measure_count, TokenCount, Second, second_count } +import extdeps.accounting.encumbrance { Held, Expended, Released } +import extdeps.api_rate_limit { upstream_rate_limit_key } import gunbc.spark.fabric_switch_observed { FabricGroup, FabricGroupA, FabricGroupB, fabric_group_wire, fabric_group_hosts } import gunbc.serving.serving_enrollment { ServingRoute, ServingRouteResolved, ServingRouteUnresolved, ServingRouteEndpoint, @@ -27,7 +29,7 @@ import gunbc.fabric_storage_client { FabricStorageBinding } import gunbc.fabric_event_log { HeadExpectation, event_log_refusal_wire, SeatStoreRefused, SeatStandingStoreRefused, HeadObserved, HeadObservationRefused, event_log_observe_head, EventAppended, EventAppendStale, EventAppendRefused, event_log_append, SeatGranted, SeatFull, SeatContended, - SeatAcquireRefused, fabric_seat_acquire, SeatRoomObserved, SeatFullObserved, SeatStandingUnobserved, + SeatAcquireRefused, fabric_seat_acquire, seat_acquisition_wire, SeatRoomObserved, SeatFullObserved, SeatStandingUnobserved, fabric_seat_observe, PartitionRead, PartitionReadOk, PartitionReadRefused, event_log_read_partition, RedemptionReading, RedemptionReadAt, RedemptionReadRefused, fabric_redemption_read, } @@ -38,7 +40,14 @@ import gunbc.harness.harness_backend { harness_route_availability, HarnessEngineReading, harness_observe_engine_at, } import gunbc.harness.harness_wire { HarnessWireShape, AnthropicMessagesShape, OpenAiChatCompletionsShape, harness_backend_url_for_endpoint } -import gunbc.harness.harness_reasoning_wire { HarnessServingUnit, harness_unit_model_id } +import gunbc.harness.harness_reasoning_wire { + HarnessServingUnit, harness_unit_model_id, HarnessReasoningLevel, harness_unit_placement, HarnessUnitPlacement, PlacedOnFleetSeat, + LeasedFromHostedQuota, harness_serving_unit_wire, +} +import gunbc.harness.harness_hosted_route { + HarnessHostedAccess, HarnessHostedQuota, HostedBound, HostedBindRefused, harness_hosted_bind, HostedReleased, HostedReleaseRefused, + harness_hosted_release, +} import product.fabric.work { ExecutionRequirements, ControlPlaneCapacity, Work, ServiceInterfaceRef, ArtifactManifestRef, EffectContractRef, OutputContractRef, identity_frame } import product.fabric.supply { EstimatedGrantDuration, GrantDuration, AffinityKeyedRendezvous, PlacementAffinityKey } import product.fabric.selection { @@ -291,6 +300,7 @@ type HarnessSeatBinding = HarnessSeatBound { url: String, grant: LeaseGrant, partition: PartitionId, ceiling: Nat, group_wire: String, launch: VllmEndpointProcessLaunch, store: FabricStorageBinding, model: NonEmptyStr, unit: HarnessServingUnit, class: ServingCoTenancyClass, seat: HarnessSeatReference } | HarnessSeatPending { detail: String } | HarnessSeatRefused { detail: String } + | HarnessHostedBound { url: String, model: NonEmptyStr, unit: HarnessServingUnit, level: HarnessReasoningLevel, access: HarnessHostedAccess } fn seat_bind_detail(group: FabricGroup, text: String) -> String { join([fabric_group_wire(g: group) as String, ": ", text], "") @@ -1204,6 +1214,7 @@ fn harness_seat_after_grant_authority(store: FabricStorageBinding, group: Fabric SeatAuthorityRefuses { cause: why } => match release_retry(store: store, partition: partition, ceiling: ceiling, reference: grant.reference, reason: "the pair-serving authority moved between admission and the seat's acquisition" as NonEmptyStr, now: now) { HarnessSeatReleased { partition: _ } => SeatReleasedAuthorityMoved { cause: why } + HarnessHostedCredentialRemoved => SeatUnreleasedAuthorityMoved { cause: why, release_detail: "a fleet seat release answered as a hosted credential removal" } HarnessSeatReleaseRefused { detail: d } => SeatUnreleasedAuthorityMoved { cause: why, release_detail: d } } } @@ -1223,6 +1234,8 @@ fn harness_fence_grant(candidate: HarnessCandidate, class: ServingCoTenancyClass } EndpointProcessWasReplaced { endpoint: _, expected: _, observed: _, observed_at: _, receipt: _ } => match release_retry(store: store, partition: partition, ceiling: candidate.ceiling, reference: grant.reference, reason: "the engine was replaced between selection and acquisition" as NonEmptyStr, now: now) { + HarnessHostedCredentialRemoved => + AcquisitionRefused { detail: seat_bind_detail(group: candidate.group, text: "a fleet seat release answered as a hosted credential removal; the seat may still be held") } HarnessSeatReleased { partition: _ } => AcquisitionLost { key: candidate.key, @@ -1421,6 +1434,37 @@ data harness_placement_rounds: Nat = 3 // that constructed the request itself could hand in a request naming one requirement beside a // screen argument naming another. fn harness_bind_seat(short_hostname: String, class: ServingCoTenancyClass, requirement: ServingModelRequirement, shape: HarnessWireShape, inputs: List, actor: NonEmptyStr, attempt: NonEmptyStr, policy: LeasePolicy) -> HarnessSeatBinding { + match harness_requirement_placement(requirement: requirement) { + LeasedFromHostedQuota => HarnessSeatRefused { detail: "the required unit is reached through a hosted quota lease, not a fleet seat; bind it with harness_bind_hosted" } + PlacedOnFleetSeat => harness_bind_fleet_seat(short_hostname: short_hostname, class: class, requirement: requirement, shape: shape, inputs: inputs, actor: actor, attempt: attempt, policy: policy) + } +} + +// WHERE A REQUIREMENT IS PLACED. Any admitted model is a fleet seat: the hosted listing is governed by +// trial terms (extdeps.nvidia.api_trial_terms), and an unnamed requirement must not quietly route a turn +// to an upstream whose terms the caller never chose. Only a caller naming the hosted unit reaches it. +fn harness_requirement_placement(requirement: ServingModelRequirement) -> HarnessUnitPlacement { + match requirement { + AnyAdmittedModel => PlacedOnFleetSeat + ExactServingUnit { unit: u } => harness_unit_placement(unit: u) + } +} + +// THE HOSTED BIND. A hosted unit takes no seat: the credential is materialized and its liveness read, the +// level is selected, and the binding carries the two quota rows each request will lease. A fleet unit +// handed here refuses by name rather than being placed on a seat behind the caller's back. +fn harness_bind_hosted(short_hostname: String, unit: HarnessServingUnit, actor: NonEmptyStr, max_tokens: TokenCount) -> HarnessSeatBinding { + match harness_unit_placement(unit: unit) { + PlacedOnFleetSeat => HarnessSeatRefused { detail: join(["the unit ", harness_serving_unit_wire(unit: unit) as String, " is served on a fleet seat; bind it with harness_bind_seat"], "") } + LeasedFromHostedQuota => + match harness_hosted_bind(unit: unit, short_hostname: short_hostname, actor: actor, max_tokens: max_tokens) { + HostedBindRefused { detail: d } => HarnessSeatRefused { detail: d } + HostedBound { url: u, model: m, unit: un, level: l, access: a } => HarnessHostedBound { url: u as String, model: m, unit: un, level: l, access: a } + } + } +} + +fn harness_bind_fleet_seat(short_hostname: String, class: ServingCoTenancyClass, requirement: ServingModelRequirement, shape: HarnessWireShape, inputs: List, actor: NonEmptyStr, attempt: NonEmptyStr, policy: LeasePolicy) -> HarnessSeatBinding { let subject = harness_turn_subject(request: harness_turn_request(shape: shape, requirement: requirement, inputs: inputs), attempt: attempt) match event_log_store_for_host(short_hostname: short_hostname) { HostStoreRefused { detail: d } => HarnessSeatRefused { detail: d } @@ -1445,8 +1489,12 @@ fn harness_bind_seat(short_hostname: String, class: ServingCoTenancyClass, requi // RELEASE IS AN EVENT ON THE SAME PARTITION, appended against the head observed at release time // and retried on stale, because the seat's neighbours keep moving the head while this turn ran. +// A HOSTED BINDING HOLDS NO SEAT, so its release is its own arm: each request already settled its quota +// leases, and what remains to give back is the credential's header file. Reporting it as a released +// partition would name a pool the binding never held. type HarnessSeatRelease = HarnessSeatReleased { partition: PartitionId } + | HarnessHostedCredentialRemoved | HarnessSeatReleaseRefused { detail: String } type ReleaseFold { @@ -1543,6 +1591,7 @@ fn harness_seat_pointer_of(binding: HarnessSeatBinding) -> HarnessSeatPointer? { Present { value: HarnessSeatPointer { partition: p, reference: g.reference, owner: r.attempt } } HarnessSeatRefused { detail: _ } => none HarnessSeatPending { detail: _ } => none + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: _ } => none } } @@ -1563,6 +1612,11 @@ fn harness_release_seat(binding: HarnessSeatBinding, reason: NonEmptyStr) -> Har match binding { HarnessSeatRefused { detail: d } => HarnessSeatReleaseRefused { detail: join(["nothing to release: ", d], "") } HarnessSeatPending { detail: d } => HarnessSeatReleaseRefused { detail: join(["nothing to release, the placement is pending: ", d], "") } + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: a } => + match harness_hosted_release(access: a) { + HostedReleased => HarnessHostedCredentialRemoved + HostedReleaseRefused { detail: d } => HarnessSeatReleaseRefused { detail: d } + } HarnessSeatBound { url: _, grant: g, partition: p, ceiling: ceiling, group_wire: _, launch: _, store: store, model: _, unit: _, class: _, seat: _ } => match now_epoch_seconds() { Absent => HarnessSeatReleaseRefused { detail: "the clock could not be read as epoch seconds" } @@ -1719,6 +1773,7 @@ fn harness_release_fenced(store: FabricStorageBinding, capability: HarnessReleas fn harness_seat_release_wire(r: HarnessSeatRelease) -> String { match r { HarnessSeatReleased { partition: p } => join(["released on ", p as String], "") + HarnessHostedCredentialRemoved => "hosted credential removed" HarnessSeatReleaseRefused { detail: d } => join(["release refused: ", d], "") } } @@ -1726,3 +1781,141 @@ fn harness_seat_release_wire(r: HarnessSeatRelease) -> String { fn harness_now_epoch() -> EpochSecs? { now_epoch_seconds() } + +data hosted_inflight_none: Nat = 0 + +// ── IN-FLIGHT HOSTED SENDS: A CHARGE THAT SURVIVES THE ROLLOVER ───────────────────────────────── +// +// A hosted request is charged to the window it is admitted in (gunbc.harness.harness_hosted_route +// harness_hosted_round_charge), but nothing on this side can bound WHEN its first byte leaves: curl's +// connect deadline stops applying once the connection is open, and a process can pause between the +// admission and the write for as long as it likes. Measured: a pause before the first header write put the +// request's first byte 8.0 s after admission, past a 5.98 s deadline, with curl exiting 0. So a send that +// has been admitted and whose transport has not returned may arrive in ANY later window, and it must be +// counted there too. +// +// THE QUOTA POOLS CANNOT CARRY THAT, because every charge in a lapsing pool belongs to one window. So an +// admitted send also holds a SEAT in a per-credential in-flight partition, under the same pool shape every +// harness seat uses (harness_seat_pool_root: no replenishment, quiescence required -- a held seat charges +// until evidence releases it, never by expiry). Each later admission counts the in-flight seats acquired +// BEFORE its window began and charges its quota pools for them as well as for itself; the seat is released +// when the transport returns, whatever it returned. +// +// TWO CONSEQUENCES, BOTH CONSERVATIVE AND STATED RATHER THAN ENGINEERED AWAY. Two admissions in one window +// each count the same carried seats, so a carried send can be charged more than once in a window. And a +// seat held by a process that crashed charges every later window until it is released; the refusal that +// starvation produces names each held seat, its holder, and the release that frees it -- the same +// harness_release_cli route a stuck serving seat takes. +fn hosted_inflight_partition(quota: HarnessHostedQuota) -> PartitionId { + join(["inflight-", upstream_rate_limit_key(l: quota.per_minute) as String], "") as PartitionId +} + +fn hosted_inflight_ceiling(quota: HarnessHostedQuota) -> Nat { + quota.per_minute.requests +} + +type HostedInflightSeat { + reference: NonEmptyStr + acquired_at: EpochSecs +} + +fn hosted_inflight_held(pool: Pool) -> List { + let held = filter(pool.ledger.entries, e => match e.state { + Held { maximum_liability: _ } => true + Expended { actual: _, settled_at: _ } => false + Released { reason: _, released_at: _ } => false + }) + fold(held, init: [] as List, f: (acc, e) => + match filter(pool.terms, t => (t.reference as String) == (e.reference as String)).first() { + Absent => acc + Present { value: t } => concat(acc, [HostedInflightSeat { reference: e.reference, acquired_at: t.acquired_at }]) + }) +} + +// THE SENDS A WINDOW MUST COVER BESIDES ITS OWN: those admitted before it began and not yet returned. +fn hosted_inflight_carried(pool: Pool, window_start: EpochSecs) -> Nat { + fold(filter(hosted_inflight_held(pool: pool), s => s.acquired_at < window_start), init: hosted_inflight_none, f: (n, _s) => n + 1) +} + +type HostedInflightReading + = HostedInflightRead { pool: Pool, generation: Nat, store: FabricStorageBinding, release_hint: String } + | HostedInflightUnread { detail: String } + +fn hosted_inflight_release_hint(quota: HarnessHostedQuota, pool: Pool, generation: Nat, walk: ChainWalk) -> String { + let seats = hosted_inflight_held(pool: pool) + if count(seats) == 0 { "" } else { + join([ + "; in-flight seats held on ", hosted_inflight_partition(quota: quota) as String, " (release each once its holder is known to be gone: gunbc run --entry dag/gunbc/harness/harness_cli.dag --function harness_release_cli --arg partition=", hosted_inflight_partition(quota: quota) as String, + " --arg ceiling=", to_string(hosted_inflight_ceiling(quota: quota)), " --arg generation=", to_string(generation), " --arg reason= --arg reference= --arg grant=): ", + join(map(seats, s => join([ + s.reference as String, " acquired_at=", to_string(s.acquired_at), + " grant=", match admitting_event_for(walk: walk, reference: s.reference) { Present { value: g } => g as String Absent => "unknown" }, + ], "")), "; "), + ], "") + } +} + +fn harness_hosted_inflight_read(quota: HarnessHostedQuota) -> HostedInflightReading { + match event_log_store_for_host(short_hostname: quota.short_hostname) { + HostStoreRefused { detail: d } => HostedInflightUnread { detail: d } + HostStoreResolved { store: store, executor: _ } => { + let partition = hosted_inflight_partition(quota: quota) + match event_log_read_partition(store: store, partition: partition, budget: 4096) { + PartitionReadRefused { cause: c } => HostedInflightUnread { detail: join(["the in-flight partition could not be read: ", event_log_refusal_wire(cause: c)], "") } + PartitionReadOk { head: _, walk: walk } => + match walk { + ChainIncomplete { missing: m, newest_first_so_far: _ } => HostedInflightUnread { detail: join(["the in-flight partition chain is missing event ", m as String], "") } + ChainBudgetExhausted { at: a } => HostedInflightUnread { detail: join(["the in-flight partition exceeded the read budget at ", a as String], "") } + ChainWalked { oldest_first: xs } => + match pool_fold(root: harness_seat_pool_root(partition: partition, ceiling: hosted_inflight_ceiling(quota: quota)), oldest_first: xs) { + PoolFoldRefused { at_event: e, wire: w } => HostedInflightUnread { detail: join(["the in-flight partition does not fold at ", e as String, ": ", w], "") } + PoolFolded { pool: p, generation: g } => + HostedInflightRead { pool: p, generation: g, store: store, release_hint: hosted_inflight_release_hint(quota: quota, pool: p, generation: g, walk: walk) } + } + } + } + } + } +} + +type HostedInflightHold + = HostedInflightHeld { reference: NonEmptyStr, store: FabricStorageBinding } + | HostedInflightRefused { detail: String } + +fn harness_hosted_inflight_acquire(quota: HarnessHostedQuota, store: FabricStorageBinding, reference: NonEmptyStr, term: Second) -> HostedInflightHold { + match now_epoch_seconds() { + Absent => HostedInflightRefused { detail: "the clock could not be read as epoch seconds" } + Present { value: now } => { + let partition = hosted_inflight_partition(quota: quota) + let term_seconds = second_count(s: term) + match fabric_seat_acquire( + store: store, partition: partition, + root: harness_seat_pool_root(partition: partition, ceiling: hosted_inflight_ceiling(quota: quota)), + actor: quota.actor, + request: SeatRequest { reference: reference, amount: Measure { count: 1 }, at: now, term_seconds: term_seconds }, + policy: LeasePolicy { maximum_duration_seconds: term_seconds, release_law: QuiescenceRequired }, + attempts: 3, budget: 4096, + ) { + SeatGranted { grant: _, generation: _, attempts_used: _ } => HostedInflightHeld { reference: reference, store: store } + SeatFull { wire: w, attempts_used: _ } => HostedInflightRefused { detail: join(["every in-flight seat is held (", w, "), so another send cannot be accounted for"], "") } + other => HostedInflightRefused { detail: seat_acquisition_wire(a: other) } + } + } + } +} + +fn harness_hosted_inflight_release(quota: HarnessHostedQuota, hold: HostedInflightHold, reason: NonEmptyStr) -> String? { + match hold { + HostedInflightRefused { detail: _ } => none + HostedInflightHeld { reference: r, store: store } => + match now_epoch_seconds() { + Absent => Present { value: join(["the in-flight seat ", r as String, " could not be released: the clock could not be read"], "") } + Present { value: now } => + match release_retry(store: store, partition: hosted_inflight_partition(quota: quota), ceiling: hosted_inflight_ceiling(quota: quota), reference: r, reason: reason, now: now) { + HarnessSeatReleased { partition: _ } => none + HarnessHostedCredentialRemoved => Present { value: join(["the in-flight seat ", r as String, " release answered as a credential removal"], "") } + HarnessSeatReleaseRefused { detail: d } => Present { value: join(["the in-flight seat ", r as String, " could not be released: ", d], "") } + } + } + } +} diff --git a/dag/gunbc/harness/harness_turn.dag b/dag/gunbc/harness/harness_turn.dag index b96b65119c4..3c56aefa019 100644 --- a/dag/gunbc/harness/harness_turn.dag +++ b/dag/gunbc/harness/harness_turn.dag @@ -13,6 +13,22 @@ import gunbc.harness.harness_reasoning_wire { harness_unit_thinking_end_token, harness_unit_reasoning_field, harness_serving_unit_wire, harness_reasoning_level_wire, } +import gunbc.harness.harness_hosted_route { + HarnessBackendAccess, BackendAdmittedBySeat, BackendAdmittedByHostedQuota, HarnessHostedQuota, HostedRoundCharged, + HostedRoundQuotaFull, HostedRoundUnleasable, harness_hosted_round_charge, harness_hosted_refusal_cause, + HostedRoundLease, +} +import extdeps.openrouter.openrouter { + OpenRouterRateLimitRefusal, OpenRouterHeaderDump, HeaderDumpRead, HeaderDumpUnread, openrouter_decode_rate_limited, openrouter_rate_limit_wire, +} +import gunbc.harness.harness_seat { + HostedInflightRead, HostedInflightUnread, HostedInflightHeld, HostedInflightRefused, harness_hosted_inflight_read, + harness_hosted_inflight_acquire, harness_hosted_inflight_release, hosted_inflight_carried, +} +import gunbc.fabric_quota { quota_window_start } +import gunbc.fabric_event_log_host { now_epoch_seconds } +import gunbc.fabric_storage_client { FabricStorageBinding +} import extdeps.clock { ClockUnixMillisRead, ClockUnixMillisObserved, ClockUnixMillisRefused, clock_unix_millis_read } import extdeps.http.client import extdeps.git.inspect @@ -244,6 +260,7 @@ type HarnessTurnConfig { request_deadline: Second environment: HarnessToolEnvironment completion: HarnessTurnCompletion + backend_access: HarnessBackendAccess } // THE WINDOW THE HARNESS WORKS IN AND THE WINDOW THE ENGINE ADMITS ARE ONE NUMBER, and this field @@ -539,6 +556,9 @@ type HarnessTurnOutcome | TurnUsageUnavailable { cause: String, steps_taken: Int } | TurnSubmissionUndecodable { reason: String, steps_taken: Int } | TurnAlignmentCheckpointDue { steps_taken: Int } + | TurnHostedQuotaFull { wire: String, steps_taken: Int } + | TurnHostedQuotaUnleasable { detail: String, steps_taken: Int } + | TurnHostedRateLimited { refusal: OpenRouterRateLimitRefusal, steps_taken: Int } fn harness_outcome_label(outcome: HarnessTurnOutcome) -> String { match outcome { @@ -557,6 +577,9 @@ fn harness_outcome_label(outcome: HarnessTurnOutcome) -> String { TurnUsageUnavailable { cause: _, steps_taken: _ } => "usage-unavailable" TurnSubmissionUndecodable { reason: _, steps_taken: _ } => "submission-undecodable" TurnAlignmentCheckpointDue { steps_taken: _ } => "alignment-checkpoint-due" + TurnHostedQuotaFull { wire: _, steps_taken: _ } => "hosted-quota-full" + TurnHostedQuotaUnleasable { detail: _, steps_taken: _ } => "hosted-quota-unleasable" + TurnHostedRateLimited { refusal: _, steps_taken: _ } => "hosted-rate-limited" } } @@ -613,6 +636,21 @@ fn harness_outcome_detail(outcome: HarnessTurnOutcome) -> String { " control to the workflow, whose adjudication lane decides whether the same lineage", " resumes, steers, re-plans, routes or stops. This is not a failure and not a completion.", ], "") + TurnHostedQuotaFull { wire: w, steps_taken: n } => + join([ + "after ", to_string(value: n), " tool rounds the local quota ledger refused the request (", w, + "); it was not sent. The ledger is this fleet's own pre-check, not an answer from the upstream, whose count it can differ from at a window boundary", + ], "") + TurnHostedQuotaUnleasable { detail: d, steps_taken: n } => + join([ + "after ", to_string(value: n), " tool rounds the hosted upstream's quota could not be leased (", d, + "); the request was not sent, because an unleased request is one the fleet cannot account for", + ], "") + TurnHostedRateLimited { refusal: r, steps_taken: k } => + join([ + "after ", to_string(value: k), " tool rounds ", openrouter_rate_limit_wire(r: r), + "; the turn stopped rather than retrying, and a caller that waits owns when it asks again", + ], "") TurnTransportRefused { detail: d } => d TurnResponseUnreadable { cause: c } => c TurnRequestUnwritable { path: p, detail: d } => @@ -1042,8 +1080,29 @@ fn harness_thinking_format_cause(unit: HarnessServingUnit, blocks: List String { + serialize_json(v: json_object([ + json_kv(key: "type", value: json_string(s: "turn.inflight_release_unrecorded")), + json_kv(key: "detail", value: json_string(s: detail)), + ])) } +// A HOSTED ROUND THAT COULD NOT BE CHARGED IS NOT POSTED, and the loop is told which way it was refused. +// The charge is recorded before the request goes out (gunbc.harness.harness_hosted_route +// harness_hosted_round_charge), so nothing about the round's quota is left to do after the answer. +type HarnessRoundPost + = RoundPosted { posted: HarnessPostedRound } + | RoundQuotaFull { wire: String } + | RoundQuotaUnleasable { detail: String } + + fn harness_wire_log_path(config: HarnessTurnConfig, round: Int, kind: String) -> String { join([config.events_path as String, ".wire/", to_string(value: round), kind], "") } @@ -1056,10 +1115,10 @@ fn harness_post_round( config: HarnessTurnConfig, messages: List, round: Int, -) -> HarnessPostedRound { +) -> HarnessRoundPost { match harness_reasoning_kwargs_for(unit: config.serving_unit, level: config.reasoning_mode) { ReasoningLevelUnsupported { unit, level, cause } => - HarnessPostedRound { + RoundPosted { posted: HarnessPostedRound { read: HarnessResponseUnreadable { cause: join([ "the serving unit ", unit as String, " cannot express reasoning level ", level as String, @@ -1067,21 +1126,93 @@ fn harness_post_round( ], ""), }, wire_log_detail: none, + inflight_release_detail: none, + rate_limited: none, + } } + ReasoningKwargsAdmitted { kwargs } => + match harness_prepare_round(config: config, messages: messages, reasoning_kwargs: kwargs, round: round) { + RoundUnprepared { posted: p } => RoundPosted { posted: p } + RoundPrepared { wire_request_detail: wd } => + match config.backend_access { + BackendAdmittedBySeat => RoundPosted { posted: harness_send_round(config: config, round: round, header_file: none, wire_request_detail: wd) } + BackendAdmittedByHostedQuota { header_file: hf, quota: q } => harness_hosted_round_post(config: config, round: round, header_file: hf, quota: q, wire_request_detail: wd) + } } - ReasoningKwargsAdmitted { kwargs } => harness_post_round_admitted(config: config, messages: messages, reasoning_kwargs: kwargs, round: round) } } -fn harness_post_round_admitted( +// A HOSTED ROUND IS PREPARED, THEN CHARGED, THEN SENT -- in that order, because the charge is counted in +// the window the request ARRIVES in. Everything that can take time on this side (serializing the body, +// writing the scratch and the wire log) is done before the charge, so the charge is as close to the send +// as this process can put it. Nothing here bounds when the first byte actually leaves, so the send also +// holds an in-flight seat from before it until the transport returns (gunbc.harness.harness_seat, in-flight +// hosted sends): every later window counts the seats still held from earlier ones and charges for them as +// well as for its own send. +// +// THE CHARGE IS A CLOSE PRE-CHECK AND THE UPSTREAM'S 429 IS THE BACKSTOP (operator decision, escalation +// msg_46a417a3; gunbc.harness.harness_hosted_route): a request the pre-check admitted that OpenRouter +// still refuses with 429 ends the turn as TurnHostedRateLimited, carrying the upstream's own retry hints, +// and is never retried here. +fn harness_hosted_round_post(config: HarnessTurnConfig, round: Int, header_file: NonEmptyStr, quota: HarnessHostedQuota, wire_request_detail: String?) -> HarnessRoundPost { + match harness_hosted_inflight_read(quota: quota) { + HostedInflightUnread { detail: d } => RoundQuotaUnleasable { detail: join(["the in-flight sends could not be counted, so the charge cannot cover them: ", d], "") } + HostedInflightRead { pool: p, generation: _, store: store, release_hint: hint } => + match now_epoch_seconds() { + Absent => RoundQuotaUnleasable { detail: "the clock could not be read as epoch seconds" } + Present { value: now } => + match quota_window_start(limit: quota.per_minute, at: now) { + Absent => RoundQuotaUnleasable { detail: "the minute window has no start at this instant" } + Present { value: minute_start } => + match quota_window_start(limit: quota.per_day, at: now) { + Absent => RoundQuotaUnleasable { detail: "the day window has no start at this instant" } + Present { value: day_start } => + harness_hosted_round_charged_post( + config: config, round: round, header_file: header_file, quota: quota, wire_request_detail: wire_request_detail, store: store, + reference: join([quota.actor as String, "@", to_string(value: now), "#round", to_string(value: round)], "") as NonEmptyStr, + charge: harness_hosted_round_charge( + quota: quota, + carried_minute: hosted_inflight_carried(pool: p, window_start: minute_start), + carried_day: hosted_inflight_carried(pool: p, window_start: day_start), + release_hint: hint, + ), + ) + } + } + } + } +} + +fn harness_hosted_round_charged_post(config: HarnessTurnConfig, round: Int, header_file: NonEmptyStr, quota: HarnessHostedQuota, wire_request_detail: String?, store: FabricStorageBinding, reference: NonEmptyStr, charge: HostedRoundLease) -> HarnessRoundPost { + match charge { + HostedRoundQuotaFull { wire: w } => RoundQuotaFull { wire: w } + HostedRoundUnleasable { detail: d } => RoundQuotaUnleasable { detail: d } + HostedRoundCharged => + match harness_hosted_inflight_acquire(quota: quota, store: store, reference: reference, term: config.request_deadline) { + HostedInflightRefused { detail: d } => RoundQuotaUnleasable { detail: d } + HostedInflightHeld { reference: r, store: st } => { + let hold = HostedInflightHeld { reference: r, store: st } + let posted = harness_send_round(config: config, round: round, header_file: Present { value: header_file }, wire_request_detail: wire_request_detail) + let released = harness_hosted_inflight_release(quota: quota, hold: hold, reason: "the transport returned" as NonEmptyStr) + RoundPosted { posted: HarnessPostedRound { read: posted.read, wire_log_detail: posted.wire_log_detail, inflight_release_detail: released, rate_limited: posted.rate_limited } } + } + } + } +} + +type HarnessRoundPreparation + = RoundPrepared { wire_request_detail: String? } + | RoundUnprepared { posted: HarnessPostedRound } + +fn harness_prepare_round( config: HarnessTurnConfig, messages: List, reasoning_kwargs: List, round: Int, -) -> HarnessPostedRound { +) -> HarnessRoundPreparation { let body = serialize_json(v: harness_request_json(config: config, messages: messages, reasoning_kwargs: reasoning_kwargs)) let written = Filesystem.Write(path: config.request_scratch_path as String, content: body) if !written.success { - HarnessPostedRound { + RoundUnprepared { posted: HarnessPostedRound { read: HarnessResponseUnreadable { cause: join([ "request body could not be written to ", config.request_scratch_path as String, @@ -1089,15 +1220,25 @@ fn harness_post_round_admitted( ], ""), }, wire_log_detail: none, - } + inflight_release_detail: none, + rate_limited: none, + } } } else { let wire_request_path = harness_wire_log_path(config: config, round: round, kind: ".request.json") let wire_request = Filesystem.Write(path: wire_request_path, content: body) - let posted = http.Client.PostJsonFromFile( - url: config.backend_url as NonEmptyStr, - request_body_file: config.request_scratch_path as String as NonEmptyStr, - max_seconds: to_string(value: second_count(s: config.request_deadline)) as NonEmptyStr, - ) + RoundPrepared { + wire_request_detail: if wire_request.success { none } else { Present { value: join(["request wire log ", wire_request_path, " could not be written: ", wire_request.error], "") } }, + } + } +} + +fn harness_send_round( + config: HarnessTurnConfig, + round: Int, + header_file: NonEmptyStr?, + wire_request_detail: String?, +) -> HarnessPostedRound { + let posted = harness_post_json(config: config, header_file: header_file) let wire_response_path = harness_wire_log_path(config: config, round: round, kind: ".response.json") let wire_response = Filesystem.Write(path: wire_response_path, content: posted.body) let kept = Filesystem.Write( @@ -1105,7 +1246,7 @@ fn harness_post_round_admitted( content: posted.body, ) let wire_details = concat( - if wire_request.success { [] } else { [join(["request wire log ", wire_request_path, " could not be written: ", wire_request.error], "")] }, + match wire_request_detail { Present { value: d } => [d] Absent => [] }, if wire_response.success { [] } else { [join(["response wire log ", wire_response_path, " could not be written: ", wire_response.error], "")] }, ) let wire_log_detail = if count(wire_details) == 0 { @@ -1117,10 +1258,12 @@ fn harness_post_round_admitted( HarnessPostedRound { read: HarnessResponseUnreadable { cause: join([ - "the backend refused the request or the transport failed; body was: ", posted.body, + "the backend refused the request or the transport failed; ", harness_refusal_body_text(config: config, body: posted.body), ], ""), }, wire_log_detail: wire_log_detail, + inflight_release_detail: none, + rate_limited: harness_hosted_rate_limited(config: config, posted: posted), } } else if !kept.success { HarnessPostedRound { @@ -1128,16 +1271,84 @@ fn harness_post_round_admitted( cause: join(["the response could not be kept beside the request: ", kept.error, "; refusing to decode a body no one can re-read"], ""), }, wire_log_detail: wire_log_detail, + inflight_release_detail: none, + rate_limited: none, } } else { HarnessPostedRound { read: harness_decode_for_shape(unit: config.serving_unit, shape: config.wire_shape, body: posted.body), wire_log_detail: wire_log_detail, + inflight_release_detail: none, + rate_limited: none, + } + } +} + +// ONE POST, TWO ADMISSIONS. A seat-admitted backend is the fleet's own engine over its network and takes +// no credential; a quota-admitted one is a hosted upstream and takes the bearer header file by path. +type HarnessPostOutcome { + body: String + success: Bool + headers: OpenRouterHeaderDump? +} + +// A 429 FROM A HOSTED UPSTREAM IS A TYPED REFUSAL WITH THE UPSTREAM'S OWN RETRY HINTS, decoded from the +// error body and the response header dump; it is never retried here. A fleet engine has no such refusal. +fn harness_hosted_rate_limited(config: HarnessTurnConfig, posted: HarnessPostOutcome) -> OpenRouterRateLimitRefusal? { + match config.backend_access { + BackendAdmittedBySeat => none + BackendAdmittedByHostedQuota { header_file: _, quota: _ } => + openrouter_decode_rate_limited( + body: posted.body, + header_dump: match posted.headers { + Present { value: d } => d + Absent => HeaderDumpUnread { cause: "no response header dump was taken for this request" } + }, + ) + } +} + +fn harness_response_headers_path(config: HarnessTurnConfig) -> String { + join([config.request_scratch_path as String, ".response.headers"], "") +} + +fn harness_post_json(config: HarnessTurnConfig, header_file: NonEmptyStr?) -> HarnessPostOutcome { + match header_file { + Absent => { + let posted = http.Client.PostJsonFromFile( + url: config.backend_url as NonEmptyStr, + request_body_file: config.request_scratch_path as String as NonEmptyStr, + max_seconds: to_string(value: second_count(s: config.request_deadline)) as NonEmptyStr, + ) + HarnessPostOutcome { body: posted.body, success: posted.success, headers: none } + } + Present { value: hf } => { + let posted = http.Client.PostJsonFromFileWithHeaderFile( + url: config.backend_url as NonEmptyStr, + request_body_file: config.request_scratch_path as String as NonEmptyStr, + header_file: hf, + response_headers_file: harness_response_headers_path(config: config) as NonEmptyStr, + max_seconds: to_string(value: second_count(s: config.request_deadline)) as NonEmptyStr, + ) + let dump = Filesystem.Read(path: harness_response_headers_path(config: config)) + HarnessPostOutcome { + body: posted.body, + success: posted.success, + headers: Present { value: if dump.success { HeaderDumpRead { text: dump.content } } else { HeaderDumpUnread { cause: join(["the curl header dump ", harness_response_headers_path(config: config), " could not be read: ", dump.error], "") } } }, } } } } +// A HOSTED UPSTREAM'S REFUSAL IS DECODED TO ITS TYPED ERROR, so a 402 says which budget refused and a +// 429 says whose limit it was; a fleet engine's body is reported as it came. +fn harness_refusal_body_text(config: HarnessTurnConfig, body: String) -> String { + match config.backend_access { + BackendAdmittedBySeat => join(["body was: ", body], "") + BackendAdmittedByHostedQuota { header_file: _, quota: _ } => join(["upstream answered: ", harness_hosted_refusal_cause(body: body)], "") + } +} + // THE LOOP. Each round either reaches a provider terminal, or executes the tool calls and recurses // with a strictly smaller budget. There is no arm that continues without either consuming a step or // terminating, which is what makes the descent argument readable rather than asserted. @@ -1276,19 +1487,61 @@ fn harness_turn_loop( harness_turn_checkpoint_due(config: config, steps_taken: steps_taken, events: events) } } else { - let posted_round = harness_post_round(config: config, messages: messages, round: steps_taken + 1) - let events_after_wire = match posted_round.wire_log_detail { - Present { value: detail } => concat(events, [harness_event_wire_log_unwritable(detail: detail)]) - Absent => events - } - match posted_round.read { - HarnessResponseUnreadable { cause: c } => { - let failed = concat(events_after_wire, [harness_event_turn_failed(message: c)]) + match harness_post_round(config: config, messages: messages, round: steps_taken + 1) { + RoundQuotaFull { wire: w } => { + let failed = concat(events, [harness_event_turn_failed(message: join(["hosted quota full: ", w], ""))]) + match harness_write_events(config: config, events: failed) { + HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) + HarnessEventsWritten => TurnHostedQuotaFull { wire: w, steps_taken: steps_taken } + } + } + RoundQuotaUnleasable { detail: qd } => { + let failed = concat(events, [harness_event_turn_failed(message: join(["hosted quota unleasable: ", qd], ""))]) match harness_write_events(config: config, events: failed) { HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) - HarnessEventsWritten => TurnResponseUnreadable { cause: c } + HarnessEventsWritten => TurnHostedQuotaUnleasable { detail: qd, steps_taken: steps_taken } } } + RoundPosted { posted: posted_round } => harness_turn_after_post(config: config, messages: messages, steps_remaining: steps_remaining, steps_taken: steps_taken, events: events, repairs_used: repairs_used, posted_round: posted_round) + } + } +} + +fn harness_turn_after_post( + config: HarnessTurnConfig, + messages: List, + steps_remaining: Int, + steps_taken: Int, + events: List, + repairs_used: Int, + posted_round: HarnessPostedRound, +) -> HarnessTurnOutcome { + let events_after_release = match posted_round.inflight_release_detail { + Present { value: detail } => concat(events, [harness_event_inflight_release_unrecorded(detail: detail)]) + Absent => events + } + let events_after_wire = match posted_round.wire_log_detail { + Present { value: detail } => concat(events_after_release, [harness_event_wire_log_unwritable(detail: detail)]) + Absent => events_after_release + } + match posted_round.read { + HarnessResponseUnreadable { cause: c } => + match posted_round.rate_limited { + Present { value: limited } => { + let failed = concat(events_after_wire, [harness_event_turn_failed(message: openrouter_rate_limit_wire(r: limited))]) + match harness_write_events(config: config, events: failed) { + HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) + HarnessEventsWritten => TurnHostedRateLimited { refusal: limited, steps_taken: steps_taken } + } + } + Absent => { + let failed = concat(events_after_wire, [harness_event_turn_failed(message: c)]) + match harness_write_events(config: config, events: failed) { + HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) + HarnessEventsWritten => TurnResponseUnreadable { cause: c } + } + } + } HarnessResponseParsed { blocks: raw_blocks, stop_reason, usage: usage_read } => match usage_read { UsageUnreadable { cause: uc } => { @@ -1315,7 +1568,6 @@ fn harness_turn_loop( ) } } - } } // THE ROUND THAT DECODED AND WAS COUNTED. Factored out of the loop when usage became a typed read: diff --git a/dag/gunbc/host/baseboard_dimm_figure.dag b/dag/gunbc/host/baseboard_dimm_figure.dag index 0067253e3e6..94f03d11b38 100644 --- a/dag/gunbc/host/baseboard_dimm_figure.dag +++ b/dag/gunbc/host/baseboard_dimm_figure.dag @@ -3,7 +3,9 @@ module gunbc.baseboard_dimm_figure import std.types { List } import extdeps.external_authority { ExternalAuthority } import extdeps.ampere.mt_collins_getting_started_guide.subject { mt_collins_gsg_authority } -import extdeps.boards.types { BaseboardModel, AsrockRackAltrad8ud1l2t, GigabyteMp72Hb0, AmpereMtCollins } +import std.optional { Present, Absent } +import extdeps.board_authority { BoardAuthorityRef, BoardAuthorityLookup, board_facts_authority, board_authority_ref_eq } +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import extdeps.ampere.mt_collins_getting_started_guide.dimm_layout { MtCollinsDimmFigureBank, MtCollinsChassisFigureOrientation, dimm_figure_banks, chassis_figure_orientation, } @@ -12,7 +14,7 @@ import extdeps.ampere.mt_collins_product_brief.memory_population { } // WHICH CITED DIMM FIGURE A BASEBOARD HAS, and nothing else. A consumer-layer dispatch over the -// closed extdeps.boards.types BaseboardModel (DESIGN §3: the dispatch that selects among upstream +// closed (extdeps.board_authority) (DESIGN §3: the dispatch that selects among upstream // authorities is not a fact of any one of them, so it lives here and never in extdeps). An arm names // the board's own cited model and carries that model's external authority; it carries no rows, so // the figure is read from its extdeps authority at every use and never restated beside it. A board with no cited figure in this repository refuses @@ -22,14 +24,20 @@ type BaseboardDimmFigure = MtCollinsGettingStartedGuideFigures { guide: ExternalAuthority } type BaseboardDimmFigureStanding - = BaseboardDimmFigureOf { board: BaseboardModel, figure: BaseboardDimmFigure } - | BoardFigureUnmodeled { board: BaseboardModel } - -fn baseboard_dimm_figure(board: BaseboardModel) -> BaseboardDimmFigureStanding { - match board { - AmpereMtCollins => BaseboardDimmFigureOf { board: board, figure: MtCollinsGettingStartedGuideFigures { guide: mt_collins_gsg_authority } } - GigabyteMp72Hb0 => BoardFigureUnmodeled { board: board } - AsrockRackAltrad8ud1l2t => BoardFigureUnmodeled { board: board } + = BaseboardDimmFigureOf { board: BoardAuthorityRef, figure: BaseboardDimmFigure } + | BoardFigureUnmodeled { board: BoardAuthorityLookup } + +// A figure is a board FACT, so only an evidence-bound lookup can reach one; a name-only, unbound, +// ambiguous or unread board is BoardFigureUnmodeled carrying the lookup that refused it. +fn baseboard_dimm_figure(board: BoardAuthorityLookup) -> BaseboardDimmFigureStanding { + match board_facts_authority(lookup: board) { + Present { value: a } => + if board_authority_ref_eq(a: a, b: mt_collins_board_authority) { + BaseboardDimmFigureOf { board: a, figure: MtCollinsGettingStartedGuideFigures { guide: mt_collins_gsg_authority } } + } else { + BoardFigureUnmodeled { board: board } + } + Absent => BoardFigureUnmodeled { board: board } } } diff --git a/dag/gunbc/host/boot_medium.dag b/dag/gunbc/host/boot_medium.dag index f3e977d9f37..2a26ef37b7f 100644 --- a/dag/gunbc/host/boot_medium.dag +++ b/dag/gunbc/host/boot_medium.dag @@ -32,9 +32,15 @@ data bmc_boot_image_export: BootImageExport = BootImageExport { // the census volume and nothing else does, and the stock installer prints no terminal at all, so a // boot of it can be refused before any controller write. A new medium adds its arm here with the // acceptance predicate its terminal is read by; the boot subject does not change. +// +// RunnerHostUp is the runner medium's terminal: the medium's volume label (so the capture binds to +// the medium that was attached) and a host whose runner is installed and whose idle memory, thread +// count and machine architecture were read before any job. Its predicate is runner-owned +// (gunbc.runner.runner_host_medium runner_host_up_readback), not a property of a boot run. type BootMediumTerminal = BootMediumNoTerminal { reason: NonEmptyStr } | BootMediumVolumeLabel { label: NonEmptyStr } + | BootMediumRunnerHostUp { label: NonEmptyStr } // THE MEDIUM IS BOOT POLICY, NOT A HOST FACT: which artifact a boot attaches, named by its content // digest, where it is exported, and what terminal it is accepted by. The published file is the diff --git a/dag/gunbc/host/host_budget_source.dag b/dag/gunbc/host/host_budget_source.dag index 2cb18c62a96..150633dd943 100644 --- a/dag/gunbc/host/host_budget_source.dag +++ b/dag/gunbc/host/host_budget_source.dag @@ -4,6 +4,11 @@ import std.types { NonEmptyStr, String, Bool, List } import std.measure { ByteSize, byte_size_count } import std.os.types { KernelFamily, Linux, Darwin, WindowsNt, LinuxGuestOnWindows } import extdeps.linux.cgroup_v2 { kernel_provides_cgroup_v2 } +import extdeps.linux.cgroup_v2_memory { + CgroupMemoryLimitValue, CgroupMemoryLimited, CgroupMemoryUnlimited, CgroupMemoryLimitUnparseable, + CgroupMemoryCount, CgroupMemoryCountUnparseable, parse_cgroup_memory_limit, parse_cgroup_memory_count, +} +import gunbc.required_ci_phase_roster { floor_log_row_fields, floor_log_field_value } import std.disposition { Disposition, Terminal, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.linux.procfs { @@ -333,8 +338,8 @@ fn kernel_budget_sourcing_static_source(g: KernelBudgetSourcing) -> HostBudgetSo // may bound a refusal; it may not stand in for a reading. // THE CGROUP-v1 ARM JOINS THIS SAME DECLARED MIRROR rather than opening a second one. The seed's -// memory_governor CgroupV1HierarchicalMemoryLimit arm, CgroupV1MemoryLimitValue, -// cgroup_v1_hierarchical_limit_from_stat and cgroup_v1_unlimited_bytes hand-realize +// v1_rt HostBudgetCgroupV1 arm, host_budget_cgroup_v1_from_stat and +// host_budget_cgroup_v1_unlimited_bytes hand-realize // BudgetSourceCgroupV1HierarchicalMemoryLimit here and extdeps.linux.cgroup_v1_memory's parse, under // the bootstrap constraint stated in the note below: the budget bounds the resolve, so it is read // before any .dag value exists. The mirror keeps the model's three states (limited / unlimited / @@ -353,3 +358,69 @@ data host_budget_source_seed_mirror_disposition: Disposition = Scaffold { data host_budget_source_seed_mirror_note: String = "THIS MODEL IS THE AUTHORITY AND v1 STILL HAND-REALIZES IT, which is a mirror of exactly the kind that just went stale elsewhere in this change — so it is declared rather than left implicit. memory_governor::read_host_budget_bytes composes its own labels and cli_run::typed_module_cache_cap_derivation computes its own degraded flag, and neither reads these rows.\n\nWhy the mirror cannot simply be deleted today: one consumer runs strictly before any .dag value could exist. cli_run's enforce_typed_cache_entry_cap derives the typed-module-cache eviction bound from this same budget WHILE MODULES ARE BEING RESOLVED — it is the thing bounding the memory used to resolve the corpus, so it cannot be a value obtained by resolving the corpus. The governor's own arm in claim_executor is different and could in principle read a resolved value (it happens after the plan resolve), so that half is ordinary seed-shrink debt rather than a bootstrap constraint. The two must not be conflated when the emit lands: the first needs the value baked into the seed, the second only needs the seed to stop duplicating it.\n\nTHAT CHECK NOW EXISTS BUT IS NOT RUN BY CI, and the distinction is the whole point of this row: test.claim.seed_mirror_constant_lens_witness_test reads the seed's constants against their authority rows and covers the union of what this note and gunbc.typed_module_cache_capacity's seed_mirror_note each asked for separately — DECLARED_RUNNER_SLOT_MEMORY_HIGH_BYTES, DECLARED_FLOOR_MINIMUM_VIABLE_ARMED_BUDGET_BYTES, and the three typed-module-cache constants. Two notes naming different overlapping subsets, neither of which was the union, was itself two partial specifications of one mechanism. THIS NOTE'S OWN WARNING CAME TRUE AND IS WORTH KEEPING FOR IT. It cited DECLARED_RUNNER_SLOT_MEMORY_HIGH_BYTES as having drifted once already; it then drifted again and sat eleven days at 13958643712 against an authority of 16106127360 (gunbc.runner_slot_desired gunbc_runner_slot_desired, moved by #8388 on 2026-08-17). Knowing about a gap and naming its fix is not the same as closing it, and the interval is the receipt. Note also that the sibling witness could not have caught it: test.claim.host_budget_source_witness built its expectation from the mirror value retyped, so it greened whichever way the mirror went — a second inert-by-construction surface, since corrected to derive from the row." + +// ── THE FLOOR'S [floor-cgroup] LEVEL ROWS, READ BACK FROM A RUN'S LOG ────────────────────────── +// +// The floor prints, at entry and on every heartbeat, one `[floor-cgroup] when= level=` +// row per cgroup level from its own cgroup upward, carrying that level's memory.max, memory.high, +// memory.current and memory.peak as the kernel spelled them (plus events and pressure, which this +// reading does not take). The budget this module resolves IS the memory.high of the slot level, so +// the bound and the peak a qualification reads are denominated in this module's vocabulary -- which is +// why gunbc.runner_throughput_qualification_route names HostBudgetResolution as their producer. The +// values are decoded by extdeps.linux.cgroup_v2_memory, the kernel interface's own authority, never +// re-parsed here: `max` is CgroupMemoryUnlimited, never a large number. +// +// The `when= path=` row the floor prints beside them is a different row (the process's +// own cgroup path, no readings) and is not a level row: it reads as none, like any other line. +type FloorCgroupLevelRow { + when: NonEmptyStr + level: NonEmptyStr + high: CgroupMemoryLimitValue + peak: ByteSize +} + +type FloorCgroupRowReading + = FloorCgroupLevelRead { row: FloorCgroupLevelRow } + | FloorCgroupRowGarbled { line: String, cause: NonEmptyStr } + +data floor_cgroup_row_tag: String = "[floor-cgroup]" + +fn floor_cgroup_garbled(line: String, cause: String) -> FloorCgroupRowReading? { + Present { value: FloorCgroupRowGarbled { line: line, cause: cause as NonEmptyStr } } +} + +fn read_floor_cgroup_level_row(line: String) -> FloorCgroupRowReading? { + match floor_log_row_fields(line: line, tag: floor_cgroup_row_tag) { + Absent => none + Present { value: fields } => + match floor_log_field_value(fields: fields, key: "level") { + Absent => none + Present { value: level } => + match floor_log_field_value(fields: fields, key: "when") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no when= field") + Present { value: when } => + if when == "" || level == "" { + floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries an empty when= or level= field") + } else { + match floor_log_field_value(fields: fields, key: "high") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no high= field") + Present { value: raw_high } => + match parse_cgroup_memory_limit(body: raw_high) { + CgroupMemoryLimitUnparseable { body: _ } => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] high= is neither a byte count nor max") + high => + match floor_log_field_value(fields: fields, key: "peak") { + Absent => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] level row carries no peak= field") + Present { value: raw_peak } => + match parse_cgroup_memory_count(body: raw_peak) { + CgroupMemoryCountUnparseable { body: _ } => floor_cgroup_garbled(line: line, cause: "[floor-cgroup] peak= is not a byte count") + CgroupMemoryCount { bytes: peak } => + Present { value: FloorCgroupLevelRead { row: FloorCgroupLevelRow { when: when as NonEmptyStr, level: level as NonEmptyStr, high: high, peak: peak } } } + } + } + } + } + } + } + } + } +} diff --git a/dag/gunbc/host/host_layout.dag b/dag/gunbc/host/host_layout.dag index dcf2c3f5d64..2382978302e 100644 --- a/dag/gunbc/host/host_layout.dag +++ b/dag/gunbc/host/host_layout.dag @@ -317,6 +317,55 @@ fn instance_receipts_dir(instance_root: String) -> String { join([instance_root, "/receipts"], "") } +// THE ATTEMPT-EXIT SPOOL, homed here for the publication spool's reason: it has consumers on both +// sides of a process boundary that share no code path. The worker unit's stop hook +// (gunbc.roadmap_dispatch_actuator) creates an entry, the deployment's path unit +// (gunbc.live_deploy.emit) watches for one, and the belt's obligation edge moves it on. One string, +// three consumers. +// +// AN ENTRY IS A DIRECTORY WHOSE NAME IS THE RECORD (gunbc.roadmap.roadmap_attempt_exit_spool). +// Three siblings, never nested, because the path unit fires on DirectoryNotEmpty of ONE of them: a +// subdirectory inside the watched directory would keep it non-empty forever. `inbox` holds exits +// not yet handed to an obligation; `queued` holds exits whose obligation has been started and has +// not yet written its receipt; `done` holds exits whose capture receipt exists; `refused` holds exits +// the drain could not hand off (attempt_exit_refused_dir). Moving an entry out +// of `inbox` when its obligation STARTS is what lets the path unit go quiet while that obligation +// runs, rather than re-firing until systemd's trigger limit fails the unit. +// +// Derived from the instance root, for instance_receipts_dir's reason: two instances on one host must +// not read each other's exits. +fn attempt_exit_spool_root(instance_root: String) -> String { + join([instance_root, "/attempt-exit-spool"], "") +} + +fn attempt_exit_inbox_dir(instance_root: String) -> String { + join([attempt_exit_spool_root(instance_root: instance_root), "/inbox"], "") +} + +fn attempt_exit_queued_dir(instance_root: String) -> String { + join([attempt_exit_spool_root(instance_root: instance_root), "/queued"], "") +} + +// `running` holds the entries a live obligation run has CLAIMED (queued -> running is the claim, taken +// before the run's first observation) and that its receipt has not yet settled to `done`. Keeping a claim +// out of `queued` is what lets the path unit watch `queued` as a LEVEL -- non-empty means an exit no run +// has claimed -- without re-firing for the whole length of a long run. +fn attempt_exit_running_dir(instance_root: String) -> String { + join([attempt_exit_spool_root(instance_root: instance_root), "/running"], "") +} + +fn attempt_exit_done_dir(instance_root: String) -> String { + join([attempt_exit_spool_root(instance_root: instance_root), "/done"], "") +} + +// `refused` holds an entry the drain could not hand to an obligation -- a name that is not a record, +// or a record whose obligation unit the manager would not start -- moved out of the inbox so the path +// unit does not re-fire on it forever, and kept, because a refused hand-off is still an exit that +// happened and the observer still reads it. +fn attempt_exit_refused_dir(instance_root: String) -> String { + join([attempt_exit_spool_root(instance_root: instance_root), "/refused"], "") +} + // THE PUBLICATION SPOOL, HOMED HERE FOR THE REASON THE TWO ROWS ABOVE ARE: it has two consumers on // opposite sides of a PRIVILEGE boundary, so neither may own the name. gunbc.roadmap_belt_actuate // writes a request into it as the service principal; the publication helper reads that request and @@ -373,7 +422,7 @@ fn publication_refused_dir(instance_root: String) -> String { // EACCES was a root-owned STALE FILE from the pre-b1 unit user, not the directory. // Publication authority is atomic rename in gunbc.fleet_converge_cli — NOT live_deploy // install-d on this path and NOT per-run elevation. -data fleet_converge_receipt_atomic_publish_note: String = "Receipt publication is gunbc.fleet_converge_cli converge_cli_atomic_receipt_write: Filesystem.Write to a same-directory *.staging file, then shell.Move.File over the final path. rename(2) takes permission from the containing directory, not the file being replaced, so a root-owned stale receipt is superseded without elevation when the service principal owns the directory. In-place Filesystem.Write on the target path fails EACCES on stale root-owned files and is crash-unsafe (truncated receipts). Deploy install-d ensures the directory exists and is owned by the service principal on fresh hosts; that bootstrap is orthogonal to publication. CORRECTED 2026-08-19: this clause named live_deploy emit FleetConvergeTimerUnit apply as the bootstrap's home, and that member is now a retired refusal poison (cadence moved to on-demand plan/apply, sleek-heron-218), so NOTHING had bootstrapped a receipts directory on any fresh instance root since that retirement — srv1's green slot has no receipts directory at all, and production's exists only because it predates it. The bootstrap now rides live_deploy emit BeltTimerUnit apply, on the consumer that needs the directory wherever it is installed — per-run sudo install on individual receipt files is not the publication fix when the directory is already service-user-owned." +data fleet_converge_receipt_atomic_publish_note: String = "Receipt publication is gunbc.fleet_converge_cli converge_cli_atomic_receipt_write: Filesystem.Write to a same-directory *.staging file, then shell.Move.File over the final path. rename(2) takes permission from the containing directory, not the file being replaced, so a root-owned stale receipt is superseded without elevation when the service principal owns the directory. In-place Filesystem.Write on the target path fails EACCES on stale root-owned files and is crash-unsafe (truncated receipts). Deploy install-d ensures the directory exists and is owned by the service principal on fresh hosts; that bootstrap is orthogonal to publication. CORRECTED 2026-08-19: this clause named live_deploy emit FleetConvergeTimerUnit apply as the bootstrap's home, and that member is now a retired refusal poison (cadence moved to on-demand plan/apply, sleek-heron-218), so NOTHING had bootstrapped a receipts directory on any fresh instance root since that retirement — srv1's green slot has no receipts directory at all, and production's exists only because it predates it. The bootstrap now rides live_deploy emit BeltEventPathUnit apply, on the consumer that needs the directory wherever it is installed — per-run sudo install on individual receipt files is not the publication fix when the directory is already service-user-owned." // ONE host-parameterized placement for provider package/exec release trees, mutable provider state, // and private wire-evidence roots. Parallel to GunbcHostLayout: a second host must be describable diff --git a/dag/gunbc/host/host_memory_qualification.dag b/dag/gunbc/host/host_memory_qualification.dag index 84d4f1efddb..b43f0d9fda3 100644 --- a/dag/gunbc/host/host_memory_qualification.dag +++ b/dag/gunbc/host/host_memory_qualification.dag @@ -1,10 +1,11 @@ module gunbc.host_memory_qualification +import extdeps.board_authority { BoardAuthorityRef } import std.types { Bool, EpochMs, List, NonEmptyStr } import std.nat { Nat } import std.measure { ByteSize } -import extdeps.boards.types { AsrockRackAltrad8ud1l2t, BaseboardModel, DimmPopulation } -import extdeps.boards.asrock_rack { asrock_altrad8ud_download_authority } +import extdeps.boards.types { DimmPopulation } +import extdeps.boards.asrock_rack { asrock_altrad8ud_download_authority, asrock_altrad8ud_board_authority } import extdeps.external_authority { ExternalAuthority } import extdeps.cpu.ampere { ampere_altra_avl_authority, ampere_altra_avl_current_issue } import product.host_identity { HostIdentity } @@ -192,7 +193,7 @@ type TrainingEvidenceStanding // only in a sparse population is weaker support for rejecting a part in a // full population, so the observation carries its own conditions. type PlatformTrainingFailure { - board: BaseboardModel + board: BoardAuthorityRef step: MemoryTrainingStep standing: TrainingEvidenceStanding observed_population: List @@ -245,7 +246,7 @@ fn implication_severity( fn implication_for_step( candidate: DramModuleCatalogRow, failure: PlatformTrainingFailure, - target_board: BaseboardModel, + target_board: BoardAuthorityRef, ) -> PartImplication { if failure.board != target_board { PartNotImplicated @@ -259,7 +260,7 @@ fn implication_for_step( fn part_implication( candidate: DramModuleCatalogRow, failures: List, - target_board: BaseboardModel, + target_board: BoardAuthorityRef, ) -> PartImplication { fold( failures, @@ -320,7 +321,7 @@ fn topology_pressure( } data altrad8ud_mrep_failure_observation: PlatformTrainingFailure = PlatformTrainingFailure { - board: AsrockRackAltrad8ud1l2t, + board: asrock_altrad8ud_board_authority, step: LrdimmDataBufferMrep, standing: AssociatedByIdenticalSignature { signature: "SK[0]: 00120001 / MC[0]: 00f100b4", @@ -477,4 +478,4 @@ data srv2_failed_64gib_population_standing: HostMemoryStanding = HostMemoryStand // guessed. Watch for recurrence at the same coordinates. A second event there is a device-level // finding; scattered single events across different addresses are not. -data platform_training_implication_note: String = "RELOCATED 2026-08-19 from the deleted gunbc.memory_part_selection, whose candidate/frontier half was a selection island; this evidence half is qualification, so it lives beside the four standings it grades. The grading rule is mechanical and derived: a candidate is graded by whether it must execute a training step THIS platform has been observed to fail, and by how strongly that failure is evidenced. For the ALTRAD8UD-1L2T the step is the DDR4 data-buffer read-phase calibration (MREP), which only load-reduced modules execute, so the grading is independent of vendor, part number, capacity, speed grade, and presence on any approved-vendor list. THE SEVERITY IS A FUNCTION OF EVIDENCE, and an earlier revision of this module got that wrong in a way worth recording: it refused every module executing the failing step, on any board, at any standing including EvidenceUnobserved. Because capacities near a generation's die-density ceiling are disproportionately load-reduced, that blanket rule removed most of the 64GB DDR4 market by construction — the exact blocklist this module's prose claimed to avoid, reached by a different route, and the opposite of letting a buyer shop the whole market. Now: a decode of this platform's own syndrome register refuses; an association carrying byte-identical controller status words, or an inference from sequence position, requires a SAMPLE before committing to a full population and stays on the Pareto frontier; an unobserved standing implicates nothing. gunbc holds MREP at AssociatedByIdenticalSignature, so the honest verdict for another ALTRAD8UD LRDIMM today is buy-one-and-test, not reject. Failures are scoped to the board they were observed on, so an ALTRAD8UD receipt cannot speak for a different platform. KNOWN GAP, stated rather than papered over: BaseboardModel currently has exactly one variant, so the board scoping is real in the code but has no executing cross-board control — minting a second board purely to test it would be a fabricated entity, and the control lands when a second real board is modeled. The second axis, topology pressure, is derived rather than observed: when a capacity cannot be reached as a dual-rank registered module within the generation's monolithic die-density ceiling, that capacity forces extra ranks or stacking, which is why 64GB DDR4 at older speed grades is disproportionately load-reduced while 32GB is not. A buyer economizing on speed grade at 64GB is therefore not buying the same module more slowly, it is a different topology — which is a reason to sample it, not a reason to refuse it." +data platform_training_implication_note: String = "RELOCATED 2026-08-19 from the deleted gunbc.memory_part_selection, whose candidate/frontier half was a selection island; this evidence half is qualification, so it lives beside the four standings it grades. The grading rule is mechanical and derived: a candidate is graded by whether it must execute a training step THIS platform has been observed to fail, and by how strongly that failure is evidenced. For the ALTRAD8UD-1L2T the step is the DDR4 data-buffer read-phase calibration (MREP), which only load-reduced modules execute, so the grading is independent of vendor, part number, capacity, speed grade, and presence on any approved-vendor list. THE SEVERITY IS A FUNCTION OF EVIDENCE, and an earlier revision of this module got that wrong in a way worth recording: it refused every module executing the failing step, on any board, at any standing including EvidenceUnobserved. Because capacities near a generation's die-density ceiling are disproportionately load-reduced, that blanket rule removed most of the 64GB DDR4 market by construction — the exact blocklist this module's prose claimed to avoid, reached by a different route, and the opposite of letting a buyer shop the whole market. Now: a decode of this platform's own syndrome register refuses; an association carrying byte-identical controller status words, or an inference from sequence position, requires a SAMPLE before committing to a full population and stays on the Pareto frontier; an unobserved standing implicates nothing. gunbc holds MREP at AssociatedByIdenticalSignature, so the honest verdict for another ALTRAD8UD LRDIMM today is buy-one-and-test, not reject. Failures are scoped to the board they were observed on, so an ALTRAD8UD receipt cannot speak for a different platform. KNOWN GAP, stated rather than papered over: BoardAuthorityRef currently has exactly one variant, so the board scoping is real in the code but has no executing cross-board control — minting a second board purely to test it would be a fabricated entity, and the control lands when a second real board is modeled. The second axis, topology pressure, is derived rather than observed: when a capacity cannot be reached as a dual-rank registered module within the generation's monolithic die-density ceiling, that capacity forces extra ranks or stacking, which is why 64GB DDR4 at older speed grades is disproportionately load-reduced while 32GB is not. A buyer economizing on speed grade at 64GB is therefore not buying the same module more slowly, it is a different topology — which is a reason to sample it, not a reason to refuse it." diff --git a/dag/gunbc/host/host_reach_identity_probe.dag b/dag/gunbc/host/host_reach_identity_probe.dag index 0e5ed3f69ec..12e752be796 100644 --- a/dag/gunbc/host/host_reach_identity_probe.dag +++ b/dag/gunbc/host/host_reach_identity_probe.dag @@ -1,6 +1,8 @@ module gunbc.host_reach_identity_probe import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.tools.id { id_user_name_argv } +import extdeps.tools.hostname { hostname_read_argv } import std.algebra { trim } import std.process { ProcessExit, ExitSuccess, exit_failure } import gunbc.typed_argv_exec { @@ -326,11 +328,13 @@ type FleetReachProbeRun { // receipt stays keyed by fleet IDENTITY — addressing is transport, never identity. // Repairs #1 + #2: consult fleet_probe_target_for FIRST. A refused target (unobserved // principal) yields ProbeTargetUnobserved and NEVER touches SSH. A ready target runs two -// typed argv ops against the SAME explicit principal@endpoint — `hostname` for host -// identity and `id -un` for the account actually reached — so ProbeReachable carries both -// dimensions and the receipt carries the requested principal for the identity verdict. +// typed argv ops against the SAME explicit principal@endpoint — `hostname_read_argv` for +// host identity and `id_user_name_argv` for the account actually reached — so ProbeReachable +// carries both dimensions and the receipt carries the requested principal for the identity +// verdict. The argv is spelled by the tool module that owns the tool, not hand-typed here: +// the probe no longer decides how `hostname` or `id` get their flags. fn probe_reachable_with_principal(target: String, hostname_stdout: String) -> ReachProbeOutcome { - match typed_argv_probe_over_ssh(host: target as NonEmptyStr, argv: ["id", "-un"]) { + match typed_argv_probe_over_ssh(host: target as NonEmptyStr, argv: id_user_name_argv()) { TypedArgvExecConverged { result: idr } => if idr.success { ProbeReachable { observed_short_hostname: trim(hostname_stdout), observed_principal: trim(idr.stdout) } @@ -343,7 +347,7 @@ fn probe_reachable_with_principal(target: String, hostname_stdout: String) -> Re fn probe_ready_target_once(host: String, endpoint: String, remote_principal: String) -> ReachProbeOutcome { let target = fleet_probe_ready_ssh_target(t_host: host, endpoint: endpoint, remote_principal: remote_principal) - match typed_argv_probe_over_ssh(host: target as NonEmptyStr, argv: ["hostname"]) { + match typed_argv_probe_over_ssh(host: target as NonEmptyStr, argv: hostname_read_argv()) { TypedArgvExecConverged { result: r } => if r.success { probe_reachable_with_principal(target: target, hostname_stdout: r.stdout) diff --git a/dag/gunbc/host/host_reset_return_run.dag b/dag/gunbc/host/host_reset_return_run.dag index 88f986938fb..e7a35003d65 100644 --- a/dag/gunbc/host/host_reset_return_run.dag +++ b/dag/gunbc/host/host_reset_return_run.dag @@ -79,7 +79,14 @@ import gunbc.host_reset_return { select_reset_observer, subject_for_observer, } -import gunbc.host_reset_subject_roster { AdmittedResetSubject, reset_observer_peer_roster, reset_subject_for, reset_subject_roster } +import gunbc.host_reset_subject_roster { + AdmittedResetSubject, + reset_lookup_subject, + reset_observer_peer_roster, + reset_roster_hosts, + reset_subject_for, + reset_subject_roster, +} import gunbc.machine_intake_oob_boot_handoff { oob_boot_handoff, BootControlIssued, @@ -417,7 +424,7 @@ fn host_reset_return_attempt( { let subject = candidate.binding.host.host as NonEmptyStr let selection = select_reset_observer( - subject_roster: reset_subject_roster(), + subject_roster: reset_roster_hosts(entries: reset_subject_roster()), peer_roster: reset_observer_peer_roster(), subject: subject, observer: observer, @@ -670,7 +677,7 @@ fn host_reset_return_wet() -> ProcessExit RouteEstablished { route: r } => r } let assignment = reset_dispatch_pairings( - subject_roster: reset_subject_roster(), + subject_roster: reset_roster_hosts(entries: reset_subject_roster()), peer_roster: reset_observer_peer_roster(), ) return match subject_for_observer(assignment: assignment, observer: observer) { @@ -680,7 +687,7 @@ fn host_reset_return_wet() -> ProcessExit remedy: "this runner is not an assigned observer, so it has no subject to watch", ) Present { value: subject } => - match reset_subject_for(host: subject) { + match reset_lookup_subject(lookup: reset_subject_for(host: subject)) { Absent => host_reset_return_preflight_exit( refusal: SubjectHasNoRosterRow { subject: subject }, diff --git a/dag/gunbc/host/host_reset_subject_roster.dag b/dag/gunbc/host/host_reset_subject_roster.dag index d4fa3e93647..bfe3791095b 100644 --- a/dag/gunbc/host/host_reset_subject_roster.dag +++ b/dag/gunbc/host/host_reset_subject_roster.dag @@ -16,9 +16,14 @@ import gunbc.managed_host { ManagedHostBound, ManagedHostEndpointMismatch, ManagedHostNotSecured, + ManagedHostMembership, + OrdinaryMember, + MemberUnderDeclaredDrop, managed_host_binding, - managed_hosts, + managed_host_memberships, } +import gunbc.machine_intake_subject { MachineIntakeSubject } +import std.decl_ref { DeclarationRef } import gunbc.host_reset_boot_selection { reset_boot_selection } import gunbc.fleet_runner_connectivity { fleet_hosts } @@ -92,14 +97,53 @@ fn reset_subject_roster_of(hosts: List) -> List { map(admitted_reset_subjects(hosts: hosts), s => s.binding.host.host as NonEmptyStr) } +type ResetRosterEntry + = OrdinaryResetRosterHost { host: NonEmptyStr } + | ResetRosterHostUnderDeclaredDrop { host: NonEmptyStr, drop: DeclarationRef } + +fn reset_roster_hosts(entries: List) -> List { + map(entries, e => match e { + OrdinaryResetRosterHost { host: h } => h + ResetRosterHostUnderDeclaredDrop { host: h, drop: _ } => h + }) +} + +type ResetSubjectLookup + = ResetSubjectNotAdmitted + | ResetSubjectAdmittedOrdinary { subject: AdmittedResetSubject } + | ResetSubjectAdmittedUnderDeclaredDrop { + subject: AdmittedResetSubject + drop: DeclarationRef + standing_subject: MachineIntakeSubject + current_subject: MachineIntakeSubject + } + +fn reset_lookup_subject(lookup: ResetSubjectLookup) -> AdmittedResetSubject? { + match lookup { + ResetSubjectNotAdmitted => none + ResetSubjectAdmittedOrdinary { subject: s } => Present { value: s } + ResetSubjectAdmittedUnderDeclaredDrop { subject: s, drop: _, standing_subject: _, current_subject: _ } => + Present { value: s } + } +} + +fn reset_roster_entries_of(m: ManagedHostMembership) -> List { + match m { + OrdinaryMember { host: h } => + map(reset_subject_roster_of(hosts: [h]), name => OrdinaryResetRosterHost { host: name }) + MemberUnderDeclaredDrop { drop: d, standing_subject: _, current_subject: _, host: h } => + map(reset_subject_roster_of(hosts: [h]), name => ResetRosterHostUnderDeclaredDrop { host: name, drop: d }) + } +} + // THE FILTER IS A PREDICATE, NOT A FAILURE ARM, AND THE DIFFERENCE MATTERS HERE. Dropping a // candidate whose controller does not admit power control is not an absorbing fallback swallowing a // diagnostic -- it is the membership condition doing its job, and the dropped host is not a loss but // a non-member. The refusal that DOES have to be loud is one level up, in // gunbc.host_reset_return reset_dispatch_options, where an unmintable observer pairing refuses the // whole option list rather than quietly shrinking it. -fn reset_subject_roster() -> List { - reset_subject_roster_of(hosts: managed_hosts()) +fn reset_subject_roster() -> List { + fold(managed_host_memberships(), init: [], f: (acc, m) => concat(acc, reset_roster_entries_of(m: m))) } // THE PEERS THAT MAY OBSERVE A RESET are the executor fleet hosts, and this is the one place the two @@ -114,13 +158,35 @@ fn reset_observer_peer_roster() -> List { // selection without re-walking the roster in its own vocabulary. Absent means the label names no // admitted subject, which a caller must treat as a refusal and never as a host it may drive with // defaults. -fn reset_subject_for(host: NonEmptyStr) -> AdmittedResetSubject? { +fn reset_subject_for(host: NonEmptyStr) -> ResetSubjectLookup { fold( - admitted_reset_subjects(hosts: managed_hosts()), - init: none, - f: (acc, s) => match acc { - Present { value: _ } => acc - Absent => if (s.binding.host.host as String) == (host as String) { Present { value: s } } else { acc } + managed_host_memberships(), + init: ResetSubjectNotAdmitted, + f: (acc, m) => match acc { + ResetSubjectAdmittedOrdinary { subject: _ } => acc + ResetSubjectAdmittedUnderDeclaredDrop { subject: _, drop: _, standing_subject: _, current_subject: _ } => acc + ResetSubjectNotAdmitted => + match m { + OrdinaryMember { host: h } => + fold(admitted_reset_subjects(hosts: [h]), init: ResetSubjectNotAdmitted, f: (inner, s) => + if (s.binding.host.host as String) == (host as String) { + ResetSubjectAdmittedOrdinary { subject: s } + } else { + inner + }) + MemberUnderDeclaredDrop { drop: d, standing_subject: standing, current_subject: current, host: h } => + fold(admitted_reset_subjects(hosts: [h]), init: ResetSubjectNotAdmitted, f: (inner, s) => + if (s.binding.host.host as String) == (host as String) { + ResetSubjectAdmittedUnderDeclaredDrop { + subject: s, + drop: d, + standing_subject: standing, + current_subject: current, + } + } else { + inner + }) + } }, ) } diff --git a/dag/gunbc/host/managed_host.dag b/dag/gunbc/host/managed_host.dag index 4743680b280..70be9fad80d 100644 --- a/dag/gunbc/host/managed_host.dag +++ b/dag/gunbc/host/managed_host.dag @@ -1,16 +1,29 @@ module gunbc.managed_host -import std.types { List, NonEmptyStr, String } -import std.decl_ref { decl_ref } +import std.types { Bool, List, NonEmptyStr, String } +import std.decl_ref { DeclarationRef, decl_ref } import product.host_identity { HostIdentity } import extdeps.bmc.endpoint { BmcControllerEndpoint } import extdeps.bmc.access_profile { BmcAccessObservation, BmcAccessObservationStanding, BmcAccessObserved, BmcAccessUnobserved } import extdeps.bmc.capability { BmcFirmwareReleaseCapabilityRow } import gunbc.machine_intake_bmc_secure { BmcSecureRefusalCause, BmcSecureRefused, BmcSecureStanding, BmcSecured } +import gunbc.machine_intake_subject { + IntakeAttemptId, + MachineIntakeSubject, + SubjectCurrent, + SubjectStale, + compare_qualification_subject, +} import gunbc.fleet_host_identity { operator_host_mtcollins1, operator_host_srv1 } +import gunbc.hostname_allocation { HostnameAllocation, gunbc_fleet_hostname_allocations } +import gunbc.rung_drop { RungDrop, Standing, Retired } +import gunbc.rung_drop.standing { standing_rung_drops } import gunbc.machine_intake_mtcollins1_access_observation { mtcollins1_access_standing, mtcollins1_capability_row } -import gunbc.machine_intake_mtcollins1_bmc_secure_observation { mtcollins1_bmc_secure_standing } -import gunbc.machine_intake_ampere_smpro_observation { SmproSiteStanding, SmproPassSubjectStanding, smpro_pass_subject } +import gunbc.machine_intake_mtcollins1_bmc_secure_observation { mtcollins1_bmc_secure_standing, mtcollins1_intake_subject } +import gunbc.machine_intake_mtcollins1_managed_host_admission { mtcollins1_current_intake_subject } +import gunbc.machine_intake_ampere_smpro_observation { + SmproSiteStanding, SmproSiteAdmitted, SmproSiteRefused, SmproPassSubjectStanding, smpro_pass_subject, +} import gunbc.machine_intake_mtcollins1_smpro_observation { mtcollins1_smpro_site } // A HOST THIS FLEET DRIVES THROUGH ITS BASEBOARD CONTROLLER, and the one record a host-generic @@ -83,37 +96,429 @@ fn managed_host_binding(host: ManagedHost) -> ManagedHostBindingStanding { } } -// A UNIT IS A ROW ONLY WHEN ITS SECURE STANDING IS DERIVED, which itself requires its intake subject -// to bind. Absent leaves the unit out rather than in with a stand-in, so a host never enters the -// managed set on a fabricated subject; the roster witness then refuses on the missing member. -fn managed_hosts() -> List { +// ── MANAGED-HOST ADMISSION (cut O1c-3 PR a) ──────────────────────────────────────────────────── +// +// One committed record per unit, authored from that unit's arrival observations. The plan's fields: +// HostIdentity ↔ MachineIntakeSubject; access and capability standing; BmcSecureStanding; unit-hold +// decision; receipt identity. smpro is on the record because cut 1 put it on ManagedHost: every +// Ampere SMpro pass reads through managed_host_smpro_subject, and omitting it here would default a +// site in the projection and fork that join. Currency is NOT receipt.subject == record.subject; +// that would quietly call a superseded standing current. Currency is compare_qualification_subject +// of the record's standing subject against the unit's CURRENT subject (current BMC firmware as last +// observed). A superseded standing is a member only through MemberUnderDeclaredDrop, minted only +// where gunbc.rung_drop.mtcollins1_bmc_secure_standing_predates_reflash is standing and its +// population names the unit. Removing that drop makes the unit absent. + +type ManagedHostAdmissionReceipt sole_constructor { + host: HostIdentity + access_host: NonEmptyStr + secure_attempt: IntakeAttemptId +} + +fn managed_host_admission_receipt(host: HostIdentity, access_host: NonEmptyStr, secure_attempt: IntakeAttemptId) -> ManagedHostAdmissionReceipt + admit_callers: [ + decl_ref(module_path: "gunbc.managed_host", decl_name: "mtcollins1_managed_host_admission"), + decl_ref(module_path: "gunbc.managed_host", decl_name: "managed_host_admission"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_record_whose_subject_is_current_is_an_ordinary_member"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_superseded_subject_with_no_declared_drop_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_missing_hold_decision_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_missing_host_allocation_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_receipt_for_another_host_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_without_the_drop_mtcollins1_is_absent_and_reset_roster_empties"), + ] +{ + ManagedHostAdmissionReceipt { host: host, access_host: access_host, secure_attempt: secure_attempt } +} + +type ManagedHostAdmissionHold + = AdmissionHoldDecided { hold: ResetUnitHold } + | AdmissionHoldMissing + +type ManagedHostAdmission sole_constructor { + host: HostIdentity + subject: MachineIntakeSubject + access: BmcAccessObservationStanding + capability_row: BmcFirmwareReleaseCapabilityRow + secure: BmcSecureStanding + hold: ManagedHostAdmissionHold + smpro: SmproSiteStanding + receipt: ManagedHostAdmissionReceipt + current_subject: MachineIntakeSubject? + covering_drop: DeclarationRef? +} + +fn managed_host_admission( + host: HostIdentity, + subject: MachineIntakeSubject, + access: BmcAccessObservationStanding, + capability_row: BmcFirmwareReleaseCapabilityRow, + secure: BmcSecureStanding, + hold: ManagedHostAdmissionHold, + smpro: SmproSiteStanding, + receipt: ManagedHostAdmissionReceipt, + current_subject: MachineIntakeSubject?, + covering_drop: DeclarationRef?, +) -> ManagedHostAdmission + admit_callers: [ + decl_ref(module_path: "gunbc.managed_host", decl_name: "mtcollins1_managed_host_admission"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_record_whose_subject_is_current_is_an_ordinary_member"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_superseded_subject_with_no_declared_drop_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_missing_hold_decision_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_missing_host_allocation_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_a_receipt_for_another_host_is_absent"), + decl_ref(module_path: "test.claim.managed_host_admission_witness", decl_name: "w_without_the_drop_mtcollins1_is_absent_and_reset_roster_empties"), + ] +{ + ManagedHostAdmission { + host: host, + subject: subject, + access: access, + capability_row: capability_row, + secure: secure, + hold: hold, + smpro: smpro, + receipt: receipt, + current_subject: current_subject, + covering_drop: covering_drop, + } +} + +type ManagedHostMembership sole_constructor + = OrdinaryMember { host: ManagedHost } + | MemberUnderDeclaredDrop { + drop: DeclarationRef + standing_subject: MachineIntakeSubject + current_subject: MachineIntakeSubject + host: ManagedHost + } + +data mtcollins1_bmc_secure_standing_drop_ref: DeclarationRef = decl_ref( + module_path: "gunbc.rung_drop.mtcollins1_bmc_secure_standing_predates_reflash", + decl_name: "mtcollins1_bmc_secure_standing_predates_reflash", +) + +// COVERING IS THE RECORD'S CITED DROP JOINED TO A STANDING ROW BY IDENTITY, NOT HOST EQUALITY +// INSIDE THIS FOLD AND NOT POPULATION PROSE. RungDrop.population is the 4b(3) English bound for +// the DESIGN projection; grepping it would admit any host whose name appears in a sentence. +// Which unit a drop covers is the covering_drop the unit's admission record cites; the generic +// join only checks that cited identity is standing in the drop list. +fn drop_is_standing_citation(drop: RungDrop, cited: DeclarationRef) -> Bool { + match drop.standing { + Retired { trigger_fired: _ } => false + Standing => (drop.identity as String) == (cited.decl_name as String) + } +} + +fn covering_bmc_secure_drop(drops: List, cited: DeclarationRef) -> RungDrop? { + filter(drops, d => drop_is_standing_citation(drop: d, cited: cited)).first() +} + +fn host_is_allocated(allocations: List, host: HostIdentity) -> Bool { + any(allocations, a => (a.fleet_slot as String) == (host as String)) +} + +fn store_host_is_allocated(allocations: List, store_host: NonEmptyStr) -> Bool { + any(allocations, a => (a.fleet_slot as String) == (store_host as String) || (a.canonical_hostname as String) == (store_host as String)) +} + +fn access_controller_host(access: BmcAccessObservationStanding) -> NonEmptyStr? { + match access { + BmcAccessUnobserved { endpoint: _ } => none + BmcAccessObserved { observation: o } => Present { value: o.endpoint.host } + } +} + +fn secure_controller_host(secure: BmcSecureStanding) -> NonEmptyStr? { + match secure.outcome { + BmcSecureRefused { cause: _ } => none + BmcSecured { endpoint: e, reference: _, evidence: _, latest_admitted_probe_at: _, published_credential: _ } => + Present { value: e.host } + } +} + +fn smpro_controller_host(smpro: SmproSiteStanding) -> NonEmptyStr? { + match smpro { + SmproSiteRefused { cause: _ } => none + SmproSiteAdmitted { site: s } => Present { value: s.endpoint.host } + } +} + +fn receipt_matches_record(record: ManagedHostAdmission) -> Bool { + (record.receipt.host as String) == (record.host as String) + && (record.receipt.secure_attempt as NonEmptyStr) == (record.subject.attempt_id as NonEmptyStr) + && match access_controller_host(access: record.access) { + Absent => false + Present { value: h } => (record.receipt.access_host as String) == (h as String) + } +} + +fn standings_bound_to_record_subject(record: ManagedHostAdmission) -> Bool { + (record.secure.subject.subject.unit_key as NonEmptyStr) == (record.subject.subject.unit_key as NonEmptyStr) + && match access_controller_host(access: record.access) { + Absent => false + Present { value: access_host } => + match secure_controller_host(secure: record.secure) { + Absent => false + Present { value: secure_host } => + match smpro_controller_host(smpro: record.smpro) { + Absent => false + Present { value: smpro_host } => + (access_host as String) == (secure_host as String) && (secure_host as String) == (smpro_host as String) + } + } + } +} + +fn decided_hold(record: ManagedHostAdmission, allocations: List) -> ResetUnitHold? { + match record.hold { + AdmissionHoldMissing => none + AdmissionHoldDecided { hold: h } => + match h { + ResetNoUnitHold => Present { value: h } + ResetUnitHoldRequired { key: _, store_host: store } => + if store_host_is_allocated(allocations: allocations, store_host: store) { + Present { value: h } + } else { + none + } + } + } +} + +fn row_from_admission(record: ManagedHostAdmission, hold: ResetUnitHold) -> ManagedHost { + ManagedHost { + host: record.host, + access: record.access, + capability_row: record.capability_row, + secure: record.secure, + unit_hold: hold, + smpro: record.smpro, + } +} + +fn ordinary_member(host: ManagedHost) -> ManagedHostMembership + admit_callers: [ + decl_ref(module_path: "gunbc.managed_host", decl_name: "project_managed_host_admission"), + ] +{ + OrdinaryMember { host: host } +} + +fn member_under_declared_drop( + drop: DeclarationRef, + standing_subject: MachineIntakeSubject, + current_subject: MachineIntakeSubject, + host: ManagedHost, +) -> ManagedHostMembership + admit_callers: [ + decl_ref(module_path: "gunbc.managed_host", decl_name: "project_managed_host_admission"), + ] +{ + MemberUnderDeclaredDrop { + drop: drop, + standing_subject: standing_subject, + current_subject: current_subject, + host: host, + } +} + +// THE PUBLIC ENTRY. Every parameter is consumed: a hard-coded current subject, drop list, or +// allocation list changes the arm. Incomplete records, unbound standings, a missing hold, a missing +// allocation, a mismatched receipt, or an unreadable current subject are Absent, never defaulted. +fn project_managed_host_admission( + record: ManagedHostAdmission, + current_subject: MachineIntakeSubject?, + drops: List, + allocations: List, +) -> ManagedHostMembership? { + if host_is_allocated(allocations: allocations, host: record.host) == false { + none + } else if receipt_matches_record(record: record) == false { + none + } else if standings_bound_to_record_subject(record: record) == false { + none + } else { + match decided_hold(record: record, allocations: allocations) { + Absent => none + Present { value: hold } => + match current_subject { + Absent => none + Present { value: current } => { + let row = row_from_admission(record: record, hold: hold) + match compare_qualification_subject(admitted: record.subject.subject, observed: current.subject) { + SubjectCurrent => Present { value: ordinary_member(host: row) } + SubjectStale { axes: _ } => + match record.covering_drop { + Absent => none + Present { value: cited } => + match covering_bmc_secure_drop(drops: drops, cited: cited) { + Absent => none + Present { value: _ } => + Present { + value: member_under_declared_drop( + drop: cited, + standing_subject: record.subject, + current_subject: current, + host: row, + ), + } + } + } + } + } + } + } + } +} + +fn membership_host(membership: ManagedHostMembership) -> ManagedHost { + match membership { + OrdinaryMember { host: h } => h + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => h + } +} + +fn membership_is_host(membership: ManagedHostMembership, host: HostIdentity) -> Bool { + match membership { + OrdinaryMember { host: h } => (h.host as String) == (host as String) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => (h.host as String) == (host as String) + } +} + +fn mtcollins1_unit_hold() -> ResetUnitHold { + ResetUnitHoldRequired { key: operator_host_mtcollins1 as NonEmptyStr, store_host: operator_host_srv1 as NonEmptyStr } +} + +fn mtcollins1_managed_host_admission() -> ManagedHostAdmission? { match mtcollins1_bmc_secure_standing() { - Absent => [] + Absent => none Present { value: secure } => - [ - ManagedHost { - host: operator_host_mtcollins1, - access: mtcollins1_access_standing, - capability_row: mtcollins1_capability_row, - secure: secure, - unit_hold: ResetUnitHoldRequired { key: operator_host_mtcollins1 as NonEmptyStr, store_host: operator_host_srv1 as NonEmptyStr }, - smpro: mtcollins1_smpro_site(), - }, - ] + match mtcollins1_intake_subject() { + Absent => none + Present { value: subject } => + match access_controller_host(access: mtcollins1_access_standing) { + Absent => none + Present { value: access_host } => + Present { + value: managed_host_admission( + host: operator_host_mtcollins1, + subject: subject, + access: mtcollins1_access_standing, + capability_row: mtcollins1_capability_row, + secure: secure, + hold: AdmissionHoldDecided { hold: mtcollins1_unit_hold() }, + smpro: mtcollins1_smpro_site(), + receipt: managed_host_admission_receipt( + host: operator_host_mtcollins1, + access_host: access_host, + secure_attempt: subject.attempt_id, + ), + current_subject: mtcollins1_current_intake_subject(), + covering_drop: Present { value: mtcollins1_bmc_secure_standing_drop_ref }, + ), + } + } + } + } +} + +fn authored_managed_host_admissions() -> List { + match mtcollins1_managed_host_admission() { + Absent => [] + Present { value: r } => [r] } } +fn current_subject_for_admission(record: ManagedHostAdmission) -> MachineIntakeSubject? { + record.current_subject +} + +fn managed_host_memberships_projected( + records: List, + drops: List, + allocations: List, +) -> List { + fold(records, init: [], f: (acc, record) => + match project_managed_host_admission( + record: record, + current_subject: current_subject_for_admission(record: record), + drops: drops, + allocations: allocations, + ) { + Absent => acc + Present { value: m } => concat(acc, [m]) + }) +} + +fn managed_hosts_projected( + records: List, + drops: List, + allocations: List, +) -> List { + fold( + managed_host_memberships_projected(records: records, drops: drops, allocations: allocations), + init: [], + f: (acc, m) => concat(acc, [membership_host(membership: m)]), + ) +} + +fn managed_host_memberships() -> List { + managed_host_memberships_projected( + records: authored_managed_host_admissions(), + drops: standing_rung_drops(), + allocations: gunbc_fleet_hostname_allocations, + ) +} + +fn managed_membership_for(host: HostIdentity) -> ManagedHostMembership? { + fold( + managed_host_memberships(), + init: none, + f: (acc, m) => match acc { + Present { value: _ } => acc + Absent => if membership_is_host(membership: m, host: host) { Present { value: m } } else { acc } + }, + ) +} + +// A UNIT IS A MEMBER ONLY THROUGH A COMMITTED ManagedHostAdmission, projected here. Membership is +// evidence, never a source roster: the record lives in the unit's observations; this module joins +// completeness, the one-subject bind, allocation, hold, receipt identity, and currency against the +// CURRENT subject (docs/plans/managed-host-untangle.md, cut O1c-3 PR a). mtjade1 has no record. +// managed_hosts() is the host-row view of managed_host_memberships(); callers that read secure +// standing, binding, reset, hold, boot, or MegaRAC admission walk the memberships and match +// MemberUnderDeclaredDrop (DESIGN §4b(3): a declared drop may not reach consumers as silence). +fn managed_hosts() -> List { + fold(managed_host_memberships(), init: [], f: (acc, m) => concat(acc, [membership_host(membership: m)])) +} + // A MANAGED HOST BY LABEL, AS ITS WHOLE BINDING STANDING. A caller that knows only a host's name reaches // the row's join here, and gets the standing -- bound with its observed endpoint and account together, // or the refusal -- never an account on its own. type ManagedHostLookup = ManagedHostFound { standing: ManagedHostBindingStanding } + | ManagedHostFoundUnderDeclaredDrop { + standing: ManagedHostBindingStanding + drop: DeclarationRef + standing_subject: MachineIntakeSubject + current_subject: MachineIntakeSubject + } | ManagedHostNotManaged { host: NonEmptyStr } fn managed_host_binding_for(host: NonEmptyStr) -> ManagedHostLookup { - match filter(managed_hosts(), h => (h.host as String) == (host as String)).first() { + match managed_membership_for(host: host as HostIdentity) { Absent => ManagedHostNotManaged { host: host } - Present { value: h } => ManagedHostFound { standing: managed_host_binding(host: h) } + Present { value: m } => + match m { + OrdinaryMember { host: h } => ManagedHostFound { standing: managed_host_binding(host: h) } + MemberUnderDeclaredDrop { drop: d, standing_subject: standing, current_subject: current, host: h } => + ManagedHostFoundUnderDeclaredDrop { + standing: managed_host_binding(host: h), + drop: d, + standing_subject: standing, + current_subject: current, + } + } } } @@ -121,17 +526,22 @@ fn managed_host_binding_for(host: NonEmptyStr) -> ManagedHostLookup { // drive `endpoint` gets the bound account only when `endpoint` is the controller the binding observed, // so one host's account is never sent to another controller. Absent is every other case: the host is // not managed, does not bind, or is bound to a different controller than the one the caller drives. +fn account_driving_standing(standing: ManagedHostBindingStanding, endpoint: BmcControllerEndpoint) -> NonEmptyStr? { + match standing { + ManagedHostBound { binding: b } => + if (b.observation.endpoint.host as String) == (endpoint.host as String) { Present { value: b.account } } else { none } + ManagedHostAccessUnobserved { endpoint: _ } => none + ManagedHostNotSecured { cause: _ } => none + ManagedHostEndpointMismatch { observed: _, secured: _ } => none + } +} + fn managed_host_account_driving(lookup: ManagedHostLookup, endpoint: BmcControllerEndpoint) -> NonEmptyStr? { match lookup { ManagedHostNotManaged { host: _ } => none - ManagedHostFound { standing: s } => - match s { - ManagedHostBound { binding: b } => - if (b.observation.endpoint.host as String) == (endpoint.host as String) { Present { value: b.account } } else { none } - ManagedHostAccessUnobserved { endpoint: _ } => none - ManagedHostNotSecured { cause: _ } => none - ManagedHostEndpointMismatch { observed: _, secured: _ } => none - } + ManagedHostFound { standing: s } => account_driving_standing(standing: s, endpoint: endpoint) + ManagedHostFoundUnderDeclaredDrop { standing: s, drop: _, standing_subject: _, current_subject: _ } => + account_driving_standing(standing: s, endpoint: endpoint) } } diff --git a/dag/gunbc/host/managed_host_boot.dag b/dag/gunbc/host/managed_host_boot.dag index 88a8a8e75d5..08cc35821d2 100644 --- a/dag/gunbc/host/managed_host_boot.dag +++ b/dag/gunbc/host/managed_host_boot.dag @@ -15,8 +15,10 @@ import gunbc.managed_host { ManagedHostBound, ManagedHostEndpointMismatch, ManagedHostNotSecured, + OrdinaryMember, + MemberUnderDeclaredDrop, managed_host_binding, - managed_hosts, + managed_membership_for, } // THE BOOT'S ONE-SHOT SELECTION IS BOOT POLICY for the pair (host, boot), so it is decided here and @@ -80,7 +82,15 @@ fn boot_subject_admission_in(hosts: List, host: HostIdentity, mediu } fn boot_subject_admission_for(host: HostIdentity, medium: BootMedium, execution_revision: NonEmptyStr) -> BootSubjectAdmission { - boot_subject_admission_in(hosts: managed_hosts(), host: host, medium: medium, execution_revision: execution_revision) + match managed_membership_for(host: host) { + Absent => BootSubjectRefused { host: host, cause: BootSubjectHostNotManaged } + Present { value: m } => + match m { + OrdinaryMember { host: h } => admit_boot_subject(host: h, medium: medium, execution_revision: execution_revision) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => + admit_boot_subject(host: h, medium: medium, execution_revision: execution_revision) + } + } } fn boot_subject_refusal_cause_text(cause: BootSubjectRefusalCause) -> String { diff --git a/dag/gunbc/host/managed_host_boot_handoff.dag b/dag/gunbc/host/managed_host_boot_handoff.dag index e291a140a94..8578b544bbb 100644 --- a/dag/gunbc/host/managed_host_boot_handoff.dag +++ b/dag/gunbc/host/managed_host_boot_handoff.dag @@ -5,7 +5,7 @@ import std.decl_ref { decl_ref } import std.optional { Present, Absent } import std.process { ExitSuccess, ProcessExit, exit_failure } import extdeps.bmc.capability { BmcFirmwareReleaseCapabilityRow } -import gunbc.managed_host { ManagedHost, ManagedHostBound, ManagedHostAccessUnobserved, ManagedHostNotSecured, ManagedHostEndpointMismatch, managed_host_binding, managed_hosts } +import gunbc.managed_host { ManagedHost, ManagedHostBound, ManagedHostAccessUnobserved, ManagedHostNotSecured, ManagedHostEndpointMismatch, OrdinaryMember, MemberUnderDeclaredDrop, managed_host_binding, managed_host_memberships } import gunbc.managed_host_unit_hold { UnitHoldProof, unit_hold_proof_refusal } import gunbc.managed_host_boot { AdmittedBootSubject, boot_subject_host } import gunbc.managed_host_boot_phase_timing { BootStart, BootStartUnobserved, boot_start_of } @@ -206,7 +206,14 @@ fn boot_handoff_admission_probe_of(host: ManagedHost) -> String? { } fn boot_handoff_admission_probe() -> ProcessExit { - let hosts = managed_hosts() + let hosts = fold( + managed_host_memberships(), + init: [], + f: (acc, m) => match m { + OrdinaryMember { host: h } => concat(acc, [h]) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => concat(acc, [h]) + }, + ) let refusals = flat_map(hosts, h => match boot_handoff_admission_probe_of(host: h) { Present { value: why } => [why] Absent => [] }) if count(hosts) == 0 { exit_failure(reason: "boot handoff admission probe: no managed host to probe") diff --git a/dag/gunbc/host/managed_host_unit_hold.dag b/dag/gunbc/host/managed_host_unit_hold.dag index e249d59d7f0..553c8dcc722 100644 --- a/dag/gunbc/host/managed_host_unit_hold.dag +++ b/dag/gunbc/host/managed_host_unit_hold.dag @@ -45,7 +45,7 @@ import gunbc.durable_exclusive_hold_file_store { import gunbc.actions_run_binding { actions_variable_trimmed } import std.decl_ref { decl_ref } import product.host_identity { HostIdentity, host_identity_eq } -import gunbc.managed_host { ManagedHost, ResetNoUnitHold, ResetUnitHoldRequired, managed_hosts } +import gunbc.managed_host { ManagedHost, ResetNoUnitHold, ResetUnitHoldRequired, OrdinaryMember, MemberUnderDeclaredDrop, managed_host_memberships, managed_membership_for } import gunbc.host_maintenance_hold_reason { maintenance_hold_reason_env } import extdeps.tools.hostname import gunbc.hostname_allocation { canonical_hostname_for_fleet_slot } @@ -129,7 +129,15 @@ fn unit_hold_admission_in(hosts: List, host: NonEmptyStr) -> UnitHo } fn unit_hold_admission_for(host: NonEmptyStr) -> UnitHoldHostAdmission { - unit_hold_admission_in(hosts: managed_hosts(), host: host) + match managed_membership_for(host: host as HostIdentity) { + Absent => UnitHoldHostNotManaged { host: host } + Present { value: m } => + match m { + OrdinaryMember { host: h } => unit_hold_admission_in(hosts: [h], host: host) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => + unit_hold_admission_in(hosts: [h], host: host) + } + } } // Absent means the host was admitted; the text is what a refused caller prints. @@ -163,7 +171,16 @@ fn unit_hold_store_hosts_of(hosts: List) -> List { } fn unit_hold_store_hosts() -> List { - unit_hold_store_hosts_of(hosts: managed_hosts()) + unit_hold_store_hosts_of( + hosts: fold( + managed_host_memberships(), + init: [], + f: (acc, m) => match m { + OrdinaryMember { host: h } => concat(acc, [h]) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => concat(acc, [h]) + }, + ), + ) } // ── THE STORE IS A LOCAL DIRECTORY, SO THE PROCESS MUST BE ON THE STORE HOST ─────────────────── @@ -766,6 +783,18 @@ type MaintenanceHoldAdmission | MaintenanceHoldHostRefused { admission: UnitHoldHostAdmission } | MaintenanceHoldNoReasonVariable { host: HostIdentity } +fn admit_maintenance_hold_for(host: NonEmptyStr) -> MaintenanceHoldAdmission { + match managed_membership_for(host: host as HostIdentity) { + Absent => MaintenanceHoldHostRefused { admission: UnitHoldHostNotManaged { host: host } } + Present { value: m } => + match m { + OrdinaryMember { host: h } => admit_maintenance_hold_in(hosts: [h], host: host) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => + admit_maintenance_hold_in(hosts: [h], host: host) + } + } +} + fn admit_maintenance_hold_in(hosts: List, host: NonEmptyStr) -> MaintenanceHoldAdmission { match unit_hold_admission_in(hosts: hosts, host: host) { UnitHoldHostNotManaged { host: h } => MaintenanceHoldHostRefused { admission: UnitHoldHostNotManaged { host: h } } @@ -797,7 +826,7 @@ fn maintenance_hold_take(host: String) -> ProcessExit { if trim(s: host) == "" { return exit_failure(reason: "maintenance hold: no host named; pass --arg host=") } - match admit_maintenance_hold_in(hosts: managed_hosts(), host: trim(s: host) as NonEmptyStr) { + match admit_maintenance_hold_for(host: trim(s: host) as NonEmptyStr) { MaintenanceHoldAdmitted { hold: h } => maintenance_hold_take_admitted(hold: h) MaintenanceHoldHostRefused { admission: a } => exit_failure(reason: maintenance_hold_admission_refusal(admission: MaintenanceHoldHostRefused { admission: a })) MaintenanceHoldNoReasonVariable { host: x } => exit_failure(reason: maintenance_hold_admission_refusal(admission: MaintenanceHoldNoReasonVariable { host: x })) @@ -847,7 +876,7 @@ fn maintenance_hold_release(host: String) -> ProcessExit { if trim(s: host) == "" { return exit_failure(reason: "maintenance hold: no host named; pass --arg host=") } - match admit_maintenance_hold_in(hosts: managed_hosts(), host: trim(s: host) as NonEmptyStr) { + match admit_maintenance_hold_for(host: trim(s: host) as NonEmptyStr) { MaintenanceHoldAdmitted { hold: h } => maintenance_hold_release_admitted(hold: h) MaintenanceHoldHostRefused { admission: a } => exit_failure(reason: maintenance_hold_admission_refusal(admission: MaintenanceHoldHostRefused { admission: a })) MaintenanceHoldNoReasonVariable { host: x } => exit_failure(reason: maintenance_hold_admission_refusal(admission: MaintenanceHoldNoReasonVariable { host: x })) diff --git a/dag/gunbc/host/megarac_managed_host.dag b/dag/gunbc/host/megarac_managed_host.dag index 8f648453ef4..5e2f59fd54c 100644 --- a/dag/gunbc/host/megarac_managed_host.dag +++ b/dag/gunbc/host/megarac_managed_host.dag @@ -10,12 +10,13 @@ import extdeps.bmc.capability { BmcFirmwareReleaseIdentity, bmc_firmware_release import extdeps.bmc.access_profile { BmcObservedIdentity } import gunbc.managed_host { ManagedHost, ManagedHostBinding, ManagedHostBindingStanding, ManagedHostBound, ManagedHostAccessUnobserved, - ManagedHostNotSecured, ManagedHostEndpointMismatch, managed_host_binding, managed_hosts, + ManagedHostNotSecured, ManagedHostEndpointMismatch, OrdinaryMember, MemberUnderDeclaredDrop, managed_host_binding, + managed_membership_for, } import gunbc.megarac_operation_standing { MegaRacOperation, MegaRacVirtualMediaAttach, MegaRacKvmCanvasStill, MegaRacSdrSocketNaming, MegaRacOperationEvidence, MegaRacCurrentBuild, CurrentBuildRead, CurrentBuildKnownUnread, CurrentBuildUnobserved, - megarac_operation_grounded_at, megarac_operation_executed_at, megarac_current_build, + megarac_operation_executed_at, megarac_current_build, } import gunbc.machine_intake_megarac_operation_evidence { mtcollins1_megarac_operation_evidence } @@ -29,7 +30,7 @@ import gunbc.machine_intake_megarac_operation_evidence { mtcollins1_megarac_oper // AN OPERATION IS ADMITTED ONLY WHERE IT IS GROUNDED. gunbc.megarac_operation_standing grounds an // operation at an observed controller identity -- baseboard and firmware together -- from committed // evidence (gunbc.machine_intake_megarac_operation_evidence): a receipt joined to a reading of that -// build, or a route recovered from what that build served. A MegaRAC appliance of another product line +// build. A MegaRAC appliance of another product line // reporting the same raw version is refused, and so is Mt. Jade's MegaRAC at 2.11, which none of these // operations was ever run on. // @@ -83,26 +84,16 @@ fn megarac_binding_step(host: ManagedHost) -> MegaRacBindingStep { } } -// WHAT GROUNDING AN ADMISSION REQUIRES: the frozen production path accepts a route recovered from a served -// artifact; the current-build gate accepts only an executed operation. -type MegaRacGroundingRequirement - = GroundedByExecutionOrServedRecovery - | GroundedByExecutionOnly - // THE ONE MINT OF AdmittedMegaRacHost, and it decides from the sealed evidence itself, never from a // supplied verdict. It is admitted only to the two admissions below, each of which runs the binding and // family step (and, for the gate, the current-build step) before reaching it. -fn megarac_grounded_admission(evidence: MegaRacOperationEvidence, binding: ManagedHostBinding, operation: MegaRacOperation, identity: BmcObservedIdentity, requirement: MegaRacGroundingRequirement) -> MegaRacHostAdmission +fn megarac_grounded_admission(evidence: MegaRacOperationEvidence, binding: ManagedHostBinding, operation: MegaRacOperation, identity: BmcObservedIdentity) -> MegaRacHostAdmission admit_callers: [ decl_ref(module_path: "gunbc.megarac_managed_host", decl_name: "admit_megarac_host_observed_in"), decl_ref(module_path: "gunbc.megarac_managed_host", decl_name: "admit_megarac_host_at_current_build"), ] { - let grounded = match requirement { - GroundedByExecutionOrServedRecovery => megarac_operation_grounded_at(evidence: evidence, operation: operation, identity: identity) - GroundedByExecutionOnly => megarac_operation_executed_at(evidence: evidence, operation: operation, identity: identity) - } - if grounded { + if megarac_operation_executed_at(evidence: evidence, operation: operation, identity: identity) { MegaRacHostAdmitted { host: AdmittedMegaRacHost { binding: binding, operation: operation, firmware: identity.firmware } } } else { MegaRacHostRefused { cause: MegaRacOperationNotGrounded { operation: operation, identity: identity } } @@ -117,7 +108,7 @@ fn admit_megarac_host(host: ManagedHost, operation: MegaRacOperation) -> MegaRac fn admit_megarac_host_observed_in(evidence: MegaRacOperationEvidence, host: ManagedHost, operation: MegaRacOperation) -> MegaRacHostAdmission { match megarac_binding_step(host: host) { MegaRacBindingRefused { cause: c } => MegaRacHostRefused { cause: c } - MegaRacBindingAdmissible { binding: b } => megarac_grounded_admission(evidence: evidence, binding: b, operation: operation, identity: b.observation.identity, requirement: GroundedByExecutionOrServedRecovery) + MegaRacBindingAdmissible { binding: b } => megarac_grounded_admission(evidence: evidence, binding: b, operation: operation, identity: b.observation.identity) } } @@ -138,7 +129,7 @@ fn admit_megarac_host_at_current_build(evidence: MegaRacOperationEvidence, host: MegaRacHostRefused { cause: MegaRacObservationNotCurrent { observed: observed, current: t.to } } CurrentBuildRead { reading: r } => if bmc_firmware_release_identity_eq(a: r.identity.firmware, b: observed) { - megarac_grounded_admission(evidence: evidence, binding: b, operation: operation, identity: r.identity, requirement: GroundedByExecutionOnly) + megarac_grounded_admission(evidence: evidence, binding: b, operation: operation, identity: r.identity) } else { MegaRacHostRefused { cause: MegaRacObservationNotCurrent { observed: observed, current: r.identity.firmware } } } @@ -157,7 +148,15 @@ fn megarac_host_admission_in(hosts: List, host: NonEmptyStr, operat } fn megarac_host_admission_for(host: NonEmptyStr, operation: MegaRacOperation) -> MegaRacHostAdmission { - megarac_host_admission_in(hosts: managed_hosts(), host: host, operation: operation) + match managed_membership_for(host: host as HostIdentity) { + Absent => MegaRacHostRefused { cause: MegaRacHostNotManaged { host: host } } + Present { value: m } => + match m { + OrdinaryMember { host: h } => admit_megarac_host(host: h, operation: operation) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => + admit_megarac_host(host: h, operation: operation) + } + } } fn megarac_operation_text(operation: MegaRacOperation) -> String { diff --git a/dag/gunbc/host/megarac_operation_standing.dag b/dag/gunbc/host/megarac_operation_standing.dag index 6c6504ba3fc..171bca1c089 100644 --- a/dag/gunbc/host/megarac_operation_standing.dag +++ b/dag/gunbc/host/megarac_operation_standing.dag @@ -1,5 +1,7 @@ module gunbc.megarac_operation_standing +import extdeps.board_authority { board_identity_authority, board_authority_ref_eq } +import gunbc.board_authority_binding { observed_board_authority } import std.types { Bool, EpochMs, List, NonEmptyStr, String } import std.content_hash { ContentHash } import std.decl_ref { DeclarationRef, decl_ref } @@ -37,8 +39,21 @@ fn megarac_operation_eq(a: MegaRacOperation, b: MegaRacOperation) -> Bool { } } +// IDENTITY-GRADE use of the board: both readings must reach a bound authority (either standing) and +// the SAME one. An unread, unbound or ambiguous board equals nothing, itself included. +fn megarac_board_identity_eq(a: BmcObservedIdentity, b: BmcObservedIdentity) -> Bool { + match board_identity_authority(lookup: observed_board_authority(observation: a.board)) { + Present { value: x } => + match board_identity_authority(lookup: observed_board_authority(observation: b.board)) { + Present { value: y } => board_authority_ref_eq(a: x, b: y) + Absent => false + } + Absent => false + } +} + fn megarac_identity_eq(a: BmcObservedIdentity, b: BmcObservedIdentity) -> Bool { - a.baseboard == b.baseboard && bmc_firmware_release_identity_eq(a: a.firmware, b: b.firmware) + megarac_board_identity_eq(a: a, b: b) && bmc_firmware_release_identity_eq(a: a.firmware, b: b.firmware) } // ── THE CARRIERS. Each is sole_constructor and minted only by a function sealed to the evidence rows @@ -74,28 +89,6 @@ type MegaRacFirmwareTransition sole_constructor { record: DeclarationRef } -// AN OPERATION'S ROUTE RECOVERED FROM AN ARTIFACT THE CONTROLLER SERVES, NOT EXECUTED. The KVM canvas -// still's route (extdeps.bmc.megarac MegaRacScreenCaptureRoute) was read from the web UI bundle the -// controller serves to any browser at a given build. That is an OBSERVATION OF WHAT THE DEVICE SERVES -- -// the same standing as gunbc.machine_intake_megarac_served_ui_catalog_observation and -// gunbc.machine_intake_megarac_ui_features -- not a disassembly of firmware and not a proprietary-notice -// document, and it is distinct from O1a's cited-source arm, which cites an upstream-published source. -// WHAT IT DOES NOT ESTABLISH: that the operation was ever executed. It grounds the route at the identity -// that served it, and nothing that requires an executed standing admits on it -// (megarac_operation_executed_at, and the current-build gate, refuse this arm). -type ServedArtifactCapture - = ServedArtifactCommitted { evidence: EvidenceRef } - | ServedArtifactDigestOnly { digest: ContentHash } - -type MegaRacOperationServedArtifactRecovery sole_constructor { - operation: MegaRacOperation - identity: BmcObservedIdentity - served_by: BmcControllerEndpoint - artifact: NonEmptyStr - capture: ServedArtifactCapture - read_at: EpochMs -} - // THE WHOLE EVIDENCE POPULATION, SEALED. An operation's grounding and a controller's current build are // conclusions over the COMPLETE population: drop a recorded transition from otherwise genuine rows and a // superseded build reads as current. So the population is sole_constructor too, and its one mint is @@ -105,14 +98,12 @@ type MegaRacOperationEvidence sole_constructor { readings: List receipts: List transitions: List - recoveries: List } fn megarac_operation_evidence_of( readings: List, receipts: List, transitions: List, - recoveries: List, ) -> MegaRacOperationEvidence admit_callers: [ decl_ref(module_path: "gunbc.machine_intake_megarac_operation_evidence", decl_name: "mtcollins1_megarac_operation_evidence"), @@ -122,7 +113,7 @@ fn megarac_operation_evidence_of( decl_ref(module_path: "test.claim.megarac_managed_host_witness", decl_name: "w_a_current_read_build_with_executed_evidence_is_admitted_by_the_shadow_gate"), ] { - MegaRacOperationEvidence { readings: readings, receipts: receipts, transitions: transitions, recoveries: recoveries } + MegaRacOperationEvidence { readings: readings, receipts: receipts, transitions: transitions } } fn megarac_build_reading(endpoint: BmcControllerEndpoint, identity: BmcObservedIdentity, evidence: EvidenceRef, observed_at: EpochMs) -> MegaRacBuildReading @@ -158,14 +149,6 @@ fn megarac_firmware_transition(endpoint: BmcControllerEndpoint, from: BmcFirmwar MegaRacFirmwareTransition { endpoint: endpoint, from: from, to: to, not_before: not_before, not_after: not_after, record: record } } -fn megarac_operation_served_artifact_recovery(operation: MegaRacOperation, identity: BmcObservedIdentity, served_by: BmcControllerEndpoint, artifact: NonEmptyStr, capture: ServedArtifactCapture, read_at: EpochMs) -> MegaRacOperationServedArtifactRecovery - admit_callers: [ - decl_ref(module_path: "gunbc.machine_intake_megarac_operation_evidence", decl_name: "mtcollins1_megarac_operation_evidence"), - ] -{ - MegaRacOperationServedArtifactRecovery { operation: operation, identity: identity, served_by: served_by, artifact: artifact, capture: capture, read_at: read_at } -} - // ── THE JOIN ───────────────────────────────────────────────────────────────────────────────────── fn same_controller(a: BmcControllerEndpoint, b: BmcControllerEndpoint) -> Bool { @@ -200,7 +183,6 @@ fn receipt_joins_reading(evidence: MegaRacOperationEvidence, receipt: MegaRacOpe type MegaRacOperationGrounding = OperationExecutedAtBuild { receipt: MegaRacOperationReceipt, build: MegaRacBuildReading } - | OperationRecoveredFromServedArtifact { recovery: MegaRacOperationServedArtifactRecovery } // ONE OPERATION GROUNDED AT ONE OBSERVED CONTROLLER IDENTITY. sole_constructor and minted only by the // join below, so a grounding cannot be authored beside the evidence that would have to produce it. @@ -211,37 +193,22 @@ type MegaRacOperationStanding sole_constructor { } fn megarac_operation_standings(evidence: MegaRacOperationEvidence) -> List { - concat( - flat_map(evidence.receipts, rc => - flat_map(filter(evidence.readings, rd => receipt_joins_reading(evidence: evidence, receipt: rc, reading: rd)), rd => - [MegaRacOperationStanding { operation: rc.operation, identity: rd.identity, grounding: OperationExecutedAtBuild { receipt: rc, build: rd } }])), - map(evidence.recoveries, rv => - MegaRacOperationStanding { operation: rv.operation, identity: rv.identity, grounding: OperationRecoveredFromServedArtifact { recovery: rv } }), - ) -} - -fn grounding_is_executed(g: MegaRacOperationGrounding) -> Bool { - match g { - OperationExecutedAtBuild { receipt: _, build: _ } => true - OperationRecoveredFromServedArtifact { recovery: _ } => false - } -} - -fn standing_grounds(standing: MegaRacOperationStanding, operation: MegaRacOperation, identity: BmcObservedIdentity, executed_only: Bool) -> Bool { - megarac_operation_eq(a: standing.operation, b: operation) && megarac_identity_eq(a: standing.identity, b: identity) && (!executed_only || grounding_is_executed(g: standing.grounding)) + flat_map(evidence.receipts, rc => + flat_map(filter(evidence.readings, rd => receipt_joins_reading(evidence: evidence, receipt: rc, reading: rd)), rd => + [MegaRacOperationStanding { operation: rc.operation, identity: rd.identity, grounding: OperationExecutedAtBuild { receipt: rc, build: rd } }])) } -fn standings_ground(standings: List, operation: MegaRacOperation, identity: BmcObservedIdentity, executed_only: Bool) -> Bool { - count(filter(standings, st => standing_grounds(standing: st, operation: operation, identity: identity, executed_only: executed_only))) > 0 +fn standing_grounds(standing: MegaRacOperationStanding, operation: MegaRacOperation, identity: BmcObservedIdentity) -> Bool { + megarac_operation_eq(a: standing.operation, b: operation) && megarac_identity_eq(a: standing.identity, b: identity) } -fn megarac_operation_grounded_at(evidence: MegaRacOperationEvidence, operation: MegaRacOperation, identity: BmcObservedIdentity) -> Bool { - standings_ground(standings: megarac_operation_standings(evidence: evidence), operation: operation, identity: identity, executed_only: false) +fn standings_ground(standings: List, operation: MegaRacOperation, identity: BmcObservedIdentity) -> Bool { + count(filter(standings, st => standing_grounds(standing: st, operation: operation, identity: identity))) > 0 } -// EXECUTED AT THIS IDENTITY, and only executed: a route recovered from a served artifact does not count. +// EXECUTED AT THIS IDENTITY. fn megarac_operation_executed_at(evidence: MegaRacOperationEvidence, operation: MegaRacOperation, identity: BmcObservedIdentity) -> Bool { - standings_ground(standings: megarac_operation_standings(evidence: evidence), operation: operation, identity: identity, executed_only: true) + standings_ground(standings: megarac_operation_standings(evidence: evidence), operation: operation, identity: identity) } // ── THE CURRENT BUILD OF A CONTROLLER ──────────────────────────────────────────────────────────── diff --git a/dag/gunbc/host/memory_configuration.dag b/dag/gunbc/host/memory_configuration.dag index 5b8ffd58cee..d336e9baaad 100644 --- a/dag/gunbc/host/memory_configuration.dag +++ b/dag/gunbc/host/memory_configuration.dag @@ -1,10 +1,10 @@ module gunbc.memory_configuration +import extdeps.board_authority { BoardAuthorityRef } import std.types { List, NonEmptyStr } import std.measure { ByteSize } import std.nat { Nat } import std.decl_ref { DeclarationRef } -import extdeps.boards.types { BaseboardModel } import extdeps.memory.types { DramModuleCatalogRow } import extdeps.firmware.tianocore_edk2.sdram_spd_ddr4 { SPD4_PRIMARY_SDRAM_PACKAGE_TYPE_STRUCT } import product.host_identity { HostIdentity } @@ -82,7 +82,7 @@ type MemoryFirmwareSetting { type MemoryPlatform { host: HostIdentity - board: BaseboardModel + board: BoardAuthorityRef board_part: DeclarationRef processor: DeclarationRef firmware: NonEmptyStr diff --git a/dag/gunbc/host/memory_configuration_evaluation.dag b/dag/gunbc/host/memory_configuration_evaluation.dag index f7d377411ac..0b09a7571b7 100644 --- a/dag/gunbc/host/memory_configuration_evaluation.dag +++ b/dag/gunbc/host/memory_configuration_evaluation.dag @@ -1,11 +1,11 @@ module gunbc.memory_configuration_evaluation +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import std.types { Bool, List, NonEmptyStr } import std.nat { Nat } import std.integer { UInt8 } import std.algebra { FreeSemigroup } import std.decl_ref { DeclarationRef, decl_ref } -import extdeps.boards.types { AmpereMtCollins } import extdeps.ampere.mt_collins_product_brief.memory_population { MtCollinsPopulationRoleRefusal, MtCollinsChannelPopulationRoleStanding, PopulationRolesResolved, PopulationRolesRefused, mt_collins_channel_population_roles, mt_collins_socket_dimm_sizes_supported, @@ -211,7 +211,7 @@ fn memory_firmware_scope_facts(configuration: MemoryConfiguration, read: MemoryM } fn memory_placement_facts(configuration: MemoryConfiguration) -> MemoryCheckFacts { - if configuration.platform.board != AmpereMtCollins || count(configuration.population) == 0 { + if configuration.platform.board != mt_collins_board_authority || count(configuration.population) == 0 { memory_unknown(fact: MemoryModelMissing { obligation: MemoryPlacement }) } else { memory_placement_from_roles(configuration: configuration, standing: mt_collins_channel_population_roles) @@ -253,7 +253,7 @@ fn memory_catalog_binding_facts(configuration: MemoryConfiguration) -> MemoryChe } fn memory_socket_capacity_facts(configuration: MemoryConfiguration) -> MemoryCheckFacts { - if configuration.platform.board != AmpereMtCollins || count(configuration.population) == 0 { + if configuration.platform.board != mt_collins_board_authority || count(configuration.population) == 0 { memory_unknown(fact: MemoryModelMissing { obligation: MemorySocketCapacity }) } else { let authority = decl_ref(module_path: "extdeps.ampere.mt_collins_product_brief.memory_population", decl_name: "mt_collins_socket_dimm_sizes_supported") diff --git a/dag/gunbc/host/mtcollins_memory_change.dag b/dag/gunbc/host/mtcollins_memory_change.dag index b5055f734f1..d3eef9cbd7c 100644 --- a/dag/gunbc/host/mtcollins_memory_change.dag +++ b/dag/gunbc/host/mtcollins_memory_change.dag @@ -1,12 +1,12 @@ module gunbc.mtcollins_memory_change +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import std.types { Bool, String, NonEmptyStr, List } import std.integer { UInt8 } import extdeps.numeric.base16 { byte_hex_lower } import std.process { ProcessExit, ExitSuccess, exit_failure } import std.decl_ref { DeclarationRef, decl_ref, decl_field_ref } import std.goal_assessment { ObservationAttempt, ObservationEstablished, ObservationRefused } -import extdeps.boards.types { AmpereMtCollins } import extdeps.ampere.mt_collins_product_brief.memory_population { MtCollinsChannelPopulationRole, MtCollinsPopulationRoleRefusal, PopulationRolesResolved, PopulationRolesRefused, mt_collins_channel_population_roles, @@ -74,8 +74,8 @@ fn mtcollins_memory_module(position: MemoryPosition, ads: Bool) -> PositionedMem // Board identity cites the existing intake observation, not a fabricated vendor catalog. fn mtcollins_witnessed_configuration(roles: List) -> MemoryConfiguration { MemoryConfiguration { - platform: MemoryPlatform { host: "mtcollins1" as HostIdentity, board: AmpereMtCollins, - board_part: decl_field_ref(module_path: "gunbc.machine_intake_mtcollins1_access_observation", decl_name: "mtcollins1_identity", field_name: "baseboard"), + platform: MemoryPlatform { host: "mtcollins1" as HostIdentity, board: mt_collins_board_authority, + board_part: decl_field_ref(module_path: "gunbc.machine_intake_mtcollins1_access_observation", decl_name: "mtcollins1_identity", field_name: "board"), processor: decl_ref(module_path: "extdeps.cpu.ampere", decl_name: "altra_q8030_catalog"), firmware: "210525", settings: [MemoryFirmwareSetting { name: "ecc_en", value: "00000002" }], }, diff --git a/dag/gunbc/host_convergence_census.dag b/dag/gunbc/host_convergence_census.dag index fe27735eb1a..8b6b122ca71 100644 --- a/dag/gunbc/host_convergence_census.dag +++ b/dag/gunbc/host_convergence_census.dag @@ -848,6 +848,15 @@ data host_convergence_census_rows: List = [ disposition: RetainAuthority, first_consumer: cite(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "converge_arrival_prefix"), ), + census_row( + identity: cite(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure"), + subject: MachineIntakeArrival, + phases: protocol_phases(false, true, true, true, false, true, false), + current_authority: cite(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure"), + persistent_host_effect: ReachesPersistentHostMutation, + disposition: RetainAuthority, + first_consumer: cite(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "converge_arrival_through_bmc_secure"), + ), ] data host_convergence_census_standing: HostConvergenceCensus = admit_host_convergence_census(rows: host_convergence_census_rows) diff --git a/dag/gunbc/hostname_allocation.dag b/dag/gunbc/hostname_allocation.dag index 8b0155845b9..923ecb22800 100644 --- a/dag/gunbc/hostname_allocation.dag +++ b/dag/gunbc/hostname_allocation.dag @@ -12,7 +12,7 @@ import std.allocation_roster { import std.disposition { Disposition, Terminal } import product.host_identity { HostIdentity } import extdeps.dns.domain_name { DnsLabel } -import gunbc.fleet_host_identity { operator_host_srv1, operator_host_srv2, operator_host_srv3, operator_host_srv4, operator_host_mtcollins1 } +import gunbc.fleet_host_identity { operator_host_srv1, operator_host_srv2, operator_host_srv3, operator_host_srv4, operator_host_mtcollins1, operator_host_srv13, operator_host_srv14, operator_host_srv15, operator_host_srv16 } type HostnameAllocation { fleet_slot: HostIdentity @@ -101,6 +101,22 @@ data gunbc_fleet_hostname_allocations: List = [ fleet_slot: operator_host_mtcollins1, canonical_hostname: "mtcollins1" as DnsLabel, }, + HostnameAllocation { + fleet_slot: operator_host_srv13, + canonical_hostname: "srv13" as DnsLabel, + }, + HostnameAllocation { + fleet_slot: operator_host_srv14, + canonical_hostname: "srv14" as DnsLabel, + }, + HostnameAllocation { + fleet_slot: operator_host_srv15, + canonical_hostname: "srv15" as DnsLabel, + }, + HostnameAllocation { + fleet_slot: operator_host_srv16, + canonical_hostname: "srv16" as DnsLabel, + }, ] fn canonical_hostname_for_fleet_slot(fleet_slot: HostIdentity) -> DnsLabel? { diff --git a/dag/gunbc/instrument_targets.dag b/dag/gunbc/instrument_targets.dag index 9eac68a8296..93f363d66c4 100644 --- a/dag/gunbc/instrument_targets.dag +++ b/dag/gunbc/instrument_targets.dag @@ -50,7 +50,7 @@ import gunbc.target_binding { DifferentialReadingExecutionRefused, DifferentialObservationAssemblyRefused, BareReferenceChannelOutcomeProducer, SelfHostBehavioralEquivalenceProducer, - V2NativeCensusProducer, ResolveRefusalReading, TypeDeclarationUseReading, + V2NativeCensusProducer, RefusalCensusReading, TypeDeclarationUseReading, DependencyDemandCensusProducer, GenericIdentityCensusProducer, InterpolationHoleCensusProducer, @@ -259,14 +259,19 @@ fn v2_native_frontier_label() -> Label { Label { package: instruments_package() target: TargetName { name: "v2-native-frontier" } } } -// THE NATIVE CENSUS INSTRUMENT: the emitted compiler's `census-resolve` verb over the self-host -// corpus, every front-end and resolve refusal GROUPED BY THE FATAL REASON -// (v2.compiler.compile native_census_cause_groups_add) with the head kept as a member field. An -// import cascade is collapsed to its root (census C): each cascaded module is one member under the -// root's reason, and the roots are ranked by closure fan-out (native_census_roots_ranked). It is -// THE route to that census: the binary's `census` / `census-resolve` verbs remain only as this row's -// realization and are not a second documented peer route, which is the --self-host precedent this -// file states for `//gunbc/instruments:self-host` (DESIGN section 3, Building & checks). +// THE NATIVE CENSUS INSTRUMENT: the emitted compiler's `census-infer` verb over the self-host +// corpus (census part 2, docs/plans/census-part-2-inference-design.md section 6: the same label, +// reaching one more stage). Every front-end, resolve and infer refusal is GROUPED BY THE FATAL REASON +// (v2.compiler.compile native_census_cause_groups_add), with the stage and head kept as member +// fields. An import cascade is collapsed to its root (census C): each cascaded module is one member +// under the root's reason. At infer the only cascade is a refused closure provider leading an +// importer's chain (native_census_infer_cascade). The roots are ranked by closure fan-out +// (native_census_roots_ranked), and each is marked against the native test target's closure +// (native_census_subject_closure). The receipt's stderr names the host memory budget's source. It is +// THE route to that census: the binary's `census` / `census-resolve` / `census-infer` verbs remain +// only as this row's realization and are not a second documented peer route, which is the +// --self-host precedent this file states for `//gunbc/instruments:self-host` (DESIGN section 3, +// Building & checks). // IT IS A REPORT, AND ITS VERDICT SAYS ONLY THAT: a census that completed is held, and a run that // printed no complete terminal is the subject unreached. It has NO did-not-hold arm on purpose. A // partition check (members summed over groups == rows reported) was tried and withdrawn in review @@ -643,7 +648,7 @@ fn type_declaration_use_census_binding() -> TargetBinding { } fn v2_native_census_binding() -> TargetBinding { - TargetBinding { target: v2_native_census_label() producer: V2NativeCensusProducer { reading: ResolveRefusalReading } } + TargetBinding { target: v2_native_census_label() producer: V2NativeCensusProducer { reading: RefusalCensusReading } } } fn v2_native_frontier_binding() -> TargetBinding { diff --git a/dag/gunbc/instruments/dag_compile_clean_shard_totality.dag b/dag/gunbc/instruments/dag_compile_clean_shard_totality.dag index 8028c3f9324..acf4647aef6 100644 --- a/dag/gunbc/instruments/dag_compile_clean_shard_totality.dag +++ b/dag/gunbc/instruments/dag_compile_clean_shard_totality.dag @@ -97,7 +97,11 @@ fn witness_totality_reds_on_synthetic_orphan() -> Bool { ) match verdict { PartitionOrphan { unclaimed_paths: paths } => - list_length(items: paths) == 1 && paths.first() == "dag/std/orphan_not_in_roster.dag" + list_length(items: paths) == 1 + && match paths.first() { + Present { value: p } => p == "dag/std/orphan_not_in_roster.dag" + Absent => false + } PartitionTotal { module_count: _ } => false PartitionDuplicateClaim { duplicated_paths: _ } => false PartitionSurplusClaim { surplus_paths: _ } => false @@ -114,7 +118,11 @@ fn witness_totality_reds_on_synthetic_double_claim() -> Bool { ) match verdict { PartitionDuplicateClaim { duplicated_paths: paths } => - list_length(items: paths) == 1 && paths.first() == "dag/std/logic.dag" + list_length(items: paths) == 1 + && match paths.first() { + Present { value: p } => p == "dag/std/logic.dag" + Absent => false + } PartitionTotal { module_count: _ } => false PartitionOrphan { unclaimed_paths: _ } => false PartitionSurplusClaim { surplus_paths: _ } => false @@ -132,7 +140,11 @@ fn witness_totality_reds_on_synthetic_surplus() -> Bool { ) match verdict { PartitionSurplusClaim { surplus_paths: paths } => - list_length(items: paths) == 1 && paths.first() == "dag/std/phantom_not_in_glob.dag" + list_length(items: paths) == 1 + && match paths.first() { + Present { value: p } => p == "dag/std/phantom_not_in_glob.dag" + Absent => false + } PartitionTotal { module_count: _ } => false PartitionOrphan { unclaimed_paths: _ } => false PartitionDuplicateClaim { duplicated_paths: _ } => false diff --git a/dag/gunbc/instruments/dag_emit_real_grammar_round_trips.dag b/dag/gunbc/instruments/dag_emit_real_grammar_round_trips.dag index f6dfe9fcf3b..906d7c5eeda 100644 --- a/dag/gunbc/instruments/dag_emit_real_grammar_round_trips.dag +++ b/dag/gunbc/instruments/dag_emit_real_grammar_round_trips.dag @@ -4,10 +4,10 @@ import std.types { String, List, Bool } import v2.std.algebra { filter } import std.compiler_entry { CompilerEntryDriver, NativeClaimDriver, NativeClaimReport, NativeClaimTerminal, NativeClaimHeld, NativeClaimNotHeld } import v2.compiler.parse { PreparedGrammar, parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize, tokenize_prepared } import v2.extdeps.languages.dag { dag_emit_parse_tree, dag_lex } -import v2.std.diagnostic { Accepted, Diagnostics, NonEmptyDiagnostics, Outcome, Rejected, bind_outcome } +import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, Outcome, Rejected, bind_outcome, Diagnostics } import v2.std.fn_index { non_empty_diagnostics_list } import v2.std.node { Node, content_hash, symbol_lexeme } @@ -38,11 +38,11 @@ type DagEmitRoundTripRow { } // The grammar is prepared once per round trip and both ingests read it. -fn dag_round_trip_parse(prepared: PreparedGrammar, residue: Diagnostics, text: String) -> Outcome { +fn dag_round_trip_parse(prepared: PreparedGrammar, text: String) -> Outcome { bind_outcome( - o: tokenize(text: text, file: ^dag_emit_round_trip_source, rules: dag_lex()), + o: tokenize_prepared(text: text, file: ^dag_emit_round_trip_source, prepared: dag_prepared_lex()), f: fn(stream) { - match parse_module_prepared(tokens: stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } @@ -59,14 +59,14 @@ fn dag_round_trip_refused(stage: String, d: NonEmptyDiagnostics) -> String { fn dag_round_trip_observed(text: String) -> String { match dag_prepared_grammar() { Rejected { diagnostics: d } => dag_round_trip_refused(stage: "grammar preparation", d: d) - Accepted { value: prepared, diagnostics: residue } => - match dag_round_trip_parse(prepared: prepared, residue: residue, text: text) { + Accepted { value: prepared, diagnostics: _ } => + match dag_round_trip_parse(prepared: prepared, text: text) { Rejected { diagnostics: d } => dag_round_trip_refused(stage: "first ingest", d: d) Accepted { value: tree, diagnostics: _ } => match dag_emit_parse_tree(tree: tree) { Rejected { diagnostics: d } => dag_round_trip_refused(stage: "emit", d: d) Accepted { value: emitted, diagnostics: _ } => - match dag_round_trip_parse(prepared: prepared, residue: residue, text: emitted) { + match dag_round_trip_parse(prepared: prepared, text: emitted) { Rejected { diagnostics: d } => dag_round_trip_refused(stage: "re-ingest", d: d) Accepted { value: again, diagnostics: _ } => if content_hash(n: tree) == content_hash(n: again) { "same" } else { concat("trees differ: ", emitted) } diff --git a/dag/gunbc/instruments/emit_host_fixtures.dag b/dag/gunbc/instruments/emit_host_fixtures.dag index 452bded96da..baa0e926936 100644 --- a/dag/gunbc/instruments/emit_host_fixtures.dag +++ b/dag/gunbc/instruments/emit_host_fixtures.dag @@ -1,6 +1,7 @@ module tools.emit_host_fixtures import std.measure { ByteSize, byte_size } +import std.integer { int_to_decimal_string } data rust_stdout_fixture_source: String = "fn main() { let _ = std::io::Write::write_all(&mut std::io::stdout(), &[0u8; 5]); }" @@ -15,7 +16,7 @@ data ts_host_fixture_right_operand: Int = 3 data ts_host_fixture_expected_sum: Int = ts_host_fixture_left_operand + ts_host_fixture_right_operand data ts_host_fixture_harness_suffix: String = " -const __gunbc_r = add({ts_host_fixture_left_operand}, {ts_host_fixture_right_operand}); +const __gunbc_r = add({int_to_decimal_string(value: ts_host_fixture_left_operand)}, {int_to_decimal_string(value: ts_host_fixture_right_operand)}); const __gunbc_b = Buffer.alloc(4); __gunbc_b.writeInt32LE(__gunbc_r, 0); process.stdout.write(__gunbc_b); diff --git a/dag/gunbc/instruments/emit_host_transport.dag b/dag/gunbc/instruments/emit_host_transport.dag index 84747bff561..aa1ae4c0e67 100644 --- a/dag/gunbc/instruments/emit_host_transport.dag +++ b/dag/gunbc/instruments/emit_host_transport.dag @@ -43,10 +43,13 @@ import tools.emit_host_fixtures { ts_signed_i32_le_byte_count } import gunbc.node_http_server_emit { gunbhub_node_http_server_source } +import v2.std.orchestration { EnvSet } import extdeps.posix.process import extdeps.posix.signal import extdeps.http.client import extdeps.tools.sleep +import extdeps.exec.command { env_prefixed_command_in } +import extdeps.tools.node { node_run_file_command, node_program } import extdeps.numeric.base16 import std.content_hash { content_hash_atom } import v2.std.collection { list_at_optional } @@ -271,15 +274,22 @@ fn run_node_http_serve_smoke() -> Bool { Absent => false Present { value: port } => { let server_js = Filesystem.Write(path: concat(dir.path, "/server.js"), content: gunbhub_node_http_server_source()) - let spawn = posix.Process.StartDetached( - program: "env" as NonEmptyStr, - arguments: [ - "-C", dir.path, - "HOST=127.0.0.1", - concat("PORT=", to_string(port)), - "node", - concat(dir.path, "/server.js") + let inner_cmd = node_run_file_command( + node_binary: node_program().invocation, + script_path: concat(dir.path, "/server.js") as NonEmptyStr, + operands: [] + ) + let prefixed_cmd = env_prefixed_command_in( + directory: dir.path, + bindings: [ + EnvSet { name: "HOST", value: "127.0.0.1" }, + EnvSet { name: "PORT", value: to_string(port) } ], + command: inner_cmd + ) + let spawn = posix.Process.StartDetached( + program: prefixed_cmd.program.invocation, + arguments: prefixed_cmd.arguments, stdout_file: concat(dir.path, "/server.stdout.log") as NonEmptyStr, stderr_file: concat(dir.path, "/server.stderr.log") as NonEmptyStr ) diff --git a/dag/gunbc/instruments/fabric_seat_probe.dag b/dag/gunbc/instruments/fabric_seat_probe.dag index 643e939fffe..1b4e8dbe30c 100644 --- a/dag/gunbc/instruments/fabric_seat_probe.dag +++ b/dag/gunbc/instruments/fabric_seat_probe.dag @@ -16,7 +16,7 @@ import product.capacity.pool_events { import product.capacity.lease { LeasePolicy, QuiescenceRequired } import gunbc.harness.harness_wire { OpenAiChatCompletionsShape } import gunbc.harness.harness_seat { harness_turn_request, - HarnessSeatBound, HarnessSeatPending, HarnessSeatRefused, harness_bind_seat, HarnessSeatReleased, + HarnessSeatBound, HarnessSeatPending, HarnessSeatRefused, HarnessHostedBound, harness_bind_seat, HarnessSeatReleased, HarnessHostedCredentialRemoved, HarnessSeatReleaseRefused, harness_release_seat, harness_seat_release_wire, harness_now_epoch, AnyAdmittedModel, } @@ -62,6 +62,7 @@ fn fabric_seat_probe(receipt: NonEmptyStr) -> ProcessExit { match binding { HarnessSeatRefused { detail: d } => exit_failure(reason: join(["seat probe: bind refused: ", d], "")) HarnessSeatPending { detail: d } => exit_failure(reason: join(["seat probe: placement pending: ", d], "")) + HarnessHostedBound { url: _, model: _, unit: _, level: _, access: _ } => exit_failure(reason: "seat probe: an any-model bind answered with a hosted binding, which harness_requirement_placement never yields") HarnessSeatBound { url: u, grant: g, partition: p, ceiling: _, group_wire: gw, launch: launch, store: _, model: m, unit: _, class: c, seat: _ } => { let line = join([ "host=", short, " group=", gw, " launch=", launch.process_start_field as String, @@ -73,6 +74,7 @@ fn fabric_seat_probe(receipt: NonEmptyStr) -> ProcessExit { let seat_release = harness_release_seat(binding: binding, reason: "seat-probe complete" as NonEmptyStr) let w = Filesystem.Write(path: receipt as String, content: join([line, harness_seat_release_wire(r: seat_release), "\n"], "")) match seat_release { + HarnessHostedCredentialRemoved => exit_failure(reason: "seat probe: a fleet seat release answered as a hosted credential removal") HarnessSeatReleased { partition: _ } => if w.success { ExitSuccess } else { exit_failure(reason: join(["seat probe: receipt write failed: ", w.error], "")) } HarnessSeatReleaseRefused { detail: d } => exit_failure(reason: join(["seat probe: granted but release refused: ", d], "")) } diff --git a/dag/gunbc/instruments/frontier_ingestion_probe.dag b/dag/gunbc/instruments/frontier_ingestion_probe.dag index 5f539b9746d..f07c204d843 100644 --- a/dag/gunbc/instruments/frontier_ingestion_probe.dag +++ b/dag/gunbc/instruments/frontier_ingestion_probe.dag @@ -7,7 +7,7 @@ import v2.std.node { Symbol } import std.content_hash { ContentHash, content_hash_atom, content_hash_combine_structural, as_content_hash_structural } import std.types { NonEmptyStr, List } import v2.std.grammar { ParseTree } -import v2.std.diagnostic { Diagnostic, Diagnostics, NonEmptyDiagnostics, Accepted, Rejected, None } +import v2.std.diagnostic { Diagnostic, NonEmptyDiagnostics, Accepted, Rejected, None } import v2.std.compilers.lexing { lex_artifact_semantic_tokens, LexRuleSet, LexArtifact } import v2.compiler.tokenize { lex_walk_artifact } import v2.compiler.parse { parse_module_prepared, prepare_grammar, PreparedGrammar, ParseArtifact } @@ -214,18 +214,16 @@ fn classify_normalize_diagnostics(d: NonEmptyDiagnostics) -> FrontendStage { type FrontendWorld { lex_rules: LexRuleSet, prepared: PreparedGrammar, - validation_residue: Diagnostics, } fn prepare_frontend_world() -> v2.std.diagnostic.Outcome { match prepare_grammar(grammar: dag_grammar()) { v2.std.diagnostic.Rejected { diagnostics: d } => Rejected { diagnostics: d } - v2.std.diagnostic.Accepted { value: prepared, diagnostics: residue } => + v2.std.diagnostic.Accepted { value: prepared, diagnostics: _ } => Accepted { value: FrontendWorld { lex_rules: dag_lex_rules(), prepared: prepared, - validation_residue: residue, }, diagnostics: None, } @@ -234,13 +232,11 @@ fn prepare_frontend_world() -> v2.std.diagnostic.Outcome { fn classify_after_lex( prepared: PreparedGrammar, - residue: Diagnostics, artifact: LexArtifact ) -> FrontendStage { match parse_module_prepared( tokens: lex_artifact_semantic_tokens(artifact: artifact), - prepared: prepared, - validation_residue: residue + prepared: prepared ) { v2.std.diagnostic.Rejected { diagnostics: _ } => ParseRefused v2.std.diagnostic.Accepted { value: parsed, diagnostics: _ } => @@ -255,7 +251,7 @@ fn classify_source_in(world: FrontendWorld, source: String) -> FrontendStage { match lex_walk_artifact(source: chars(s: source), file: ^frontier_probe, rules: world.lex_rules) { v2.std.diagnostic.Rejected { diagnostics: _ } => LexRefused v2.std.diagnostic.Accepted { value: a, diagnostics: _ } => - classify_after_lex(prepared: world.prepared, residue: world.validation_residue, artifact: a) + classify_after_lex(prepared: world.prepared, artifact: a) } } @@ -279,9 +275,9 @@ fn classify_source(source: String) -> v2.std.diagnostic.Outcome { v2.std.diagnostic.Accepted { value: a, diagnostics: _ } => match prepare_grammar(grammar: dag_grammar()) { v2.std.diagnostic.Rejected { diagnostics: d } => Rejected { diagnostics: d } - v2.std.diagnostic.Accepted { value: prepared, diagnostics: residue } => + v2.std.diagnostic.Accepted { value: prepared, diagnostics: _ } => Accepted { - value: classify_after_lex(prepared: prepared, residue: residue, artifact: a), + value: classify_after_lex(prepared: prepared, artifact: a), diagnostics: None, } } diff --git a/dag/gunbc/instruments/instrument_dispatch_workflow.dag b/dag/gunbc/instruments/instrument_dispatch_workflow.dag index b6cba720031..ea6c1bf7aa9 100644 --- a/dag/gunbc/instruments/instrument_dispatch_workflow.dag +++ b/dag/gunbc/instruments/instrument_dispatch_workflow.dag @@ -126,9 +126,9 @@ data instrument_dispatch_timeout_minutes: Int = 240 // receipt is taken on purpose (gunbc.self_host_behavioral_equivalence_receipt), never per change. // The type-declaration-use census (the nominal-type plan's M0) is another: it emits and builds the // compiler, then resolves and infers the whole corpus, past any per-change lane and the same hour cap. -// The native resolve census is another: `census-resolve` emits and builds the compiler closure and then -// resolves every indexed module, which outlives the free remote runner's one-hour cap twice over, and -// its saved whole-tree receipt is what the resolve-frontier root ranking and the census lane's step D +// The native refusal census is another: `census-infer` emits and builds the compiler closure and then +// resolves and infers every indexed module, which outlives the free remote runner's one-hour cap twice +// over, and its saved whole-tree receipt is what the frontier root ranking and the census lane's step D // read. The stage0 regen round's price is another: a seed build plus a whole-population emit and a // one-mirror emit, which needs a memory-bounded slot the free remote runner does not expose (its // host budget is unreadable there and the run refuses). A dispatchable label is on-demand only; it diff --git a/dag/gunbc/instruments/native_emission_controls.dag b/dag/gunbc/instruments/native_emission_controls.dag index 83958efe5cd..1ebbd024048 100644 --- a/dag/gunbc/instruments/native_emission_controls.dag +++ b/dag/gunbc/instruments/native_emission_controls.dag @@ -1,7 +1,7 @@ module gunbc.instruments.native_emission_controls import std.types { Bool, Int, List, NonEmptyStr, Set, String } -import v2.std.algebra { filter } +import v2.std.algebra { filter, length } import std.list { duplicated_by_key } import gunbc.instruments.native_emission_controls_homonym { ControlBoxed, narrow_slot_value, narrow_slots_equal } import std.resource_contract { Observation, Read } @@ -247,6 +247,38 @@ fn filter_in_branch_condition_cases() -> List { ] } +// RULE: a data row keeps its declared `?`, in its signature and in its body (v1.compiler.emit_rust +// emit_data_def's named-refinement arm; v1.compiler.emit emit_optional_data_value_json). A +// where-refinement such as NonEmptyStr took the one arm of emit_data_def with no optional wrap, so +// gunbc.fleet_tailnet fleet_tailnet_domain (`NonEmptyStr? = Present { .. }`) was emitted +// `-> NonEmptyStr`; and the JSON body spelled Present as a tagged map the native option cannot +// parse. +// Both optional rows pass through a `T?`-typed parameter, so a dropped `?` stops this closure +// compiling, and a wrong body stops it at run time; the cases decide each row answers as declared. +data control_optional_brand_present: NonEmptyStr? = Present { value: "tailnet.example" } + +data control_optional_brand_absent: NonEmptyStr? = none + +fn control_optional_brand_value(d: NonEmptyStr?) -> String { + match d { + Present { value: v } => v + Absent => "" + } +} + +fn optional_data_row_cases() -> List { + [ + NativeEmissionCase { + identity: "optional_data_row_present_answers_its_value", + held: control_optional_brand_value(d: control_optional_brand_present) == "tailnet.example" + }, + NativeEmissionCase { + identity: "optional_data_row_red_absent_answers_absent", + held: control_optional_brand_value(d: control_optional_brand_absent) == "" + } + ] +} + // RULE: a data row takes the JSON serializer only when its value is a literal tree // (v1.compiler.emit_rust data_value_is_json_literal_tree). A `"x" as NonEmptyStr` brand cast, bare or // inside a record literal, and an operator inside a record literal used to reach the serializer and @@ -350,7 +382,7 @@ fn list_index_cases() -> List { } // RULE: the NativeClaimDriver binds its declared Filesystem resource (std.compiler_entry -// NativeClaimDriver; v1.compiler.emit_rust native_claim_report_effect_demand). Reaching Filesystem +// NativeClaimDriver; v1.compiler.emit NativeEffectRealizationAdmission under native_claim_route_denominator). Reaching Filesystem // makes native_claim_report effectful, so this whole program runs only if the rendered main binds // the filesystem_io handler and awaits the report: remove the binding and the main calls an async, // handler-taking report with neither, which rustc refuses -- the red is the label's, not a row's. @@ -417,6 +449,47 @@ fn effectful_fold_step_cases() -> List { ] } +// RULE: a type parameter carries its own cardinality (v1.compiler.emit_rust rust_carrier_optional_wrap, +// gunbc.recurring_failure_mode optional_type_parameter_emitted_without_its_cardinality). `get: K?` on a +// generic record emitted `pub get: K`, so an Absent had no representation and this carrier's +// construction with Absent did not compile -- the red is the label's. The cases read the field back +// both ways, and each has a red asking for an answer it does not give. +type ControlOptionalParam { + get: K? + pair: K +} + +fn control_optional_param_absent() -> ControlOptionalParam { + ControlOptionalParam { get: Absent, pair: 1 } +} + +fn control_optional_param_present() -> ControlOptionalParam { + ControlOptionalParam { get: Present { value: 7 }, pair: 2 } +} + +fn optional_param_is_absent(c: ControlOptionalParam) -> Bool { + match c.get { + Present { value: _ } => false + Absent => true + } +} + +fn optional_param_value_or_zero(c: ControlOptionalParam) -> Int { + match c.get { + Present { value: v } => v + Absent => 0 + } +} + +fn optional_type_parameter_cases() -> List { + [ + NativeEmissionCase { identity: "optional_type_param_holds_absent", held: optional_param_is_absent(c: control_optional_param_absent()) }, + NativeEmissionCase { identity: "optional_type_param_red_present_is_not_absent", held: !optional_param_is_absent(c: control_optional_param_present()) }, + NativeEmissionCase { identity: "optional_type_param_holds_present_value", held: optional_param_value_or_zero(c: control_optional_param_present()) == 7 }, + NativeEmissionCase { identity: "optional_type_param_red_absent_reads_no_value", held: optional_param_value_or_zero(c: control_optional_param_absent()) == 0 } + ] +} + // RULE: a resource capability's output renders through the signature renderer // (v1.compiler.emit_rust emit_capability_method). Its anonymous output product used to fall through // to the env-less render_node_type, so a zero-parameter closed alias such as std.integer UInt8 = @@ -481,6 +554,22 @@ fn control_outcome_is_bad(o: ControlOutcome) -> Bool { } } +// RULE: `v2.std.algebra` `length` over HOST text (kernel `String`) lowers to +// `v1_rt::string_length`. Corpus `v2.std.text.String` is structural FreeMonoid and keeps the +// generic body. Without the host arm, kernel `String` at `request_field` is E0308; with a +// leaf-name arm, structural text at `string_split_step` is E0308 the other way. +fn control_algebra_length_of_string(s: String) -> Int { + length(xs: s) +} + +fn algebra_length_on_string_cases() -> List { + [ + NativeEmissionCase { identity: "algebra_length_of_a_string_counts_scalars", held: control_algebra_length_of_string(s: "abc") == 3 }, + NativeEmissionCase { identity: "algebra_length_of_a_string_red_is_not_another_count", held: control_algebra_length_of_string(s: "abc") != 4 }, + NativeEmissionCase { identity: "algebra_length_of_a_list_is_unchanged", held: length(xs: ["a", "b"]) == 2 } + ] +} + fn fold_accumulator_formals_cases() -> List { [ NativeEmissionCase { identity: "fold_constructor_init_sums_all_nonnegative", held: control_outcome_good_value(o: control_sum_until_negative(xs: [1, 2, 3])) == 6 }, @@ -489,6 +578,80 @@ fn fold_accumulator_formals_cases() -> List { ] } +// RULE: a variant literal of a generic owner takes the expected application of that owner +// (v1.compiler.infer variant_literal_expected_application). Seeded with a bare constructor in result +// position, the fold's accumulator used to type as the uninstantiated declaration, so the payload +// binding `items` carried the formal and `items |> list_push(x)` emitted a `.list_push(` method no +// Rust type has -- E0599, extdeps.dns.domain_name parse_dns_label_list. +fn control_collect_nonempty(xs: List) -> ControlOutcome, String> { + fold(xs, init: ControlGood { value: [] }, f: (acc, x) => match acc { + ControlBad { value: e } => ControlBad { value: e } + ControlGood { value: items } => if x == "" { ControlBad { value: "empty" } } else { ControlGood { value: items |> list_push(x) } } + }) +} + +fn control_collected_count(o: ControlOutcome, String>) -> Int { + match o { + ControlGood { value: items } => items |> count + ControlBad { value: _ } => 0 - 1 + } +} + +fn variant_literal_application_cases() -> List { + [ + NativeEmissionCase { identity: "variant_literal_application_collects_every_element", held: control_collected_count(o: control_collect_nonempty(xs: ["a", "b", "c"])) == 3 }, + NativeEmissionCase { identity: "variant_literal_application_red_empty_element_refuses", held: control_collected_count(o: control_collect_nonempty(xs: ["a", "", "c"])) == 0 - 1 }, + NativeEmissionCase { identity: "variant_literal_application_red_empty_list_keeps_init", held: control_collected_count(o: control_collect_nonempty(xs: [])) == 0 } + ] +} + +// RULE: every match whose Rc scrutinee is dereferenced and cloned owes Clone on the generics it +// carries, wherever the match sits (v1.compiler.emit_rust v1_fn_body_rc_match_clone_bound_param_names). +// control_first_refusal has the shape of gunbc.product.data_class admit_data_effect: one generic +// (B) earns Clone elsewhere because the seed stores it by value, the other (A) is carried only in a +// phantom position (ControlTag, as Sink), and the step matches on the accumulator, a generic +// coproduct it clones out of its Rc. Read only at the fn body's top level, the bound was +// missed and the header stayed ``: E0599, `clone` exists for ControlAdmission but its +// trait bounds were not satisfied. +type ControlTag { + name: String +} + +type ControlAdmission = ControlAdmitted { tag: ControlTag, value: B } | ControlRefused { cause: String } + +fn control_admitted_seed(tag: ControlTag, value: B) -> ControlAdmission { + ControlAdmitted { tag: tag, value: value } +} + +fn control_first_refusal(tag: ControlTag, value: B, causes: List) -> ControlAdmission { + fold(causes, init: control_admitted_seed(tag: tag, value: value), f: (acc, c) => match acc { + ControlAdmitted { tag: _, value: _ } => ControlRefused { cause: c } + ControlRefused { cause: _ } => acc + }) +} + +fn control_admitted_value(a: ControlAdmission) -> Int { + match a { + ControlAdmitted { tag: _, value: v } => v + ControlRefused { cause: _ } => 0 - 1 + } +} + +fn control_refusal_cause(a: ControlAdmission) -> String { + match a { + ControlAdmitted { tag: _, value: _ } => "" + ControlRefused { cause: c } => c + } +} + +fn rc_match_clone_cases() -> List { + [ + NativeEmissionCase { identity: "rc_match_clone_no_refusal_keeps_the_seed", held: control_admitted_value(a: control_first_refusal(tag: ControlTag { name: "t" }, value: 7, causes: [])) == 7 }, + NativeEmissionCase { identity: "rc_match_clone_red_first_refusal_wins", held: control_refusal_cause(a: control_first_refusal(tag: ControlTag { name: "t" }, value: 7, causes: ["a", "b"])) == "a" }, + NativeEmissionCase { identity: "rc_match_clone_red_refused_is_not_admitted", held: control_admitted_value(a: control_first_refusal(tag: ControlTag { name: "t" }, value: 7, causes: ["x"])) == 0 - 1 } + ] +} + data native_emission_expected_identities: List = [ "set_struct_eq_same_members_in_another_order", "set_struct_eq_red_bare_field_differs", @@ -526,6 +689,9 @@ data native_emission_expected_identities: List = [ "effectful_fold_step_red_empty_keeps_init", "capability_alias_output_octet_in_range", "capability_alias_output_red_octet_out_of_range", + "algebra_length_of_a_string_counts_scalars", + "algebra_length_of_a_string_red_is_not_another_count", + "algebra_length_of_a_list_is_unchanged", "fold_constructor_init_sums_all_nonnegative", "fold_constructor_init_red_negative_switches_arm", "fold_constructor_init_red_empty_keeps_init", @@ -535,15 +701,27 @@ data native_emission_expected_identities: List = [ "list_index_past_the_end_is_absent", "list_index_negative_is_absent", "list_index_into_empty_is_absent", + "optional_data_row_present_answers_its_value", + "optional_data_row_red_absent_answers_absent", + "rc_match_clone_no_refusal_keeps_the_seed", + "rc_match_clone_red_first_refusal_wins", + "rc_match_clone_red_refused_is_not_admitted", "generic_return_full_arm_answers_its_value", "generic_return_red_empty_arm_answers_empty", "generic_return_effectful_call_answers_empty_on_absent_path", "generic_empty_map_duplicated_by_key_reports_each_repeat_once", - "generic_empty_map_red_no_repeat_reports_nothing" + "generic_empty_map_red_no_repeat_reports_nothing", + "variant_literal_application_collects_every_element", + "variant_literal_application_red_empty_element_refuses", + "variant_literal_application_red_empty_list_keeps_init", + "optional_type_param_holds_absent", + "optional_type_param_red_present_is_not_absent", + "optional_type_param_holds_present_value", + "optional_type_param_red_absent_reads_no_value" ] fn native_emission_cases() -> List { - concat(concat(concat(concat(concat(same_leaf_declaration_cases(), concat(concat(concat(concat(concat(concat(header_bound_cases(), fn_value_bound_cases()), filter_in_branch_condition_cases()), claim_driver_filesystem_cases()), effectful_fold_step_cases()), capability_alias_output_cases()), fold_accumulator_formals_cases())), list_index_cases()), effectful_generic_return_cases()), generic_empty_map_turbofish_cases()), data_literal_tree_cases()) + concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(same_leaf_declaration_cases(), concat(concat(concat(concat(concat(concat(header_bound_cases(), fn_value_bound_cases()), filter_in_branch_condition_cases()), claim_driver_filesystem_cases()), effectful_fold_step_cases()), capability_alias_output_cases()), fold_accumulator_formals_cases())), list_index_cases()), optional_data_row_cases()), effectful_generic_return_cases()), generic_empty_map_turbofish_cases()), data_literal_tree_cases()), variant_literal_application_cases()), rc_match_clone_cases()), optional_type_parameter_cases()), algebra_length_on_string_cases()) } fn case_row(c: NativeEmissionCase) -> String { diff --git a/dag/gunbc/instruments/native_emission_nested_present_specimen.dag b/dag/gunbc/instruments/native_emission_nested_present_specimen.dag new file mode 100644 index 00000000000..24485e19d10 --- /dev/null +++ b/dag/gunbc/instruments/native_emission_nested_present_specimen.dag @@ -0,0 +1,38 @@ +module gunbc.instruments.native_emission_nested_present_specimen + +import std.types { Bool, String } + +// AN EXPECTING-RED SPECIMEN for gunbc.recurring_failure_mode +// emitter_nested_variant_pattern_inside_present_drops_the_rc_deref. It is NOT enrolled in +// gunbc.instruments.native_emission_controls: that label requires every carrier to emit as +// compilable Rust, and specimen_nested_is_lit does not until the emitter rule lands. Enrolling it +// there, as a case beside the green control, is the row's next-rung step. +// +// The shape: a record pattern whose field is an Optional of a coproduct, with the coproduct variant +// pattern nested inside Present. The emitter renders the inner pattern against the Rc carrier +// without dereferencing it (Some(Shade::Lit{..}) against Option>, rustc E0308). +type SpecimenNestedShade = SpecimenNestedLit { name: String } | SpecimenNestedDark + +type SpecimenNestedCard { + shade: SpecimenNestedShade? +} + +fn specimen_nested_is_lit(card: SpecimenNestedCard) -> Bool { + match card { + SpecimenNestedCard { shade: Present { value: SpecimenNestedLit { name: _ } } } => true + SpecimenNestedCard { shade: Present { value: SpecimenNestedDark } } => false + SpecimenNestedCard { shade: Absent } => false + } +} + +fn specimen_nested_is_lit_split(card: SpecimenNestedCard) -> Bool { + match card { + SpecimenNestedCard { shade: s } => match s { + Present { value: v } => match v { + SpecimenNestedLit { name: _ } => true + SpecimenNestedDark => false + } + Absent => false + } + } +} diff --git a/dag/gunbc/instruments/native_serve_fixture.dag b/dag/gunbc/instruments/native_serve_fixture.dag index 898ad10c97a..fc7b1eb742d 100644 --- a/dag/gunbc/instruments/native_serve_fixture.dag +++ b/dag/gunbc/instruments/native_serve_fixture.dag @@ -1,9 +1,13 @@ module gunbc.instruments.native_serve_fixture -import std.types { String, CommitSha } +import std.types { String, Int, Bool, CommitSha } import std.compiler_entry { CompilerEntryDriver, NativeServeDriver } import extdeps.http.server { ServeWireResponse, ServeWireHeader, WireHeaderLocation } import gunbc.native_serve { native_serve_text_plain } +import extdeps.clock { Clock } +import extdeps.transports.rest { RestResult, RestAnswered, RestRefused, RestStatusRefused, RestTransportRefused, RestBodyUndecodable } +import std.resources { Network } +import std.algebra { trim } // THE SMALLEST SERVICE THAT EXERCISES EVERY DECISION OF THE NATIVE SERVE DOOR, served by // `gunbc test //gunbc/instruments:native-serve` (gunbc.native_serve_probe judges what it answers). @@ -46,7 +50,79 @@ fn native_serve_handle(method: String, path: String, body: String, tailscale_ide fixture_text(status: 200, body: concat("spun ", concat(to_string(value: fixture_spin(n: 1000000000000000, acc: 7)), "\n"))) } else if path == "/echo" && method == "POST" { fixture_text(status: 200, body: body) + } else if path == "/effects/clock" && method == "GET" { + fixture_clock_readings() + } else if path == "/effects/rest" && method == "POST" { + fixture_peer_liveness(port_text: body) } else { fixture_text(status: 404, body: concat("no route for ", concat(method, concat(" ", concat(path, "\n"))))) } } + +// ── THE BOUND EFFECT HANDLERS, EXERCISED (native broker 2N) ───────────────────────────────────── +// These two routes make this entry demand effects, so its rendered main binds the handlers +// v1.compiler.emit NativeEffectRealizationAdmission admitted for it -- extdeps.clock Clock on the +// existing shell transport, and FixturePeer on the existing REST lowering -- and nothing else. +// gunbc.native_serve_probe judges what they answer; this handler only reports what it observed. +// +// /effects/clock reads Clock.Now twice in order. The probe holds the pair plausible (an RFC 3339 +// UTC second in a sane year) and monotone (the second reading is not before the first). +fn fixture_clock_readings() -> ServeWireResponse { + let first = Clock.Now() + let second = Clock.Now() + fixture_text(status: 200, body: concat(first.timestamp as String, concat("\n", concat(second.timestamp as String, "\n")))) +} + +// /effects/rest asks another served process for its liveness document over the REST transport. +// The body is the port to ask: the instrument starts a second instance of this fixture as the +// local test server and sends its bound port (the answered arm), and sends a port nothing listens +// on (the RestTransportRefused arm). The answer names the arm the transport produced, nothing more. +fn fixture_peer_liveness(port_text: String) -> ServeWireResponse { + match parse_int(s: trim(s: port_text)) { + Absent => fixture_text(status: 400, body: "the body is not a port\n") + Present { value: port } => fixture_text(status: 200, body: concat(fixture_rest_arm(rest: native_serve_fixture.Peer.Liveness(port: port)), "\n")) + } +} + +fn fixture_rest_arm(rest: RestResult) -> String { + match rest { + RestAnswered { answer } => if answer { "answered live" } else { "answered not-live" } + RestRefused { refusal } => match refusal { + RestStatusRefused { status, body } => concat("refused status ", to_string(value: status)) + RestTransportRefused { cause } => "refused transport" + RestBodyUndecodable { status, cause } => concat("refused body ", to_string(value: status)) + } + } +} + +// The peer is another native-serve process on loopback. The ORIGIN is the endpoint and the port the +// path's first segment, because the port is the one fact only known after the peer binds port 0: +// the REST transport's endpoint is per-service configuration, so a per-call port can only be carried +// in the request target. This is the fixture's shape, not a pattern for production services. +service native_serve_fixture.Peer { + config { + endpoint: "http://127.0.0.1" + } + + operation Liveness { + requires Network + input { + port: Int + } + output { + live: Bool from "live" + } + readonly + transport rest { + method: GET, + path: ":\{port\}/livez" + } + response { + 200 => FixturePeerLiveness + } + } +} + +type FixturePeerLiveness { + live: Bool +} diff --git a/dag/gunbc/instruments/self_host_03_normalize_declared_source_refs.dag b/dag/gunbc/instruments/self_host_03_normalize_declared_source_refs.dag index 159cbde0661..05199167a2f 100644 --- a/dag/gunbc/instruments/self_host_03_normalize_declared_source_refs.dag +++ b/dag/gunbc/instruments/self_host_03_normalize_declared_source_refs.dag @@ -2,6 +2,7 @@ module tools.self_host_03_normalize_declared_source_refs import std.types { List } import v2.compiler.source_authority { SourceRef, source_ref_for_storage_path } +import v2.std.cross_tree.import_model { V2Tree } // declared_source_refs for the 03_normalize behavioral witness only (review 41433 fix): this list // opts src/v2/compiler/03_normalize.dag's witness entry into the cli_run.rs affected-set selection @@ -13,9 +14,9 @@ import v2.compiler.source_authority { SourceRef, source_ref_for_storage_path } // this selection axis onto all 16 entries; a dedicated per-entry module keeps the axis scoped to // 03_normalize alone, matching the roster row's module_path and contract entry exactly. -data sn_source_ref: SourceRef = source_ref_for_storage_path(path: "src/v2/compiler/03_normalize.dag") +data sn_source_ref: SourceRef = source_ref_for_storage_path(path: "src/v2/compiler/03_normalize.dag", source_root: V2Tree) -data sn_contract_ref: SourceRef = source_ref_for_storage_path(path: "src/v2/test/claim/self_host_contract/normalize_emitted_contract_test.dag") +data sn_contract_ref: SourceRef = source_ref_for_storage_path(path: "src/v2/test/claim/self_host_contract/normalize_emitted_contract_test.dag", source_root: V2Tree) // Hand-verified completeness (docs/plans/declared-source-ref-selection-design.md §5.1 flagship): // every storage path this transport reads from the repository is listed -- the subject .dag and the diff --git a/dag/gunbc/instruments/source_reference_repoint.dag b/dag/gunbc/instruments/source_reference_repoint.dag index 14a83e11708..e2226440d77 100644 --- a/dag/gunbc/instruments/source_reference_repoint.dag +++ b/dag/gunbc/instruments/source_reference_repoint.dag @@ -348,7 +348,7 @@ fn repoint_pending_verdict(pending: List) -> ProcessExit { } fn repoint_root_tasks() -> List { - source_reference_repoint_roots |> map(path => SourceRootWalkTask { path: path, root: true }) + source_reference_repoint_roots |> map(path => SourceRootWalkTask { path: path, root: true, origin: path }) } // The wet entry: rewrite every file under the roots that still references a moved module. @@ -357,10 +357,10 @@ fn repoint() -> ProcessExit { SourceRootPathsRefused { path: path, kind: kind, cause: cause } => exit_failure(reason: join(["source reference repoint: cannot walk ", path, " (", kind, "): ", cause], "")) SourceRootPathsRead { paths: paths } => - fold(paths, init: ExitSuccess, f: fn(acc, path) { + fold(paths, init: ExitSuccess, f: fn(acc, walked) { match acc { ExitFailure { code, reason } => ExitFailure { code: code, reason: reason } - ExitSuccess => repoint_one_path(path: path) + ExitSuccess => repoint_one_path(path: walked.path) } }) } @@ -372,6 +372,10 @@ fn repoint_pending() -> ProcessExit { SourceRootPathsRefused { path: path, kind: kind, cause: cause } => exit_failure(reason: join(["source reference repoint: cannot walk ", path, " (", kind, "): ", cause], "")) SourceRootPathsRead { paths: paths } => - repoint_pending_verdict(pending: paths |> filter(path => repoint_path_is_pending(path: path))) + repoint_pending_verdict( + pending: paths + |> filter(walked => repoint_path_is_pending(path: walked.path)) + |> map(walked => walked.path) + ) } } diff --git a/dag/gunbc/ipmitool_fru_text.dag b/dag/gunbc/ipmitool_fru_text.dag new file mode 100644 index 00000000000..1669b254b14 --- /dev/null +++ b/dag/gunbc/ipmitool_fru_text.dag @@ -0,0 +1,68 @@ +module gunbc.ipmitool_fru_text + +import std.types { String, NonEmptyStr } +import std.algebra { trim } +import std.optional { Present, Absent } +import v2.std.algebra { filter } + +// THE PURE LINE DECODE of `ipmitool fru print` text: each field is read from exactly one line. +// Shared by every consumer of a committed FRU capture (the Mt. Jade identity read and the board +// authority read), so the line grammar has one home. +// The two grains are two types: a field read can only be absent, repeated or empty, so the +// capture-level failures are not representable in what `fru_field` returns. +type FruFieldGapKind + = FruFieldAbsent { field: NonEmptyStr } + | FruFieldRepeated { field: NonEmptyStr } + | FruFieldEmpty { field: NonEmptyStr } + +type FruCaptureGap + = FruCaptureUnreadable { error: String } + | FruCaptureDigestMismatch { expected: NonEmptyStr, observed: String } + | FruFieldGapped { gap: FruFieldGapKind } + | FruSubjectLinkUnobserved + | FruBoundToAnotherLink { board_extra: String } + +// THE ONE DIGEST CHECK of a committed capture against the sha256 a record was authored from. +// Every reader of a committed FRU capture asks this; a mismatch is never decoded. +type FruCaptureDigestCheck + = FruCaptureDigestAgrees + | FruCaptureDigestDiffers { gap: FruCaptureGap } + +fn fru_capture_digest_check(content: String, expected_sha256: NonEmptyStr) -> FruCaptureDigestCheck { + let observed = sha256_hex_of_text(text: content) + if observed != (expected_sha256 as String) { + FruCaptureDigestDiffers { gap: FruCaptureDigestMismatch { expected: expected_sha256, observed: observed } } + } else { + FruCaptureDigestAgrees + } +} + +type FruFieldRead + = FruFieldValue { value: NonEmptyStr } + | FruFieldGap { gap: FruFieldGapKind } + +fn fru_line_key(line: String) -> String { + match split(s: line, delimiter: " : ").first() { + Present { value: k } => trim(k) + Absent => "" + } +} + +fn fru_line_value(line: String) -> String { + trim(join(split(s: line, delimiter: " : ").skip(n: 1), " : ")) +} + +fn fru_field(content: String, field: NonEmptyStr) -> FruFieldRead { + let hits = split(s: content, delimiter: "\n") |> filter(l => fru_line_key(line: l) == (field as String)) + match hits.first() { + Absent => FruFieldGap { gap: FruFieldAbsent { field: field } } + Present { value: line } => + if length(hits) > 1 { + FruFieldGap { gap: FruFieldRepeated { field: field } } + } else if fru_line_value(line: line) == "" { + FruFieldGap { gap: FruFieldEmpty { field: field } } + } else { + FruFieldValue { value: fru_line_value(line: line) as NonEmptyStr } + } + } +} diff --git a/dag/gunbc/legacy_extdeps_scope_frontier.tsv b/dag/gunbc/legacy_extdeps_scope_frontier.tsv index 8d4ae726ae2..f867a464377 100644 --- a/dag/gunbc/legacy_extdeps_scope_frontier.tsv +++ b/dag/gunbc/legacy_extdeps_scope_frontier.tsv @@ -240,13 +240,11 @@ dag/extdeps/provisioning/ubuntu_seeded_install_media_toolchain.dag dag/extdeps/python/python.dag dag/extdeps/realization/artifact_store_fs.dag dag/extdeps/realization/cache_purity.dag -dag/extdeps/realization/compile_stage_memo.dag dag/extdeps/realization/emit_on_demand_host.dag dag/extdeps/realization/eval_memo.dag dag/extdeps/realization/frame_boundary_simulated.dag dag/extdeps/realization/hermetic_fixture.dag dag/extdeps/realization/native_artifact_cache.dag -dag/extdeps/realization/parse_table_memo.dag dag/extdeps/realization/reconcile_in_process.dag dag/extdeps/realization/resolved_graph.dag dag/extdeps/realization/v1_handler.dag diff --git a/dag/gunbc/live_deploy/apply.dag b/dag/gunbc/live_deploy/apply.dag index b6e8cd72b3f..74e4a2f79a6 100644 --- a/dag/gunbc/live_deploy/apply.dag +++ b/dag/gunbc/live_deploy/apply.dag @@ -23,7 +23,18 @@ import gunbc.live_deploy.spec { deployment_plan_host_identity, deployment_tailnet_door_socket, } -import gunbc.live_deploy.desired { deployment_spec_srv1 } +import gunbc.live_deploy.desired { + deployment_spec_srv1, + desired_deployment_for_instance_id, + selected_deployment_spec, + srv1_production_desired_deployment, + instance_id_text, + DesiredDeploymentFound, + DesiredDeploymentUnknownInstanceId, + DesiredDeploymentRosterRefused, + DesiredRosterSameHostCollision, + DesiredRosterDuplicateInstanceId, +} import std.types { String, CommitSha, NonEmptyStr } import gunbc.live_deploy.candidate { CandidateRelease, @@ -1127,19 +1138,66 @@ fn approval_broker_dark_install_srv1_wet() -> ProcessExit { ) } +// ONE APPLY ROUTE, SELECTED BY INSTANCE ID. The roster (gunbc.live_deploy.desired +// desired_deployment_roster) is the selection authority; this is the only place an apply entry +// turns an instance id into a DeploymentSpec, and the production entries below are this route +// called with production's id rather than a second fold. A row whose target host is not the host +// the access reaches refuses: an access is a principal on ONE host, and pointing it at another +// row would mutate the wrong machine under a correct-looking spec. +type DesiredApplySelection + = DesiredApplySelected { spec: DeploymentSpec } + | DesiredApplyRefused { reason: String } + +fn live_deploy_desired_apply_selection(instance_id: String, access: DeployAccess) -> DesiredApplySelection { + match desired_deployment_for_instance_id(instance_id: instance_id) { + DesiredDeploymentUnknownInstanceId { instance_id: id } => + DesiredApplyRefused { reason: join(["live_deploy apply: DesiredDeploymentUnknownInstanceId '", id, "' names no row of desired_deployment_roster"], "") } + DesiredDeploymentRosterRefused { refusal } => + match refusal { + DesiredRosterSameHostCollision { host } => + DesiredApplyRefused { reason: join(["live_deploy apply: DesiredRosterSameHostCollision on host ", host], "") } + DesiredRosterDuplicateInstanceId { instance_id: dup } => + DesiredApplyRefused { reason: join(["live_deploy apply: DesiredRosterDuplicateInstanceId ", dup], "") } + } + DesiredDeploymentFound { row } => + if (row.target.host.identity as String) != (access.host.identity as String) { + DesiredApplyRefused { reason: join(["live_deploy apply: DesiredApplyHostMismatch — row '", instance_id, "' targets ", row.target.host.identity as String, " but the access reaches ", access.host.identity as String], "") } + } else { + DesiredApplySelected { spec: selected_deployment_spec(row: row) } + } + } +} + +fn live_deploy_apply_desired_wet(instance_id: String, access: DeployAccess) -> ProcessExit { + match live_deploy_desired_apply_selection(instance_id: instance_id, access: access) { + DesiredApplyRefused { reason } => exit_failure(reason: reason) + DesiredApplySelected { spec } => live_deploy_wet_with_access(spec: spec, access: access) + } +} + +// The operator-local apply of any srv1 roster row, e.g. +// `--function live_deploy_apply_desired_operator_local_wet --arg instance_id=srv1-lab-codex-feedback-v0`. +fn live_deploy_apply_desired_operator_local_wet(instance_id: String) -> ProcessExit { + live_deploy_apply_desired_wet(instance_id: instance_id, access: ci_deploy_srv1_operator_local_access) +} + +fn srv1_production_instance_id() -> String { + instance_id_text(id: srv1_production_desired_deployment().instance.instance_id) +} + fn live_deploy_apply_srv1_wet() -> ProcessExit { - live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_access) + live_deploy_apply_desired_wet(instance_id: srv1_production_instance_id(), access: ci_deploy_srv1_access) } fn live_deploy_apply_srv1_operator_wet() -> ProcessExit { - live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_operator_access) + live_deploy_apply_desired_wet(instance_id: srv1_production_instance_id(), access: ci_deploy_srv1_operator_access) } // The converge verb's deploy mode: the same admitted path the CI entry runs, under the operator // principal on the host itself. Candidate observation, preflight, mutation and the readiness // gate are all shared; nothing here is a second deploy. fn live_deploy_apply_srv1_operator_local_wet() -> ProcessExit { - live_deploy_wet_with_access(spec: deployment_spec_srv1(), access: ci_deploy_srv1_operator_local_access) + live_deploy_apply_desired_operator_local_wet(instance_id: srv1_production_instance_id()) } fn live_deploy_retract_srv1_wet() -> ProcessExit { diff --git a/dag/gunbc/live_deploy/belt_cadence.dag b/dag/gunbc/live_deploy/belt_cadence.dag index 9caf801d698..621d3645369 100644 --- a/dag/gunbc/live_deploy/belt_cadence.dag +++ b/dag/gunbc/live_deploy/belt_cadence.dag @@ -1,6 +1,6 @@ module gunbc.live_deploy.belt_cadence -import std.types { String } +import std.measure { Second, second } // OnUnitInactiveSec, NOT OnUnitActiveSec, and the difference is a fail-closed choice rather than a // formatting one. OnUnitActiveSec measures from when the tick STARTED, so a tick that runs longer @@ -23,4 +23,23 @@ import std.types { String } // produces SpawnFailed rows for a condition that resolves itself within seconds. The delay costs // one cadence period and removes a class of self-inflicted refusals from the receipt. -data belt_tick_cadence: String = "60s" +data belt_tick_cadence: Second = second(count: 60) + +// THE PER-TICK WATCHDOG, and the 2026-09-21 measurement that put it here. A Type=oneshot unit with +// no start bound runs as long as the tick runs, and OnUnitInactiveSec only schedules the next tick +// from INACTIVITY -- so one tick that wedged in the SCM seed (measured that night: 2.25 hours at +// 100% CPU inside one capture, the timer showing no next elapse and no receipt distinguishing +// "nothing to do" from "stuck") halted the whole pipeline invisibly; the operator could not tell +// the frozen cadence from an idle belt. TimeoutStartSec is the bounded-reconciliation backstop in +// the workflow-execution ruling's sense: past the bound systemd kills the tick, the unit goes +// inactive(failed) LOUDLY, and the timer resumes on the next cadence -- one lost pass, never a +// lost night. The bound must clear the worst HONEST tick, which is first-capture-heavy: a fresh +// project envelope seeds the whole ~7,050-path intake base through the git bridge once (measured +// 2026-09-21 at roughly seven minutes of bridge time per project), and verification then runs the +// node's validations; it must stay far below the hours a wedge needs to be noticed by a human. +// The bound names the instrument that re-derives it, never a transcribed number (DESIGN section 6): +// a green tick's receipt at receipts/belt-last-tick.json carries its observed_at, and the belt's +// own stage timings re-derive the worst honest pass. 2700s (45 minutes) is the policy row: roughly six times +// the 2026-09-21 measured seed-bridge cost, far below the hours a wedge needs to be noticed by a +// human, and any honest growth beyond it must move this row with a reason, not the wedge. +data belt_tick_run_bound: Second = second(count: 2700) diff --git a/dag/gunbc/live_deploy/candidate.dag b/dag/gunbc/live_deploy/candidate.dag index 38449b1ffb6..cd5b100e84a 100644 --- a/dag/gunbc/live_deploy/candidate.dag +++ b/dag/gunbc/live_deploy/candidate.dag @@ -14,22 +14,20 @@ import gunbc.running_release_identity { running_release_surface_equals, } -// A REVISION NAMES A COMMIT; THE DEPLOY SHIPS A WORKING TREE. Those are the same bytes only when the -// checkout is clean, and nothing above established that. The sync's working-tree leg rsyncs the runner -// checkout wholesale except gunbc.live_deploy.deployed_tree_scope deployed_tree_excluded_prefixes, so an edit -// or an untracked file is DEPLOYED while HEAD does not name it — and the resulting candidate -// { revision: A, surface: B } is self-consistent all the way down: the emitter writes A into ExecStart, the -// process reports A, readiness compares A against A and greens, and the ancestry decision downstream -// orders the deployment as A. Nothing in the chain can see that the bytes were never A's, because every -// party is reading the same wrong answer. This is the one class the identity channel cannot catch by -// construction, which is why it is refused at admission instead. +// A REVISION NAMES A COMMIT, AND ADMISSION ESTABLISHES THAT THE CHECKOUT IS THAT COMMIT. The deployed +// tree is now converged git-natively (gunbc.live_deploy.repository_convergence repository_converge_wet +// installs the candidate COMMIT, never the runner's working tree), but the deploying process still +// builds and renders from this checkout: an edit or an untracked file here would make the candidate +// { revision: A, surface: B } self-consistent all the way down -- the emitter writes A into ExecStart, +// the process reports A, readiness compares A against A and greens -- while B was rendered from bytes A +// does not name. That class cannot be caught by the identity channel by construction, which is why it is +// refused at admission instead. Historically the working-tree leg was an rsync of the checkout with +// `--delete`, which made a stray file load-bearing on the host; that transport is gone, and the refusal +// stays because the render-from-checkout half of the hazard did not go with it. // -// THE UNTRACKED ARM IS NOT A COURTESY. `--delete` on the working-tree leg means a stray file in the -// checkout is copied to the host and then persists there; the deployment's own source sync is the transport -// that makes an untracked path load-bearing. It is scoped to DEPLOYED paths for the same reason: a file -// under target/ or .git/ is excluded by the sync, so refusing on it would be refusing on bytes that never -// leave the runner — a fail-closed arm that fires on a state which is not the invalid one is just noise, -// and noise is what gets suppressed later. +// Paths under gunbc.live_deploy.deployed_tree_scope deployed_tree_excluded_prefixes (target/, .git/, +// host session and cache state) are out of scope: refusing on them would fire on a state that is not +// the invalid one, and noise is what gets suppressed later. // // FOUR SEPARATE PATH LISTS, NOT A COUNT OR A BOOL. Staged, unstaged-tracked, untracked-deployed and // ignored-deployed have different remedies — commit the index, commit or discard the edit, add or clean the @@ -43,14 +41,12 @@ import gunbc.running_release_identity { // a path that .gitignore exists to keep out of the index. // // It closes a hole the first version of this admission had. `git status --porcelain=v1 -z -// --untracked-files=all` does not report ignored files at all, while the source sync excludes only the -// prefixes deployed_tree_scope names — so an ignored path outside them (the residue is now the .env family, -// which stays refused because `.env.*.local` is a wildcard no prefix can spell, and, inside a source root the -// graph actually reads, src/v2/test/claim/manual/sg2_scratch_probe.dag) was copied by rsync while the -// admission reported the tree clean. The resulting state is self-consistent and therefore invisible to every later comparison: HEAD names -// revision A, rsync installs bytes B, the candidate's expected surface is rendered from B, the process reports -// A and B, and readiness compares A against A and B against B and greens. That is exactly the false identity -// this admission exists to make unrepresentable, surviving inside it. +// --untracked-files=all` does not report ignored files at all, so an ignored path outside the excluded +// prefixes (the .env family, which stays refused because `.env.*.local` is a wildcard no prefix can +// spell; inside a source root the graph actually reads, src/v2/test/claim/manual/sg2_scratch_probe.dag; +// and a legacy ledger roster.dag an older gunbc wrote) was invisible while the admission reported the +// tree clean -- and an ignored file under a source root changes what this checkout compiles. That is +// exactly the false identity this admission exists to make unrepresentable, surviving inside it. type CandidateCheckoutObservation = CandidateCheckoutClean | CandidateCheckoutDirty { diff --git a/dag/gunbc/live_deploy/deployed_tree_scope.dag b/dag/gunbc/live_deploy/deployed_tree_scope.dag index a9924e7f206..43788269f7b 100644 --- a/dag/gunbc/live_deploy/deployed_tree_scope.dag +++ b/dag/gunbc/live_deploy/deployed_tree_scope.dag @@ -3,11 +3,15 @@ module gunbc.live_deploy.deployed_tree_scope import std.types { Bool, String, List } import v2.std.algebra { any } -// WHAT THE DEPLOY ACTUALLY INSTALLS, DECLARED ONCE BECAUSE TWO THINGS NEED IT AND MUST NOT DISAGREE. -// The working-tree leg of the source sync rsyncs the runner checkout wholesale except these -// prefixes, so the deployed source is the WORKING TREE, not the commit. Two consumers read it: +// WHICH CHECKOUT PATHS CANDIDATE ADMISSION HOLDS TO "CLEAN", DECLARED ONCE. +// The deployed tree is converged GIT-NATIVELY (gunbc.live_deploy.repository_convergence +// repository_converge_wet: objects fetched from the runner checkout, the base ref moved by +// compare-and-swap, `reset --hard` to the candidate), so what is installed is the COMMIT, not the +// runner's working tree. Admission still refuses a checkout carrying an untracked or ignored path +// outside these prefixes, because such a path means the candidate is not the clean revision it +// names. One consumer reads it: // -// gunbc.live_deploy.candidate decides whether an untracked path would actually be shipped +// gunbc.live_deploy.candidate decides whether an untracked or ignored path makes the checkout unclean // // A second reader once rendered it as rsync `--exclude` flags (deployed_tree_rsync_exclude_flags). // The tree-sync unit and then the approval broker's job-user rsync were its last production callers; @@ -21,8 +25,7 @@ import v2.std.algebra { any } // and acyclicity is the import graph's one structural law (DESIGN §3). A fact needed by two modules // on opposite sides of a dependency chain belongs beneath both, not inside either. // WHY THIS LIST IS NOT JUST target AND .git, AND WHY IT IS STILL NOT EVERY IGNORED PATH. -// The working-tree leg is rsync, which does not read .gitignore, so an ignored path outside these -// prefixes is copied to the host under a revision that does not name it. Candidate admission +// An ignored path outside these prefixes is state the revision does not name. Candidate admission // refuses on exactly that state (gunbc.live_deploy.candidate, the ignored_deployed arm), naming two // remedies: widen these exclusions, or remove the file. The four appended here take the first // because they are host state by construction -- agent session state and local tool/build caches @@ -30,8 +33,8 @@ import v2.std.algebra { any } // refusing on one is pure friction. // // THE .env FAMILY IS DELIBERATELY ABSENT AND STAYS REFUSED. The repository ignores `.env`, -// `.env.local` and `.env.*.local`; the third is a wildcard, and both this predicate and rsync's -// `--exclude /

` match a literal segment. Listing the two spellable forms would silence admission +// `.env.local` and `.env.*.local`; the third is a wildcard, and this predicate matches a literal +// segment. Listing the two spellable forms would silence admission // on them while `.env.*.local` kept shipping -- partial cover on the one class where the shipped // bytes are secrets, strictly worse than the present loud refusal on all three. It stays refused // until the exclusion carrier can express a pattern. src/v2/test/claim/manual/sg2_scratch_probe.dag @@ -54,23 +57,10 @@ data deployed_tree_excluded_prefixes: List = [ ".gunbc-build-receipts", ".gunbc-converge-receipts", ] -// A COMPILER-DERIVED FILE INSIDE A SOURCE ROOT IS EXCLUDED AS A FILE, NOT A PREFIX. The -// recurring-failure-mode roster is gitignored and written by every gunbc invocation as a read-path -// side effect of listing .dag files (src/v1 cli_run derived_row_roster), so the deploying process -// itself creates it before candidate admission scans the checkout, and the first RLM-2 -// dashboard_deploy refused itself on it (run 35189302828: ignored-deployed=dag/gunbc/ -// recurring_failure_mode/roster.dag). Unlike sg2_scratch_probe, excluding it changes nothing the -// host compiles: the host's own first invocation derives the identical file from the sibling rows -// the revision DOES name. Exact path, segment-exact, so a sibling row file is never excluded. -data deployed_tree_excluded_derived_files: List = [ - "dag/gunbc/recurring_failure_mode/roster.dag", -] - // Segment-wise, never a character run: `targetish/x` starts with `target` but is not under it, and treating -// it as excluded would hide from admission a path rsync goes on to ship. +// it as excluded would hide from admission a path that is not host state. fn path_is_deployed(path: String) -> Bool { !any(deployed_tree_excluded_prefixes, p => path == p || starts_with(s: path, prefix: concat(p, "/"))) - && !any(deployed_tree_excluded_derived_files, f => path == f) } // WHICH REPOSITORY TRANSITION THIS DEPLOYMENT IS BOUND TO, AND WHY IT IS A WALL RATHER THAN A NOTE. diff --git a/dag/gunbc/live_deploy/desired.dag b/dag/gunbc/live_deploy/desired.dag index 3eeb3ead16c..21907fcb938 100644 --- a/dag/gunbc/live_deploy/desired.dag +++ b/dag/gunbc/live_deploy/desired.dag @@ -108,7 +108,7 @@ fn deployment_srv2_target() -> DeploymentHostTarget { tailscale_client: ScopedRemovalUnobserved { reason: "srv2 tailscale Serve scoped-removal capability has not been observed", }, - ensured_directories: [], + host_directories: [], } } diff --git a/dag/gunbc/live_deploy/emit.dag b/dag/gunbc/live_deploy/emit.dag index ddf5413f7a0..c9eabd701d1 100644 --- a/dag/gunbc/live_deploy/emit.dag +++ b/dag/gunbc/live_deploy/emit.dag @@ -14,7 +14,7 @@ import gunbc.fleet_generation_execution { fleet_generation_lock } import gunbc.runner_microvm_cell_readiness_location { cell_readiness_store_location } import gunbc.workspace_commissioning_location { workspace_commissioning_store_location } -import gunbc.live_deploy.belt_cadence { belt_tick_cadence } +import gunbc.live_deploy.belt_cadence { belt_tick_cadence, belt_tick_run_bound } import gunbc.auth.approval_device_redemption { approval_device_store_directory } import gunbc.live_deploy.release_locus { @@ -47,7 +47,6 @@ import gunbc.managed_directory { managed_directory_admit, ManagedDirectoryAdmission, ManagedDirectoryAdmitted, ManagedDirectoryRefused, managed_directory_mode_octal, - attempt_state_directory_at, EntriesUnmanaged, EntriesReadableByDependents, managed_directory_entry_mode, ManagedEntryModeDerived, ManagedEntryModeRefused, entry_mode_add_read_symbolic, } @@ -66,7 +65,7 @@ import extdeps.systemd.systemctl { systemctl_disable_now_argv, systemctl_show_load_state_argv, } -import extdeps.systemd { systemd_unit_load_state_not_found_wire } +import extdeps.systemd { systemd_unit_load_state_not_found_wire, systemd_time_span_of_seconds } import gunbc.live_deploy.repository_convergence_placement { convergence_receipt_path, convergence_unit_doc, @@ -93,7 +92,8 @@ import gunbc.executor_privileged_operation { import gunbc.fleet_posix_accounts { fleet_posix_nologin_shell_path, fleet_posix_operator_user, fleet_posix_ci_runner_user, fleet_rm_binary_path } import gunbc.cli_invoke { NamedArgumentAdmission, admit_named_argument } import extdeps.systemd.unit_file { - SystemdUnitFile, ServiceUnitFile, TimerUnitFile, SliceUnitFile, + SystemdUnitFile, ServiceUnitFile, TimerUnitFile, SliceUnitFile, PathUnitFile, + SystemdPathDirective, PathUnit, DirectoryNotEmpty, PathChanged, ServiceSlice, slice_memory_max, slice_memory_high, slice_memory_swap_max, SystemdInstallSection, NotInstallable, Installable, SystemdUnitDirective, Description, After, Wants, @@ -139,6 +139,12 @@ import gunbc.live_deploy.slice_bounds { repository_convergence_slice_memory_swap_max, } import gunbc.fabric_storage_placement { fabric_storage_door_socket } +import gunbc.owned_directory { + OwnedDirectoryTarget, OwnedDirectoryManaged, OwnedDirectoryDeclared, + owned_directory_path, owned_directory_member, owned_directory_install_plan_of, owned_directory_install_argv, +} +import gunbc.ensure { EnsurePlanAvailable, EnsurePlanUnavailable } +import gunbc.roadmap_dashboard_instance { dashboard_dispatch_worktree_member, dashboard_attempt_state_member } import gunbc.live_deploy.spec { deployment_slice_unit_name, deployment_slice_unit_path, deployment_fabric_storage_unit_name, deployment_fabric_storage_serve_endpoint, deployment_approval_broker_front_door_endpoints, @@ -149,15 +155,16 @@ import gunbc.live_deploy.spec { DeploymentSpec, DeploymentStep, ArtifactStep, Dependency, DeploymentDependencyStep, - EnsuredSubject, EnsuredPackage, EnsuredHostDirectory, EnsuredManagedHostDirectory, ensured_subject_identity, - deployment_provider_state_step, + EnsuredSubject, EnsuredPackage, ensured_subject_identity, + deployment_directory_demands, DeploymentArtifactStep, - ServeBinary, GunbcSourceTree, DispatchWorktreeRoot, AttemptStateRoot, ComputeRoot, SystemdUnit, BeltTimerUnit, FleetConvergeTimerUnit, TailscaleServeMapping, DeployedTreeRemoteUnit, PublicationHelperTimerUnit, FabricStorageServeUnit, FabricStorageServeMapping, ApprovalBrokerFrontDoor, TailnetDoorServeUnit, + ServeBinary, GunbcSourceTree, SystemdUnit, BeltEventPathUnit, FleetConvergeTimerUnit, TailscaleServeMapping, DeployedTreeRemoteUnit, PublicationHelperTimerUnit, FabricStorageServeUnit, FabricStorageServeMapping, ApprovalBrokerFrontDoor, TailnetDoorServeUnit, deployment_tailnet_door_socket, deployment_tailnet_door_unit_name, deployment_tailnet_door_function_suffix, deployment_belt_service_unit_name, deployment_tree_remote_unit_name, - deployment_belt_timer_unit_name, - deployment_belt_service_unit_path, + deployment_belt_exit_path_unit_name, deployment_belt_exit_drain_service_unit_name, deployment_belt_answer_path_unit_name, deployment_belt_discovery_service_unit_name, + deployment_belt_exit_drain_service_unit_path, deployment_belt_answer_path_unit_path, deployment_belt_discovery_service_unit_path, + deployment_belt_service_unit_path, deployment_belt_unit_install_target, deployment_publication_service_unit_name, deployment_publication_timer_unit_name, deployment_publication_service_unit_path, @@ -180,6 +187,7 @@ import gunbc.host_layout { publication_request_dir, publication_answer_dir, publication_refused_dir, + attempt_exit_spool_root, attempt_exit_inbox_dir, attempt_exit_queued_dir, attempt_exit_running_dir, attempt_exit_done_dir, attempt_exit_refused_dir, } import gunbc.host_axis_caps { CapAuthorizationRefused, CapAuthorizationWithheld, @@ -740,36 +748,18 @@ fn emit_approval_broker_unit_doc_on(spec: DeploymentSpec, revision: ReleaseRevis -// THE PER-TICK WATCHDOG, and the 2026-09-21 measurement that put it here. A Type=oneshot unit with -// no start bound runs as long as the tick runs, and OnUnitInactiveSec only schedules the next tick -// from INACTIVITY -- so one tick that wedged in the SCM seed (measured that night: 2.25 hours at -// 100% CPU inside one capture, the timer showing no next elapse and no receipt distinguishing -// "nothing to do" from "stuck") halted the whole pipeline invisibly; the operator could not tell -// the frozen cadence from an idle belt. TimeoutStartSec is the bounded-reconciliation backstop in -// the workflow-execution ruling's sense: past the bound systemd kills the tick, the unit goes -// inactive(failed) LOUDLY, and the timer resumes on the next cadence -- one lost pass, never a -// lost night. The bound must clear the worst HONEST tick, which is first-capture-heavy: a fresh -// project envelope seeds the whole ~7,050-path intake base through the git bridge once (measured -// 2026-09-21 at roughly seven minutes of bridge time per project), and verification then runs the -// node's validations; it must stay far below the hours a wedge needs to be noticed by a human. -// The bound names the instrument that re-derives it, never a transcribed number (DESIGN section 6): -// a green tick's receipt at receipts/belt-last-tick.json carries its observed_at, and the belt's -// own stage timings re-derive the worst honest pass. 45min is the policy row: roughly six times -// the 2026-09-21 measured seed-bridge cost, far below the hours a wedge needs to be noticed by a -// human, and any honest growth beyond it must move this row with a reason, not the wedge. -data belt_tick_run_bound: String = "45min" fn live_deploy_belt_service_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { let svc = spec.service ServiceUnitFile { unit: [ - Description { text: "gunbc roadmap belt tick (one reconcile pass over the ready frontier)" as NonEmptyStr }, + Description { text: "gunbc roadmap belt launch (queues the one launch obligation: admission, spawn, teardown, discovery)" as NonEmptyStr }, After { target: "network-online.target" as NonEmptyStr }, Wants { target: "network-online.target" as NonEmptyStr }, ], service: concat([ ServiceType { mode: OneshotMode }, - TimeoutStartSec { duration: belt_tick_run_bound as NonEmptyStr }, + TimeoutStartSec { duration: systemd_time_span_of_seconds(d: belt_tick_run_bound) }, ServiceUser { name: svc.service_user }, ServiceGroup { name: svc.service_user }, ServiceSlice { name: deployment_slice_unit_name(names: spec.names) }, @@ -781,7 +771,7 @@ fn live_deploy_belt_service_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { ExecStart { command: join([ svc.serve_binary as String, " run --source-root dag --source-root src/v2", - " --entry dag/gunbc/roadmap/roadmap_belt_tick_cli.dag --function belt_run_once_cli", + " --entry dag/gunbc/roadmap/roadmap_belt_tick_cli.dag --function belt_launch_enqueue_cli", ], "") as NonEmptyStr }, ])), install: NotInstallable, @@ -850,7 +840,7 @@ fn emit_publication_timer_unit_doc(spec: DeploymentSpec) -> Doc { timer: [ TimerUnit { target: deployment_publication_service_unit_name(names: spec.names) }, OnBootSec { duration: "2min" as NonEmptyStr }, - OnUnitInactiveSec { duration: belt_tick_cadence as NonEmptyStr }, + OnUnitInactiveSec { duration: systemd_time_span_of_seconds(d: belt_tick_cadence) }, AccuracySec { duration: "5s" as NonEmptyStr }, ], install: Installable { directives: [WantedBy { target: "timers.target" as NonEmptyStr }] }, @@ -923,21 +913,91 @@ fn apply_publication_spool_steps(spec: DeploymentSpec) -> List { ] } -fn live_deploy_belt_timer_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { - TimerUnitFile { - unit: [Description { text: "gunbc roadmap belt tick cadence" as NonEmptyStr }], - timer: [ - TimerUnit { target: deployment_belt_service_unit_name(names: spec.names) }, - OnBootSec { duration: "2min" as NonEmptyStr }, - OnUnitInactiveSec { duration: belt_tick_cadence as NonEmptyStr }, - AccuracySec { duration: "5s" as NonEmptyStr }, +// THE BELT'S EVENT UNITS (dogfood intervention #4). Each service is one `gunbc run` entry of +// gunbc.roadmap_belt_tick_cli under the belt service's principal, slice and spawn root, so the two +// event services resolve the same instance the belt service does and start obligation units in the +// same user manager the belt's workers run in. +// +// THE EXIT PATH IS A LEVEL, NOT AN EDGE (extdeps.systemd.unit_file SystemdPathDirective): with +// DirectoryNotEmpty= systemd re-checks the inbox when the drain finishes, so an exit recorded while +// the drain was running starts it again instead of waiting for the next one. The drain moves every +// entry it hands off out of the inbox, so the inbox empties and the path goes quiet. `queued` is a +// second level on the same unit: it holds exits no run has CLAIMED (a run claims queued -> running +// before its first observation, so a long run does not keep the level up), so an exit that arrived +// while its attempt's unit was held, or that a lost unit or a reboot left behind, starts the drain +// again with no other event, and the drain's sweep hands it to a run. +// +// THE DRAIN SERVICE IS ALSO WANTED BY multi-user.target: starting once at boot is the reconciliation that +// returns a claim orphaned by a reboot (no stop hook ran) to `queued`. The path unit cannot do it -- the +// orphan sits in `running`, and DirectoryNotEmpty= on a directory a long run legitimately holds would +// re-trigger while the run lives and fail the path unit at its rate limit. A unit that ends while the +// manager is up is rescued by its own stop hook instead (belt_attempt_obligations_enqueue). +// +// THE PUBLICATION ANSWER PATH IS AN EDGE: answers are kept, not moved, so a level would fire forever. +// A change while discovery is running is coalesced into that run; discovery reads every attempt's +// evidence, so the coalesced answer is still seen. +fn live_deploy_belt_event_service_unit_file(spec: DeploymentSpec, description: String, function: String, install: SystemdInstallSection) -> SystemdUnitFile { + let svc = spec.service + ServiceUnitFile { + unit: [ + Description { text: description as NonEmptyStr }, + After { target: "network-online.target" as NonEmptyStr }, + Wants { target: "network-online.target" as NonEmptyStr }, ], - install: Installable { directives: [WantedBy { target: "timers.target" as NonEmptyStr }] }, + service: concat([ + ServiceType { mode: OneshotMode }, + TimeoutStartSec { duration: systemd_time_span_of_seconds(d: belt_tick_run_bound) }, + ServiceUser { name: svc.service_user }, + ServiceGroup { name: svc.service_user }, + ServiceSlice { name: deployment_slice_unit_name(names: spec.names) }, + WorkingDirectory { path: svc.repo_root }, + Environment { assignment: join([ + belt_spawn_workdir_env_var as String, "=", svc.repo_root as String, + ], "") as NonEmptyStr }, + ], concat(user_manager_environment_directives(uid: svc.service_uid), [ + ExecStart { command: join([ + svc.serve_binary as String, + " run --source-root dag --source-root src/v2", + " --entry dag/gunbc/roadmap/roadmap_belt_tick_cli.dag --function ", function, + ], "") as NonEmptyStr }, + ])), + install: install, + } +} + +fn live_deploy_belt_exit_drain_service_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { + live_deploy_belt_event_service_unit_file(spec: spec, description: "gunbc roadmap belt exit drain (hands recorded unit exits to their attempt obligations)", function: "belt_exit_drain_cli", install: Installable { directives: [WantedBy { target: deployment_belt_unit_install_target(names: spec.names) }] }) +} + +fn live_deploy_belt_discovery_service_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { + live_deploy_belt_event_service_unit_file(spec: spec, description: "gunbc roadmap belt discovery (queues outstanding obligations, bounded)", function: "belt_discover_once_cli", install: NotInstallable) +} + +fn live_deploy_belt_exit_path_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { + PathUnitFile { + unit: [Description { text: "gunbc roadmap belt: a unit exit was recorded" as NonEmptyStr }], + path: [ + PathUnit { target: deployment_belt_exit_drain_service_unit_name(names: spec.names) }, + DirectoryNotEmpty { path: attempt_exit_inbox_dir(instance_root: spec.service.instance_root as String) as NonEmptyStr }, + DirectoryNotEmpty { path: attempt_exit_queued_dir(instance_root: spec.service.instance_root as String) as NonEmptyStr }, + ], + install: Installable { directives: [WantedBy { target: "paths.target" as NonEmptyStr }] }, + } +} + +fn live_deploy_belt_answer_path_unit_file(spec: DeploymentSpec) -> SystemdUnitFile { + PathUnitFile { + unit: [Description { text: "gunbc roadmap belt: a publication answer arrived" as NonEmptyStr }], + path: [ + PathUnit { target: deployment_belt_discovery_service_unit_name(names: spec.names) }, + PathChanged { path: publication_answer_dir(instance_root: spec.service.instance_root as String) as NonEmptyStr }, + ], + install: Installable { directives: [WantedBy { target: "paths.target" as NonEmptyStr }] }, } } -fn emit_belt_timer_unit_doc(spec: DeploymentSpec) -> Doc { - doc_text(text: serialize_systemd_unit_file(file: live_deploy_belt_timer_unit_file(spec: spec))) +fn emit_belt_unit_file_doc(file: SystemdUnitFile) -> Doc { + doc_text(text: serialize_systemd_unit_file(file: file)) } // THE DEPLOYED TREE'S REMOTE, CONVERGED BY ITS OWNER. @@ -1014,7 +1074,7 @@ fn apply_belt_service_unit_write_step(spec: DeploymentSpec) -> PipelineStep { // Receipt *directory bootstrap* (existence + service-principal ownership) is on deploy, but NOT // here: this member's apply arm is a retirement poison (the cadence is on-demand via // fleet-converge.yml plan/apply, sleek-heron-218), so the bootstrap it used to carry has not run -// on any fresh instance root since that retirement. It rides BeltTimerUnit apply against the +// on any fresh instance root since that retirement. It rides BeltEventPathUnit apply against the // per-instance receipts directory instead — see the annotation on emit_artifact_upsert. Publication and // bootstrap remain orthogonal problems; only the bootstrap's home moved. fn emit_fleet_converge_service_unit_doc(spec: DeploymentSpec) -> Doc { @@ -1066,10 +1126,18 @@ fn apply_fleet_converge_timer_unit_write_step(spec: DeploymentSpec) -> PipelineS )) } -fn apply_belt_timer_unit_write_step(spec: DeploymentSpec) -> PipelineStep { +fn apply_belt_unit_file_write_step(stage_name: NonEmptyStr, file: SystemdUnitFile) -> PipelineStep { deploy_raw(command: deploy_stage_write_command( - stage_name: deployment_belt_timer_unit_name(names: spec.names) as String, - content: render(doc: emit_belt_timer_unit_doc(spec: spec), proto: live_deploy_protocol), + stage_name: stage_name as String, + content: render(doc: emit_belt_unit_file_doc(file: file), proto: live_deploy_protocol), + )) +} + +fn apply_belt_unit_file_install_step(stage_name: NonEmptyStr, dest: NonEmptyStr) -> PipelineStep { + deploy_raw(command: deploy_stage_install_command( + stage_name: stage_name as String, + mode: install_file_shared_mode, + dest: dest as String, )) } @@ -1115,10 +1183,7 @@ fn identity_member_of_step(step: DeploymentStep) -> ConvergedMember? { GunbcSourceTree => Present { value: TreePublication } ServeBinary => Present { value: Executable } SystemdUnit => Present { value: UnitFile } - DispatchWorktreeRoot => Present { value: DirectoryRoot { path: art.path } } - AttemptStateRoot => Present { value: DirectoryRoot { path: art.path } } - ComputeRoot => Present { value: DirectoryRoot { path: art.path } } - BeltTimerUnit => none + BeltEventPathUnit => none FleetConvergeTimerUnit => none TailscaleServeMapping => none DeployedTreeRemoteUnit => none @@ -1463,26 +1528,12 @@ fn install_d_owned_command(spec: DeploymentSpec, path: String) -> String { install_d_owned_for_user(user: spec.service.service_user, path: path) } -// THE DISPATCH THAT ADMITS A SECOND KIND OF ENSURED RESOURCE, written so the package arm is -// untouched. A package still lowers to exactly the ensure step it always did, at both privilege -// settings, so no existing emitted byte moves -- the widening adds a kind, it does not redefine -// what ensuring a package means.\n\nA directory lowers to the same owned install -d the artifact -// steps use, through the same single authority, with the owner taken from the ensured subject -// rather than from a deployment spec. That is what lets a host-level resource be stated without an -// instance: an ensured host directory is a fact about the host, and threading a spec through it -// would have made it look like a per-instance fact again -- the exact confusion that put this root -// in the owned roster to begin with.\n\nprivileged is consumed only by the package arm because it -// describes how apt is invoked; install -d is privileged in both contexts and takes the same form -// either way. +// AN ENSURED DEPENDENCY IS A PACKAGE. The directory arms that lowered an ensured host directory to an +// owned install -d are gone: every directory is an owned-directory target, emitted by +// owned_directory_apply_steps below from the provider's plan. privileged describes how apt is invoked. fn ensure_dependency_steps(dep: DeploymentDependencyStep, privileged: Bool) -> List { match dep.subject { EnsuredPackage { package } => [ensure_apt_package_step(pkg: package, privileged: privileged)] - EnsuredHostDirectory { path, owner } => [ - deploy_raw(command: install_d_owned_for_user(user: owner, path: path as String)), - ] - EnsuredManagedHostDirectory { admission } => - concat([deploy_raw(command: ensure_managed_directory_command(admission: admission))], - map(managed_directory_entry_repair_commands(admission: admission), c => deploy_raw(command: c))) } } @@ -1581,28 +1632,52 @@ fn deploy_publication_principal_ensure_command(spec: DeploymentSpec) -> String { }) } -// THE PROVIDER-STATE ENSURE RIDES THE PREAMBLE, DERIVED FROM THE SPEC RATHER THAN FOUND IN ITS STEP -// LIST (C7). Every apply emits it because gunbc.live_deploy.spec deployment_provider_state_step is a -// function of the spec, not a row an author may forget: there is no spec for which it returns -// nothing, so there is no state in which the deploy silently stops creating the directory the -// provider login writes into. That is the construction the earlier refusal arm was standing in for. -// -// Ensured means converged on every apply and never torn down, so it emits unconditionally rather -// than gated on a plan mutation: `install -d` is idempotent, and a converged host re-running it is -// what the category means. The read-back member reports the directory's OWNER; it does not decide -// whether the directory is created. -fn provider_state_ensure_steps(spec: DeploymentSpec) -> List { - ensure_dependency_steps(dep: deployment_provider_state_step(spec: spec), privileged: true) +fn deploy_apply_preamble_steps(access: DeployAccess, spec: DeploymentSpec) -> List { + [ + deploy_raw(command: deploy_sudoers_install_shell(access: access)), + deploy_raw(command: deploy_publication_principal_ensure_command(spec: spec)), + ] } -fn deploy_apply_preamble_steps(access: DeployAccess, spec: DeploymentSpec) -> List { - concat( - [ - deploy_raw(command: deploy_sudoers_install_shell(access: access)), - deploy_raw(command: deploy_publication_principal_ensure_command(spec: spec)), - ], - provider_state_ensure_steps(spec: spec), - ) +// ── EVERY DIRECTORY THE DEPLOYMENT DEMANDS, THROUGH THE ONE PROVIDER ───────────────────────────── +// +// The apply half of gunbc.owned_directory's lifecycle on this route. The decision was made at plan time +// over a host observation (gunbc.live_deploy.member_observe observe_owned_directory, the provider's +// owned_directory_decide), and it arrives here as each DirectoryRoot member's mutation: keep emits +// nothing, create and repair emit the provider's install plan (owned_directory_install_plan_of) as the +// granted `sudo -n install -d -m -o -g`. A refused decision never reaches the emitter -- it refused +// the whole derivation -- and the post-apply readback re-derives the plan through the same observer. +// A member the plan makes no claim about is emitted, never skipped (plan_mutation_of's rule). +// +// These run after the preamble (the sudoers grant they ride) and before every owned step, so no +// member -- the unit's process, the belt timer's tick -- starts before the directories it writes. +fn owned_directory_apply_steps(spec: DeploymentSpec, plan: ReleaseEffectPlan) -> List { + flat_map(deployment_directory_demands(spec: spec), t => + match plan_mutation_of(plan: plan, member: DirectoryRoot { path: owned_directory_path(t: t) as String as NonEmptyStr }) { + MemberNotInPlan => owned_directory_install_steps(t: t) + MutationInPlan { mutation } => + if mutation_is_keep(e: mutation) { [] } else { owned_directory_install_steps(t: t) } + }) +} + +// A TARGET WHOSE MODE DOES NOT DERIVE LOWERS TO A MARKER THAT IS NOT SHELL, never to an install with a +// guessed mode. A managed target whose dependents read each other's entries also carries the entry +// repair (managed_directory_entry_repair_commands), riding the same ensure as before. +data owned_directory_unplannable_poison: String = "__GUNBC_DEPLOY_REFUSED__ owned directory NOT ENSURED: gunbc.owned_directory owned_directory_install_plan_of has no plan: " + +fn owned_directory_install_steps(t: OwnedDirectoryTarget) -> List { + match owned_directory_install_plan_of(t: t) { + EnsurePlanUnavailable { reason } => [deploy_raw(command: concat(owned_directory_unplannable_poison, reason))] + EnsurePlanAvailable { plan } => + concat( + [deploy_raw(command: shell_privileged_command_text_of_argv(argv: owned_directory_install_argv(plan: plan)))], + match t { + OwnedDirectoryManaged { directory } => + map(managed_directory_entry_repair_commands(admission: managed_directory_admit(d: directory)), c => deploy_raw(command: c)) + OwnedDirectoryDeclared { member: _, path: _, owner: _, mode: _ } => [] + }, + ) + } } // THE CAP APPLY CONSUMES RETIREMENT AUTHORIZATION, AND WITHOUT ONE IT REFUSES. @@ -2784,17 +2859,6 @@ fn emit_artifact_upsert(art: DeploymentArtifactStep, spec: DeploymentSpec, revis tree_sync_restart_step_with_diagnosis(spec: spec), deploy_raw(command: repository_convergence_readback_command(spec: spec)), ])) - DispatchWorktreeRoot => [ - deploy_raw(command: install_d_owned_command(spec: spec, path: art.path as String)), - ] - AttemptStateRoot => [ - deploy_raw(command: install_d_managed_command( - dir: attempt_state_directory_at(path: art.path as String as FilePath), - )), - ] - ComputeRoot => [ - deploy_raw(command: install_d_owned_command(spec: spec, path: art.path as String)), - ] SystemdUnit => concat(concat([ apply_systemd_unit_write_step(spec: spec, revision: revision), deploy_raw(command: deploy_stage_install_command( @@ -2827,26 +2891,37 @@ fn emit_artifact_upsert(art: DeploymentArtifactStep, spec: DeploymentSpec, revis deploy_effect(inv: systemctl_restart_effect(unit: deployment_publication_timer_unit_name(names: spec.names))), ] ) - BeltTimerUnit => [ + BeltEventPathUnit => [ deploy_raw(command: install_d_owned_command( spec: spec, path: instance_receipts_dir(instance_root: spec.service.instance_root as String), )), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_spool_root(instance_root: spec.service.instance_root as String))), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_inbox_dir(instance_root: spec.service.instance_root as String))), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_queued_dir(instance_root: spec.service.instance_root as String))), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_running_dir(instance_root: spec.service.instance_root as String))), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_done_dir(instance_root: spec.service.instance_root as String))), + deploy_raw(command: install_d_owned_command(spec: spec, path: attempt_exit_refused_dir(instance_root: spec.service.instance_root as String))), apply_belt_service_unit_write_step(spec: spec), - apply_belt_timer_unit_write_step(spec: spec), + apply_belt_unit_file_write_step(stage_name: deployment_belt_exit_drain_service_unit_name(names: spec.names), file: live_deploy_belt_exit_drain_service_unit_file(spec: spec)), + apply_belt_unit_file_write_step(stage_name: deployment_belt_discovery_service_unit_name(names: spec.names), file: live_deploy_belt_discovery_service_unit_file(spec: spec)), + apply_belt_unit_file_write_step(stage_name: deployment_belt_exit_path_unit_name(names: spec.names), file: live_deploy_belt_exit_path_unit_file(spec: spec)), + apply_belt_unit_file_write_step(stage_name: deployment_belt_answer_path_unit_name(names: spec.names), file: live_deploy_belt_answer_path_unit_file(spec: spec)), deploy_raw(command: deploy_stage_install_command( stage_name: deployment_belt_service_unit_name(names: spec.names) as String, mode: install_file_shared_mode, dest: deployment_belt_service_unit_path(names: spec.names) as String, )), - deploy_raw(command: deploy_stage_install_command( - stage_name: deployment_belt_timer_unit_name(names: spec.names) as String, - mode: install_file_shared_mode, - dest: art.path as String, - )), + apply_belt_unit_file_install_step(stage_name: deployment_belt_exit_drain_service_unit_name(names: spec.names), dest: deployment_belt_exit_drain_service_unit_path(names: spec.names)), + apply_belt_unit_file_install_step(stage_name: deployment_belt_discovery_service_unit_name(names: spec.names), dest: deployment_belt_discovery_service_unit_path(names: spec.names)), + apply_belt_unit_file_install_step(stage_name: deployment_belt_answer_path_unit_name(names: spec.names), dest: deployment_belt_answer_path_unit_path(names: spec.names)), + apply_belt_unit_file_install_step(stage_name: deployment_belt_exit_path_unit_name(names: spec.names), dest: art.path), deploy_effect(inv: systemctl_daemon_reload_effect()), - deploy_effect(inv: systemctl_enable_effect(unit: deployment_belt_timer_unit_name(names: spec.names))), - deploy_effect(inv: systemctl_restart_effect(unit: deployment_belt_timer_unit_name(names: spec.names))), + deploy_effect(inv: systemctl_enable_effect(unit: deployment_belt_exit_drain_service_unit_name(names: spec.names))), + deploy_effect(inv: systemctl_enable_effect(unit: deployment_belt_exit_path_unit_name(names: spec.names))), + deploy_effect(inv: systemctl_restart_effect(unit: deployment_belt_exit_path_unit_name(names: spec.names))), + deploy_effect(inv: systemctl_enable_effect(unit: deployment_belt_answer_path_unit_name(names: spec.names))), + deploy_effect(inv: systemctl_restart_effect(unit: deployment_belt_answer_path_unit_name(names: spec.names))), ] FleetConvergeTimerUnit => [ deploy_raw(command: "__GUNBC_FLEET_CONVERGE_REFUSED__ FleetConvergeTimerUnit retired from deployment membership — timer cadence is on-demand via fleet-converge.yml plan/apply, not live_deploy apply (sleek-heron-218)"), @@ -2914,11 +2989,11 @@ fn emit_artifact_upsert(art: DeploymentArtifactStep, spec: DeploymentSpec, revis // remove this store even by mistake -- the property the previous `rm -d` arm tried to approximate // by refusing on a non-empty directory is now structural, and the awkward consequence that arm // carried (a full retract could never complete once the store existed) simply does not arise. -// DispatchWorktreeRoot teardown is rm -d (empty-directory removal via the granted rm — the +// The dispatch worktree root's refusal is rm -d (empty-directory removal via the granted rm — the // unprivileged principal has no rmdir grant and no write on the root-owned parent), NEVER rm -rf: a // non-empty worktree root means live agent worktrees exist, so the retract FAILS LOUDLY there // instead of silently destroying sessions (DESIGN 5 — the refusal is the empty-dir precondition -// itself, identical to the former rmdir). GunbcSourceTree teardown runs AFTER the worktree rm -d +// itself, identical to the former rmdir). GunbcSourceTree teardown runs its rm -rf only AFTER that rm -d // succeeds, so the repo the worktrees are registered in is never deleted from under them. fn emit_artifact_teardown(art: DeploymentArtifactStep, spec: DeploymentSpec) -> List { @@ -2956,11 +3031,17 @@ fn emit_artifact_teardown(art: DeploymentArtifactStep, spec: DeploymentSpec) -> deploy_effect(inv: systemctl_daemon_reload_effect()), ] ) - BeltTimerUnit => concat( - systemctl_disable_now_if_present_steps(unit: deployment_belt_timer_unit_name(names: spec.names), privileged: true), + BeltEventPathUnit => concat( + concat( + systemctl_disable_now_if_present_steps(unit: deployment_belt_exit_path_unit_name(names: spec.names), privileged: true), + systemctl_disable_now_if_present_steps(unit: deployment_belt_answer_path_unit_name(names: spec.names), privileged: true), + ), [ deploy_raw(command: gunbc.live_deploy.operations.rm_force_command(paths: [ art.path as String, + deployment_belt_answer_path_unit_path(names: spec.names) as String, + deployment_belt_exit_drain_service_unit_path(names: spec.names) as String, + deployment_belt_discovery_service_unit_path(names: spec.names) as String, deployment_belt_service_unit_path(names: spec.names) as String, ])), deploy_effect(inv: systemctl_daemon_reload_effect()), @@ -2993,30 +3074,38 @@ fn emit_artifact_teardown(art: DeploymentArtifactStep, spec: DeploymentSpec) -> deploy_effect(inv: systemctl_daemon_reload_effect()), ] ) - DispatchWorktreeRoot => [ - deploy_raw(command: rm_empty_directory_command(path: art.path as String)), - ] - AttemptStateRoot => [ - deploy_raw(command: rm_empty_directory_command(path: art.path as String)), - ] - ComputeRoot => [ - deploy_raw(command: rm_force_recursive_command(path: art.path as String)), - ] ServeBinary => [ deploy_raw(command: gunbc.live_deploy.operations.rm_force_command(paths: [art.path as String])), ] - GunbcSourceTree => [ - deploy_raw(command: gunbc.live_deploy.operations.rm_force_command(paths: [ - tree_sync_unit_path(names: spec.names) as String, - spec.names.tree_sync_env_path as String, - deployment_slice_unit_path(names: spec.names) as String, - ])), - deploy_effect(inv: systemctl_daemon_reload_effect()), - deploy_raw(command: rm_force_recursive_command(path: art.path as String)), - ] + GunbcSourceTree => concat( + map(dispatch_root_refusal_paths(spec: spec), p => deploy_raw(command: rm_empty_directory_command(path: p))), + [ + deploy_raw(command: gunbc.live_deploy.operations.rm_force_command(paths: [ + tree_sync_unit_path(names: spec.names) as String, + spec.names.tree_sync_env_path as String, + deployment_slice_unit_path(names: spec.names) as String, + ])), + deploy_effect(inv: systemctl_daemon_reload_effect()), + deploy_raw(command: rm_force_recursive_command(path: art.path as String)), + ], + ) } } +// THE SOURCE TREE'S TEARDOWN KEEPS THE DISPATCH ROOTS' EMPTY-DIRECTORY REFUSAL. The two roots were +// owned steps whose teardown ran before the tree's and refused on a live worktree or published attempt +// evidence; they are owned-directory demands now and a retract does not tear directories down, but +// deleting the tree a live worktree is registered in is the hazard that refusal stood for, so the +// tree's own teardown begins with it. The paths are the spec's demands, read by member name. +fn dispatch_root_refusal_paths(spec: DeploymentSpec) -> List { + map( + filter(xs: spec.directories, predicate: t => + (owned_directory_member(t: t) as String) == (dashboard_dispatch_worktree_member as String) + || (owned_directory_member(t: t) as String) == (dashboard_attempt_state_member as String)), + t => owned_directory_path(t: t) as String, + ) +} + // The per-member realization HANDLER: share the DIFF (membership_reconcile), dispatch the APPLY // (this). One handler for both fold directions — EffectAdd dispatches on member kind to the // artifact/dependency install fragments; EffectRemove to the removal fragments. Orchestration is @@ -3093,12 +3182,7 @@ fn apply_step_effects( ) -> List { match identity_member_of_step(step: step) { Present { value: member } => match member { - DirectoryRoot { path: _ } => match plan_mutation_of(plan: plan, member: member) { - MemberNotInPlan => step_membership_effects(step: step, effects: effects, spec: spec, plan: plan) - MutationInPlan { mutation } => - if mutation_is_keep(e: mutation) { [] } - else { step_membership_effects(step: step, effects: effects, spec: spec, plan: plan) } - } + DirectoryRoot { path: _ } => [] TreePublication => emit_release_member_effects(member: member, plan: plan, spec: spec) Executable => emit_release_member_effects(member: member, plan: plan, spec: spec) UnitFile => emit_release_member_effects(member: member, plan: plan, spec: spec) @@ -3168,7 +3252,10 @@ fn apply_intent_from_effects( steps: concat( deploy_apply_frame_steps(spec: spec), concat( - deploy_apply_preamble_steps(access: access, spec: spec), + concat( + deploy_apply_preamble_steps(access: access, spec: spec), + owned_directory_apply_steps(spec: spec, plan: plan), + ), concat( deploy_memory_cap_apply_steps(spec: spec, authorization: cap_authorization), concat( diff --git a/dag/gunbc/live_deploy/fleet_request.dag b/dag/gunbc/live_deploy/fleet_request.dag index 72306cea507..be630906f8d 100644 --- a/dag/gunbc/live_deploy/fleet_request.dag +++ b/dag/gunbc/live_deploy/fleet_request.dag @@ -50,6 +50,7 @@ import gunbc.live_deploy.member_identity { MemberObserved, MemberAbsentOnHost, MemberUnobservable, + MemberRefusedOnHost, MemberDecision, AlreadyConverged, Create, @@ -169,6 +170,7 @@ fn live_deploy_observe_supplied( MemberUnobservable { member: _, cause: c } => ObservationRefused { cause: c } MemberObserved { identity: _ } => ObservationEstablished { observed: o } MemberAbsentOnHost { member: _ } => ObservationEstablished { observed: o } + MemberRefusedOnHost { member: _, cause: _ } => ObservationEstablished { observed: o } } } } @@ -235,6 +237,7 @@ fn first_unreadable(observed: List) -> LiveDeployAdmissionRefusa Present { value: LiveDeployUnreadableMember { member: m, cause: c } } MemberObserved { identity: _ } => none MemberAbsentOnHost { member: _ } => none + MemberRefusedOnHost { member: _, cause: _ } => none } }) } diff --git a/dag/gunbc/live_deploy/member_identity.dag b/dag/gunbc/live_deploy/member_identity.dag index ca0d9674e63..6d0d8f9ed07 100644 --- a/dag/gunbc/live_deploy/member_identity.dag +++ b/dag/gunbc/live_deploy/member_identity.dag @@ -35,8 +35,8 @@ import extdeps.systemd { SystemdUnitActiveState, Active, Inactive, Activating, D // executable fixture in member_identity_witness_test is exactly that control. // A MEMBER CARRIES ITS OWN KEY WHERE IT HAS ONE. The release members are singular -- there is one // executable, one published tree, one serve unit -- so they are nullary. A directory root is not: -// the deployment owns several, they differ only by path, and minting DispatchWorktreeRoot / -// AttemptStateRoot / ComputeRoot as nullary variants would be several names +// the deployment owns several, they differ only by path, and minting a nullary variant per root +// (one per dispatch, compute or store directory) would be several names // for one concept (DESIGN section 2, one concept at every breadth). The path is the key, so it // lives on the variant and converged_member_label renders it -- which is also what keeps // converged_member_eq correct for a payload-carrying member, since that equality is defined @@ -94,15 +94,21 @@ type ServiceProcessIdentity = ProcessRunningRelease { revision: CommitSha } | ProcessNotRunning { active: SystemdUnitActiveState } -// A ROOT'S IDENTITY IS ITS OWNING PRINCIPAL, not its presence. Presence is already carried by -// ObservedMember (an established absence is MemberAbsentOnHost and derives a Create), so an identity -// that repeated it would be the same fact in two carriers. What presence CANNOT say is whether the -// directory that is there belongs to the principal that must write it -- a root owned by root:root -// is present, and the service user still cannot use it, which is the failure a fresh host and a -// hand-repaired host both produce. So the identity is the owner, and a wrong owner is a Replace. +// A ROOT'S IDENTITY IS THE OWNED-DIRECTORY PROVIDER'S STANDING FOR IT, not a second judgement of owner +// and mode made here. Presence is already carried by ObservedMember (an established absence is +// MemberAbsentOnHost and derives a Create), and gunbc.owned_directory owned_directory_decide is the one +// authority on what a present directory's owner, group and mode mean: converged, a mode the provider +// will re-apply on a directory the demanded principal already owns, or a refusal. The refusal is not an +// identity at all -- a directory under another principal is never re-owned -- so it arrives as +// MemberRefusedOnHost and refuses the whole derivation. The owner used to be this identity, and a wrong +// owner derived a Replace that re-owned the directory: adoption from an observation, now deleted. +type DirectoryRootStanding + = RootConverged + | RootModeRepairable { observed_mode: NonEmptyStr } + type DirectoryRootIdentity { path: NonEmptyStr - owner: NonEmptyStr + standing: DirectoryRootStanding } type MemberIdentity @@ -132,12 +138,14 @@ type ObservedMember = MemberObserved { identity: MemberIdentity } | MemberAbsentOnHost { member: ConvergedMember } | MemberUnobservable { member: ConvergedMember, cause: String } + | MemberRefusedOnHost { member: ConvergedMember, cause: String } fn observed_member_member(o: ObservedMember) -> ConvergedMember { match o { MemberObserved { identity } => member_identity_member(i: identity) MemberAbsentOnHost { member } => member MemberUnobservable { member, cause: _ } => member + MemberRefusedOnHost { member, cause: _ } => member } } @@ -186,8 +194,15 @@ fn service_process_identity_wire(p: ServiceProcessIdentity) -> String { } } +fn directory_root_standing_wire(s: DirectoryRootStanding) -> String { + match s { + RootConverged => "converged" + RootModeRepairable { observed_mode } => join(["mode-repairable=", observed_mode as String], "") + } +} + fn directory_root_identity_wire(r: DirectoryRootIdentity) -> String { - join([r.path as String, ":owner=", r.owner as String], "") + join([r.path as String, ":", directory_root_standing_wire(s: r.standing)], "") } // TOTAL: every MemberIdentity arm contributes every field that distinguishes it. A forgotten @@ -209,6 +224,8 @@ fn observed_member_wire(o: ObservedMember) -> String { MemberAbsentOnHost { member: m } => join(["member-absent:", converged_member_label(m: m)], "") MemberUnobservable { member: m, cause: c } => join(["member-unobservable:", converged_member_label(m: m), ":", c], "") + MemberRefusedOnHost { member: m, cause: c } => + join(["member-refused:", converged_member_label(m: m), ":", c], "") } } @@ -224,6 +241,7 @@ type MemberRefusalCause = DesiredIdentityIsForAnotherMember { desired: ConvergedMember } | ObservationIsOfAnotherMember { observed: ConvergedMember } | HostUnobservable { cause: String } + | HostStateRefused { cause: String } | DecisionFiledUnderAnotherMember { found: ConvergedMember } | DesiredExecutableUnreadable { cause: String } | DesiredExecutableAbsent @@ -233,6 +251,7 @@ fn member_refusal_text(c: MemberRefusalCause) -> String { DesiredIdentityIsForAnotherMember { desired } => join(["desired identity is for ", converged_member_label(m: desired)], "") ObservationIsOfAnotherMember { observed } => join(["observation is of ", converged_member_label(m: observed)], "") HostUnobservable { cause } => join(["could not observe the host: ", cause], "") + HostStateRefused { cause } => join(["the host holds a state this deploy refuses to change: ", cause], "") DecisionFiledUnderAnotherMember { found } => join(["decision slot holds a decision about ", converged_member_label(m: found)], "") DesiredExecutableUnreadable { cause } => join(["the release executable could not be read in the deploying checkout: ", cause], "") DesiredExecutableAbsent => "the release executable target/release/gunbc is absent from the deploying checkout; build before deploying" @@ -325,7 +344,7 @@ fn member_identity_eq(a: MemberIdentity, b: MemberIdentity) -> Bool { RootAt { root: _ } => false } RootAt { root: ra } => match b { - RootAt { root: rb } => (ra.path as String) == (rb.path as String) && (ra.owner as String) == (rb.owner as String) + RootAt { root: rb } => (ra.path as String) == (rb.path as String) && directory_root_standing_wire(s: ra.standing) == directory_root_standing_wire(s: rb.standing) TreeIdentity { tree: _ } => false ExecutableIdentity { digest: _ } => false UnitFileAt { file: _ } => false @@ -351,6 +370,7 @@ fn decide_member(member: ConvergedMember, desired: MemberIdentity, observed: Obs } else { match observed { MemberUnobservable { member: _, cause } => MemberRefused { member: member, cause: HostUnobservable { cause: cause } } + MemberRefusedOnHost { member: _, cause } => MemberRefused { member: member, cause: HostStateRefused { cause: cause } } MemberAbsentOnHost { member: _ } => Create { member: member, to: desired } MemberObserved { identity } => if member_identity_eq(a: identity, b: desired) { AlreadyConverged { member: member } } @@ -475,7 +495,7 @@ type MemberMutation | DaemonReload | RestartService | CreateRoot - | ReownRoot + | RepairRootMode fn member_mutation_label(e: MemberMutation) -> String { match e { @@ -486,7 +506,7 @@ fn member_mutation_label(e: MemberMutation) -> String { DaemonReload => "daemon-reload" RestartService => "restart-service" CreateRoot => "create-root" - ReownRoot => "reown-root" + RepairRootMode => "repair-root-mode" } } @@ -524,7 +544,7 @@ type EffectDerivation // unit file changed -> WriteUnit // manager stale OR the unit file changed -> DaemonReload // any release member changed, or the process itself is stale or down -> RestartService -// a root is absent -> CreateRoot; a root is owned by the wrong principal -> ReownRoot +// a root is absent -> CreateRoot; a root the demanded principal owns at another mode -> RepairRootMode (a root under another principal refuses, never re-owned) // nothing changed -> KeepMember // // A ROOT DOES NOT RESTART THE SERVE PROCESS, and that is a claim rather than an omission. Roots are @@ -555,7 +575,7 @@ fn member_mutation_for(d: MemberDecision, decisions: List) -> Me ServiceProcess => if release_changed || decision_changes(d: d) { RestartService } else { KeepMember } DirectoryRoot { path: _ } => match d { Create { member: _, to: _ } => CreateRoot - Replace { member: _, from: _, to: _ } => ReownRoot + Replace { member: _, from: _, to: _ } => RepairRootMode AlreadyConverged { member: _ } => KeepMember MemberRefused { member: _, cause: _ } => KeepMember } @@ -648,7 +668,7 @@ fn mutation_is_keep(e: MemberMutation) -> Bool { DaemonReload => false RestartService => false CreateRoot => false - ReownRoot => false + RepairRootMode => false } } @@ -684,7 +704,7 @@ fn member_row_label(r: MemberEffectRow, target: ReleaseTarget) -> List { DaemonReload => ["daemon-reload"] RestartService => [join(["restart ", target.unit as String], "")] CreateRoot => [join(["create-root ", converged_member_label(m: r.member)], "")] - ReownRoot => [join(["reown-root ", converged_member_label(m: r.member)], "")] + RepairRootMode => [join(["repair-root-mode ", converged_member_label(m: r.member)], "")] } } diff --git a/dag/gunbc/live_deploy/member_observe.dag b/dag/gunbc/live_deploy/member_observe.dag index 76ada9499ed..5db06bfb4fe 100644 --- a/dag/gunbc/live_deploy/member_observe.dag +++ b/dag/gunbc/live_deploy/member_observe.dag @@ -1,6 +1,7 @@ module gunbc.live_deploy.member_observe import std.types { String, Bool, List, Int, NonEmptyStr, CommitSha, FilePath } +import std.optional { Optional, Present, Absent } import std.algebra { trim } import std.content_hash { content_hash_atom } import extdeps.crypto.hash { Digest, sha256sum_argv, sha256sum_line_digest } @@ -20,11 +21,21 @@ import gunbc.host_effect_realize { run_typed_argv_transport } import gunbc.typed_argv_exec { TypedArgvExecOutcome, TypedArgvExecConverged, TypedArgvExecRefused } import gunbc.live_deploy.spec { DeploymentSpec, deployment_systemd_unit_path, deployment_plan_listen_port, - DeploymentStep, ArtifactStep, Dependency, - DispatchWorktreeRoot, AttemptStateRoot, ComputeRoot, - deployment_provider_state_path, + deployment_directory_demands, } import gunbc.live_deploy.candidate_identity { CandidateRelease } +import gunbc.executor_privileged_operation { ExecutorPrivilegedOperation, ListDirectoryEntries, ReadDirectoryIdentity, executor_privileged_operation_elevated_command, executor_privileged_operation_elevated_argv } +import gunbc.command_runner { run_shell_command_observe_outcome } +import extdeps.exec.command { LocalExec } +import extdeps.shell.exec { ProcessRan, ProcessNotSpawned } +import gunbc.ensure { EnsureMayApply, EnsureObservation, EnsureObserved, EnsureAbsent, EnsureInaccessible } +import std.upsert_decision { Noop, Apply, Refuse } +import extdeps.access.posix { file_mode_octal } +import gunbc.owned_directory { + OwnedDirectoryTarget, OwnedDirectoryReading, owned_directory_path, owned_directory_demand_at, + owned_directory_observe, owned_directory_decide, + ObserverRead, ObserverReadOut, ObserverReadFailed, ObserverNotSpawned, ObserverTransportRefused, ObserverExited, +} import gunbc.live_deploy.emit { live_deploy_systemd_unit_for } import gunbc.live_deploy.release_locus { ReleaseRevisionBinding, RevisionBoundAtEmission } import gunbc.live_deploy.readiness { live_deploy_healthz_get_for_port, healthz_release_identity_refusal, healthz_refusal_reason } @@ -43,7 +54,7 @@ import gunbc.live_deploy.member_identity { ObservedMember, MemberObserved, MemberAbsentOnHost, MemberUnobservable, MemberDecision, MemberRefused, DesiredExecutableUnreadable, DesiredExecutableAbsent, DesiredIdentityIsForAnotherMember, decide_member, release_member_roster, MemberDecision, ConvergedMember, ReleaseTarget, - DirectoryRoot, DirectoryRootIdentity, RootAt, + DirectoryRoot, DirectoryRootIdentity, RootAt, DirectoryRootStanding, RootConverged, RootModeRepairable, MemberRefusedOnHost, EffectDerivation, EffectsDerived, EffectsRefused, derive_member_plan, effect_labels, plan_mutation_count, } @@ -159,20 +170,26 @@ fn entry_presence_in_parent(arm: ObservationArm, parent: String, name: String) - } fn entry_presence(arm: ObservationArm, path: String) -> EntryPresence { + entry_presence_above(arm: arm, path: path, below: Absent) +} + +// The ancestor walk in tail position, so the Rust realization lowers it to a loop. `below` is the +// cause read one level down: an ancestor that is present answers with that cause, an absent one +// answers absent, and the root answers with its own cause -- the same answers the nested walk gave. +fn entry_presence_above(arm: ObservationArm, path: String, below: Optional) -> EntryPresence { let parent = path_parent(path: path) let name = path_basename(path: path) match entry_presence_in_parent(arm: arm, parent: parent, name: name) { - EntryPresent => EntryPresent + EntryPresent => match below { + Absent => EntryPresent + Present { value: cause } => EntryPresenceIndeterminate { cause: cause } + } EntryAbsent => EntryAbsent EntryPresenceIndeterminate { cause } => if parent == "/" { EntryPresenceIndeterminate { cause: cause } } else { - match entry_presence(arm: arm, path: parent) { - EntryAbsent => EntryAbsent - EntryPresent => EntryPresenceIndeterminate { cause: cause } - EntryPresenceIndeterminate { cause: ancestor_cause } => EntryPresenceIndeterminate { cause: ancestor_cause } - } + entry_presence_above(arm: arm, path: parent, below: Present { value: cause }) } } } @@ -493,97 +510,111 @@ fn desired_tree(candidate: CandidateRelease) -> MemberIdentity { // mapping the roster makes them one. Adding a member is a row in the roster and an arm in each of // these two total functions -- the compiler names the arm it is missing, which a record of slots // could not do because a slot left unwritten is a type error somewhere else entirely. -// THE ROOT ROSTER COMES FROM THE SPEC, not from a list here. A second host, or the twin, owns -// different roots at different paths, and a roster written here would be a second authority for -// what the deployment owns -- the steps already say it. The four directory roots join as -// DirectoryRoot; the timer units and the tailscale route stay on the apply-all path and -// contribute no member. A kind that names no member emits exactly as it does now. The -// provider-state root is Ensured, not Owned: retract never tears it down. It is still a -// DirectoryRoot for readback (C7). Its presence and owner are read like any root's, so a host whose -// provider login has nowhere to write shows as a member to create, not as a converged host. -fn root_members_of_step(st: DeploymentStep) -> List { - match st { - ArtifactStep { step: art } => match art.kind { - DispatchWorktreeRoot => [DirectoryRoot { path: art.path }] - AttemptStateRoot => [DirectoryRoot { path: art.path }] - ComputeRoot => [DirectoryRoot { path: art.path }] - ServeBinary => [] - GunbcSourceTree => [] - SystemdUnit => [] - BeltTimerUnit => [] - FleetConvergeTimerUnit => [] - TailscaleServeMapping => [] - DeployedTreeRemoteUnit => [] - PublicationHelperTimerUnit => [] - FabricStorageServeUnit => [] - FabricStorageServeMapping => [] - ApprovalBrokerFrontDoor => [] - TailnetDoorServeUnit => [] - } - Dependency { step: _ } => [] - } -} - -// THE PROVIDER-STATE ROOT JOINS THE ROSTER FROM THE SPEC, NOT FROM ITS STEPS (C7). Its path and -// owner are derived by gunbc.live_deploy.spec deployment_provider_state_step, the same function the -// emitted ensure is built from, so the directory that is created and the directory that is read -// back cannot name two different paths or two different owners. Deriving it here rather than -// matching a step is what makes its absence unwritable: no spec can drop the member, so nothing -// has to refuse a spec that did. +// THE ROOT ROSTER IS THE DEPLOYMENT'S DIRECTORY DEMANDS, not a list here and not the steps. Every +// directory the deployment creates is an owned-directory target in +// gunbc.live_deploy.spec deployment_directory_demands -- the instance's consumers' demands +// (gunbc.roadmap.dashboard_instance_directories) and the host's -- and each joins the roster as one +// DirectoryRoot, so the directories created and the directories read back are one list. The first +// roster read the dispatch roots and the compute root off the step kinds and the provider-state root +// off the spec, and left out every auth store, the event worktree and the inbox: the production route +// neither created nor read them back. fn deployment_member_roster(spec: DeploymentSpec) -> List { concat( - concat(release_member_roster(), flat_map(spec.steps, st => root_members_of_step(st: st))), - [DirectoryRoot { path: deployment_provider_state_path(spec: spec) }], + release_member_roster(), + map(deployment_directory_demands(spec: spec), t => DirectoryRoot { path: owned_directory_path(t: t) as String as NonEmptyStr }), ) } -// A ROOT'S OWNER IS READ, NOT ASSUMED. Presence alone would call a root:root-owned directory -// converged, and the service user still could not write it -- which is the state a hand-repaired -// host leaves and the state a fresh host reaches if install ran under the wrong principal. -// THE LOCAL ARM CALLS THE MODELLED OPERATION; ONLY THE REMOTE ARM GOES THROUGH THE TRANSPORT. -// -// The first version of this routed BOTH arms through run_typed_argv_transport, handing it LocalShell -// for the host-itself case. That refuses by construction -- "LocalShell has no remote target" -- so -// root ownership could not be read on the host the deploy actually runs on, and the whole member -// plan refused. It failed closed and mutated nothing, but it meant the roots could never be -// observed in production. -// -// IT SURVIVED ITS WITNESSES BECAUSE THEY NEVER EXECUTED A HOST EFFECT. The member-identity witnesses -// are hermetic and supply observations directly, so nothing exercised this leg until the first wet -// operator deploy. Every other local arm in this module calls its modelled operation directly -// (Filesystem.List, git.Inspect.HeadCommitIn, shell.Mktemp.FileInDirectory); this one now does too. -fn root_owner_leg(arm: ObservationArm, path: String) -> Leg { - let leg = join(["reading the owner of ", path], "") +// THE DIRECTORY'S METADATA, READ UNDER THE DEPLOY PRINCIPAL'S PINNED READ-ONLY GRANTS. A managed +// directory may admit only its owner (the fabric store's root derives owner-execute alone), so the job +// user can neither list it nor stat what is under it, while a correctly protected tree must still read +// as converged in planning and readback. Both reads are gunbc.executor_privileged_operation operations +// bound to one named directory -- ListDirectoryEntries and ReadDirectoryIdentity -- rendered into the +// runner host's sudoers by gunbc.runner_host_grants for exactly the directories this deployment +// demands and their ancestors, the same authorization machinery every other root operation of this +// principal goes through; no path-unrestricted grant is added. The local arm runs the elevated command +// with its spawn as a value (gunbc.command_runner run_shell_command_observe_outcome); the remote arm +// sends the same elevated argv through the transport. Either way the read's origin stays typed +// (gunbc.owned_directory ObserverFailure) and presence and absence are the provider's to decide. +fn privileged_directory_read(arm: ObservationArm, op: ExecutorPrivilegedOperation) -> ObserverRead { match arm { - OnTheHostItself => { - let r = shell.Stat.OwnerOf(path: path as FilePath) - if r.success { LegOut { stdout: r.stdout } } - else { LegRefused { cause: join([leg, " failed: ", r.stderr], "") } } - } + OnTheHostItself => + match run_shell_command_observe_outcome(command: executor_privileged_operation_elevated_command(op: op), transport: LocalExec) { + ProcessNotSpawned { program: _, cause } => ObserverReadFailed { origin: ObserverNotSpawned { cause: cause as String } } + ProcessRan { observation: o } => + if o.exit_code == 0 { ObserverReadOut { stdout: o.stdout } } + else { ObserverReadFailed { origin: ObserverExited { exit_code: o.exit_code, stderr: o.stderr } } } + } OverTransport { transport } => - remote_leg(transport: transport, argv: ["stat", "-c", "%U", "--", path], leg: leg) + match run_typed_argv_transport(argv: executor_privileged_operation_elevated_argv(op: op), transport: transport) { + TypedArgvExecRefused { reason } => ObserverReadFailed { origin: ObserverTransportRefused { cause: reason } } + TypedArgvExecConverged { result: r } => + if r.exit_code == 0 { ObserverReadOut { stdout: r.stdout } } + else { ObserverReadFailed { origin: ObserverExited { exit_code: r.exit_code, stderr: r.stderr } } } + } } } -fn observe_directory_root(arm: ObservationArm, path: NonEmptyStr) -> ObservedMember { +fn owned_directory_observation_on_host(arm: ObservationArm, path: String) -> EnsureObservation { + owned_directory_observe( + path: path as FilePath, + list: p => privileged_directory_read(arm: arm, op: ListDirectoryEntries { path: p as String as NonEmptyStr }), + stat: p => privileged_directory_read(arm: arm, op: ReadDirectoryIdentity { path: p as String as NonEmptyStr }), + ) +} + +// THE PROVIDER DECIDES; THIS ONLY PROJECTS ITS DECISION ONTO THE MEMBER VOCABULARY. The decision is +// gunbc.owned_directory owned_directory_decide's -- the same one the dashboard apply's provider makes -- +// so a directory is converged, created, repaired or refused here for exactly the reasons it would be +// there: +// Noop -> observed at RootConverged (the desired identity: keep) +// Apply, the directory absent -> MemberAbsentOnHost (Create -> CreateRoot) +// Apply, the directory present -> observed at RootModeRepairable (Replace -> RepairRootMode) +// Refuse, nothing could be read -> MemberUnobservable (refuses the derivation) +// Refuse, a state it will not change (another principal's directory, a non-directory, a mode that +// cannot be derived) -> MemberRefusedOnHost (refuses the derivation; never re-owned) +// The post-apply readback (deployment_identity_readback) re-derives through this same function, so +// the readback is a second independent observation of every demanded directory. +fn observe_owned_directory(arm: ObservationArm, spec: DeploymentSpec, path: NonEmptyStr) -> ObservedMember { let member = DirectoryRoot { path: path } - match entry_presence(arm: arm, path: path as String) { - EntryPresenceIndeterminate { cause } => MemberUnobservable { member: member, cause: cause } - EntryAbsent => MemberAbsentOnHost { member: member } - EntryPresent => match root_owner_leg(arm: arm, path: path as String) { - LegRefused { cause } => MemberUnobservable { member: member, cause: cause } - LegOut { stdout } => { - let owner = trim(stdout) - if owner == "" { - MemberUnobservable { member: member, cause: join(["the owner of ", path as String, " read back empty"], "") } - } else { - MemberObserved { identity: RootAt { root: DirectoryRootIdentity { path: path, owner: owner as NonEmptyStr } } } - } + match owned_directory_demand_at(ts: deployment_directory_demands(spec: spec), path: path as String) { + Absent => MemberRefusedOnHost { member: member, cause: join(["no owned-directory demand names ", path as String], "") } + Present { value: target } => { + let observation = owned_directory_observation_on_host(arm: arm, path: path as String) + match observation { + EnsureInaccessible { reason } => MemberUnobservable { member: member, cause: reason } + EnsureAbsent { reason: _ } => owned_directory_member_of_decision(member: member, path: path, target: target, observation: observation) + EnsureObserved { evidence: _ } => owned_directory_member_of_decision(member: member, path: path, target: target, observation: observation) } } } } +fn owned_directory_member_of_decision( + member: ConvergedMember, + path: NonEmptyStr, + target: OwnedDirectoryTarget, + observation: EnsureObservation, +) -> ObservedMember { + match owned_directory_decide(policy: EnsureMayApply, target: target, observation: observation) { + Noop => MemberObserved { identity: RootAt { root: DirectoryRootIdentity { path: path, standing: RootConverged } } } + Refuse { reason } => match observation { + EnsureInaccessible { reason: why } => MemberUnobservable { member: member, cause: why } + EnsureObserved { evidence: _ } => MemberRefusedOnHost { member: member, cause: reason as String } + EnsureAbsent { reason: _ } => MemberRefusedOnHost { member: member, cause: reason as String } + } + Apply { plan: _ } => match observation { + EnsureObserved { evidence } => + MemberObserved { identity: RootAt { root: DirectoryRootIdentity { + path: path, + standing: RootModeRepairable { observed_mode: file_mode_octal(mode: evidence.mode) as NonEmptyStr }, + } } } + EnsureAbsent { reason: _ } => MemberAbsentOnHost { member: member } + EnsureInaccessible { reason } => MemberUnobservable { member: member, cause: reason } + } + } +} + fn desired_member_identity( member: ConvergedMember, spec: DeploymentSpec, @@ -596,7 +627,7 @@ fn desired_member_identity( UnitFile => desired_unit_file(spec: spec, candidate: candidate) UnitManager => desired_unit_manager() ServiceProcess => desired_service_process(candidate: candidate) - DirectoryRoot { path } => RootAt { root: DirectoryRootIdentity { path: path, owner: spec.service.service_user } } + DirectoryRoot { path } => RootAt { root: DirectoryRootIdentity { path: path, standing: RootConverged } } } } @@ -613,7 +644,7 @@ fn observed_member_on_host( UnitFile => observe_unit_file(arm: arm, spec: spec) UnitManager => observe_unit_manager(arm: arm, unit: spec.service.unit_name) ServiceProcess => observe_service_process(spec: spec, transport: transport) - DirectoryRoot { path } => observe_directory_root(arm: arm, path: path) + DirectoryRoot { path } => observe_owned_directory(arm: arm, spec: spec, path: path) } } @@ -645,6 +676,8 @@ fn deployment_effect_plan(spec: DeploymentSpec, candidate: CandidateRelease, tra EffectsRefused { refusals: [MemberRefused { member: Executable, cause: DesiredExecutableUnreadable { cause: cause } }] } MemberAbsentOnHost { member: _ } => EffectsRefused { refusals: [MemberRefused { member: Executable, cause: DesiredExecutableAbsent }] } + MemberRefusedOnHost { member: _, cause } => + EffectsRefused { refusals: [MemberRefused { member: Executable, cause: DesiredExecutableUnreadable { cause: cause } }] } MemberObserved { identity: desired_bin } => match desired_bin { ExecutableIdentity { digest } => release_member_plan( diff --git a/dag/gunbc/live_deploy/repository_convergence.dag b/dag/gunbc/live_deploy/repository_convergence.dag index 5485e1cea5a..e4c6756ed8f 100644 --- a/dag/gunbc/live_deploy/repository_convergence.dag +++ b/dag/gunbc/live_deploy/repository_convergence.dag @@ -14,7 +14,31 @@ module gunbc.live_deploy.repository_convergence import std.types { String, Bool, Int, List, FilePath, NonEmptyStr } import extdeps.git { git_local_fetch_source_grant, git_local_fetch_upload_pack_command } -import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.filesystem.filesystem_io { + Filesystem, + filesystem_delete, + FilesystemDeleted, + FilesystemDeleteTargetAbsent, + FilesystemDeleteRefused, + FilesystemDeleteKindUnrecognized, + filesystem_exact_read, + FilesystemExactPathRead, + FilesystemExactPathAbsent, + FilesystemExactPathUnreadable, + FilesystemExactPathKindUnrecognized, +} +import gunbc.repo_workspace { repo_workspace_footprints } +import std.workspace_artifact { + WorkspaceArtifact, + RegenerableFromSource, + LocalCacheState, + PerDeveloperState, + HostOsMetadata, + SessionRuntimeState, + MayContainSecrets, + OutsideSubstrate, + SupersededAuthority, +} import gunbc.repository { gunbc_repository } import std.algebra { trim } import std.process { ProcessExit, ExitSuccess, ExitFailure } @@ -156,6 +180,7 @@ type RepositoryConvergenceOutcome | ConvergenceHeadNotAtCandidate { candidate: String, observed: String } | ConvergenceHeadMovedWithoutReflog { candidate: String } | ConvergenceUntrackedResidue { first_path: String } + | ConvergenceSupersededFileNotRetired { path: String, detail: String } | ConvergenceResidueUnreadable { exit_code: Int, stderr: String } | ConvergencePrimaryBranchNotAtCandidate { branch: String, observed: String } | ConvergenceIndexNotAtCandidateTree { candidate_tree: String, observed_tree: String } @@ -738,6 +763,7 @@ fn repository_convergence_converged(outcome: RepositoryConvergenceOutcome) -> Bo ConvergenceHeadNotAtCandidate { candidate: _, observed: _ } => false ConvergenceHeadMovedWithoutReflog { candidate: _ } => false ConvergenceUntrackedResidue { first_path: _ } => false + ConvergenceSupersededFileNotRetired { path: _, detail: _ } => false ConvergenceResidueUnreadable { exit_code: _, stderr: _ } => false ConvergencePrimaryBranchNotAtCandidate { branch: _, observed: _ } => false ConvergenceIndexNotAtCandidateTree { candidate_tree: _, observed_tree: _ } => false @@ -823,6 +849,11 @@ fn repository_convergence_detail(outcome: RepositoryConvergenceOutcome) -> Strin "the tree carries untracked files after the transition, starting at ", p, " -- `reset --hard` moves tracked bytes only, so this tree is the candidate plus residue and not the candidate", ], "") + ConvergenceSupersededFileNotRetired { path: p, detail: d } => + join([ + "the superseded workspace file ", p, " could not be retired: ", d, + " -- the current loader refuses a physical file at a derived roster identity, so a tree that keeps it cannot serve", + ], "") ConvergenceResidueUnreadable { exit_code: x, stderr: e } => join([ "the untracked-file listing could not be read (exit ", to_string(x), "): ", e, @@ -1143,6 +1174,92 @@ fn convergence_fetch_upload_pack(remote: String) -> String { git_local_fetch_upload_pack_command(grant: git_local_fetch_source_grant(source: remote as NonEmptyStr)) } +// RETIRING SUPERSEDED FILES IS PART OF CONVERGING THE TREE, AND ITS POPULATION IS DERIVED, NOT LISTED. +// Older gunbc binaries wrote each ledger roster into the checkout they read, so a deployed tree that +// ran one carries `roster.dag` files nothing tracks. The current loader derives those modules in +// memory and REFUSES a physical file at a reserved roster identity (it never deletes one: source +// loading performs no cleanup), so a converged tree must not carry one. The files are ignored, so +// the untracked-residue arm (`--exclude-standard`) cannot see them, which is why retirement is its +// own explicit step rather than an assumption about what reset leaves behind. +// +// The population is every FILE-GRAIN, root-anchored workspace artifact whose declared reason is +// `SupersededAuthority` in gunbc.repo_workspace -- the one existing descriptor that already says +// "this path is dead, its facts live elsewhere". Directory and glob patterns are not file grain and +// are never retired here. Bounded by construction: the rows name their own paths, so adding a row is +// the only way to widen it. +fn artifact_is_superseded_file(a: WorkspaceArtifact) -> Bool { + match a.reason { + SupersededAuthority { canonical: _ } => + starts_with(s: a.pattern, prefix: "/") + && !ends_with(s: a.pattern, suffix: "/") + && !string_contains(s: a.pattern, pattern: "*") + RegenerableFromSource => false + LocalCacheState => false + PerDeveloperState => false + HostOsMetadata => false + SessionRuntimeState => false + MayContainSecrets => false + OutsideSubstrate => false + } +} + +fn superseded_workspace_file_paths() -> List { + fold( + repo_workspace_footprints(), + init: [], + f: (acc, footprint) => concat( + acc, + footprint.artifacts + |> filter(a => artifact_is_superseded_file(a: a)) + |> map(a => substring(s: a.pattern, start: 1, end: string_length(s: a.pattern))), + ), + ) +} + +type SupersededFileRetirement + = SupersededFilesAbsent + | SupersededFileRetirementRefused { path: String, detail: String } + +// ONE PATH: delete, then READ BACK ABSENCE through the exact-read authority. An absent target is +// already retired; any other delete refusal, or a read-back that is not `FilesystemExactPathAbsent`, +// is a located refusal -- a file that may still be on disk is never reported as retired. +fn retire_superseded_file(repo: String, rel: String) -> SupersededFileRetirement { + let path = join([repo, "/", rel], "") + let d = Filesystem.Delete(path: path) + match filesystem_delete(path: path, success: d.success, error: d.error, error_kind: d.error_kind) { + FilesystemDeleteRefused { path: p, kind: _, error: e } => + SupersededFileRetirementRefused { path: p, detail: join(["delete refused: ", e], "") } + FilesystemDeleteKindUnrecognized { path: p, observed: o, error: e } => + SupersededFileRetirementRefused { path: p, detail: join(["delete failed with unrecognized kind ", o, ": ", e], "") } + FilesystemDeleted { path: _ } => superseded_file_read_back(path: path) + FilesystemDeleteTargetAbsent { path: _ } => superseded_file_read_back(path: path) + } +} + +fn superseded_file_read_back(path: String) -> SupersededFileRetirement { + let r = Filesystem.Read(path: path) + match filesystem_exact_read(path: path, content: r.content, success: r.success, error: r.error, error_kind: r.error_kind) { + FilesystemExactPathAbsent { path: _ } => SupersededFilesAbsent + FilesystemExactPathRead { path: p, content: _ } => + SupersededFileRetirementRefused { path: p, detail: "the file is still readable after its delete" } + FilesystemExactPathUnreadable { path: p, kind: _, error: e } => + SupersededFileRetirementRefused { path: p, detail: join(["absence could not be read back: ", e], "") } + FilesystemExactPathKindUnrecognized { path: p, observed: o, error: e } => + SupersededFileRetirementRefused { path: p, detail: join(["absence read-back failed with unrecognized kind ", o, ": ", e], "") } + } +} + +fn retire_superseded_workspace_files(repo: String) -> SupersededFileRetirement { + fold( + superseded_workspace_file_paths(), + init: SupersededFilesAbsent, + f: (acc, rel) => match acc { + SupersededFilesAbsent => retire_superseded_file(repo: repo, rel: rel) + SupersededFileRetirementRefused { path: _, detail: _ } => acc + }, + ) +} + fn repository_converge_wet( repo: String, remote: String, @@ -1203,12 +1320,16 @@ fn repository_converge_wet( observed_head: git_text_or_empty(observation: observe_repository_state(repo: repo, candidate: candidate).head), }) } else { - convergence_exit(outcome: repository_convergence_verdict( - before: before, - after: observe_repository_state(repo: repo, candidate: candidate), - candidate: candidate, - base_ref: base_ref, - )) + match retire_superseded_workspace_files(repo: repo) { + SupersededFileRetirementRefused { path: p, detail: d } => + convergence_exit(outcome: ConvergenceSupersededFileNotRetired { path: p, detail: d }) + SupersededFilesAbsent => convergence_exit(outcome: repository_convergence_verdict( + before: before, + after: observe_repository_state(repo: repo, candidate: candidate), + candidate: candidate, + base_ref: base_ref, + )) + } } } } diff --git a/dag/gunbc/live_deploy/spec.dag b/dag/gunbc/live_deploy/spec.dag index 1bc03c1b494..7cb7a89887a 100644 --- a/dag/gunbc/live_deploy/spec.dag +++ b/dag/gunbc/live_deploy/spec.dag @@ -12,7 +12,7 @@ import extdeps.apt { package_tailscale, package_tmux, } -import extdeps.version.semver { SemVerVersion } +import extdeps.version.semver { SemVerVersion, semver_version_parse } import extdeps.tailscale.serve { tailscale_scoped_removal_capability, tailscale_serve_unix_target, @@ -38,17 +38,15 @@ import gunbc.host_layout { srv1_gunbc_repo_root, srv1_gunbc_approval_broker_root, srv1_gunbc_serve_binary, - srv1_dispatch_worktree_root, - srv1_dispatch_attempt_state_root, srv1_devboot_artifact_store_root, } -import gunbc.fabric_storage_placement { fabric_storage_store_directories, fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_door_socket } -import gunbc.managed_directory { ManagedDirectoryAdmission, managed_directory_admission_path } +import gunbc.fabric_storage_placement { fabric_storage_placed_on, fabric_storage_https_port, fabric_storage_door_socket } +import gunbc.managed_directory { DirectoryOwnerAllEveryoneTraverse } +import gunbc.owned_directory { OwnedDirectoryTarget, OwnedDirectoryDeclared } +import extdeps.posix.identity { PosixOwnerNames } +import gunbc.roadmap.dashboard_instance_directories { dashboard_instance_directory_demands } import gunbc.roadmap_dashboard_instance { HostDashboardInstance, - dashboard_instance_dispatch_worktree_root, - dashboard_instance_dispatch_attempt_state_root, - dashboard_instance_compute_root, dashboard_instance_provider_state_root, dashboard_instance_tailnet_door_socket, } @@ -60,7 +58,7 @@ import gunbc.ownership { Ownership, Owned, Ensured } // Belt B membership (docs/plans/gunbc-served-dashboard-design.md section 5): the service is `gunbc // serve` (the seed binary), not an emitted node server.js, so the members flip — ServeBinary + -// GunbcSourceTree + DispatchWorktreeRoot in (paths from the ONE gunbc.host_layout authority the +// GunbcSourceTree + the dispatch worktree root in (paths from the ONE gunbc.host_layout authority the // belt also projects), ServerScript and the NodeJsPackage dependency out (the Ensured nodejs is // never torn down — R5; the stale /opt/gunbc/server.js file is left inert on disk and reaped when // belt A's interim retires). Deploy principal is ghrunner (DeployRunnerPrincipal.job_user; @@ -81,26 +79,24 @@ data gunbc_service_user: NonEmptyStr = "briansrls" // User= line's. The user manager itself must be running, which is loginctl linger for this account. data gunbc_service_uid: Int = 1000 -// AttemptStateRoot is an owned member for the same reason DispatchWorktreeRoot is: the belt WRITES -// there, under the service user, on every dispatch. It was missing until 2026-07-30, and the gap -// was not visible as a gap — #7368 made a published attempt state a hard precondition of dispatch -// while the path lived only in gunbc.roadmap_dashboard_instance's srv1 row, so this roster had -// nothing to add it to. The result on the live host was EACCES from `install -d` against a -// root-owned /opt/gunbc, AFTER the worktree had been created: an orphan branch per click and a -// progress row reading `legacy attempt`. Ownership is Owned, not Ensured, because the deploy -// creates it; teardown mirrors DispatchWorktreeRoot's empty-directory refusal rather than a -// recursive remove, since a non-empty attempt-state root means published attempt evidence for -// sessions that may still be live (the annotation on gunbc.live_deploy.emit emit_artifact_teardown -// carries the same argument for the same reason). +// NO DIRECTORY IS AN OWNED ARTIFACT OR AN ENSURED DEPENDENCY ANY MORE. The dispatch worktree root, +// the attempt-state root, the compute root, the provider-state root, the fabric store's areas and the +// srv1 host roots were each a kind here, created by an install -d the emitter spelled per kind and +// read back by owner alone -- one of two creators for the dashboard's directories, the other being +// the dashboard provision plan's mkdir. Both are deleted: a directory is an owned-directory target +// (gunbc.owned_directory) its consumer demands, the spec carries the instance's demands +// (DeploymentSpec.directories, from gunbc.roadmap.dashboard_instance_directories) and the host's +// (DeploymentHostTarget.host_directories), and the one provider decides, installs and reads back +// every one of them (gunbc.live_deploy.emit owned_directory_apply_steps, +// gunbc.live_deploy.member_observe observe_owned_directory). No directory is torn down by a retract: +// the roots that were Owned were removed by an empty-directory rmdir, and that teardown is not carried +// forward -- a retract leaves directories exactly as it found them. type OwnedArtifactKind = ServeBinary | GunbcSourceTree - | DispatchWorktreeRoot - | AttemptStateRoot - | ComputeRoot | SystemdUnit - | BeltTimerUnit + | BeltEventPathUnit | FleetConvergeTimerUnit | TailscaleServeMapping | DeployedTreeRemoteUnit @@ -110,43 +106,18 @@ type OwnedArtifactKind = | ApprovalBrokerFrontDoor | TailnetDoorServeUnit +// AN ENSURED HOST RESOURCE: converged on every apply, never torn down, TeardownRefuses{ +// TeardownOutsideOwnership}. Only packages remain; the directory arms became owned-directory targets. type EnsuredDependencyKind = TailscalePackage | TmuxPackage - | DevbootArtifactStoreDirectory - | CodexRuntimeReleaseDirectory - | FabricStorageStoreDirectory - | ProviderStateRootDirectory - -// ONE CONCEPT -- AN ENSURED HOST RESOURCE -- WITH N KINDS, not a second ensured mechanism beside -// the first. What Ensured means was already exactly right for the build service's artifact store: -// converged on every apply, never torn down, TeardownRefuses{TeardownOutsideOwnership}. What was -// wrong was that a dependency could only BE a package, so a directory had nowhere to live and got -// put in the owned roster instead -- where Owned means torn down by retract, and produced artifacts -// are the one thing a deploy must never delete.\n\nTHE HAZARD THAT RULED THIS, recorded because it -// was caught rather than reasoned: appending a host-level root to every per-instance spec made a -// lab twin's retract remove the production artifact store. Two existing claims went red on it. The -// claims were right and the placement was wrong, so the category moved and the claims were not -// touched.\n\nThe package arm keeps its exact prior behaviour -- same ensure step, same emitted -// bytes -- because this widening is about admitting a second KIND of ensured resource, not about -// changing what ensuring a package means. The directory arm carries its own owner rather than -// reading a deployment spec, so an ensured host resource stays a fact about the HOST and needs no -// instance to be stated. - -// A MANAGED host directory is the arm whose owner, group and mode are DERIVED from its declared -// dependents (gunbc.managed_directory) rather than asserted as one owner at the shared 0755. It -// carries the ADMISSION, not the directory, so a directory with a writer the owner and group do not -// cover reaches the emitter as a refusal and can never be lowered to an install at all. + type EnsuredSubject = EnsuredPackage { package: NonEmptyStr } - | EnsuredHostDirectory { path: NonEmptyStr, owner: NonEmptyStr } - | EnsuredManagedHostDirectory { admission: ManagedDirectoryAdmission } fn ensured_subject_identity(subject: EnsuredSubject) -> NonEmptyStr { match subject { EnsuredPackage { package } => package - EnsuredHostDirectory { path, owner } => path - EnsuredManagedHostDirectory { admission } => managed_directory_admission_path(a: admission) as String as NonEmptyStr } } @@ -203,11 +174,8 @@ fn owned_artifact_teardown_disposition(kind: OwnedArtifactKind) -> TeardownDispo ServeBinary => TeardownActs DeployedTreeRemoteUnit => TeardownActs GunbcSourceTree => TeardownActs - DispatchWorktreeRoot => TeardownActs - AttemptStateRoot => TeardownActs - ComputeRoot => TeardownActs SystemdUnit => TeardownActs - BeltTimerUnit => TeardownActs + BeltEventPathUnit => TeardownActs FleetConvergeTimerUnit => TeardownActs TailscaleServeMapping => TeardownActs PublicationHelperTimerUnit => TeardownActs @@ -249,7 +217,7 @@ type DeploymentHostTarget { tailnet_domain: NonEmptyStr? os_package_manager: OsPackageManager tailscale_client: TailscaleServeCapability - ensured_directories: List + host_directories: List } // serve_function WAS DROPPED HERE AND THE EMITTER FELL BACK TO A LITERAL. HostDashboardInstance @@ -640,7 +608,7 @@ fn deployment_belt_unit_stem(names: DeploymentNames) -> String { // THE FABRIC DB ENDPOINT'S UNIT, derived like the belt's: one per deployment identity, and emitted // only by the deployment whose instance holds the fabric DB placement (fabric_storage_placed_on). It is // deployment state -- a retract removes the unit and the tailscale mapping -- while the store's files -// are an ensured host directory no retract touches (FabricStorageStoreDirectory). +// are owned-directory demands no retract touches (gunbc.fabric_storage_placement fabric_storage_directory_demands). fn deployment_fabric_storage_unit_name(names: DeploymentNames) -> NonEmptyStr { join([deployment_belt_unit_stem(names: names), "-fabric-storage.service"], "") as NonEmptyStr } @@ -690,8 +658,26 @@ fn deployment_belt_service_unit_name(names: DeploymentNames) -> NonEmptyStr { join([deployment_belt_unit_stem(names: names), "-belt.service"], "") as NonEmptyStr } -fn deployment_belt_timer_unit_name(names: DeploymentNames) -> NonEmptyStr { - join([deployment_belt_unit_stem(names: names), "-belt.timer"], "") as NonEmptyStr +// THE BELT'S EVENT UNITS (dogfood intervention #4: event-driven, not timer-driven). There is no belt +// timer. A worker, reviewer or auditor ending writes an exit record into the instance's exit inbox +// (gunbc.roadmap.roadmap_attempt_exit_spool), and the exit path unit starts the drain service, which +// hands each recorded exit to its attempt's obligation unit. A publication answer arriving starts the +// discovery service, which queues the attempts whose publication is still owed. The belt service +// stays: it is the launch obligation, started by a drained worker exit or by an operator. +fn deployment_belt_exit_path_unit_name(names: DeploymentNames) -> NonEmptyStr { + join([deployment_belt_unit_stem(names: names), "-belt-exit.path"], "") as NonEmptyStr +} + +fn deployment_belt_exit_drain_service_unit_name(names: DeploymentNames) -> NonEmptyStr { + join([deployment_belt_unit_stem(names: names), "-belt-exit.service"], "") as NonEmptyStr +} + +fn deployment_belt_answer_path_unit_name(names: DeploymentNames) -> NonEmptyStr { + join([deployment_belt_unit_stem(names: names), "-belt-discovery.path"], "") as NonEmptyStr +} + +fn deployment_belt_discovery_service_unit_name(names: DeploymentNames) -> NonEmptyStr { + join([deployment_belt_unit_stem(names: names), "-belt-discovery.service"], "") as NonEmptyStr } // THE PUBLICATION PRINCIPAL'S ALLOCATED NAME ON srv1, stated once and carried as a DeploymentNames @@ -814,8 +800,30 @@ fn deployment_belt_service_unit_path(names: DeploymentNames) -> NonEmptyStr { join(["/etc/systemd/system/", deployment_belt_service_unit_name(names: names) as String], "") as NonEmptyStr } -fn deployment_belt_timer_unit_path(names: DeploymentNames) -> NonEmptyStr { - join(["/etc/systemd/system/", deployment_belt_timer_unit_name(names: names) as String], "") as NonEmptyStr +fn deployment_belt_exit_path_unit_path(names: DeploymentNames) -> NonEmptyStr { + join(["/etc/systemd/system/", deployment_belt_exit_path_unit_name(names: names) as String], "") as NonEmptyStr +} + +// THE BOOT TARGET FOLLOWS THE MANAGER THE UNIT IS INSTALLED INTO. Every belt unit this deployment emits is +// written under /etc/systemd/system, so it belongs to the SYSTEM manager, whose boot target is +// multi-user.target; a user manager would install under a user unit directory and boot at default.target +// (and only if the principal lingers). The dispatched attempt and worker units are transient units in the +// principal's USER manager, but they are started by these system units and are never installed. One +// derivation, so a unit-directory change cannot leave a boot target naming a manager that never runs it. +fn deployment_belt_unit_install_target(names: DeploymentNames) -> NonEmptyStr { + if starts_with(s: deployment_belt_exit_drain_service_unit_path(names: names) as String, prefix: "/etc/systemd/system/") { "multi-user.target" as NonEmptyStr } else { "default.target" as NonEmptyStr } +} + +fn deployment_belt_exit_drain_service_unit_path(names: DeploymentNames) -> NonEmptyStr { + join(["/etc/systemd/system/", deployment_belt_exit_drain_service_unit_name(names: names) as String], "") as NonEmptyStr +} + +fn deployment_belt_answer_path_unit_path(names: DeploymentNames) -> NonEmptyStr { + join(["/etc/systemd/system/", deployment_belt_answer_path_unit_name(names: names) as String], "") as NonEmptyStr +} + +fn deployment_belt_discovery_service_unit_path(names: DeploymentNames) -> NonEmptyStr { + join(["/etc/systemd/system/", deployment_belt_discovery_service_unit_name(names: names) as String], "") as NonEmptyStr } // CONVERGE-0 loop member (operator directive 2026-08-09: 'timer in deployment membership'). @@ -982,6 +990,7 @@ type DeploymentSpec { approval_broker: DeploymentServiceConfig names: DeploymentNames instance_id: NonEmptyStr + directories: List } // TWO CONSUMERS, ONE SCOPE-FREE ROUTE LABEL (KEYING-0 follow-on, node://adhoc-907f1da5-c20): @@ -1038,8 +1047,15 @@ type DeploymentSpec { // and this member is unconditional again — the mirror holds because the twin genuinely has a // correct timer, not because the asymmetry was argued away. // -// One member, two files, exactly as GunbcSourceTree owns gunbc-tree-sync.service plus its env file: -// the member's identity is the .timer path and its teardown removes the .service beside it. The +// THE TIMER IS GONE (dogfood intervention #4, gunbc#13124: event-driven, not timer-driven; the timer +// is not a product requirement and does not become a desired-state row). The member is now the belt's +// EVENT units: the exit path unit and the drain service it starts, the publication-answer path unit +// and the discovery service it starts, and the belt service itself, which no timer starts -- a drained +// worker exit or an operator does. Everything the paragraphs above say about lifecycle and per-instance +// roots holds unchanged for these units. +// +// One member, five files, as GunbcSourceTree owns gunbc-tree-sync.service plus its env file: the +// member's identity is the exit .path unit and its teardown removes the four files beside it. The // names DERIVE from unit_name rather than being carried as DeploymentNames fields the way tree- // sync's are, and the difference is that tree-sync had migration debt to express — production runs // under names the derivation would not mint, so both constructors must state them. No belt unit @@ -1057,15 +1073,12 @@ fn deployment_owned_steps( kind: DeployedTreeRemoteUnit, path: deployment_tree_remote_unit_path(names: names), }, - DeploymentArtifactStep { kind: DispatchWorktreeRoot, path: dashboard_instance_dispatch_worktree_root(instance: instance) as String as NonEmptyStr }, - DeploymentArtifactStep { kind: AttemptStateRoot, path: dashboard_instance_dispatch_attempt_state_root(instance: instance) as String as NonEmptyStr }, - DeploymentArtifactStep { kind: ComputeRoot, path: dashboard_instance_compute_root(instance: instance) as String as NonEmptyStr }, DeploymentArtifactStep { kind: SystemdUnit, path: deployment_systemd_unit_path(names: names) }, DeploymentArtifactStep { kind: PublicationHelperTimerUnit, path: deployment_publication_timer_unit_path(names: names), }, - DeploymentArtifactStep { kind: BeltTimerUnit, path: deployment_belt_timer_unit_path(names: names) }, + DeploymentArtifactStep { kind: BeltEventPathUnit, path: deployment_belt_exit_path_unit_path(names: names) }, ], concat(deployment_tailnet_door_steps(names: names), concat([ DeploymentArtifactStep { kind: TailscaleServeMapping, @@ -1127,25 +1140,6 @@ fn deployment_fabric_storage_steps(instance: HostDashboardInstance, names: Deplo } } -// THE BUILD SERVICE'S ROOTS ARE MEMBERSHIP, PROJECTED, AND SEPARATE FROM THE PER-INSTANCE STEPS -- -// three claims, and the third is the one that took a correction. Membership: these are directories -// the service WRITES, which is the same property that makes DispatchWorktreeRoot and -// AttemptStateRoot owned members, and attempt_state_root_member_note records what it cost to learn -// that a written path outside the roster is a path nothing creates. Projected: the paths come from -// the gunbc.host_layout rows, never spelled here, so the belt and the deploy cannot drift to two -// spellings of one directory.\n\nSEPARATE, because the first cut put them in deployment_owned_steps -// beside the dashboard's own members and that was wrong in a way the srv1-only scope hides. Those -// steps are built PER INSTANCE, and every instance would then have received srv1's literal paths -// under its own name -- srv2's preview slot creating /var/lib/gunbc directories for a service that -// does not run there. gunbc_host_layout_note names that exact defect for the exact reason it is -// dangerous: no value is visibly wrong, so nothing surfaces, and the first host whose layout -// genuinely diverged would silently inherit srv1's body.\n\nSo the projection is its own function, -// called for the host that actually runs the build service, rather than folded into a per-instance -// list that would answer for hosts nobody asked about. Dissolve-on: devboot deployed to a second -// host. At that point the roots become fields of a host layout resolved through a total -// host-identity lookup -- the next-rung trigger dispatch_state_scope_carries_its_roots_note already -// names -- and this srv1-shaped function retires into that derivation. - fn deployment_ensured_packages() -> List { [ DeploymentDependencyStep { @@ -1159,135 +1153,62 @@ fn deployment_ensured_packages() -> List { ] } -// THE FABRIC DB STORE IS ENSURED, NEVER OWNED: it is durable history every partition's chain -// lives in, so no retract may remove it -- the property this category makes structural. It -// replaced the per-instance event-log mirror root, which was owned and torn down with rm -rf; -// that was harmless for a cache of a remote repository and would destroy the log itself here. -// -// THE ROOT ALONE IS NOT A STORE. The file store writes objects under `objects/` and heads under -// `heads/` (gunbc.fabric_storage_file_store fabric_storage_file_root), and a create-new under an -// absent area refuses not_found -- fleet-converge run 35818578751's host-effect claim did exactly -// that. The areas are derived from the store's own root function, so the ensured paths and the -// written paths cannot disagree, and this deployment is the store's one production creator. -// -// AND AN AREA THE WRITER CANNOT WRITE IS NOT A STORE EITHER. The three directories are managed: -// their owner, group and mode come from the writers gunbc.fabric_storage_placement -// fabric_storage_store_directories declares (the service principal and the CI job principal), not -// from gunbc_service_user at the shared 0755 -- which is what refused fleet-converge run -// 36139624393's claim permission_denied. install -d re-applies owner, group and mode to a directory -// that already exists, so the next apply repairs the areas #12126 created. -fn deployment_ensured_srv1_host_directories() -> List { - concat([ - DeploymentDependencyStep { - kind: DevbootArtifactStoreDirectory, - subject: EnsuredHostDirectory { - path: srv1_devboot_artifact_store_root as String as NonEmptyStr, - owner: gunbc_service_user, - }, +// THE BUILD SERVICE'S ROOTS ARE THE HOST'S DEMAND, NOT ANY INSTANCE'S. The devboot artifact store and +// the codex runtime release root are written by host-level services, so a per-instance list would hand +// srv2's preview slot srv1's literal paths (gunbc_host_layout_note names that defect). They are +// owned-directory targets on the host target, created and read back by the same provider as the +// instance's demands, at the shared 0755 they were installed at, and no retract removes them. +// Dissolve-on: devboot deployed to a second host, at which point the roots become fields of a host +// layout resolved through a total host-identity lookup. +fn deployment_srv1_host_directories() -> List { + let owner = PosixOwnerNames { user: gunbc_service_user, group: gunbc_service_user } + [ + OwnedDirectoryDeclared { + member: "devboot-artifact-store" as NonEmptyStr, + path: srv1_devboot_artifact_store_root, + owner: owner, + mode: DirectoryOwnerAllEveryoneTraverse, }, - ], concat(map(fabric_storage_store_directories(), a => DeploymentDependencyStep { - kind: FabricStorageStoreDirectory, - subject: EnsuredManagedHostDirectory { admission: a }, - }), [ - DeploymentDependencyStep { - kind: CodexRuntimeReleaseDirectory, - subject: EnsuredHostDirectory { - path: srv1_codex_runtime_release_root as String as NonEmptyStr, - owner: gunbc_service_user, - }, + OwnedDirectoryDeclared { + member: "codex-runtime-release" as NonEmptyStr, + path: srv1_codex_runtime_release_root, + owner: owner, + mode: DirectoryOwnerAllEveryoneTraverse, }, - ])) + ] } fn deployment_ensured_steps(target: DeploymentHostTarget) -> List { - concat(deployment_ensured_packages(), target.ensured_directories) -} - -// THE PROVIDER-STATE ROOT IS AN ENSURED MEMBER OF THE INSTANCE, NOT OF THE HOST (CONVERGENCE-ONE -// C7, operator ruling 2026-09-19: live_deploy owns provider-state creation). The dashboard -// launcher's provision plan was its only creator, so deleting that launcher without this member -// would lose the creation silently: a fresh host would get a provider login with nowhere to write. -// -// It is Ensured rather than Owned for the reason the provision op already gave: the directory is -// never torn down and never populated by the deploy. Credentials inside it are user-authored secret -// state, so a retract that removed it would destroy a login the deploy did not create. -// -// IT IS DERIVED FROM THE SPEC, NOT AUTHORED INTO ITS STEP LIST, AND THAT IS THE WHOLE SAFETY -// ARGUMENT. The first cut of this change appended the member to deployment_steps_apply_order and -// then had the emitter REFUSE a spec whose steps omitted it -- a five-armed standing type, a poison -// string and two witnesses, all validating a constraint the model could carry instead. DESIGN 5 is -// explicit that construction beats validation, and review 68784 was right that the check restated -// what the model already knew. So the path became a field of DeploymentServiceConfig, beside the -// other instance-projected paths it already carries (serve_binary, instance_root, repo_root), the -// owner is read from that same config's service principal, and every consumer -- the emitted ensure -// and the read-back member -- derives from this one function. A spec cannot omit the member, carry -// it twice, name a package instead of a directory, or give it an owner other than the principal -// that writes the login: none of those four states has a constructor any more, so the arms that -// once refused them are deleted rather than left as permanently-green decoration (DESIGN 4b: ask -// whether the RED is authorable before writing the check). -// -// The directory is installed with the ensured-directory arm's mode (install_d_shared_mode), the -// same mode the provision op's mkdir produced. Credential files inside carry their own modes. A -// tighter mode is a ManagedDirectory derivation, and adopting one would change what -// EnsuredHostDirectory means for every kind. -// -// DISPOSITION CENSUS, stated here because deleting a creator with a blank row is the serve-binary -// deadlock. After this change, the provider-state root of each instance is created by: -// srv1_live -- this member, on the production route (deployment_spec_srv1 -> -// live_deploy_apply_script_for), read back as a DirectoryRoot. The provision -// plan's DashboardEnsureProviderStateRoot is now redundant for this instance -// and goes with the launcher deletion, not before. -// srv1_lab -- the provision plan (gunbc.roadmap_dashboard_instance -// srv2_lab dashboard_instance_provision_plan DashboardEnsureProviderStateRoot) stays -// srv2_deploy the ONLY creator. Their specs carry this member, but no production caller -// macbook_local applies those specs, so the member creates nothing for them yet. The -// provision op may be deleted for an instance only when a production caller -// applies its spec. -// The approval broker's own service config is a SECOND unit on an instance, not a sixth instance: -// its provider-state root is that instance's root, from the same projection, so the broker's apply -// converges the same directory the dashboard's does. That is why the field is not optional for it -- -// an idempotent ensure of the same path is what Ensured means, and a broker-specific path would be -// a second root for one host's login state. -fn deployment_provider_state_step(spec: DeploymentSpec) -> DeploymentDependencyStep { - DeploymentDependencyStep { - kind: ProviderStateRootDirectory, - subject: EnsuredHostDirectory { - path: spec.service.provider_state_root, - owner: spec.service.service_user, - }, - } + deployment_ensured_packages() } -// The path the ensure creates and the path the readback reads, from one function so they cannot -// disagree. -fn deployment_provider_state_path(spec: DeploymentSpec) -> NonEmptyStr { - ensured_subject_identity(subject: deployment_provider_state_step(spec: spec).subject) +// EVERY DIRECTORY THIS DEPLOYMENT CREATES AND READS BACK: the instance's demands, then the host's. The +// emitted install and the read-back roster both iterate this, so a directory created and a directory +// read back cannot be two lists. +fn deployment_directory_demands(spec: DeploymentSpec) -> List { + concat(spec.directories, spec.target.host_directories) } - // Apply order is dependency-honest: the binary lands before the tree, because the tree is published // by a service-user oneshot that RUNS the admitted binary (gunbc.live_deploy.repository_convergence_placement // convergence_unit_doc) -- the order used to read tree-then-binary on the grounds that the binary serves // the tree, which is true of the running process but not of the installation, and the identity emitter // had to override it downstream until this authority said it (review on PR #10696); both before the -// two belt-spawn roots (the worktree root and the attempt-state root, which a -// dispatch writes in that order — worktree first, then the published attempt evidence), all before -// the unit (ExecStart references binary + tree), then the belt timer (its ExecStart references that -// same binary and tree, and every tick it fires writes the two roots above — so it is ordered after -// them for the same dependency reason the unit is), and the tailscale mapping last (routes to a -// live service). FleetConvergeTimerUnit is RETIRED from deployment membership (operator directive -// 2026-08-13, sleek-heron-218): unattended cadence is replaced by the emitted fleet-converge.yml -// on-demand plan/apply workflow; timer teardown for hosts that still carry legacy units is owned by +// unit (ExecStart references binary + tree), then the belt timer (its ExecStart references that same +// binary and tree), and the tailscale mapping last (routes to a live service). The directories every +// one of these writes into are not steps: the apply ensures all of them before the first step +// (gunbc.live_deploy.emit owned_directory_apply_steps), so no member runs before the roots it writes. +// FleetConvergeTimerUnit is RETIRED from deployment membership (operator directive 2026-08-13, +// sleek-heron-218): unattended cadence is replaced by the emitted fleet-converge.yml on-demand +// plan/apply workflow; timer teardown for hosts that still carry legacy units is owned by // gunbc.fleet_converge_plan timer-retirement reconcile, not this spec's apply pole. Retract is the -// reverse, with DispatchWorktreeRoot AND AttemptStateRoot torn down by empty-directory removal — -// LOUD failure when worktrees or published attempts exist, so a retract cannot silently destroy -// live agent sessions or the evidence of what they did (a construction-level refusal, not a check). +// reverse of the owned steps and touches no directory. fn deployment_steps_apply_order( instance: HostDashboardInstance, target: DeploymentHostTarget, names: DeploymentNames, ) -> List { concat( - map(deployment_ensured_steps(target: target), s => Dependency { step: s }), + map(deployment_ensured_steps(instance: instance, target: target), s => Dependency { step: s }), map( deployment_owned_steps(instance: instance, names: names), s => ArtifactStep { step: s }, @@ -1344,20 +1265,20 @@ fn deployment_srv1_target() -> DeploymentHostTarget { tailnet_domain: fleet_intent_network.tailnet_domain, os_package_manager: ubuntu_noble_os_package_manager(), tailscale_client: srv1_observed_tailscale_client(), - ensured_directories: deployment_ensured_srv1_host_directories(), + host_directories: deployment_srv1_host_directories(), } } -data srv1_tailscale_client_observed_version: SemVerVersion = SemVerVersion { - major: 1, - minor: 98, - patch: 3, - pre_release: [], - build: [], -} +// THE OBSERVED VERSION IS THE PARSE RESULT, never a hand-built record: SemVerVersion fields +// are confined code-point runs, so the label arrives through the parser and an unadmitted +// label is carried as a refusal, not a fabricated version (review 77150). +data srv1_tailscale_client_observed_version: SemVerVersion? = semver_version_parse(cps: chars(s: "1.98.3" as String)) fn srv1_observed_tailscale_client() -> TailscaleServeCapability { - tailscale_scoped_removal_capability(observed: srv1_tailscale_client_observed_version) + match srv1_tailscale_client_observed_version { + Absent => ScopedRemovalUnobserved { reason: "the observed client version label failed to parse — a defect in the observed-version row, not an observation gap" } + Present { value: observed } => tailscale_scoped_removal_capability(observed: observed) + } } fn deployment_service_config( @@ -1413,6 +1334,7 @@ fn deployment_spec_for( ), names: names, instance_id: instance.instance_id, + directories: dashboard_instance_directory_demands(instance: instance), } } diff --git a/dag/gunbc/live_deploy/unit_standing.dag b/dag/gunbc/live_deploy/unit_standing.dag index 43159220b4e..3b3c98f5247 100644 --- a/dag/gunbc/live_deploy/unit_standing.dag +++ b/dag/gunbc/live_deploy/unit_standing.dag @@ -2,49 +2,38 @@ module gunbc.live_deploy.unit_standing import std.types { String, Bool, NonEmptyStr, Int, List, list_length } import std.algebra { trim } -import std.measure { Microsecond, microsecond_count } import extdeps.systemd.systemctl import extdeps.systemd { - systemd_unit_property_wire, SystemdUnitProperty, systemd_duration_usec, AccuracyUSec, - LoadState, UnitFileState, ActiveState, SubState, Result, ExecMainStatus, ExecStartProperty, User, WorkingDirectoryProperty, Unit, TimersMonotonic, + systemd_unit_property_wire, SystemdUnitProperty, + LoadState, UnitFileState, ActiveState, SubState, Result, ExecMainStatus, ExecStartProperty, User, WorkingDirectoryProperty, Unit, } import extdeps.systemd.unit_file { - SystemdUnitFile, ServiceUnitFile, TimerUnitFile, SliceUnitFile, - SystemdServiceDirective, SystemdTimerDirective, - OnBootSec, - OnUnitInactiveSec, - AccuracySec, -} - -// EXACT SYSTEMD SERVICE AND TIMER STANDING, MODELED ON WHAT THE EMITTED UNITS ACTUALLY ARE. -// Readiness plus a fresh tick receipt cannot prove the belt timer is enabled now: a service can be -// invoked by hand, write one fresh receipt, and sit behind a disabled timer. The RLM-2 ruling -// (section 7) therefore keeps unit standing in scope and rejects the naive 'service and timer both -// enabled and active', because gunbc.live_deploy.emit deliberately gives the three units different -// semantics -- the dashboard serve service is Type=simple, the belt service is Type=oneshot with no -// [Install] section, and the belt timer is what is enabled under timers.target. The positive -// standing is one row per unit, and the discriminating POSITIVE control is that a correctly -// completed, currently INACTIVE oneshot stays green. -// + SystemdUnitFile, ServiceUnitFile, TimerUnitFile, SliceUnitFile, PathUnitFile, + SystemdServiceDirective, + SystemdPathDirective, PathUnit, DirectoryNotEmpty, PathChanged, MakeDirectory, +} + +// EXACT SYSTEMD UNIT STANDING, MODELED ON WHAT THE EMITTED UNITS ACTUALLY ARE. Readiness alone +// cannot prove the belt's units are installed and armed. The RLM-2 ruling (section 7) keeps unit +// standing in scope and rejects the naive 'every unit enabled and active', because +// gunbc.live_deploy.emit gives the units different semantics -- the dashboard serve service is +// Type=simple, the belt service is Type=oneshot with no [Install] section, and the belt's event path +// units are what is enabled, under paths.target. (There was a belt timer; dogfood intervention #4 +// removed it, and with it this module's timer standing.) The positive standing is one row per unit, +// and the discriminating POSITIVE control is that a correctly completed, currently INACTIVE oneshot +// stays green. +// // THE OBSERVER IS `systemctl show --property=X --value` (extdeps.systemd.systemctl ShowProperty), -// system scope, which this module widened with the properties it needs rather than adding a second -// show surface. There is still no modeled system-scope `is-enabled` operation (systemctl.dag's own -// note); UnitFileState carries the same fact through the show surface, which is the read that -// already existed. -// -// WHAT IS COMPARED EXACTLY, member by member, stated so any residue would be visible. -// ExecStart, User and WorkingDirectory are compared exactly against the directives of R's emitted -// unit file (gunbc.live_deploy.emit live_deploy_serve_unit_file / -// live_deploy_belt_service_unit_file), reading systemd's `{ path=... ; argv[]=... ; ... }` -// rendering by its `argv[]=` segment. The timer's target unit is compared exactly through the Unit -// property. The timer's cadence VALUES are compared exactly too: OnBootUSec and OnUnitInactiveUSec -// from TimersMonotonic and the AccuracyUSec property are each normalized through -// extdeps.systemd systemd_duration_usec (systemd.time(7), microsecond resolution) and compared -// against the Microsecond expectations derived from R's emitted timer unit -- '60s' and '1min' -// are one value, and a spelling that stops normalizing refuses rather than passing structurally. -// This dissolves the former declared residue (cadence compared by entry presence only), whose -// trigger -- an extdeps.systemd duration model -- is the mechanism that landed beside this text. -// +// system scope. There is still no modeled system-scope `is-enabled` operation (systemctl.dag's own +// note); UnitFileState carries the same fact through the show surface. +// +// WHAT IS COMPARED EXACTLY, member by member, stated so any residue would be visible. ExecStart, User +// and WorkingDirectory are compared exactly against the directives of R's emitted unit file +// (gunbc.live_deploy.emit live_deploy_serve_unit_file / live_deploy_belt_service_unit_file), reading +// systemd's `{ path=... ; argv[]=... ; ... }` rendering by its `argv[]=` segment. A path unit's target +// is compared exactly through the Unit property; its watched path is NOT read back (see +// BeltEventPathStanding), which is this module's one declared residue. +// // EVERY REFUSAL NAMES THE UNIT AND THE PROPERTY that disagreed; an unreadable property is a // refusal, never a pass by vacancy. type UnitPropertyRead @@ -72,24 +61,10 @@ type ServiceUnitObserved { working_directory: String } -type TimerUnitObserved { - load_state: String - unit_file_state: String - active_state: String - sub_state: String - unit: String - timers_monotonic: String - accuracy_usec_value: String -} - type ServiceUnitObservation = ServiceUnitObservedOk { observed: ServiceUnitObserved } | ServiceUnitUnobservable { unit: String, property: String } -type TimerUnitObservation - = TimerUnitObservedOk { observed: TimerUnitObserved } - | TimerUnitUnobservable { unit: String, property: String } - fn observe_service_unit(unit: NonEmptyStr) -> ServiceUnitObservation { match read_unit_property(unit: unit, property: LoadState) { UnitPropertyUnreadable { unit: u, property: p } => ServiceUnitUnobservable { unit: u, property: p } @@ -142,48 +117,6 @@ fn observe_service_unit(unit: NonEmptyStr) -> ServiceUnitObservation { } } -fn observe_timer_unit(unit: NonEmptyStr) -> TimerUnitObservation { - match read_unit_property(unit: unit, property: LoadState) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: load_state } => - match read_unit_property(unit: unit, property: UnitFileState) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: unit_file_state } => - match read_unit_property(unit: unit, property: ActiveState) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: active_state } => - match read_unit_property(unit: unit, property: SubState) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: sub_state } => - match read_unit_property(unit: unit, property: Unit) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: target } => - match read_unit_property(unit: unit, property: TimersMonotonic) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: timers } => - match read_unit_property(unit: unit, property: AccuracyUSec) { - UnitPropertyUnreadable { unit: u, property: p } => TimerUnitUnobservable { unit: u, property: p } - UnitPropertyValue { value: accuracy } => - TimerUnitObservedOk { - observed: TimerUnitObserved { - load_state: load_state, - unit_file_state: unit_file_state, - active_state: active_state, - sub_state: sub_state, - unit: target, - timers_monotonic: timers, - accuracy_usec_value: accuracy, - }, - } - } - } - } - } - } - } - } -} - // THE EXPECTATION IS DERIVED FROM R'S EMITTED UNIT FILE, never typed beside it. type ServiceUnitExpectation { exec_start: String @@ -321,121 +254,7 @@ fn service_unit_expectation_of(file: SystemdUnitFile) -> ServiceUnitExpectationD } TimerUnitFile { unit: _, timer: _, install: _ } => ServiceExpectationUnderivable { detail: "expected a service unit file, got a timer" } SliceUnitFile { unit: _, slice: _, install: _ } => ServiceExpectationUnderivable { detail: "expected a service unit file, got a slice" } - } -} - -type TimerUnitExpectation { - target_unit: String - on_boot_usec: Microsecond - on_unit_inactive_usec: Microsecond - accuracy_usec: Microsecond -} - -// The value rendered after `Name=` in a systemctl show composite like -// `{ OnBootUSec=2min ; next_elapse=... }, { OnUnitInactiveUSec=1min ; ... }`: the token between -// the prefix and the next ` ;`, `,` or `}` boundary, normalized through systemd.time(7). -fn show_rendered_duration_usec(rendering: String, prefix: String) -> Microsecond? { - let parts = split(s: rendering, delimiter: prefix) - if list_length(items: parts) < 2 { - none - } else { - let tail = fold(parts, init: "", f: (a, seg) => seg) - let token = fold(split(s: tail, delimiter: " ; "), init: "", f: (acc, seg) => if acc == "" { seg } else { acc }) - systemd_duration_usec(text: trim(s: fold(split(s: token, delimiter: "}"), init: "", f: (acc, seg) => if acc == "" { seg } else { acc }))) - } -} - -fn timer_on_boot_duration(directives: List) -> Microsecond? { - fold(directives, init: none, f: (acc, d) => - match acc { - Present { value: v } => Present { value: v } - Absent => match d { - TimerUnit { target: _ } => none - OnCalendar { expression: _ } => none - OnBootSec { duration: t } => systemd_duration_usec(text: t as String) - OnUnitInactiveSec { duration: _ } => none - AccuracySec { duration: _ } => none - RandomizedDelaySec { duration: _ } => none - Persistent { enabled: _ } => none - } - }) -} - -fn timer_on_unit_inactive_duration(directives: List) -> Microsecond? { - fold(directives, init: none, f: (acc, d) => - match acc { - Present { value: v } => Present { value: v } - Absent => match d { - TimerUnit { target: _ } => none - OnCalendar { expression: _ } => none - OnBootSec { duration: _ } => none - OnUnitInactiveSec { duration: t } => systemd_duration_usec(text: t as String) - AccuracySec { duration: _ } => none - RandomizedDelaySec { duration: _ } => none - Persistent { enabled: _ } => none - } - }) -} - -fn timer_accuracy_duration(directives: List) -> Microsecond? { - fold(directives, init: none, f: (acc, d) => - match acc { - Present { value: v } => Present { value: v } - Absent => match d { - TimerUnit { target: _ } => none - OnCalendar { expression: _ } => none - OnBootSec { duration: _ } => none - OnUnitInactiveSec { duration: _ } => none - AccuracySec { duration: t } => systemd_duration_usec(text: t as String) - RandomizedDelaySec { duration: _ } => none - Persistent { enabled: _ } => none - } - }) -} - -type TimerUnitExpectationDerivation - = TimerExpectationDerived { expectation: TimerUnitExpectation } - | TimerExpectationUnderivable { detail: String } - -fn timer_directive_target(directives: List) -> String? { - fold(directives, init: none, f: (acc, d) => match acc { - Present { value: v } => Present { value: v } - Absent => match d { - TimerUnit { target: t } => Present { value: t as String } - OnCalendar { expression: _ } => none - OnBootSec { duration: _ } => none - OnUnitInactiveSec { duration: _ } => none - AccuracySec { duration: _ } => none - RandomizedDelaySec { duration: _ } => none - Persistent { enabled: _ } => none - } - }) -} - -fn timer_unit_expectation_of(file: SystemdUnitFile) -> TimerUnitExpectationDerivation { - match file { - TimerUnitFile { unit: _, timer: directives, install: _ } => - match timer_directive_target(directives: directives) { - Absent => TimerExpectationUnderivable { detail: "emitted timer unit carries no Unit= target" } - Present { value: t } => - match timer_on_boot_duration(directives: directives) { - Absent => TimerExpectationUnderivable { detail: "emitted timer unit carries no normalizable OnBootSec" } - Present { value: boot } => - match timer_on_unit_inactive_duration(directives: directives) { - Absent => TimerExpectationUnderivable { detail: "emitted timer unit carries no normalizable OnUnitInactiveSec" } - Present { value: inactive } => - match timer_accuracy_duration(directives: directives) { - Absent => TimerExpectationUnderivable { detail: "emitted timer unit carries no normalizable AccuracySec" } - Present { value: accuracy } => - TimerExpectationDerived { expectation: TimerUnitExpectation { - target_unit: t, on_boot_usec: boot, on_unit_inactive_usec: inactive, accuracy_usec: accuracy, - } } - } - } - } - } - ServiceUnitFile { unit: _, service: _, install: _ } => TimerExpectationUnderivable { detail: "expected a timer unit file, got a service" } - SliceUnitFile { unit: _, slice: _, install: _ } => TimerExpectationUnderivable { detail: "expected a timer unit file, got a slice" } + PathUnitFile { unit: _, path: _, install: _ } => ServiceExpectationUnderivable { detail: "expected a service unit file, got a path unit" } } } @@ -461,9 +280,15 @@ type DashboardServiceStanding = DashboardServiceConverged { unit: String } | DashboardServiceRefused { unit: String, property: String, expected: String, observed: String } -type BeltTimerStanding - = BeltTimerConverged { unit: String } - | BeltTimerRefused { unit: String, property: String, expected: String, observed: String } +// THE BELT'S EVENT PATH UNITS (dogfood intervention #4: there is no belt timer). A path unit is +// converged when it is loaded, enabled, active and armed (`waiting`, or `running` while its unit runs) +// and activates the unit the emitted file names. Its watched path is not read back here -- systemctl +// renders Paths= as a composite this row does not parse -- so the path is established by the emitted +// file the deploy installed and its byte comparison, not by this read; and this row proves only that +// the watcher is ARMED. Whether events are CONSUMED is the receipt's event backlog row. +type BeltEventPathStanding + = BeltEventPathConverged { unit: String } + | BeltEventPathRefused { unit: String, property: String, expected: String, observed: String } type BeltOneshotStanding = BeltOneshotConverged { unit: String, active_state: String } @@ -499,45 +324,6 @@ fn judge_dashboard_service(unit: String, expect: ServiceUnitExpectation, o: Serv } } -fn judge_belt_timer(unit: String, expect: TimerUnitExpectation, o: TimerUnitObserved) -> BeltTimerStanding { - if o.load_state != "loaded" { - BeltTimerRefused { unit: unit, property: "LoadState", expected: "loaded", observed: o.load_state } - } else if o.unit_file_state != "enabled" { - BeltTimerRefused { unit: unit, property: "UnitFileState", expected: "enabled", observed: o.unit_file_state } - } else if o.active_state != "active" { - BeltTimerRefused { unit: unit, property: "ActiveState", expected: "active", observed: o.active_state } - } else if o.sub_state != "waiting" && o.sub_state != "running" { - BeltTimerRefused { unit: unit, property: "SubState", expected: "waiting|running", observed: o.sub_state } - } else if o.unit != expect.target_unit { - BeltTimerRefused { unit: unit, property: "Unit", expected: expect.target_unit, observed: o.unit } - } else { - match show_rendered_duration_usec(rendering: o.timers_monotonic, prefix: "OnBootUSec=") { - Absent => BeltTimerRefused { unit: unit, property: "TimersMonotonic", expected: "a normalizable OnBootUSec entry", observed: o.timers_monotonic } - Present { value: boot } => - if microsecond_count(boot) != microsecond_count(expect.on_boot_usec) { - BeltTimerRefused { unit: unit, property: "OnBootUSec", expected: to_string(microsecond_count(expect.on_boot_usec)), observed: to_string(microsecond_count(boot)) } - } else { - match show_rendered_duration_usec(rendering: o.timers_monotonic, prefix: "OnUnitInactiveUSec=") { - Absent => BeltTimerRefused { unit: unit, property: "TimersMonotonic", expected: "a normalizable OnUnitInactiveUSec entry", observed: o.timers_monotonic } - Present { value: inactive } => - if microsecond_count(inactive) != microsecond_count(expect.on_unit_inactive_usec) { - BeltTimerRefused { unit: unit, property: "OnUnitInactiveUSec", expected: to_string(microsecond_count(expect.on_unit_inactive_usec)), observed: to_string(microsecond_count(inactive)) } - } else { - match systemd_duration_usec(text: trim(s: o.accuracy_usec_value)) { - Absent => BeltTimerRefused { unit: unit, property: "AccuracyUSec", expected: "a normalizable duration", observed: o.accuracy_usec_value } - Present { value: accuracy } => - if microsecond_count(accuracy) != microsecond_count(expect.accuracy_usec) { - BeltTimerRefused { unit: unit, property: "AccuracyUSec", expected: to_string(microsecond_count(expect.accuracy_usec)), observed: to_string(microsecond_count(accuracy)) } - } else { - BeltTimerConverged { unit: unit } - } - } - } - } - } - } - } -} // THE ONESHOT IS NOT SEPARATELY ENABLED (it has no [Install] section, so its UnitFileState is // `static`), and it is normally INACTIVE between ticks. Both are the correct state and both are @@ -574,7 +360,8 @@ fn judge_belt_oneshot(unit: String, expect: ServiceUnitExpectation, o: ServiceUn type LaunchUnitStanding { dashboard: DashboardServiceStanding - timer: BeltTimerStanding + exit_path: BeltEventPathStanding + answer_path: BeltEventPathStanding oneshot: BeltOneshotStanding } @@ -586,11 +373,11 @@ fn dashboard_service_standing_text(s: DashboardServiceStanding) -> String { } } -fn belt_timer_standing_text(s: BeltTimerStanding) -> String { +fn belt_event_path_standing_text(s: BeltEventPathStanding) -> String { match s { - BeltTimerConverged { unit: u } => join(["belt timer ", u, " converged"], "") - BeltTimerRefused { unit: u, property: p, expected: e, observed: o } => - join(["belt timer ", u, " ", p, " expected ", e, " observed ", o], "") + BeltEventPathConverged { unit: u } => join(["belt event path ", u, " converged"], "") + BeltEventPathRefused { unit: u, property: p, expected: e, observed: o } => + join(["belt event path ", u, " ", p, " expected ", e, " observed ", o], "") } } @@ -605,7 +392,8 @@ fn belt_oneshot_standing_text(s: BeltOneshotStanding) -> String { fn launch_unit_standing_text(s: LaunchUnitStanding) -> String { join([ dashboard_service_standing_text(s: s.dashboard), "; ", - belt_timer_standing_text(s: s.timer), "; ", + belt_event_path_standing_text(s: s.exit_path), "; ", + belt_event_path_standing_text(s: s.answer_path), "; ", belt_oneshot_standing_text(s: s.oneshot), ], "") } @@ -624,15 +412,56 @@ fn observe_dashboard_service_standing(unit: NonEmptyStr, file: SystemdUnitFile) } } -fn observe_belt_timer_standing(unit: NonEmptyStr, file: SystemdUnitFile) -> BeltTimerStanding { - match timer_unit_expectation_of(file: file) { - TimerExpectationUnderivable { detail: d } => - BeltTimerRefused { unit: unit as String, property: "expectation", expected: "derivable from emitted unit", observed: d } - TimerExpectationDerived { expectation: e } => - match observe_timer_unit(unit: unit) { - TimerUnitUnobservable { unit: u, property: p } => - BeltTimerRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } - TimerUnitObservedOk { observed: o } => judge_belt_timer(unit: unit as String, expect: e, o: o) +fn path_unit_target_of(file: SystemdUnitFile) -> String? { + match file { + PathUnitFile { unit: _, path: directives, install: _ } => + fold(directives, init: none, f: (acc, d) => match acc { + Present { value: v } => Present { value: v } + Absent => match d { + PathUnit { target: t } => Present { value: t as String } + DirectoryNotEmpty { path: _ } => none + PathChanged { path: _ } => none + MakeDirectory { enabled: _ } => none + } + }) + ServiceUnitFile { unit: _, service: _, install: _ } => none + TimerUnitFile { unit: _, timer: _, install: _ } => none + SliceUnitFile { unit: _, slice: _, install: _ } => none + } +} + +fn observe_belt_event_path_standing(unit: NonEmptyStr, file: SystemdUnitFile) -> BeltEventPathStanding { + match path_unit_target_of(file: file) { + Absent => BeltEventPathRefused { unit: unit as String, property: "expectation", expected: "derivable from emitted unit", observed: "the emitted unit is not a path unit naming a Unit= target" } + Present { value: target } => + match read_unit_property(unit: unit, property: LoadState) { + UnitPropertyUnreadable { unit: u, property: p } => BeltEventPathRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } + UnitPropertyValue { value: load } => + if load != "loaded" { BeltEventPathRefused { unit: unit as String, property: "LoadState", expected: "loaded", observed: load } } else { + match read_unit_property(unit: unit, property: UnitFileState) { + UnitPropertyUnreadable { unit: u, property: p } => BeltEventPathRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } + UnitPropertyValue { value: file_state } => + if file_state != "enabled" { BeltEventPathRefused { unit: unit as String, property: "UnitFileState", expected: "enabled", observed: file_state } } else { + match read_unit_property(unit: unit, property: ActiveState) { + UnitPropertyUnreadable { unit: u, property: p } => BeltEventPathRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } + UnitPropertyValue { value: active } => + if active != "active" { BeltEventPathRefused { unit: unit as String, property: "ActiveState", expected: "active", observed: active } } else { + match read_unit_property(unit: unit, property: SubState) { + UnitPropertyUnreadable { unit: u, property: p } => BeltEventPathRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } + UnitPropertyValue { value: sub } => + if sub != "waiting" && sub != "running" { BeltEventPathRefused { unit: unit as String, property: "SubState", expected: "waiting|running", observed: sub } } else { + match read_unit_property(unit: unit, property: Unit) { + UnitPropertyUnreadable { unit: u, property: p } => BeltEventPathRefused { unit: u, property: p, expected: "readable", observed: "systemctl show refused" } + UnitPropertyValue { value: observed_target } => + if observed_target != target { BeltEventPathRefused { unit: unit as String, property: "Unit", expected: target, observed: observed_target } } else { BeltEventPathConverged { unit: unit as String } } + } + } + } + } + } + } + } + } } } } diff --git a/dag/gunbc/machine_intake/access.dag b/dag/gunbc/machine_intake/access.dag index fec49c0c310..f8aa3af4621 100644 --- a/dag/gunbc/machine_intake/access.dag +++ b/dag/gunbc/machine_intake/access.dag @@ -6,6 +6,7 @@ import extdeps.bmc.endpoint { BmcControllerEndpoint } import extdeps.bmc.capability { BmcFirmwareReleaseCapabilityRow } import extdeps.bmc.access_profile { AccessProfileAmbiguous, + AccessProfileBoardNotFactsBound, AccessProfileCatalogRowRefused, AccessProfileKnown, AccessProfileUncatalogued, @@ -22,6 +23,8 @@ import extdeps.bmc.access_profile { ProfilePromotedFromObservation, bmc_access_profile_for, } +import extdeps.board_authority { BoardAuthorityLookup } +import gunbc.board_authority_binding { board_authority_bindings } import gunbc.machine_intake_subject { MachineIntakeSubject } import gunbc.machine_intake_receipt { EvidenceRef, same_intake_subject } @@ -108,6 +111,7 @@ type BoundBmcAccessContextStanding | AccessContextObservationEvidenceMismatch { endpoint: BmcControllerEndpoint, manifest: List, raw_response: ContentHash } | AccessContextProbeEvidenceMismatch { endpoint: BmcControllerEndpoint, manifest: List, raw_response: ContentHash } | AccessContextControlRouteEvidenceMismatch { endpoint: BmcControllerEndpoint, manifest: List, raw_response: ContentHash } + | AccessContextBoardNotFactsBound { observed_identity: BmcObservedIdentity, board: BoardAuthorityLookup } | AccessContextProfileUncatalogued { observed_identity: BmcObservedIdentity } | AccessContextProfileAmbiguous { observed_identity: BmcObservedIdentity, count: Int } | AccessContextCatalogRefused { standing: BmcAccessProfileCatalogRowStanding } @@ -193,9 +197,10 @@ fn bind_looked_up_access_context( observation: BmcAccessObservation, catalog: List, ) -> BoundBmcAccessContextStanding { - match bmc_access_profile_for(identity: observation.identity, catalog: catalog) { + match bmc_access_profile_for(identity: observation.identity, catalog: catalog, bindings: board_authority_bindings) { AccessProfileKnown { profile: p, provenance: prov } => bind_known_profile(subject: subject, receipt: receipt, observation: observation, profile: p, provenance: prov) + AccessProfileBoardNotFactsBound { observed_identity: i, board: b } => AccessContextBoardNotFactsBound { observed_identity: i, board: b } AccessProfileUncatalogued { observed_identity: i } => AccessContextProfileUncatalogued { observed_identity: i } AccessProfileAmbiguous { observed_identity: i, count: n } => AccessContextProfileAmbiguous { observed_identity: i, count: n } AccessProfileCatalogRowRefused { standing: s } => AccessContextCatalogRefused { standing: s } diff --git a/dag/gunbc/machine_intake/arrival_admission.dag b/dag/gunbc/machine_intake/arrival_admission.dag new file mode 100644 index 00000000000..e6ce1410017 --- /dev/null +++ b/dag/gunbc/machine_intake/arrival_admission.dag @@ -0,0 +1,45 @@ +module gunbc.machine_intake_arrival_admission + +import std.types { String } +import std.process { ProcessExit, ExitSuccess, exit_failure } +import gunbc.actions_run_binding { + actions_variable_read, ActionsVariablePresent, ActionsVariableAbsent, + fleet_converge_expected_host_env_name, fleet_converge_arrival_subject_env_name, +} +import gunbc.fleet_host_identity { operator_host_srv1 } +import gunbc.machine_intake_arrival_subject { + admit_arrival_subject, + ArrivalSubjectInPopulation, ArrivalSubjectUnknown, + ArrivalSubjectIsManagedHostExecutorConfusion, ArrivalSubjectOutsideGrant, +} + +// CREDENTIAL-FREE ARRIVAL ADMISSION. Same split as mtcollins1_boot_admission: the job's first +// gunbc run typechecks this closure only. The dispatch's host input must name the store host +// (srv1); the arrival_subject input must be in arrival_converge_population. Neither skip nor +// WIF happens first (review 70894). + +fn arrival_admit_executor() -> ProcessExit { + match actions_variable_read(name: fleet_converge_expected_host_env_name) { + ActionsVariableAbsent { variable: v, detail: d } => + exit_failure(reason: concat("arrival_converge: ", concat(v, concat(" ", concat(d, "; the arrival store host is srv1 only"))))) + ActionsVariablePresent { value: host } => + if host == (operator_host_srv1 as String) { + match actions_variable_read(name: fleet_converge_arrival_subject_env_name) { + ActionsVariableAbsent { variable: v, detail: d } => + exit_failure(reason: concat("arrival_converge: ", concat(v, concat(" ", d)))) + ActionsVariablePresent { value: wire } => + match admit_arrival_subject(wire: wire) { + ArrivalSubjectInPopulation { subject: _ } => ExitSuccess + ArrivalSubjectUnknown { wire: w } => + exit_failure(reason: concat("arrival_converge refuses unknown arrival_subject '", concat(w, "'"))) + ArrivalSubjectIsManagedHostExecutorConfusion { wire: w } => + exit_failure(reason: concat("arrival_converge refuses executor/host as arrival_subject '", concat(w, "'"))) + ArrivalSubjectOutsideGrant { wire: w } => + exit_failure(reason: concat("arrival_converge refuses arrival_subject '", concat(w, "' outside the standing grant"))) + } + } + } else { + exit_failure(reason: concat("arrival_converge refuses host '", concat(host, concat("': the arrival slot store is on ", operator_host_srv1 as String)))) + } + } +} diff --git a/dag/gunbc/machine_intake/arrival_converge.dag b/dag/gunbc/machine_intake/arrival_converge.dag index 53321df64ec..187a98004c1 100644 --- a/dag/gunbc/machine_intake/arrival_converge.dag +++ b/dag/gunbc/machine_intake/arrival_converge.dag @@ -1,6 +1,7 @@ module gunbc.machine_intake_arrival_converge -import std.types { List, NonEmptyStr, String } +import std.types { Bool, Int, List, NonEmptyStr, String } +import std.content_hash { content_hash_equal } import std.decl_ref { DeclarationRef, decl_ref } import std.goal_assessment { ObservationAttempt, @@ -19,7 +20,7 @@ import std.upsert_decision { UpsertDecision, Noop, Apply, Refuse } import extdeps.bmc.endpoint { BmcControllerEndpoint, BmcEndpoint, BmcFirmwareFamily, Redfish } import extdeps.bmc.capability { BmcFirmwareReleaseIdentity, bmc_firmware_release_identity_of_wire } import extdeps.bmc.access_profile { SurfaceManagerIdentity } -import gunbc.machine_intake_receipt { EvidenceRef } +import gunbc.machine_intake_receipt { EvidenceRef, same_intake_subject } import gunbc.machine_intake_phase { ArrivalPhase, AccessDiscover, @@ -40,6 +41,7 @@ import gunbc.machine_intake_phase { Placement, arrival_phases_all, intake_phase_rank, + machine_qualification_policy_digest, } import gunbc.machine_intake_subject { AssemblyManifest, @@ -77,6 +79,7 @@ import gunbc.machine_intake_mtjade1_access_observation { fru_gap_text, smbios_gap_text, sol_session_controller, + mtjade1_smbios_sol_attribution, provisional_assembly_of_fru, read_mtjade1_current_manager_read, read_mtjade1_fru, @@ -85,9 +88,12 @@ import gunbc.machine_intake_mtjade1_access_observation { import gunbc.machine_intake_mtcollins1_bmc_secure_observation { mtcollins1_unit_key_standing } import gunbc.machine_intake_bmc_rotation_route { BmcRotationRouteStanding, + BmcAccountWriteRequest, ObservedSurfaceRead, rotation_route_candidate, redfish_account_password_write, + rotation_apply_authority, + rotation_route_request_identity, } import gunbc.machine_intake_bmc_rotation_route_evidence { manager_read_receipt, mtjade1_observed_endpoint } import gunbc.host_convergence_protocol { HostEffectDomainProjection, inspect_via_host_effect_projection } @@ -104,6 +110,39 @@ import gunbc.machine_intake_prior_life_boundary { prior_life_receipt_key, prior_life_receipt_consumed_identity, } +import gunbc.machine_intake_bmc_secure { + BmcSecureGoal, + BmcAccountStateObservation, + BmcSecureStateInspected, + BmcSecureStateUnobserved, + BmcSecurePhaseConverged, + BmcSecurePhaseRefused, + BmcSecurePhaseNoop, + BmcSecurePhaseApplied, + BmcSecurePhaseReceipt, + inspect_bmc_secure_state, + bmc_secure_phase_noop, + bmc_secure_phase_applied, + BmcAccountApplication, + ManagedCredentialReference, + managed_reference_same, + bmc_account_state_observation_same, +} +import gunbc.auth.authorization_pattern_selection { + InterlockedBy, InterlockPending, +} +import gunbc.auth.standing_operator_grant { + StandingOperatorGrant, + StandingGrantHost, + StandingBmcSecureAccountWrite, + standing_grant_covers, + standing_grant_arm_interlock, +} +import gunbc.auth.privileged_effect_census { ruling_interlock_is_rostered } +import product.host_identity { HostIdentity, host_identity_eq } +import gunbc.fleet_host_identity { operator_host_named, operator_host_mtjade1 } +import gunbc.auth.approval_capability { ProposeApprove, ProposeDeny } +import gunbc.auth.approval_broker { Redemption, RedemptionAdmitted, RedemptionRefused } import gunbc.fleet.convergence_fold { ConvergenceRunId, StepSubjectKey, @@ -123,24 +162,35 @@ import gunbc.fleet.convergence_fold { StepIncomplete, ConvergenceReceiptProjection, ConvergenceRunVerdict, + RunConverged, + RunStopped, + RunIncomplete, + RunOutcomesRefused, admit_convergence_plan, fold_convergence_run, step_instance_key, step_outcome_receipt, + ungated_readonly_step, + StepGatedOnAuthorization, + InstanceAuthorization, + instance_authorization, } -// THE ARRIVAL CONVERGENCE PREFIX (docs/plans/managed-host-untangle.md, cuts O1c-1 and O1c-2): the first -// real consumer of gunbc.fleet.convergence_fold. Its three steps are the first three phases of -// gunbc.machine_intake_phase arrival_phases_all -- AccessDiscover, IdentityBindProvisional and -// PriorLifeBoundary -- each a protocol step through gunbc.host_convergence_protocol +// THE ARRIVAL CONVERGENCE PREFIX (docs/plans/managed-host-untangle.md, cuts O1c-1 through O1c-3): the +// first real consumer of gunbc.fleet.convergence_fold. Its four steps are the first four phases of +// gunbc.machine_intake_phase arrival_phases_all -- AccessDiscover, IdentityBindProvisional, +// PriorLifeBoundary and BmcSecure -- each a protocol step through gunbc.host_convergence_protocol // HostEffectDomainProjection (observe, assess, decide) with its own row in // gunbc.host_convergence_census. The first two are read-only: the only decision either can reach is // Noop (the phase's goal is read back as already holding) or Refuse; neither has an Apply, so an Apply // decision is itself a typed refusal, and the read each consumes is a committed capture. // PriorLifeBoundary is the first effectful step: its archive arm writes the controller's prior-life // records to the evidence store and reads them back, DRY in this cut -// (gunbc.machine_intake_prior_life_boundary), so it is the one step that can be INCOMPLETE. Nothing -// here performs a live BMC call or a live store write. gunbc.bmc_onboarding is not imported. +// (gunbc.machine_intake_prior_life_boundary), so it is the one step that can be INCOMPLETE. +// BmcSecure is the first GATED step: Apply refuses unless authorization is discharged for that unit +// and that run (standing grant, operator capability, or federated workload principal selected before +// the fold); Noop needs no discharge. Nothing here performs a live BMC credential write. +// gunbc.bmc_onboarding is not imported. // // ORDERING TIME. No field of the read-only steps is an ordering clock. The controller's own DateTime is // kept as an AccessDiscover observation only; the observer clock (gunbc.clock_read) orders the @@ -154,6 +204,8 @@ data identity_bind_provisional_step_ref: DeclarationRef = decl_ref(module_path: data prior_life_boundary_step_ref: DeclarationRef = decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "prior_life_boundary") +data bmc_secure_step_ref: DeclarationRef = decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure") + // A read-only phase has no remedy. The plan type of its projection is sealed to this module and this // module constructs none, so an Apply over a read-only phase has no value to carry. type ReadOnlyPhaseRemedy sole_constructor { @@ -454,6 +506,7 @@ type ArrivalStepReceipt = AccessDiscoverReceipt { receipt: AccessDiscoverEstablished } | IdentityBindReceipt { receipt: IdentityBoundProvisional } | PriorLifeBoundaryReceipt { receipt: PriorLifeBoundaryEstablished } + | BmcSecureReceipt { receipt: BmcSecureStepEstablished } type ArrivalStepRefusal = AccessDiscoverObserveRefused { cause: AccessObserveRefusal } @@ -464,17 +517,37 @@ type ArrivalStepRefusal | ReadOnlyPhaseSatisfiedWithoutEvidence { phase: ArrivalPhase } | PriorLifeIdentityForAnotherInstance { identity: StepInstanceKey } | PriorLifeBoundaryRefused { cause: PriorLifeRefusal } + | BmcSecureIdentityForAnotherInstance { identity: StepInstanceKey } + | BmcSecureAuthorizationUndischarged + | BmcSecurePhaseDomainRefused + | BmcSecureApplyInputsMissing + | BmcSecureApplicationForAnotherInstance { subject: MachineIntakeSubject } + | BmcSecureApplicationNotTheInspectedTransition -// A STEP WHOSE EFFECT WAS ATTEMPTED AND IS NOT GROUNDED BY ITS READBACK. Only PriorLifeBoundary has -// an effect in this prefix; the read-only phases cannot be incomplete. type ArrivalStepIncomplete = PriorLifeBoundaryIncomplete { cause: PriorLifeIncomplete } +type BmcSecureStepEstablished sole_constructor { + key: StepInstanceKey + consumed_prior_life: StepInstanceKey + phase: BmcSecurePhaseReceipt +} + +fn bmc_secure_step_established(key: StepInstanceKey, consumed_prior_life: StepInstanceKey, phase: BmcSecurePhaseReceipt) -> BmcSecureStepEstablished + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure"), + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure_bound"), + ] +{ + BmcSecureStepEstablished { key: key, consumed_prior_life: consumed_prior_life, phase: phase } +} + fn arrival_receipt_key(r: ArrivalStepReceipt) -> StepInstanceKey { match r { AccessDiscoverReceipt { receipt: a } => a.key IdentityBindReceipt { receipt: i } => i.key PriorLifeBoundaryReceipt { receipt: p } => prior_life_receipt_key(r: p) + BmcSecureReceipt { receipt: b } => b.key } } @@ -483,11 +556,25 @@ fn arrival_receipt_consumed(r: ArrivalStepReceipt) -> List { AccessDiscoverReceipt { receipt: _ } => [] IdentityBindReceipt { receipt: i } => [i.consumed_access] PriorLifeBoundaryReceipt { receipt: p } => [prior_life_receipt_consumed_identity(r: p)] + BmcSecureReceipt { receipt: b } => [b.consumed_prior_life] + } +} + +fn arrival_receipt_authorization_required(r: ArrivalStepReceipt) -> Bool { + match r { + AccessDiscoverReceipt { receipt: _ } => false + IdentityBindReceipt { receipt: _ } => false + PriorLifeBoundaryReceipt { receipt: _ } => false + BmcSecureReceipt { receipt: b } => + match b.phase.decision { + BmcSecurePhaseNoop { observation: _ } => false + BmcSecurePhaseApplied { plan: _, application: _, post_read: _ } => true + } } } fn arrival_receipt_projection() -> ConvergenceReceiptProjection { - convergence_receipt_projection(key_of: arrival_receipt_key, consumed_of: arrival_receipt_consumed) + convergence_receipt_projection(key_of: arrival_receipt_key, consumed_of: arrival_receipt_consumed, authorization_required_of: arrival_receipt_authorization_required) } fn read_only_refusal(phase: ArrivalPhase, decision: UpsertDecision) -> ArrivalStepRefusal { @@ -582,6 +669,251 @@ fn prior_life_boundary(key: StepInstanceKey, identity: IdentityBoundProvisional, } } +type BmcSecureApplyInputs { + application: BmcAccountApplication + post_read: BmcAccountStateObservation +} + +type BmcSecureInputs { + goal: BmcSecureGoal + reading: BmcAccountStateObservation + grant: StandingOperatorGrant? + capability: Redemption? + apply: BmcSecureApplyInputs? +} + +// A federated principal is not a discharge input. StandingBmcSecureAccountWrite on the operator-ssh +// units is WorkloadIdentityUnbindable, so FederatedScopedGrant is not a selected pattern for this +// effect. A NonEmptyStr here was a silent widen (review 38602): any string minted Apply. + +fn bmc_secure_account_rotation_site() -> DeclarationRef { + decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "plan_bmc_account_action") +} + +fn grant_discharges_bmc_secure_account_write(g: StandingOperatorGrant, host: HostIdentity) -> Bool { + standing_grant_covers(g: g, scope: StandingGrantHost { host: host }, effect: StandingBmcSecureAccountWrite) + && match standing_grant_arm_interlock(g: g, e: StandingBmcSecureAccountWrite) { + InterlockPending { obligation: _ } => false + InterlockedBy { hold: h } => + ruling_interlock_is_rostered(site_ref: bmc_secure_account_rotation_site(), interlock: h) + } +} + +fn capability_discharges_bmc_secure_account_write( + r: Redemption, + standing: BmcRotationRouteStanding, + request: BmcAccountWriteRequest, + key: StepInstanceKey, +) -> Bool { + match r { + RedemptionRefused { cause: _ } => false + RedemptionAdmitted { claims: cl, decided_by: _ } => + match cl.decision { + ProposeDeny => false + ProposeApprove => + cl.authority == rotation_apply_authority + && (cl.attempt as NonEmptyStr) == (key.run as NonEmptyStr) + && (cl.request_revision as String) == (rotation_route_request_identity(standing: standing, request: request) as String) + } + } +} + +fn bound_bmc_secure_host(key: StepInstanceKey) -> HostIdentity? { + operator_host_named(label: key.subject as NonEmptyStr) +} + +fn bound_host_owns_this_controller(host: HostIdentity, endpoint: BmcControllerEndpoint) -> Bool { + host_identity_eq(a: host, b: operator_host_mtjade1) + && (endpoint.host as String) == (sol_session_controller(a: mtjade1_smbios_sol_attribution).host as String) +} + +fn bmc_secure_instance_joins_this_controller( + key: StepInstanceKey, + identity_subject: MachineIntakeSubject, + reading: BmcAccountStateObservation, + standing: BmcRotationRouteStanding, +) -> Bool { + same_intake_subject(left: reading.subject, right: identity_subject) + && (key.run as String) == (identity_subject.attempt_id as String) + && (standing.endpoint.host as String) == (reading.endpoint.host as String) + && match bound_bmc_secure_host(key: key) { + Absent => false + Present { value: host } => bound_host_owns_this_controller(host: host, endpoint: standing.endpoint) + } +} + +fn capability_discharge( + key: StepInstanceKey, + inputs: BmcSecureInputs, + standing: BmcRotationRouteStanding, + request: BmcAccountWriteRequest, +) -> InstanceAuthorization? + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "discharge_bmc_secure_apply"), + ] +{ + match inputs.capability { + Present { value: r } => + if capability_discharges_bmc_secure_account_write(r: r, standing: standing, request: request, key: key) { + match r { + RedemptionAdmitted { claims: _, decided_by: by } => Present { value: instance_authorization(key: key, principal: by) } + RedemptionRefused { cause: _ } => none + } + } else { + none + } + Absent => none + } +} + +fn discharge_bmc_secure_apply( + key: StepInstanceKey, + identity_subject: MachineIntakeSubject, + inputs: BmcSecureInputs, + standing: BmcRotationRouteStanding, + request: BmcAccountWriteRequest, +) -> InstanceAuthorization? + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure_bound"), + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "converge_arrival_through_bmc_secure"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_uncovered_grant_falls_through_to_capability"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_uncovered_grant_refuses_a_capability_for_another_request"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_a_foreign_bound_host_key_cannot_mint_from_this_instance_controller"), + ] +{ + if !bmc_secure_instance_joins_this_controller(key: key, identity_subject: identity_subject, reading: inputs.reading, standing: standing) { + none + } else { + match bound_bmc_secure_host(key: key) { + Absent => none + Present { value: host } => + match inputs.grant { + Present { value: g } => + if grant_discharges_bmc_secure_account_write(g: g, host: host) { + Present { value: instance_authorization(key: key, principal: g.principal) } + } else { + capability_discharge(key: key, inputs: inputs, standing: standing, request: request) + } + Absent => + capability_discharge(key: key, inputs: inputs, standing: standing, request: request) + } + } + } +} + +fn managed_generation_advances_the_requested(requested: ManagedCredentialReference, planned: ManagedCredentialReference) -> Bool { + requested.account == planned.account + && requested.role == planned.role + && (requested.secret.project as String) == (planned.secret.project as String) + && requested.secret.secret == planned.secret.secret + && planned.credential_epoch > requested.credential_epoch +} + +fn bmc_account_observation_is_the_inspected_pre_state(inspected: BmcAccountStateObservation, diverged: BmcAccountStateObservation) -> Bool { + bmc_account_state_observation_same(a: inspected, b: diverged) +} + +fn bmc_secure_application_joins_this_instance( + identity_subject: MachineIntakeSubject, + inspected_reading: BmcAccountStateObservation, + requested_goal: BmcSecureGoal, + application: BmcAccountApplication, +) -> ArrivalStepRefusal? { + if !same_intake_subject(left: application.plan.subject, right: identity_subject) { + Present { value: BmcSecureApplicationForAnotherInstance { subject: application.plan.subject } } + } else if !bmc_account_observation_is_the_inspected_pre_state(inspected: inspected_reading, diverged: application.plan.diverged) { + Present { value: BmcSecureApplicationNotTheInspectedTransition } + } else if !managed_reference_same(a: requested_goal.published_break_glass, b: application.plan.goal.published_break_glass) { + Present { value: BmcSecureApplicationNotTheInspectedTransition } + } else if !content_hash_equal(left: machine_qualification_policy_digest(policy: requested_goal.policy), right: machine_qualification_policy_digest(policy: application.plan.goal.policy)) { + Present { value: BmcSecureApplicationNotTheInspectedTransition } + } else if !managed_generation_advances_the_requested(requested: requested_goal.managed, planned: application.plan.goal.managed) { + Present { value: BmcSecureApplicationNotTheInspectedTransition } + } else { + none + } +} + +// Apply and discharge consume the bound subject's identity and the instance keys, not the +// AccessDiscover/IdentityBind capture parses. Those steps remain the prefix; this is the BMC +// grain after PriorLifeBoundary. +fn bmc_secure_bound( + key: StepInstanceKey, + identity_key: StepInstanceKey, + identity_subject: MachineIntakeSubject, + prior_key: StepInstanceKey, + inputs: BmcSecureInputs, +) -> StepOutcome + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "bmc_secure"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "bmc_secure_bound_refuses_foreign_apply"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_without_authorization_refuses_after_a_real_apply"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_for_a_later_break_glass_generation_is_refused"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_another_channel_state_is_refused"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_a_different_present_controller_clock_is_refused"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_a_present_controller_clock_over_absent_is_refused"), + ] +{ + if (identity_key.run as NonEmptyStr) != (key.run as NonEmptyStr) || (identity_key.subject as NonEmptyStr) != (key.subject as NonEmptyStr) { + StepRefused { key: key, cause: BmcSecureIdentityForAnotherInstance { identity: identity_key } } + } else if (prior_key.run as NonEmptyStr) != (key.run as NonEmptyStr) || (prior_key.subject as NonEmptyStr) != (key.subject as NonEmptyStr) { + StepRefused { key: key, cause: BmcSecureIdentityForAnotherInstance { identity: prior_key } } + } else { + match inspect_bmc_secure_state(subject: identity_subject, goal: inputs.goal, reading: inputs.reading) { + BmcSecureStateUnobserved { cause: _ } => StepRefused { key: key, cause: BmcSecurePhaseDomainRefused } + BmcSecureStateInspected { observed: _, assessment: a, decision: d } => + match d { + Noop => + match bmc_secure_phase_noop(subject: identity_subject, goal: inputs.goal, reading: inputs.reading) { + BmcSecurePhaseConverged { receipt: phase } => + StepEstablished { receipt: BmcSecureReceipt { receipt: bmc_secure_step_established(key: key, consumed_prior_life: prior_key, phase: phase) } } + BmcSecurePhaseRefused { cause: _ } => StepRefused { key: key, cause: BmcSecurePhaseDomainRefused } + } + Refuse { reason: _ } => StepRefused { key: key, cause: BmcSecurePhaseDomainRefused } + Apply { plan: _ } => + match inputs.apply { + Absent => StepRefused { key: key, cause: BmcSecureApplyInputsMissing } + Present { value: ap } => + match bmc_secure_application_joins_this_instance( + identity_subject: identity_subject, + inspected_reading: inputs.reading, + requested_goal: inputs.goal, + application: ap.application, + ) { + Present { value: c } => StepRefused { key: key, cause: c } + Absent => + match discharge_bmc_secure_apply( + key: key, + identity_subject: identity_subject, + inputs: inputs, + standing: ap.application.plan.managed_write.route, + request: ap.application.plan.managed_write.request, + ) { + Absent => StepRefused { key: key, cause: BmcSecureAuthorizationUndischarged } + Present { value: _ } => + match bmc_secure_phase_applied(application: ap.application, post_read: ap.post_read) { + BmcSecurePhaseConverged { receipt: phase } => + StepEstablished { receipt: BmcSecureReceipt { receipt: bmc_secure_step_established(key: key, consumed_prior_life: prior_key, phase: phase) } } + BmcSecurePhaseRefused { cause: _ } => StepRefused { key: key, cause: BmcSecurePhaseDomainRefused } + } + } + } + } + } + } + } +} + +fn bmc_secure(key: StepInstanceKey, identity: IdentityBoundProvisional, prior: PriorLifeBoundaryEstablished, inputs: BmcSecureInputs) -> StepOutcome { + bmc_secure_bound( + key: key, + identity_key: identity.key, + identity_subject: identity.subject, + prior_key: prior_life_receipt_key(r: prior), + inputs: inputs, + ) +} + // ── THE PLAN: a prefix of the authority's order, never a second list ─────────────────────────── fn arrival_order_authority() -> ConvergenceOrderAuthority { @@ -599,10 +931,15 @@ fn arrival_planning_authority() -> ConvergencePlanningAuthority { // the plan; it is never skipped. fn arrival_step_of_phase(phase: ArrivalPhase) -> ConvergenceStep? { match phase { - AccessDiscover => Present { value: ConvergenceStep { identity: access_discover_step_ref, order_member: Arrival { phase: AccessDiscover }, preconditions: [] } } - IdentityBindProvisional => Present { value: ConvergenceStep { identity: identity_bind_provisional_step_ref, order_member: Arrival { phase: IdentityBindProvisional }, preconditions: [SameSubject { step: access_discover_step_ref }] } } - PriorLifeBoundary => Present { value: ConvergenceStep { identity: prior_life_boundary_step_ref, order_member: Arrival { phase: PriorLifeBoundary }, preconditions: [SameSubject { step: identity_bind_provisional_step_ref }] } } - BmcSecure => none + AccessDiscover => Present { value: ungated_readonly_step(identity: access_discover_step_ref, order_member: Arrival { phase: AccessDiscover }, preconditions: []) } + IdentityBindProvisional => Present { value: ungated_readonly_step(identity: identity_bind_provisional_step_ref, order_member: Arrival { phase: IdentityBindProvisional }, preconditions: [SameSubject { step: access_discover_step_ref }]) } + PriorLifeBoundary => Present { value: ungated_readonly_step(identity: prior_life_boundary_step_ref, order_member: Arrival { phase: PriorLifeBoundary }, preconditions: [SameSubject { step: identity_bind_provisional_step_ref }]) } + BmcSecure => Present { value: ConvergenceStep { + identity: bmc_secure_step_ref, + order_member: Arrival { phase: BmcSecure }, + preconditions: [SameSubject { step: prior_life_boundary_step_ref }], + gate: StepGatedOnAuthorization, + } } BootDeliveryEstablish => none DiagnosticBootAttest => none InventoryConform => none @@ -682,21 +1019,56 @@ fn arrival_prefix_outcomes(run: ConvergenceRunId, subject: StepSubjectKey, read: [access, identity, prior_life_boundary(key: step_instance_key(run: run, step: prior_life_boundary_step_ref, subject: subject), identity: i, platform: prior_life.platform, world: prior_life.world)] AccessDiscoverReceipt { receipt: _ } => [access, identity] PriorLifeBoundaryReceipt { receipt: _ } => [access, identity] + BmcSecureReceipt { receipt: _ } => [access, identity] } Absent => [access, identity] } } IdentityBindReceipt { receipt: _ } => [access] PriorLifeBoundaryReceipt { receipt: _ } => [access] + BmcSecureReceipt { receipt: _ } => [access] + } + } +} + +fn arrival_prefix_outcomes_through_bmc_secure(run: ConvergenceRunId, subject: StepSubjectKey, read: CurrentManagerRead, inputs: IdentityBindInputs, prior_life: PriorLifeInputs, bmc: BmcSecureInputs) -> List> { + let prefix = arrival_prefix_outcomes(run: run, subject: subject, read: read, inputs: inputs, prior_life: prior_life) + match prefix.last() { + Absent => prefix + Present { value: last } => + match step_outcome_receipt(outcome: last) { + Absent => prefix + Present { value: r } => + match r { + PriorLifeBoundaryReceipt { receipt: p } => + match prefix |> filter(o => match step_outcome_receipt(outcome: o) { Present { value: ir } => match ir { IdentityBindReceipt { receipt: _ } => true _ => false } Absent => false }) |> first() { + Absent => prefix + Present { value: id_out } => + match step_outcome_receipt(outcome: id_out) { + Absent => prefix + Present { value: ir } => + match ir { + IdentityBindReceipt { receipt: i } => + concat(prefix, [bmc_secure(key: step_instance_key(run: run, step: bmc_secure_step_ref, subject: subject), identity: i, prior: p, inputs: bmc)]) + AccessDiscoverReceipt { receipt: _ } => prefix + PriorLifeBoundaryReceipt { receipt: _ } => prefix + BmcSecureReceipt { receipt: _ } => prefix + } + } + } + AccessDiscoverReceipt { receipt: _ } => prefix + IdentityBindReceipt { receipt: _ } => prefix + BmcSecureReceipt { receipt: _ } => prefix + } } } } -fn fold_arrival_prefix(steps: List>, run: ConvergenceRunId, subject: StepSubjectKey, outcomes: List>) -> ArrivalPrefixVerdict { +fn fold_arrival_prefix(steps: List>, run: ConvergenceRunId, subject: StepSubjectKey, outcomes: List>, discharges: List) -> ArrivalPrefixVerdict { match admit_convergence_plan(planning: arrival_planning_authority(), steps: steps) { ConvergencePlanRefused { cause: c } => ArrivalPlanRefused { cause: c } ConvergencePlanAdmitted { plan: plan } => - ArrivalRunFolded { verdict: fold_convergence_run(plan: plan, run: run, subject: subject, outcomes: outcomes, projection: arrival_receipt_projection()) } + ArrivalRunFolded { verdict: fold_convergence_run(plan: plan, run: run, subject: subject, outcomes: outcomes, projection: arrival_receipt_projection(), discharges: discharges) } } } @@ -704,7 +1076,34 @@ fn converge_arrival_prefix(run: ConvergenceRunId, subject: StepSubjectKey, read: match arrival_prefix_steps(phases: arrival_prefix_through(last: arrival_prefix_last)) { ArrivalPrefixPhaseUnstepped { phase: p } => ArrivalPrefixUnstepped { phase: p } ArrivalPrefixStepped { steps: steps } => - fold_arrival_prefix(steps: steps, run: run, subject: subject, outcomes: arrival_prefix_outcomes(run: run, subject: subject, read: read, inputs: inputs, prior_life: prior_life)) + fold_arrival_prefix(steps: steps, run: run, subject: subject, outcomes: arrival_prefix_outcomes(run: run, subject: subject, read: read, inputs: inputs, prior_life: prior_life), discharges: []) + } +} + +// Apply mints through discharge_bmc_secure_apply. Noop never requires a fold discharge +// (arrival_receipt_authorization_required is false on BmcSecurePhaseNoop), so a grant +// copied here would be unused. +fn converge_arrival_through_bmc_secure(run: ConvergenceRunId, subject: StepSubjectKey, read: CurrentManagerRead, inputs: IdentityBindInputs, prior_life: PriorLifeInputs, bmc: BmcSecureInputs) -> ArrivalPrefixVerdict { + match arrival_prefix_steps(phases: arrival_prefix_through(last: BmcSecure)) { + ArrivalPrefixPhaseUnstepped { phase: p } => ArrivalPrefixUnstepped { phase: p } + ArrivalPrefixStepped { steps: steps } => { + let key = step_instance_key(run: run, step: bmc_secure_step_ref, subject: subject) + let discharges = match bmc.apply { + Present { value: ap } => + match discharge_bmc_secure_apply( + key: key, + identity_subject: bmc.reading.subject, + inputs: bmc, + standing: ap.application.plan.managed_write.route, + request: ap.application.plan.managed_write.request, + ) { + Present { value: d } => [d] + Absent => [] + } + Absent => [] + } + fold_arrival_prefix(steps: steps, run: run, subject: subject, outcomes: arrival_prefix_outcomes_through_bmc_secure(run: run, subject: subject, read: read, inputs: inputs, prior_life: prior_life, bmc: bmc), discharges: discharges) + } } } @@ -742,3 +1141,14 @@ fn converge_mtjade1_arrival_prefix(run: ConvergenceRunId, prior_life: DryPriorLi prior_life: PriorLifeInputs { platform: mt_jade_platform_ref, world: prior_life }, ) } + +fn converge_mtjade1_arrival_through_bmc_secure(run: ConvergenceRunId, prior_life: DryPriorLifeWorld, bmc: BmcSecureInputs) -> ArrivalPrefixVerdict uses fs: std.resources.Filesystem { + converge_arrival_through_bmc_secure( + run: run, + subject: "mtjade1" as StepSubjectKey, + read: read_mtjade1_current_manager_read(), + inputs: mtjade1_identity_bind_inputs(), + prior_life: PriorLifeInputs { platform: mt_jade_platform_ref, world: prior_life }, + bmc: bmc, + ) +} diff --git a/dag/gunbc/machine_intake/arrival_interlock.dag b/dag/gunbc/machine_intake/arrival_interlock.dag new file mode 100644 index 00000000000..28c706f3869 --- /dev/null +++ b/dag/gunbc/machine_intake/arrival_interlock.dag @@ -0,0 +1,184 @@ +module gunbc.machine_intake_arrival_interlock + +import std.types { NonEmptyStr, String } +import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } +import std.durable_compare_and_set { CasGeneration } +import std.durable_exclusive_hold { DurableHoldOwnerRef, DurableHoldReleaseRef } +import gunbc.durable_exclusive_hold_file_store { + file_hold_acquire, file_hold_release_assess, file_hold_release_commit, + FileHoldAcquireOutcome, + FileHoldAcquired, FileHoldOccupied, FileHoldAcquireLost, FileHoldAcquireStoreUnavailable, + FileHoldSlotUndecodable, FileHoldKeyNotSlotAddressable, + FileHoldReleasePlanned, FileHoldReleaseNotEligible, + FileHoldReleased, FileHoldReleaseLost, FileHoldReleaseStoreUnavailable, FileHoldReleaseKeyNotSlotAddressable, +} +import gunbc.machine_intake_bmc_rotation_route { + BmcRotationRouteStanding, rotation_route_is_grounded, +} +import gunbc.machine_intake_arrival_subject { ArrivalSubjectAdmitted } + +// ── THE INTERLOCK AN MTJADE1 ARRIVAL WRITE TAKES ────────────────────────────────────────────── +// The standing grant's words cover the class; this is the physical-safety / route-ground fact the +// run must observe. It is not UnitHoldProof: mtjade1 has no ManagedHost unit-hold row until J4 +// admission. The store host is operator_host_srv1 (the same runner the arrival job pins). A wet +// acquire is arrival_archive_slot_acquire (file_hold_acquire on arrival_slot_key); the caller +// runs the gated effect while ArrivalArchiveSlotHeld is in hand, then arrival_archive_slot_release. +// admit_arrival_* are supplied-slot checks, not the live hold. +// +// Archive (J2) needs the exclusive slot only (reads, evidence write, no account rotation). +// BmcSecure (J4) is InterlockedBy admit_rotation_apply (#13517). Firmware qualification stays +// InterlockPending until a writer under ArrivalArchiveSlotHeld lands in the wet door; the prefix +// is PriorLifeBoundary only. admit_arrival_* remain supplied-slot checks for witnesses. + +fn arrival_slot_key(subject_wire: NonEmptyStr) -> NonEmptyStr { + join(["arrival:", subject_wire as String], "") as NonEmptyStr +} + +type ArrivalSlotStanding + = ArrivalSlotHeld { subject: ArrivalSubjectAdmitted } + | ArrivalSlotNotHeld { cause: NonEmptyStr } + +type ArrivalArchiveInterlockStanding + = ArrivalArchiveInterlockAdmitted { subject: ArrivalSubjectAdmitted } + | ArrivalArchiveInterlockRefused { cause: NonEmptyStr } + +type ArrivalBmcSecureInterlockStanding + = ArrivalBmcSecureInterlockAdmitted { subject: ArrivalSubjectAdmitted } + | ArrivalBmcSecureInterlockRefused { cause: NonEmptyStr } + +fn admit_arrival_archive_interlock(subject: ArrivalSubjectAdmitted, slot: ArrivalSlotStanding) -> ArrivalArchiveInterlockStanding { + match slot { + ArrivalSlotHeld { subject: held } => + if (held.wire as String) == (subject.wire as String) { + ArrivalArchiveInterlockAdmitted { subject: subject } + } else { + ArrivalArchiveInterlockRefused { cause: "arrival slot was held for a different subject" as NonEmptyStr } + } + ArrivalSlotNotHeld { cause: c } => ArrivalArchiveInterlockRefused { cause: c } + } +} + +fn admit_arrival_bmc_secure_interlock( + subject: ArrivalSubjectAdmitted, + slot: ArrivalSlotStanding, + route: BmcRotationRouteStanding, +) -> ArrivalBmcSecureInterlockStanding { + match admit_arrival_archive_interlock(subject: subject, slot: slot) { + ArrivalArchiveInterlockRefused { cause: c } => ArrivalBmcSecureInterlockRefused { cause: c } + ArrivalArchiveInterlockAdmitted { subject: s } => + if rotation_route_is_grounded(standing: route) { + ArrivalBmcSecureInterlockAdmitted { subject: s } + } else { + ArrivalBmcSecureInterlockRefused { cause: "rotation route is not grounded; admit_rotation_apply refuses" as NonEmptyStr } + } + } +} + +fn arrival_slot_acquire_refusal(outcome: FileHoldAcquireOutcome) -> NonEmptyStr { + match outcome { + FileHoldAcquired { slot_key: _, owner: _, generation: _ } => "arrival slot acquire succeeded" as NonEmptyStr + FileHoldOccupied { slot_key: k, holder: h, generation: _ } => + join(["arrival slot ", k as String, " is held by '", h as String, "'"], "") as NonEmptyStr + FileHoldAcquireLost { slot_key: k } => join(["lost the race for arrival slot ", k as String], "") as NonEmptyStr + FileHoldAcquireStoreUnavailable { slot_key: k, cause: _ } => + join(["arrival slot store unavailable for ", k as String], "") as NonEmptyStr + FileHoldSlotUndecodable { slot_key: k, generation: _, detail: _ } => + join(["arrival slot ", k as String, " is undecodable"], "") as NonEmptyStr + FileHoldKeyNotSlotAddressable { slot_key: k } => + join(["arrival slot key ", k as String, " is not slot-addressable"], "") as NonEmptyStr + } +} + +type ArrivalArchiveSlotHeld sole_constructor { + subject: ArrivalSubjectAdmitted + slot_key: NonEmptyStr + owner: DurableHoldOwnerRef + generation: CasGeneration +} + +type ArrivalArchiveSlotAcquire + = ArrivalArchiveSlotAcquired { held: ArrivalArchiveSlotHeld } + | ArrivalArchiveSlotAcquireRefused { cause: NonEmptyStr } + +type ArrivalArchiveSlotRelease + = ArrivalArchiveSlotReleased { generation: CasGeneration } + | ArrivalArchiveSlotReleaseRefused { cause: NonEmptyStr } + +fn arrival_archive_slot_acquire(root: NonEmptyStr, subject: ArrivalSubjectAdmitted, owner: DurableHoldOwnerRef) -> ArrivalArchiveSlotAcquire { + let key = arrival_slot_key(subject_wire: subject.wire) + match file_hold_acquire(root: root, slot_key: key, requested_owner: owner) { + FileHoldOccupied { slot_key: k, holder: h, generation: g } => + ArrivalArchiveSlotAcquireRefused { cause: arrival_slot_acquire_refusal(outcome: FileHoldOccupied { slot_key: k, holder: h, generation: g }) } + FileHoldAcquireLost { slot_key: k } => + ArrivalArchiveSlotAcquireRefused { cause: arrival_slot_acquire_refusal(outcome: FileHoldAcquireLost { slot_key: k }) } + FileHoldAcquireStoreUnavailable { slot_key: k, cause: c } => + ArrivalArchiveSlotAcquireRefused { cause: arrival_slot_acquire_refusal(outcome: FileHoldAcquireStoreUnavailable { slot_key: k, cause: c }) } + FileHoldSlotUndecodable { slot_key: k, generation: g, detail: d } => + ArrivalArchiveSlotAcquireRefused { cause: arrival_slot_acquire_refusal(outcome: FileHoldSlotUndecodable { slot_key: k, generation: g, detail: d }) } + FileHoldKeyNotSlotAddressable { slot_key: k } => + ArrivalArchiveSlotAcquireRefused { cause: arrival_slot_acquire_refusal(outcome: FileHoldKeyNotSlotAddressable { slot_key: k }) } + FileHoldAcquired { slot_key: k, owner: o, generation: g } => + ArrivalArchiveSlotAcquired { + held: ArrivalArchiveSlotHeld { subject: subject, slot_key: k, owner: o, generation: g }, + } + } +} + +fn arrival_archive_slot_release(root: NonEmptyStr, held: ArrivalArchiveSlotHeld) -> ArrivalArchiveSlotRelease { + match file_hold_release_assess( + root: root, + slot_key: held.slot_key, + owner: held.owner, + acquired_generation: held.generation, + released_by: "arrival-converge-archive-interlock" as DurableHoldReleaseRef, + ) { + FileHoldReleaseNotEligible { slot_key: _, refusal: _ } => + ArrivalArchiveSlotReleaseRefused { cause: "arrival slot release was not eligible" as NonEmptyStr } + FileHoldReleasePlanned { plan: p } => + match file_hold_release_commit(plan: p) { + FileHoldReleased { slot_key: _, generation: g } => ArrivalArchiveSlotReleased { generation: g } + FileHoldReleaseLost { slot_key: _ } => + ArrivalArchiveSlotReleaseRefused { cause: "arrival slot release lost the race" as NonEmptyStr } + FileHoldReleaseStoreUnavailable { slot_key: _, cause: _ } => + ArrivalArchiveSlotReleaseRefused { cause: "arrival slot store unavailable on release" as NonEmptyStr } + FileHoldReleaseKeyNotSlotAddressable { slot_key: _ } => + ArrivalArchiveSlotReleaseRefused { cause: "arrival slot key not addressable on release" as NonEmptyStr } + } + } +} + +fn arrival_combine_prefix_and_release(prefix: ProcessExit, slot: ArrivalArchiveSlotRelease) -> ProcessExit { + match slot { + ArrivalArchiveSlotReleased { generation: _ } => prefix + ArrivalArchiveSlotReleaseRefused { cause: c } => + match prefix { + ExitSuccess => exit_failure(reason: concat("arrival_converge: prefix held, then slot release refused: ", c as String)) + ExitFailure { code: code, reason: reason } => + ExitFailure { code: code, reason: concat(reason, concat("; then slot release refused: ", c as String)) } + } + } +} + +fn arrival_archive_slot_held( + subject: ArrivalSubjectAdmitted, + slot_key: NonEmptyStr, + owner: DurableHoldOwnerRef, + generation: CasGeneration, +) -> ArrivalArchiveSlotHeld { + ArrivalArchiveSlotHeld { subject: subject, slot_key: slot_key, owner: owner, generation: generation } +} + +// Acquire, body, then the caller-supplied release. Production binds arrival_archive_slot_release +// at the one wet-door call site. One inhabitance claim also binds that same function. +fn arrival_after_acquire( + outcome: ArrivalArchiveSlotAcquire, + body: fn() -> ProcessExit, + release_fn: fn(ArrivalArchiveSlotHeld) -> ArrivalArchiveSlotRelease, +) -> ProcessExit { + match outcome { + ArrivalArchiveSlotAcquireRefused { cause: c } => + exit_failure(reason: concat("arrival_converge: ", c as String)) + ArrivalArchiveSlotAcquired { held: h } => + arrival_combine_prefix_and_release(prefix: body(), slot: release_fn(h)) + } +} diff --git a/dag/gunbc/machine_intake/arrival_run.dag b/dag/gunbc/machine_intake/arrival_run.dag new file mode 100644 index 00000000000..6a4fe275861 --- /dev/null +++ b/dag/gunbc/machine_intake/arrival_run.dag @@ -0,0 +1,89 @@ +module gunbc.machine_intake_arrival_run + +import std.types { NonEmptyStr, String } +import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } +import std.durable_exclusive_hold { DurableHoldOwnerRef } +import gunbc.actions_run_binding { + actions_variable_read, ActionsVariablePresent, ActionsVariableAbsent, + fleet_converge_arrival_subject_env_name, +} +import extdeps.github.actions_environment { github_run_id_variable_name } +import gunbc.auth.access_token_source { + select_access_token_source, AccessTokenSourceSelected, AccessTokenSourceUnavailable, + WorkloadIdentityToken, GcloudPrintToken, OperatorSuppliedToken, +} +import gunbc.machine_intake_arrival_admission { arrival_admit_executor } +import gunbc.machine_intake_arrival_subject { + admit_arrival_subject, ArrivalSubjectAdmitted, ArrivalSubjectInPopulation, + ArrivalSubjectUnknown, ArrivalSubjectIsManagedHostExecutorConfusion, ArrivalSubjectOutsideGrant, +} +import gunbc.machine_intake_arrival_interlock { + arrival_archive_slot_acquire, arrival_archive_slot_release, arrival_after_acquire, +} +import gunbc.managed_host_unit_hold { unit_hold_store_root } + +// THE ARRIVAL JOB'S WET DOOR (review 77263, review 77435, review 77746). Re-runs credential-free +// admit so WIF cannot outrun a refused subject, then requires WorkloadIdentityToken (the minted +// WIF bearer, in-process — not interpolated onto argv), takes arrival_archive_slot_acquire, and +// arrival_archive_slot_release. It does not fold a dry/unobserved PriorLifeBoundary into +// ExitSuccess: that would be fabricated plausible output (DESIGN §5). Until a live BMC/archive +// observation is bound as the prefix input, the held door refuses with a typed cause. Witnesses +// still supply a DryPriorLifeWorld to converge_mtjade1_arrival_prefix. BMC account writes and +// the pinned bmc-mtjade1-gunbc accessor stay behind this door: the locus is +// SecretLocusReservedNotOnAccessorRoster until FamilyObserved. + +fn arrival_wet_prefix_from_unobserved_world() -> ProcessExit { + exit_failure(reason: "arrival_converge refuses unobserved PriorLifeBoundary: no production observation is bound") +} + +fn arrival_converge_wet() -> ProcessExit uses fs: std.resources.Filesystem { + match arrival_admit_executor() { + ExitFailure { code, reason } => ExitFailure { code: code, reason: reason } + ExitSuccess => arrival_converge_after_admit() + } +} + +fn arrival_converge_after_admit() -> ProcessExit uses fs: std.resources.Filesystem { + match select_access_token_source() { + AccessTokenSourceUnavailable { cause: c } => + exit_failure(reason: concat("arrival_converge: ", c as String)) + AccessTokenSourceSelected { source: WorkloadIdentityToken } => + match actions_variable_read(name: fleet_converge_arrival_subject_env_name) { + ActionsVariableAbsent { variable: v, detail: d } => + exit_failure(reason: concat("arrival_converge: ", concat(v, concat(" ", d)))) + ActionsVariablePresent { value: wire } => + match admit_arrival_subject(wire: wire) { + ArrivalSubjectInPopulation { subject: s } => arrival_converge_under_archive_interlock(subject: s) + ArrivalSubjectUnknown { wire: w } => + exit_failure(reason: concat("arrival_converge refuses unknown arrival_subject '", concat(w, "' after admit"))) + ArrivalSubjectIsManagedHostExecutorConfusion { wire: w } => + exit_failure(reason: concat("arrival_converge refuses executor/host as arrival_subject '", concat(w, "' after admit"))) + ArrivalSubjectOutsideGrant { wire: w } => + exit_failure(reason: concat("arrival_converge refuses arrival_subject '", concat(w, "' outside the standing grant after admit"))) + } + } + AccessTokenSourceSelected { source: GcloudPrintToken } => + exit_failure(reason: "arrival_converge: this job federates; select_access_token_source must be WorkloadIdentityToken, not GcloudPrintToken") + AccessTokenSourceSelected { source: OperatorSuppliedToken { token: _ } } => + exit_failure(reason: "arrival_converge: this job federates; select_access_token_source must be WorkloadIdentityToken, not an operator-supplied token") + } +} + +fn arrival_converge_under_archive_interlock(subject: ArrivalSubjectAdmitted) -> ProcessExit uses fs: std.resources.Filesystem { + match actions_variable_read(name: github_run_id_variable_name) { + ActionsVariableAbsent { variable: v, detail: d } => + exit_failure(reason: concat("arrival_converge: ", concat(v, concat(" ", d)))) + ActionsVariablePresent { value: run_id } => + arrival_after_acquire( + outcome: arrival_archive_slot_acquire( + root: unit_hold_store_root, + subject: subject, + owner: join(["arrival-converge:", run_id], "") as DurableHoldOwnerRef, + ), + body: fn() { + arrival_wet_prefix_from_unobserved_world() + }, + release_fn: fn(h) { arrival_archive_slot_release(root: unit_hold_store_root, held: h) }, + ) + } +} diff --git a/dag/gunbc/machine_intake/arrival_subject.dag b/dag/gunbc/machine_intake/arrival_subject.dag new file mode 100644 index 00000000000..aab008ad2c9 --- /dev/null +++ b/dag/gunbc/machine_intake/arrival_subject.dag @@ -0,0 +1,85 @@ +module gunbc.machine_intake_arrival_subject + +import std.types { List, NonEmptyStr, String } +import std.optional { Present, Absent } +import std.decl_ref { DeclarationRef, decl_ref } +import gunbc.auth.standing_operator_grant { + mtjade1_live_standing_grant, standing_grant_covers, StandingGrantHost, + StandingJadeArrivalArchive, StandingJadeFirmwareQualification, +} +import gunbc.fleet_host_identity { operator_host_mtjade1, operator_host_mtcollins1 } + +// ── ARRIVAL-CONVERGE SUBJECT POPULATION ─────────────────────────────────────────────────────── +// J2/J4 cannot read gunbc.managed_host managed_hosts(): mtjade1 is not a member until J4's +// ManagedHostAdmission. The dispatch input for those effects is this population, distinct from the +// executor `host` input and from `managed_host`. Options are derived from the rows, never a second +// hand list in fleet-converge.yml. +// +// A row is a unit that has an arrival-prefix producer (today: converge_mtjade1_arrival_prefix). It +// is not a HostIdentity enrollment and not a managed-host row. + +// This cut's population is one row, mtjade1. Dispatch options project from the +// rows. admit_arrival_subject then joins that singleton to mtjade1_live_standing_grant +// on operator_host_mtjade1; the grant is not a field of the row. A second row +// would appear in the options and still refuse ArrivalSubjectOutsideGrant until +// the admit fold joins each row to its own standing grant and host. +type ArrivalConvergePopulationRow { + wire: NonEmptyStr + producer: DeclarationRef +} + +data arrival_converge_population: List = [ + ArrivalConvergePopulationRow { + wire: "mtjade1" as NonEmptyStr, + producer: decl_ref(module_path: "gunbc.machine_intake_arrival_converge", decl_name: "converge_mtjade1_arrival_prefix"), + }, +] + +fn arrival_converge_subject_options() -> List { + map(arrival_converge_population, r => r.wire as String) +} + +type ArrivalSubjectAdmitted sole_constructor { + wire: NonEmptyStr + producer: DeclarationRef +} + +type ArrivalSubjectAdmission + = ArrivalSubjectInPopulation { subject: ArrivalSubjectAdmitted } + | ArrivalSubjectUnknown { wire: String } + | ArrivalSubjectIsManagedHostExecutorConfusion { wire: String } + | ArrivalSubjectOutsideGrant { wire: String } + +fn arrival_population_row(wire: String) -> ArrivalConvergePopulationRow? { + filter(arrival_converge_population, r => (r.wire as String) == wire).first() +} + +// THE ADMIT FOLD THE ARRIVAL JOB RUNS BEFORE WIF. mtcollins1 is not a member. A spelling that is +// not in arrival_converge_population refuses. Grant coverage for this singleton is +// mtjade1_live_standing_grant on operator_host_mtjade1 (archive and firmware arms). +fn admit_arrival_subject(wire: String) -> ArrivalSubjectAdmission { + if wire == (operator_host_mtcollins1 as String) { + ArrivalSubjectIsManagedHostExecutorConfusion { wire: wire } + } else { + match arrival_population_row(wire: wire) { + Absent => ArrivalSubjectUnknown { wire: wire } + Present { value: row } => + if standing_grant_covers( + g: mtjade1_live_standing_grant, + scope: StandingGrantHost { host: operator_host_mtjade1 }, + effect: StandingJadeArrivalArchive, + ) && standing_grant_covers( + g: mtjade1_live_standing_grant, + scope: StandingGrantHost { host: operator_host_mtjade1 }, + effect: StandingJadeFirmwareQualification, + ) && (row.wire as String) == (operator_host_mtjade1 as String) { + ArrivalSubjectInPopulation { + subject: ArrivalSubjectAdmitted { wire: row.wire, producer: row.producer }, + } + } else { + ArrivalSubjectOutsideGrant { wire: wire } + } + } + } +} + diff --git a/dag/gunbc/machine_intake/bmc_rotation_route.dag b/dag/gunbc/machine_intake/bmc_rotation_route.dag index b997639aa7c..5deebbaa799 100644 --- a/dag/gunbc/machine_intake/bmc_rotation_route.dag +++ b/dag/gunbc/machine_intake/bmc_rotation_route.dag @@ -152,6 +152,8 @@ fn rotation_route_from_citation( decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "cited_lab_route_managed_user"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "cited_lab_route_published_user"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "cited_lab_route_managed_user_previous_build"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "cited_mtjade1_managed_route"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "cited_mtjade1_published_route"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "an_open_channel_is_closed_only_over_a_grounded_channel_access_route"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "the_channel_close_readback_is_bound_to_the_observed_run_and_account"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "a_planned_channel_close_is_applied_read_back_and_converges"), diff --git a/dag/gunbc/machine_intake/bmc_secure.dag b/dag/gunbc/machine_intake/bmc_secure.dag index 473f2ae3885..4a3b782bd43 100644 --- a/dag/gunbc/machine_intake/bmc_secure.dag +++ b/dag/gunbc/machine_intake/bmc_secure.dag @@ -3,7 +3,7 @@ module gunbc.machine_intake_bmc_secure import std.types { Bool, EpochMs, Int, List, Map, NonEmptyStr, String, list_length } import v2.std.algebra { filter } import std.decl_ref { decl_ref } -import std.content_hash { ContentHash, content_hash_of_value } +import std.content_hash { ContentHash, content_hash_of_value, content_hash_equal } import std.dissolution { DissolutionCondition, unbound_dissolution } import std.algebra { FreeSemigroup } import std.goal_assessment { @@ -1286,6 +1286,7 @@ fn observed_bmc_account_state( ) -> BmcAccountStateObservation admit_callers: [ decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "dry_read_bmc_account_state"), + decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "observation_with_controller_clock"), ] { BmcAccountStateObservation { @@ -1328,6 +1329,35 @@ fn observed_published_account_at_join(account: ObservedPublishedAccount) -> Publ } } +fn observation_with_controller_clock(base: BmcAccountStateObservation, controller_clock: ControllerClockReading?) -> BmcAccountStateObservation + admit_callers: [ + decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "bmc_account_state_observation_same"), + decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "apply_with_diverged_controller_clock"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_a_different_present_controller_clock_is_refused"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_a_present_controller_clock_over_absent_is_refused"), + ] +{ + observed_bmc_account_state( + subject: base.subject, + endpoint: base.endpoint, + firmware: base.firmware, + managed: base.managed, + managed_user: base.managed_user, + attempt: base.attempt, + read_floor: base.read_floor, + managed_probe: base.managed_probe, + channel_census: base.channel_census, + census_evidence: base.census_evidence, + census_observed_at: base.census_observed_at, + published_probes: base.published_probes, + published_user: base.published_user, + published_account: base.published_account, + break_glass: base.break_glass, + break_glass_probe: base.break_glass_probe, + controller_clock: controller_clock, + ) +} + fn managed_reference_same(a: ManagedCredentialReference, b: ManagedCredentialReference) -> Bool { a.account == b.account && a.role == b.role @@ -1337,6 +1367,34 @@ fn managed_reference_same(a: ManagedCredentialReference, b: ManagedCredentialRef && a.credential_epoch == b.credential_epoch } +// THE OBSERVATION IS THE PAYLOAD, NOT A START TIME. Record `==` is the language's structural +// relation on the closed type, except controller_clock: ControllerClockReading? inherits +// optional_equality_answers_by_representation, so that field is matched explicitly and the rest +// compared by `==` after zeroing it. That match and observation_with_controller_clock are still +// authored field lists: a new ControllerClockReading field or a new Optional on the observation +// is not derived from the type declaration (no .dag value-layer reader of declared fields). The +// class is gunbc.guarantee_stall.authored_record_field_list_standing_for_type_population_stall +// authored_record_field_list_standing_for_type_population_stall -- a stall below ceiling, not a +// section 4b(3) drop. +fn controller_clock_reading_same(left: ControllerClockReading?, right: ControllerClockReading?) -> Bool { + match left { + Absent => match right { Absent => true Present { value: _ } => false } + Present { value: a } => match right { + Absent => false + Present { value: b } => + (a.reported_millis as Int) == (b.reported_millis as Int) + && a.evidence.label == b.evidence.label + && content_hash_equal(left: a.evidence.digest, right: b.evidence.digest) + } + } +} + +fn bmc_account_state_observation_same(a: BmcAccountStateObservation, b: BmcAccountStateObservation) -> Bool { + controller_clock_reading_same(left: a.controller_clock, right: b.controller_clock) + && (observation_with_controller_clock(base: a, controller_clock: none) + == observation_with_controller_clock(base: b, controller_clock: none)) +} + // WHAT THE PHASE REQUIRES OF THE CONTROLLER: this managed generation accepted, the published // credential closed, under this qualification policy. // published_break_glass is the exact generation the published account is retired TO (or, under a @@ -2065,6 +2123,34 @@ type BmcAccountApplication sole_constructor { applied_at: ObserverClockInstant } +fn apply_with_diverged_controller_clock(application: BmcAccountApplication, controller_clock: ControllerClockReading?) -> BmcAccountApplication + admit_callers: [ + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_a_different_present_controller_clock_is_refused"), + ] +{ + let p = application.plan + BmcAccountApplication { + plan: BmcAccountActionPlan { + subject: p.subject, + endpoint: p.endpoint, + firmware: p.firmware, + goal: p.goal, + diverged: observation_with_controller_clock(base: p.diverged, controller_clock: controller_clock), + due: p.due, + managed_write: p.managed_write, + published_write: p.published_write, + channel_writes: p.channel_writes, + planned_at: p.planned_at, + }, + managed_write_completed: application.managed_write_completed, + published_write_completed: application.published_write_completed, + channel_writes_completed: application.channel_writes_completed, + request_receipt: application.request_receipt, + response_receipt: application.response_receipt, + applied_at: application.applied_at, + } +} + type BmcSecurePhaseDecision = BmcSecurePhaseNoop { observation: BmcAccountStateObservation } | BmcSecurePhaseApplied { plan: BmcAccountActionPlan, application: BmcAccountApplication, post_read: BmcAccountStateObservation } @@ -2189,6 +2275,8 @@ fn dry_bmc_account_instant(millis: EpochMs) -> ObserverClockInstant admit_callers: [ decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "dry_read_bmc_account_state"), decl_ref(module_path: "gunbc.machine_intake_bmc_secure", decl_name: "dry_apply_bmc_account_plan"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "mtjade1_fetched_generation"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "mtjade1_dry_applied_bmc"), ] { observer_clock_modeled(millis: millis, realization: bmc_secure_dry_realization_name) @@ -2260,6 +2348,11 @@ fn dry_read_bmc_account_state( ) -> BmcAccountStateObservation admit_callers: [ decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "read_in"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_mtjade1_converges_through_bmc_secure_noop_on_the_real_prefix"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_mtjade1_bmc_secure_apply_inputs_missing_refuses_on_the_real_path"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "mtjade1_dry_applied_bmc"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_for_a_later_break_glass_generation_is_refused"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "w_bmc_secure_apply_of_another_channel_state_is_refused"), ] { let at = dry_bmc_account_instant(millis: read_at_millis) @@ -2323,6 +2416,7 @@ fn dry_apply_bmc_account_plan( decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "applied_with"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "the_post_read_is_independent_of_the_reading_that_diverged"), decl_ref(module_path: "test.claim.machine_intake_bmc_secure_state_witness", decl_name: "a_planned_channel_close_is_applied_read_back_and_converges"), + decl_ref(module_path: "test.claim.machine_intake_arrival_converge_witness", decl_name: "mtjade1_dry_applied_bmc"), ] { let managed_step = match dry_fetch_resolves(fetch: managed_fetch, write: plan.managed_write) { diff --git a/dag/gunbc/machine_intake/dimm_physical_orientation.dag b/dag/gunbc/machine_intake/dimm_physical_orientation.dag index 3c9d417fa70..690662823b7 100644 --- a/dag/gunbc/machine_intake/dimm_physical_orientation.dag +++ b/dag/gunbc/machine_intake/dimm_physical_orientation.dag @@ -1,12 +1,13 @@ module gunbc.machine_intake_dimm_physical_orientation +import extdeps.board_authority { BoardAuthorityRef, BoardAuthorityLookup } +import gunbc.board_authority_binding { observed_board_authority } import v2.std.algebra { filter } import std.types { String, List, list_length } import std.nat { Nat } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.roster_frontier { FrontierRow, DeclSubject } import std.dissolution { UnboundDissolution } -import extdeps.boards.types { BaseboardModel } import product.host_identity { HostIdentity } import extdeps.ampere.mt_collins_product_brief.memory_population { MtCollinsDimmPopulationRow } import gunbc.mt_collins_dimm_physical_identity { @@ -48,17 +49,17 @@ import gunbc.machine_intake_dimm_row_orientation { // handed one host's name beside another board's figure. type DimmOrientedHost sole_constructor { host: HostIdentity - board: BaseboardModel + board: BoardAuthorityRef figure: BaseboardDimmFigure outcome: DimmOrientationOutcome } type HostDimmOrientation = HostDimmOriented { oriented: DimmOrientedHost } - | HostDimmBoardUnmodeled { host: HostIdentity, board: BaseboardModel } + | HostDimmBoardUnmodeled { host: HostIdentity, board: BoardAuthorityLookup } fn host_dimm_orientation(binding: ManagedHostBinding, anchors: List) -> HostDimmOrientation { - match baseboard_dimm_figure(board: binding.observation.identity.baseboard) { + match baseboard_dimm_figure(board: observed_board_authority(observation: binding.observation.identity.board)) { BoardFigureUnmodeled { board: b } => HostDimmBoardUnmodeled { host: binding.host.host, board: b } BaseboardDimmFigureOf { board: b, figure: f } => HostDimmOriented { oriented: DimmOrientedHost { host: binding.host.host, diff --git a/dag/gunbc/machine_intake/megarac_kvm_still.dag b/dag/gunbc/machine_intake/megarac_kvm_still.dag index ee6a5829efd..f367c406d57 100644 --- a/dag/gunbc/machine_intake/megarac_kvm_still.dag +++ b/dag/gunbc/machine_intake/megarac_kvm_still.dag @@ -1,24 +1,13 @@ module gunbc.machine_intake_megarac_kvm_still -import std.types { Bool, FilePath, Int, List, NonEmptyStr, Unit } +import std.types { Bool, Int, List, NonEmptyStr, Unit } import std.algebra { trim } import std.measure { Second, second, second_count, Millisecond, millisecond, millisecond_count, second_to_millisecond } import std.checked_arithmetic { checked_int_to_nat } -import std.decl_ref { DeclarationRef, WholeDeclaration, decl_ref } -import std.disposition { Disposition, Scaffold, SingleAuthority } -import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.tools.sha256sum { Sha256FileDigest, Sha256FileDigestUnavailable, sha256sum_file_digest_via_shell } import extdeps.crypto.hash { Digest } -import extdeps.exec.command { argv_words } import gunbc.runner_browser_toolchain { - RunnerBrowserToolchainResolution, BrowserToolchainReady, BrowserToolchainNotReady, BrowserToolchainUnresolved, - RunnerBrowserToolchainLocation, runner_browser_toolchain_node_command, runner_browser_toolchain_standing_text, -} -import gunbc.machine_intake_megarac_ui_features { megarac_ui_features } -import extdeps.bmc.megarac { - megarac_ui_features_json, - ScreenCaptureFactKnown, ScreenCaptureFirmwareUncatalogued, - ScreenCaptureClientSideOnly, KvmClientCanvasJpeg, megarac_screen_capture_standing, + RunnerBrowserToolchainResolution, } import gunbc.megarac_managed_host { AdmittedMegaRacHost } import gunbc.megarac_operation_standing { MegaRacKvmCanvasStill, MegaRacVirtualMediaAttach, MegaRacSdrSocketNaming } @@ -26,112 +15,25 @@ import gunbc.clock_read { clock_now_probed_at_or_unknown } import gunbc.owned_process { OwnedRelease, OwnedProcessStanding, OwnedRunning, owned_process_release, owned_release_text, owned_process_standing, owned_process_standing_text } import std.optional { Present, Absent } import v2.std.algebra { filter } -import extdeps.shell import extdeps.filesystem.filesystem_io { Filesystem } import extdeps.tools.sleep { sleep_delay_seconds_second_carrier_projection } -// THE BMC KVM SCREEN AS BOOT EVIDENCE (operator ruling 2026-09-27; route A, eager-owl-205 the same -// day). On run 36335369059 the SOL collector died about 12 s into Linux and the watch went blind, -// while the KVM screen showed casper's "Unable to find a medium containing a live file system" -// prompt. The screen was the only witness, so this module RETAINS it: JPEG stills from one held viewer, -// bound to the attempt, the phase that asked for it, its instants and an independently taken -// sha256. It does not READ the image: turning a still into text or a diagnosis is the frontier -// named at the bottom of this file. +// THE BMC KVM SCREEN AS BOOT EVIDENCE (operator ruling 2026-09-27). On run 36335369059 the SOL +// collector died about 12 s into Linux and the watch went blind, while the KVM screen showed casper's +// "Unable to find a medium containing a live file system" prompt. This module retains a screen still +// as boot evidence: a still bound to the attempt, the phase that asked for it, its instants and an +// independently taken sha256. A CAPTURE IS AN OBSERVATION, NEVER A GATE: every arm is a value the boot +// records beside its own verdict, and nothing here can change an attempt's outcome. // -// WHY A BROWSER. extdeps.bmc.megarac megarac_screen_capture_standing records that firmware 0.32 -// serves no on-demand still: the UI's "Capture Screen" is the H5 viewer painting the AST2100 stream -// from wss:///kvm onto canvas#kvm and calling toDataURL("image/jpeg"). A still therefore exists -// only where a KVM client runs, and this module runs the vendor's own client headless and takes the -// same toDataURL the button takes -- never a decoder of our own for a proprietary stream. +// THE OBSERVER IS NOT STARTED IN PUBLIC. The only client it ever drove was the controller vendor's own +// served viewer, which is not public; megarac_kvm_observer_start therefore answers NotStarted with a +// typed reason, and the capability that restores it is the declared rung drop +// handoff_observer_is_sol_not_kvm_viewer. The journal vocabulary, parsers, standing and receipt +// readers below read the committed journal of an observer run that did execute (the receipt cited by +// gunbc.megarac_operation_evidence), so what was observed stays readable. // -// A CAPTURE IS AN OBSERVATION, NEVER A GATE. Every arm below, including a refusal, is a value the -// boot records beside its own verdict; nothing here can change an attempt's outcome, and the boot -// run binds no decision to a still. - // THE CONTROLLER IS AN ADMITTED MEGARAC HOST (gunbc.megarac_managed_host AdmittedMegaRacHost, operation -// MegaRacKvmCanvasStill): its endpoint, account and firmware come off one managed host's binding, and -// the admission refuses a controller that is not MegaRAC, is unobserved, or reports a build the -// screen-capture route and the viewer feature list were never read on. The firmware the capture is -// keyed to is that observed firmware, never a unit constant. - -// THE TOOLCHAIN IS RESOLVED, NEVER ASSUMED. gunbc.runner_browser_toolchain places the pinned node, -// Playwright and Chromium on the fleet runners through its own converge, and hands a consumer a -// location only over a standing that read back ready. The observer is launched through -// runner_browser_toolchain_node_command -- the pinned node and PLAYWRIGHT_BROWSERS_PATH from that -// location, run in the observer's own directory -- and never through a node on PATH. Playwright is -// required by the location's absolute playwright_module path, so no workspace node_modules can shadow it. A runner the converge has not placed -// answers NotReady or Unresolved, which the observer records as NotStarted with that text; the boot -// does not read it. - -// DRIVING THE VENDOR'S VIEWER IS REALIZATION, NOT THE MEGARAC INTERFACE. The steps are the UI's own, -// recovered from the served bundles cited on extdeps.bmc.megarac MegaRacScreenCaptureRoute: POST -// /api/session and the sessionStorage keys its login handler sets; GET /api/settings/services and -// the `kvm` row's viewer_count, which the UI's own launch() requires to be 0 before it opens -// viewer.html (else "only one KVM session is allowed"); then viewer.html, and captureScreen()'s -// canvas#kvm.toDataURL("image/jpeg"). -// -// ONLY THE VIEWER IS A DOCUMENT (fleet-converge run 36721915217, 2026-09-30). The session POST, the -// services read and the session DELETE are HTTP exchanges that need a cookie jar, not a page, so they -// go through the browser context's own request client (it shares the jar the viewer's /kvm WebSocket -// uses); the sessionStorage keys are seeded by a context init script, which runs in every document of -// the controller's origin before that document's own scripts. The observer therefore never loads the -// UI's root page. It used to: it loaded "/" to borrow an origin and ran the login inside it, and on -// that run the journal read "page.evaluate: Execution context was destroyed, most likely because of a -// navigation" with session-release login-unobserved -- the root document was replaced under the login. -// That the vendor SPA reloads itself to #login is read from its bundle (see the navigation note below); -// the repair does not depend on it, because no step now runs in a document the observer did not -// navigate to itself. The only in-page evaluations left are on viewer.html: the -// canvas wait and the toDataURL. -// -// ONE OBSERVER, HELD FOR THE WHOLE BOOT (side-chat acceptance (a), 2026-09-28). The observer is a -// background process, started under the unit hold after the SOL acquire and BEFORE the handoff, that -// logs in once, takes the seat once, opens the viewer once and keeps it. It writes a journal: one -// line per event, each carrying the epoch-millisecond and ISO instant it was written -- -// established (bound to host, BMC web session id and attempt), connection-open, connection-lost, -// still, acquisition-failed, refused, stop-requested, stopped. Stills are taken on a cadence (bounded -// by count) and IMMEDIATELY on a trigger file the boot drops (power on verified, SOL channel lost, -// terminal verdict). -// -// THE BOUNDARY IS OBSERVED, NEVER INFERRED FROM PIXELS (acceptance (b)). Connection is the viewer's -// own /kvm WebSocket as the browser reports it: open, then close or error. After a close no still is -// claimed; each asked-for still is journalled as acquisition-failed cause=not-connected. A canvas -// that exists but cannot be read (toDataURL throws) is acquisition-failed cause=canvas-unreadable. An -// unchanged picture is never read as a stale one: the observer does not compare pixels at all. -// -// EVERY MAIN-FRAME NAVIGATION IS JOURNALLED (operator, 2026-09-30: the navigation is MODELED, not -// avoided). What the SPA does is now read from its own bundle, not inferred: the UI fetched by -// fleet-converge run 36778446928 (gunbc.machine_intake_mtcollins1_ui_bundle_observe, firmware 0.32, -// digest matching the 2026-09-27 read) routes by Backbone hash routing, and on session expiry, on an -// unauthorized answer and on its logout paths sets location.href "#login" and then calls -// location.reload() -- a FULL document reload, which is what destroys an evaluate in flight. The only -// document the observer loads is viewer.html; every commit of its main frame is a `navigated` line (seq, -// the observer's phase -- viewer while it opens the viewer, held after establishment -- and the URL the -// frame committed to), so a reload to #login under the held viewer is on the journal with the instant it -// happened, before the connection-lost or failed line it causes. A navigation is recorded, never retried -// into a different answer. The viewer's own console errors and warnings, and any uncaught page error, -// are journalled too (page-console / page-error, with the phase): fleet-converge run 36779479938 loaded -// viewer.html and never requested /kvm, and what the viewer said about why is the first thing to read. -// -// THE SEAT. A held seat (viewer_count > 0) refuses with its count -- an operator's session is never -// evicted -- and every exit the process reaches (stop file, SIGTERM, refusal, failure) DELETEs its web -// session and closes the browser, journalling stopped session=. The -// one exit it cannot reach is SIGKILL: the boot then records that no stopped line was written, so the -// release was NOT observed, and the BMC's own session timeout frees the seat. -// -// It is JavaScript handed to node as a string because process-hold of a browser is not a Node in the -// grammar -- the same standing as gunbc.machine_intake_sol_hold's bash hold, marked the same way. -// ADMISSION OF THIS SCAFFOLD (DESIGN §5, external to this diff): operator escalation msg_d5eebfff, -// default-approved on recommendation A, 2026-09-28 -- not an explicit operator ruling. -data megarac_kvm_observer_script_scaffold: Disposition = Scaffold { - dissolves_to: SingleAuthority, - bind: DeclarationRef { - module_path: "gunbc.machine_intake_megarac_kvm_still", - decl_name: "megarac_kvm_observer_script_dissolution_trigger", - field: WholeDeclaration - } -} - -data megarac_kvm_observer_script_dissolution_trigger: DissolutionCondition = unbound_dissolution(description: "🟡 dissolve-on: gunbc.machine_intake.kvm_observer LaunchObserver -- the held MegaRAC H5 viewer is a node -e script string launched as a bash background hold. RETIRED BY THE CAPABILITY: a .dag browser-automation realization SUFFICIENT TO DRIVE THE MegaRAC H5 VIEWER AND READ ITS CANVAS -- navigate and evaluate in page, observe the /kvm WebSocket open and close, read canvas#kvm as image/jpeg on cadence and on trigger, and release the web session on every reachable exit -- held by a typed process-hold realization. An artifact that covers only part of that (navigation without WebSocket lifecycle, or a canvas read without the guaranteed release) does not retire it. Admission: operator escalation msg_d5eebfff, default-approved on recommendation A, 2026-09-28") +// MegaRacKvmCanvasStill). // THE OBSERVER'S JOURNAL VOCABULARY IS DECLARED HERE ONCE. The observer script receives it as the // EV (event) and CA (cause) constants megarac_kvm_observer_vocabulary_js renders from these rows, @@ -237,22 +139,6 @@ fn kvm_key(keys: List, i: Int) -> String { match keys.skip(n: i).first() { Present { value: k } => k Absent => "" } } -fn kvm_journal_words_js(name: String, words: List) -> String { - concat(concat(concat("const ", name), "=\{"), concat(join(map(words, w => concat(concat(w.id, ":'"), concat(w.word, "'"))), ","), "};")) -} - -fn megarac_kvm_observer_vocabulary_js() -> String { - concat(concat(kvm_journal_words_js(name: "EV", words: kvm_journal_event_words), kvm_journal_words_js(name: "CA", words: kvm_journal_cause_words)), kvm_journal_words_js(name: "RS", words: kvm_journal_result_words)) -} - -data megarac_kvm_observer_script_body: String = "const fs=require('fs');const path=require('path');const [base,user,pwFile,dir,attempt,cadenceMs,frameMs,maxPeriodic,pwModule,featuresFile]=process.argv.slice(1);const J=path.join(dir,'journal');function j(ev,kv)\{const t=Date.now();try\{fs.appendFileSync(J,t+' '+new Date(t).toISOString()+' '+ev+(kv?' '+kv:'')+'\\n')}catch(e)\{try\{process.stderr.write('journal write failed: '+ev+' '+String(e&&e.message)+'\\n')}catch(e2)\{}}}function one(e)\{return String(e&&e.message||e).replace(/[^ -~]+/g,' ').replace(/ +/g,' ').slice(0,300)}let browser=null,page=null,api=null,stopping=false,login='not-attempted',token=null,seq=0,periodic=0;let gen=0,live=null;let nav=0,phase='launch';async function teardown(why,code)\{if(stopping)return;stopping=true; j(EV.stopRequested,'by='+why); let rel; if(login==='accepted'&&api&&token)\{try\{const r=await api.delete(base+'/api/session',\{headers:\{'X-CSRFTOKEN':token}});rel=r.ok()?RS.released:RS.httpPrefix+r.status()}catch(e)\{rel=RS.releaseFailed+' detail='+JSON.stringify(one(e))}} else if(login==='unobserved')\{rel=RS.loginUnobserved} else\{rel=RS.noSession} j(EV.sessionRelease,'result='+rel); let closed=RS.closed;try\{if(browser)await browser.close();else closed=RS.noBrowser}catch(e)\{closed=RS.closeFailed+' detail='+JSON.stringify(one(e))} j(EV.browserClose,'result='+closed); j(EV.stopped,'session='+rel.split(' ')[0]+' browser='+closed.split(' ')[0]); process.exit(code)}process.on('SIGTERM',()=>teardown('sigterm',0));process.on('SIGINT',()=>teardown('sigint',0));async function still(reason)\{seq+=1;const n=seq;const g=gen;const startedMs=Date.now(); if(!live||live.gen!==g)\{j(EV.acquisitionFailed,'seq='+n+' reason='+reason+' gen='+g+' cause='+CA.notConnected+'');return} try\{const url=await page.evaluate(()=>\{const c=document.getElementById('kvm');if(!c)throw new Error('no canvas#kvm');return c.toDataURL('image/jpeg')}); if(!live||live.gen!==g)\{j(EV.acquisitionFailed,'seq='+n+' reason='+reason+' gen='+g+' started_ms='+startedMs+' cause='+CA.connectionChangedDuringAcquisition+'');return} if(!url.startsWith('data:image/jpeg;base64,'))throw new Error('toDataURL answered '+url.slice(0,40)); const b=Buffer.from(url.slice(url.indexOf(',')+1),'base64');const f='still-'+n+'.jpg';fs.writeFileSync(path.join(dir,f),b); j(EV.still,'seq='+n+' reason='+reason+' gen='+g+' started_ms='+startedMs+' file='+f+' bytes='+b.length)} catch(e)\{j(EV.acquisitionFailed,'seq='+n+' reason='+reason+' gen='+g+' started_ms='+startedMs+' cause='+CA.canvasUnreadable+' detail='+JSON.stringify(one(e)))}}(async()=>\{ let pw;try\{pw=fs.readFileSync(pwFile,'utf8')}catch(e)\{j(EV.refused,'cause='+CA.credentialUnreadable+' detail='+JSON.stringify(one(e)));return teardown('refused',14)} let chromium;try\{chromium=require(pwModule).chromium}catch(e)\{j(EV.refused,'cause='+CA.toolchain+' detail='+JSON.stringify(one(e)));return teardown('refused',13)} try\{browser=await chromium.launch(\{headless:true})}catch(e)\{j(EV.refused,'cause='+CA.toolchain+' detail='+JSON.stringify(one(e)));return teardown('refused',13)} try\{const ctx=await browser.newContext(\{ignoreHTTPSErrors:true,viewport:\{width:1280,height:1024}});api=ctx.request;page=await ctx.newPage(); page.on('framenavigated',f=>\{if(f===page.mainFrame())\{nav+=1;j(EV.navigated,'seq='+nav+' phase='+phase+' url='+JSON.stringify(f.url()))}}); page.on('console',m=>\{const l=m.type();if(l==='error'||l==='warning')j(EV.pageConsole,'phase='+phase+' level='+l+' detail='+JSON.stringify(one(m.text())))}); page.on('pageerror',e=>\{j(EV.pageError,'phase='+phase+' detail='+JSON.stringify(one(e)))}); const expectedHost=new URL(base).host; login='unobserved'; const lr=await api.post(base+'/api/session',\{form:\{username:user,password:pw}});let lb=null;try\{lb=await lr.json()}catch(e)\{}const reply=\{status:lr.status(),b:lb}; if(reply.status!==200||!reply.b||reply.b.ok!==0)\{login='refused';j(EV.refused,'cause='+CA.login+' http='+reply.status);return teardown('refused',11)} login='accepted';token=reply.b.CSRFToken; let feats='';if(featuresFile)\{try\{feats=fs.readFileSync(featuresFile,'utf8')}catch(e)\{j(EV.refused,'cause='+CA.featuresUnreadable+' detail='+JSON.stringify(one(e)));return teardown('refused',14)}} await ctx.addInitScript(([h,b,u,f])=>\{if(location.host!==h)return;if(f)sessionStorage.setItem('features',f);sessionStorage.setItem('garc',b.CSRFToken);sessionStorage.setItem('privilege_id',String(b.privilege));sessionStorage.setItem('extended_privilege',String(b.extendedpriv));sessionStorage.setItem('session_id',String(b.racsession_id));sessionStorage.setItem('username',u);sessionStorage.setItem('kvm_access','1');sessionStorage.setItem('vmedia_access','1')},[expectedHost,reply.b,user,feats]); const sa=await(await api.get(base+'/api/settings/services',\{headers:\{'X-CSRFTOKEN':token}})).json();const kr=Array.isArray(sa)?sa.find(s=>s.service_name==='kvm'):null;const seat=kr?kr.viewer_count:null; if(seat===null||seat===undefined)\{j(EV.refused,'cause='+CA.noKvmService+'');return teardown('refused',15)} if(seat!==0)\{j(EV.refused,'cause='+CA.seatBusy+' viewer_count='+seat);return teardown('refused',10)} page.on('websocket',ws=>\{let u;try\{u=new URL(ws.url())}catch(e)\{return} if(u.host!==expectedHost||u.pathname!=='/kvm')return; j(EV.connectionRequested,'url='+JSON.stringify(ws.url()));let mine=null; ws.on('framereceived',()=>\{if(!mine)\{gen+=1;mine=\{gen:gen};live=mine;j(EV.connectionOpen,'gen='+mine.gen+' url='+JSON.stringify(ws.url()))}}); ws.on('socketerror',e=>\{j(EV.connectionError,'gen='+(mine?mine.gen:0)+' detail='+JSON.stringify(one(e)));if(mine&&live===mine)live=null}); ws.on('close',()=>\{if(mine&&live===mine)\{live=null;j(EV.connectionLost,'gen='+mine.gen)}else if(!mine)\{j(EV.connectionRefused,'url='+JSON.stringify(ws.url()))}})}); phase='viewer';await page.goto(base+'/viewer.html',\{waitUntil:'domcontentloaded'}); try\{await page.waitForFunction(()=>\{const c=document.getElementById('kvm');return !!c&&c.width>1&&c.height>1},null,\{timeout:Number(frameMs)})} catch(e)\{j(EV.refused,'cause='+CA.noFrame+' detail='+JSON.stringify(one(e)));return teardown('refused',12)} const deadline=Date.now()+Number(frameMs);while(!live&&Date.now()setTimeout(r,100))} if(!live)\{j(EV.refused,'cause='+CA.noKvmFrame+'');return teardown('refused',12)} j(EV.established,'host='+JSON.stringify(expectedHost)+' session='+String(reply.b.racsession_id)+' attempt='+attempt+' gen='+live.gen); phase='held';let next=Date.now()+Number(cadenceMs); while(!stopping)\{ if(fs.existsSync(path.join(dir,'stop')))\{return teardown('stop-file',0)} let tf=[];try\{tf=fs.readdirSync(dir).filter(n=>n.startsWith('trigger-')).sort()}catch(e)\{j(EV.triggerScanFailed,'detail='+JSON.stringify(one(e)))} for(const n of tf)\{const p=path.join(dir,n);try\{fs.unlinkSync(p)}catch(e)\{continue}await still('trigger:'+n.slice(8))} if(Date.now()>=next&&periodicsetTimeout(r,250))} }catch(e)\{j(EV.failed,'detail='+JSON.stringify(one(e)));return teardown('failed',1)}})();" - -data megarac_kvm_observer_script: String = concat(megarac_kvm_observer_vocabulary_js(), megarac_kvm_observer_script_body) - -// THE LAUNCH is gunbc.owned_process's: the toolchain's node command in the background, its output in -// observer.log beside the journal, its holder recorded as " " in observer.pid, and its -// release OBSERVED after the signal rather than assumed. - // THE BOUNDS. A periodic still every cadence, at most periodic_limit of them; the boot waits at most // establish_wait for the observer to say established or refused, at most trigger_wait for a // triggered still to be journalled, and at most stop_wait for the stopped line before the release. @@ -855,9 +741,12 @@ fn kvm_await_standing(dir: String, subject: KvmObserverSubject, remaining: Int) } } -// START, BEFORE THE HANDOFF. The carrier must have been admitted for this operation: a MegaRAC host -// admitted for virtual media is not thereby admitted for the viewer. The observer's directory is the -// caller's -- it is where the caller retains the journal and stills -- and is passed in. +// START, BEFORE THE HANDOFF. The carrier must have been admitted for this operation. No public +// KVM client exists: the only client this module ever drove was the controller vendor's own served +// viewer, whose source is not public, so the observer is not started here and every start is a typed +// NotStarted that the boot records beside its verdict. The journal, standing and receipt readers +// below still read the committed journal of an observer that did run (the executed receipt). +// Capability that restores it: gunbc.rung_drop handoff_observer_is_sol_not_kvm_viewer. fn megarac_kvm_observer_start( host: AdmittedMegaRacHost, dir: String, @@ -866,104 +755,10 @@ fn megarac_kvm_observer_start( attempt: String, ) -> KvmObserverStart { match host.operation { - MegaRacKvmCanvasStill => {} - MegaRacVirtualMediaAttach => return KvmObserverNotStarted { reason: "the MegaRAC host was admitted for another operation, not the KVM canvas still; no viewer was driven" } - MegaRacSdrSocketNaming => return KvmObserverNotStarted { reason: "the MegaRAC host was admitted for another operation, not the KVM canvas still; no viewer was driven" } - } - let firmware = host.firmware - let bmc_host = host.binding.observation.endpoint.host - match megarac_screen_capture_standing(firmware: firmware, route: KvmClientCanvasJpeg) { - ScreenCaptureFirmwareUncatalogued { firmware: _ } => KvmObserverNotStarted { reason: "the BMC firmware is not catalogued for screen capture; no viewer was driven" } - ScreenCaptureFactKnown { firmware: _, standing: s } => - match s { - ScreenCaptureClientSideOnly => - if password_file == "" { KvmObserverNotStarted { reason: "no BMC credential path is bound, so no viewer login was attempted" } } - else if attempt == "" { KvmObserverNotStarted { reason: "no attempt identity is bound, so the observer could not be named for it" } } - else if dir == "" { KvmObserverNotStarted { reason: "no observer directory is bound, so the journal would have nowhere to be retained" } } - else { - match toolchain { - BrowserToolchainReady { location: loc, standing: _ } => - match megarac_ui_features(firmware: firmware) { - Absent => KvmObserverNotStarted { reason: "no viewer feature list is observed for this firmware (gunbc.machine_intake_megarac_ui_features megarac_ui_features), and the viewer's KVM view reads it unguarded; no viewer was driven" } - Present { value: names } => kvm_observer_launch(dir: dir, base_url: concat("https://", bmc_host as String), host: bmc_host as String, username: host.binding.account, password_file: password_file, toolchain: loc, attempt: attempt, features_json: megarac_ui_features_json(names: names)) - } - BrowserToolchainNotReady { standing: st } => KvmObserverNotStarted { reason: concat("the runner browser toolchain is not ready: ", runner_browser_toolchain_standing_text(s: st)) as NonEmptyStr } - BrowserToolchainUnresolved { cause: c } => KvmObserverNotStarted { reason: concat("the runner browser toolchain is unresolved: ", c) as NonEmptyStr } - } - } - _ => KvmObserverNotStarted { reason: "the catalogued viewer route is not client-side; nothing was driven" } - } - } -} - -// THE LAUNCH OVER A BASE URL. The boot passes https://; the protocol controls pass a local -// transport. `host` is the host:port the established line must name. -fn kvm_observer_launch(dir: String, base_url: String, host: String, username: NonEmptyStr, password_file: String, toolchain: RunnerBrowserToolchainLocation, attempt: String, features_json: String) -> KvmObserverStart - admit_callers: [ - decl_ref(module_path: "gunbc.machine_intake_megarac_kvm_still", decl_name: "megarac_kvm_observer_start"), - ] -{ - let made = shell.Mkdir.Parents(path: dir as FilePath) - if !made.success { - return KvmObserverNotStarted { reason: concat("the observer directory could not be made: ", dir) as NonEmptyStr } - } - kvm_observer_launch_in(dir_given: dir, base_url: base_url, host: host, username: username, password_file: password_file, toolchain: toolchain, attempt: attempt, features_json: features_json) -} - -// ONE ABSOLUTE LOCATION (hold on gunbc#12492): the directory is canonicalised once, here, and that -// absolute path is the launch's working directory, the observer's own `dir` operand, and what the boot -// reads the journal, stills and triggers from and writes the stop request to. A relative directory -// passed as both working directory and operand would be resolved twice --

//journal -- and the -// boot and the observer would address different files. -fn kvm_observer_launch_in(dir_given: String, base_url: String, host: String, username: NonEmptyStr, password_file: String, toolchain: RunnerBrowserToolchainLocation, attempt: String, features_json: String) -> KvmObserverStart - admit_callers: [ - decl_ref(module_path: "gunbc.machine_intake_megarac_kvm_still", decl_name: "kvm_observer_launch"), - decl_ref(module_path: "gunbc.machine_intake_megarac_kvm_still", decl_name: "kvm_observer_protocol_launch"), - ] -{ - let resolved = shell.Path.Canonical(path: dir_given as FilePath) - if !resolved.success { - return KvmObserverNotStarted { reason: concat("the observer directory could not be resolved: ", concat(dir_given, concat(": ", trim(s: resolved.stderr)))) as NonEmptyStr } - } - let dir = trim(s: resolved.stdout) - let features_path = concat(dir, "/features.json") - if features_json != "" && !Filesystem.Write(path: features_path, content: features_json).success { - return KvmObserverNotStarted { reason: concat("the viewer feature list could not be written: ", features_path) as NonEmptyStr } - } - let node = runner_browser_toolchain_node_command( - location: toolchain, - directory: dir, - script: megarac_kvm_observer_script, - operands: [ - base_url, username as String, password_file, dir, attempt, - to_string(millisecond_count(m: second_to_millisecond(s: megarac_kvm_observer_cadence)) as Int), - to_string(millisecond_count(m: second_to_millisecond(s: megarac_kvm_observer_frame_wait)) as Int), - to_string(megarac_kvm_observer_periodic_limit), - toolchain.playwright_module as String, - if features_json == "" { "" } else { features_path }, - ], - ) - let launched = gunbc.owned_process.launch.LaunchOwned(pid_path: concat(dir, "/observer.pid") as NonEmptyStr, log_path: concat(dir, "/observer.log") as NonEmptyStr, command: argv_words(command: node)) - if !launched.success { - return KvmObserverNotStarted { reason: concat("the observer launch failed: ", trim(s: launched.transport_stderr)) as NonEmptyStr } + MegaRacKvmCanvasStill => KvmObserverNotStarted { reason: "no public KVM client is available to drive; no viewer was driven" } + MegaRacVirtualMediaAttach => KvmObserverNotStarted { reason: "the MegaRAC host was admitted for another operation, not the KVM canvas still; no viewer was driven" } + MegaRacSdrSocketNaming => KvmObserverNotStarted { reason: "the MegaRAC host was admitted for another operation, not the KVM canvas still; no viewer was driven" } } - let subject = KvmObserverSubject { host: host, attempt: attempt } - let standing = kvm_await_standing(dir: dir, subject: subject, remaining: kvm_polls_within(wait: megarac_kvm_observer_establish_wait)) - KvmObserverStarted { dir: dir as NonEmptyStr, subject: subject, standing: standing, checked_at: clock_now_probed_at_or_unknown() } -} - -// THE PROTOCOL TESTS' LAUNCH, AND ONLY THEIRS. The production launch takes its endpoint and account -// from an admitted MegaRAC carrier (megarac_kvm_observer_start) and is sealed to it; the protocol -// witnesses drive the same observer against a local transport they stand up themselves, which has no -// managed host behind it. This facade is the one door for that, admitted to those witness functions by -// name, so no production caller reaches the launcher with a raw host and account. -fn kvm_observer_protocol_launch(dir_given: String, base_url: String, host: String, username: NonEmptyStr, password_file: String, toolchain: RunnerBrowserToolchainLocation, attempt: String, features_json: String) -> KvmObserverStart - admit_callers: [ - decl_ref(module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", decl_name: "rig_with"), - decl_ref(module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", decl_name: "a_relative_observer_directory_is_resolved_once"), - ] -{ - kvm_observer_launch_in(dir_given: dir_given, base_url: base_url, host: host, username: username, password_file: password_file, toolchain: toolchain, attempt: attempt, features_json: features_json) } // A TRIGGER: one file the observer consumes and answers with one still or one acquisition failure, diff --git a/dag/gunbc/machine_intake/megarac_media_attach.dag b/dag/gunbc/machine_intake/megarac_media_attach.dag index 517dbb216ba..0cce493fce5 100644 --- a/dag/gunbc/machine_intake/megarac_media_attach.dag +++ b/dag/gunbc/machine_intake/megarac_media_attach.dag @@ -4,9 +4,9 @@ import std.decl_ref { decl_ref } import std.types { Bool, EpochMs, Int, List, Milliseconds, NonEmptyStr, String } import std.content_hash { content_hash_of_value } import std.measure { Second, second, second_count } -import extdeps.bmc.capability { BmcFirmwareReleaseIdentity } +import extdeps.bmc.capability { BmcFirmwareReleaseIdentity, bmc_firmware_release_identity_eq } import extdeps.bmc.megarac -import gunbc.machine_intake_megarac_ui_features { MegaRacRmediaStatus, RmediaStatusNamed, RmediaStatusOutsideTable, RmediaStatusTableUnread, megarac_rmedia_status_of_wire } +import extdeps.bmc.megarac { megarac_firmware_0_32 } import extdeps.clock { ClockUnixMillisObserved, ClockUnixMillisRefused, clock_unix_millis_read } import extdeps.provisioning.ubuntu_seeded_install_media { ubuntu_seeded_install_media_name_digest_prefix } import extdeps.tools.sleep { sleep_delay_seconds_second_carrier_projection } @@ -129,7 +129,6 @@ type MegaRacAttachOutcome | MegaRacMediaStateUnestablished { image_name: NonEmptyStr, route: MegaRacAttachRoute, look: ReadinessLook, looks: Int, cd_before_any_write: CdErrorReading, cd_at_ready: CdErrorReading, general_at_ready: MegaRacRawRead } | MegaRacMediaStateUnestablishedBeforeWrite { cd_before_any_write: CdErrorReading, reason: NonEmptyStr } | MegaRacReadinessRefused { image_name: NonEmptyStr, route: MegaRacAttachRoute, look: ReadinessLook, looks: Int, cd_before_any_write: CdErrorReading, cd_at_refusal: CdErrorReading } - | MegaRacPresentationStopped { image_name: NonEmptyStr, route: MegaRacAttachRoute, look: ReadinessLook, looks: Int, cd_before_any_write: CdErrorReading, stop: MegaRacRmediaStatus, class: MegaRacStopReasonClass, general_at_refusal: MegaRacRawRead } | MegaRacReadinessClockUnreadable { image_name: NonEmptyStr, route: MegaRacAttachRoute, detail: String } | MegaRacReadinessClockIncoherent { image_name: NonEmptyStr, route: MegaRacAttachRoute, opened_at: EpochMs, previous: EpochMs?, look: ReadinessLook, looks: Int } | MegaRacConfigurationsUnreadable { detail: String } @@ -291,73 +290,36 @@ type MediaGeneralShareRead = MediaGeneralShareObserved { observed: MegaRacObservedShare } | MediaGeneralShareUnreadable { detail: String } -// THE CD ERROR CODE IS THE VENDOR'S LAST-STOP REASON, AND THE VENDOR'S OWN UI SAYS WHEN IT MEANS -// ANYTHING (operator-approved 2026-10-01, superseding the knowledge refusal of gunbc#12425 option A and -// the held "census END marker decides delivery" design of msg_abc42f62 for every code the table names). -// settings/media/general carries cd_error_code beside mount_cd. The SP-X UI bundle this firmware serves -// (source.min.js sha256 5029fae2..., extdeps.bmc.megarac megarac_rmedia_status_0_32) reads it in exactly -// one place, the single-image rmedia view, and only as `0==mount_cd && 1<=code<=17`, rendering -// "Stopped" + the code's name; while the medium is started or starting it never consults the code, and -// in the stopped state it leaves Start enabled whatever the code is. So the code is a LATCHED reason -// for the last stop, not a live fault: boot run 36721915217 read 16 before any write, after its own -// stop and at readiness, while the configurations row read Started with session_index 0. -// Hence three readings, each a different fact: -// CdErrorNone -- 0; -// CdErrorStopReason -- a code the build's table names, with the class below; -// CdErrorUncatalogued -- a code outside the table, a build with no table, or a named code this -// policy has not classified. -// Where the vendor does not consult the code -- the image Started or connecting -- every read code is -// recorded and none withholds. Where it does -- nothing presented -- a named reason is refused or -// attached past BY NAME, and an uncatalogued code stays a knowledge refusal. An UNREAD code is never a -// reading, and withholds everywhere. +// THE CD ERROR CODE IS AN OBSERVED NUMBER, AND NO MEANING IS ESTABLISHED FOR ANY VALUE. +// settings/media/general carries cd_error_code beside mount_cd. Boot run 36721915217 (an executed +// receipt) read 16 before any write, after its own stop and at readiness, while the configurations row +// read Started with session_index 0. That is a COEXISTENCE: the value was already present before the +// attempt stopped anything, so it does not show what produced it. No public document or executed +// receipt gives any value a cause, so the model keeps the raw observation and names none. Two readings: +// CdErrorNone -- 0; +// CdErrorNonzero -- any other value, with the firmware it was read from. +// An UNREAD code is never a reading, and withholds everywhere. type MegaRacCdErrorCode = CdErrorNone - | CdErrorStopReason { status: MegaRacRmediaStatus, class: MegaRacStopReasonClass } - | CdErrorUncatalogued { wire: Int } - -// WHETHER A STOP REASON SAYS THE CONTROLLER FAILED TO PRESENT, FROM THE VENDOR'S NAME FOR IT. The table -// is the vendor's (derived, never typed here); the class is this repository's reading of each name, and -// is keyed by code so that a table row this function does not name stays unclassified and refuses. -// NO FAULT -- the stop was asked for, or is the end of a presentation rather than a failure of one: -// 1 Connection Accepted the redirection's own acceptance status; it names no failure. -// 7 Media Detach Stage the medium was being detached, which is what a stop request does. -// 15 Mount Cancelled By User an operator stopped it. -// 16 Device Ejected the HOST ejected the virtual device (an OS or installer eject), or the -// stop of a presentation ejected it; the controller presented, and the -// consumer let go. Boot run 36721915217's stop of the stale image left -// exactly this code. -// PRESENTATION FAILURE -- the controller could not present, or lost, the medium: -// 2 Connection Denied, 3 Login Failed, 4 MAX Session Reached, 5 Permission Denied: the share or the -// redirection refused this presentation; -// 6 Unknown Error: the vendor's own catch-all for a failure it could not name; -// 8 All device instances are occupied: no virtual device was free to present on; -// 9 Unable to Connect, 14 Connection Lost: the share was unreachable, or stopped answering; -// 10 Invalid Image, 11 Mount Error, 12 Unable to Open: the image could not be mounted or read; -// 13 Media License Expired: the controller refuses remote media altogether; -// 17 Session Terminated: the session ended under the medium with no stop requested. -type MegaRacStopReasonClass - = StopReasonNoFault - | StopReasonPresentationFailure - -fn megarac_stop_reason_class(status: MegaRacRmediaStatus) -> MegaRacStopReasonClass? { - let c = status.code - if c == 1 || c == 7 || c == 15 || c == 16 { Present { value: StopReasonNoFault } } - else if (c >= 2 && c <= 6) || (c >= 8 && c <= 14) || c == 17 { Present { value: StopReasonPresentationFailure } } - else { none } + | CdErrorNonzero { wire: Int, firmware: BmcFirmwareReleaseIdentity } + +// POLICY, NOT A DECODED CAUSE. A nonzero code with nothing presented admits the attach only when it is +// 16 read from firmware 0.32: boot run 36721915217 read exactly that before it attached, and the +// attached presentation reached Started and the run proceeded. Admitting it claims that the run +// proceeded under this reading, not what the code means. Every other nonzero code, and 16 on another +// build, has no such run behind it, so the fresh-presentation experiment must be admitted first. +data cd_error_attach_policy_wire: Int = 16 + +fn cd_error_attach_policy_admits(code: MegaRacCdErrorCode) -> Bool { + match code { + CdErrorNone => true + CdErrorNonzero { wire: w, firmware: f } => w == cd_error_attach_policy_wire && bmc_firmware_release_identity_eq(a: f, b: megarac_firmware_0_32) + } } fn megarac_cd_error_code_of_wire(firmware: BmcFirmwareReleaseIdentity, wire: Int) -> MegaRacCdErrorCode { - if wire == 0 { CdErrorNone } else { - match megarac_rmedia_status_of_wire(firmware: firmware, wire: wire) { - RmediaStatusNamed { status: st } => - match megarac_stop_reason_class(status: st) { - Present { value: k } => CdErrorStopReason { status: st, class: k } - Absent => CdErrorUncatalogued { wire: wire } - } - RmediaStatusOutsideTable { firmware: _, wire: _ } => CdErrorUncatalogued { wire: wire } - RmediaStatusTableUnread { firmware: _, wire: _ } => CdErrorUncatalogued { wire: wire } - } - } + if wire == 0 { CdErrorNone } + else { CdErrorNonzero { wire: wire, firmware: firmware } } } type CdErrorReading @@ -526,9 +488,7 @@ fn images_array_elements(doc: JsonValue) -> List? { // THE START-MEDIA BODY, rendered here rather than by the caller so image_redirection cannot be // omitted by a caller who does not know it is required. extdeps.bmc.megarac records that omitting -// it makes the firmware refuse with opaque codes 13410/13460, recovered from the firmware's own UI -// bundle rather than from documentation — exactly the kind of fact that is lost when a body is -// assembled at a call site. +// it makes the firmware refuse, a fact that is lost when a body is assembled at a call site. fn start_media_body(image_name: NonEmptyStr, image_index: String) -> NonEmptyStr { join([ "{\"image_name\":\"", image_name as String, @@ -655,6 +615,20 @@ fn megarac_close_and_reobserve( return megarac_session_release_by_reobservation(before: before, after: after, csrf: csrf) } +// THE SESSION REPLY'S DECODER, a pure function of the body so a claim supplies the body at this +// interface: the one member read is CSRFToken, and a body that is not a JSON document or lacks it +// reads as absent, which the caller refuses as MegaRacSessionTokenUnreadable. +fn megarac_session_csrf(session_body: String) -> String? { + let session_doc = match parse_json_document(s: session_body) { + JsonDocumentParsed { value: v } => some_json_value(v) + JsonDocumentUnreadable { gap: _ } => none + } + return match session_doc { + Absent => none + Present { value: doc } => json_string_member(doc: doc, key: "CSRFToken") + } +} + fn megarac_attach_remote_image( bmc_host: NonEmptyStr, username: NonEmptyStr, @@ -685,16 +659,7 @@ fn megarac_attach_remote_image( } } - let session_doc = match parse_json_document(s: session.body) { - JsonDocumentParsed { value: v } => some_json_value(v) - JsonDocumentUnreadable { gap: _ } => none - } - let csrf = match session_doc { - Absent => none - Present { value: doc } => json_string_member(doc: doc, key: "CSRFToken") - } - - return match csrf { + return match megarac_session_csrf(session_body: session.body) { Absent => megarac_release_unreadable_session( bmc_host: bmc_host, @@ -1606,12 +1571,15 @@ fn megarac_convergence_step( ConvergeNothingPresented => match cd_before { CdErrorRead { code: CdErrorNone } => ConvergeStepAttachFromListing - CdErrorRead { code: CdErrorStopReason { status: _, class: _ } } => ConvergeStepAttachFromListing CdErrorUnread { detail: _ } => ConvergeStepRefuse { outcome: unestablished_before_write(cd_before: cd_before, reason: "cd_error_code could not be read, so this attempt cannot establish the media state it would attach into") } - CdErrorRead { code: CdErrorUncatalogued { wire: _ } } => - match detach { - MegaRacDetachAdmitted => ConvergeStepAttachFromListing - MegaRacDetachNotAdmitted { reason: why } => ConvergeStepRefuse { outcome: unestablished_before_write(cd_before: cd_before, reason: concat("nothing is presented, cd_error_code is nonzero, and the fresh-presentation experiment was not admitted: ", why as String)) } + CdErrorRead { code: c } => + match cd_error_attach_policy_admits(code: c) { + true => ConvergeStepAttachFromListing + false => + match detach { + MegaRacDetachAdmitted => ConvergeStepAttachFromListing + MegaRacDetachNotAdmitted { reason: why } => ConvergeStepRefuse { outcome: unestablished_before_write(cd_before: cd_before, reason: concat("nothing is presented, cd_error_code is nonzero, and the fresh-presentation experiment was not admitted: ", why as String)) } + } } } } @@ -1927,36 +1895,7 @@ fn megarac_readiness_outcome( } // THE CODE IS READ AT THE END OF THE WAIT WHATEVER THE WAIT ANSWERED, so a refusal carries the code -// that stood beside the unhealthy presentation, and a ready presentation carries it as historical. -// A STOPPED PRESENTATION WITH A NAMED REASON: the look found nothing presented (the row was stopped, -// or cleared) and the code reads a reason the build's table names. That is the one state in which the -// vendor's UI reads the code at all, and it reads it as "Stopped - ", so the refusal carries the -// name and its class instead of a timeout. Any other look, or any other code, is not this state. -type StopReasonReading { - status: MegaRacRmediaStatus - class: MegaRacStopReasonClass -} - -fn stopped_with_reason(look: ReadinessLook, cd: CdErrorReading) -> StopReasonReading? { - match look.reading { - ReadinessNotPresented { configurations: _ } => - match cd { - CdErrorRead { code: CdErrorStopReason { status: st, class: k } } => Present { value: StopReasonReading { status: st, class: k } } - CdErrorRead { code: CdErrorNone } => none - CdErrorRead { code: CdErrorUncatalogued { wire: _ } } => none - CdErrorUnread { detail: _ } => none - } - ReadinessStarted { row: _ } => none - ReadinessConnecting { row: _ } => none - ReadinessStartedUnbound { row: _ } => none - ReadinessUnanswered { detail: _ } => none - ReadinessUncatalogued { image_name: _, wire: _, configurations: _ } => none - ReadinessMalformed { detail: _ } => none - ReadinessDuplicate { configurations: _ } => none - ReadinessWrongPresentation { image_name: _, configurations: _ } => none - } -} - +// that stood beside the unhealthy presentation, and a ready presentation carries it beside the Started row, as an observation. // PURE: the wait's answer as the attach outcome. Only a Started look reaches MegaRacPresentationReady, // and the evidence is read from that look's row, so the record a boot is gated on is the row that // satisfied the policy. @@ -1973,15 +1912,9 @@ fn readiness_wait_outcome( ReadinessWaitClockUnreadable { detail: d } => MegaRacReadinessClockUnreadable { image_name: image_name, route: route, detail: d } ReadinessWaitClockIncoherent { opened_at: o, previous: p, look: l, looks: n } => MegaRacReadinessClockIncoherent { image_name: image_name, route: route, opened_at: o, previous: p, look: l, looks: n } ReadinessWaitClosed { last: l, deadline: d, looks: n } => - match stopped_with_reason(look: l, cd: cd_at_end) { - Present { value: st } => MegaRacPresentationStopped { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, stop: st.status, class: st.class, general_at_refusal: general_at_end } - Absent => MegaRacReadinessTimedOut { image_name: image_name, route: route, last: l, deadline: d, looks: n, cd_before_any_write: cd_before, cd_at_refusal: cd_at_end } - } + MegaRacReadinessTimedOut { image_name: image_name, route: route, last: l, deadline: d, looks: n, cd_before_any_write: cd_before, cd_at_refusal: cd_at_end } ReadinessWaitRefused { look: l, looks: n } => - match stopped_with_reason(look: l, cd: cd_at_end) { - Present { value: st } => MegaRacPresentationStopped { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, stop: st.status, class: st.class, general_at_refusal: general_at_end } - Absent => MegaRacReadinessRefused { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, cd_at_refusal: cd_at_end } - } + MegaRacReadinessRefused { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, cd_at_refusal: cd_at_end } ReadinessWaitReady { look: l, looks: n } => { let not_ready = MegaRacReadinessRefused { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, cd_at_refusal: cd_at_end } let state_unestablished = MegaRacMediaStateUnestablished { image_name: image_name, route: route, look: l, looks: n, cd_before_any_write: cd_before, cd_at_ready: cd_at_end, general_at_ready: general_at_end } @@ -2052,11 +1985,10 @@ fn megarac_observe_with_token( } // THREE ANSWERS, BECAUSE THEY ARE DIFFERENT FACTS (side-chat review 5331738657). Affirmed: the look -// was taken and the medium is not being served (a withdrawn or non-Started row, a share mismatch; a -// stopped row beside a cd_error_code the build's table names says which stop it was). Unobserved: the +// was taken and the medium is not being served (a withdrawn or non-Started row, a share mismatch). Unobserved: the // look itself failed (session, transport, an unread member, an unread cd_error_code), which says -// nothing about the medium. A read cd_error_code beside a Started row is the last stop's latched -// reason, which the vendor's UI does not consult while started, so it does not withhold. +// nothing about the medium. A read cd_error_code beside a Started row is recorded as an observation and +// does not withhold, because a Started row with a bound session is what the readiness policy asks for. // And the look's own session must be released: a ready medium observed through a session the // controller still holds is a cleanup obligation unmet, exactly as at the initial gate. Only // PresentationStillReady admits a handoff; each other answer withholds and says which it was. @@ -2090,10 +2022,7 @@ fn presentation_still_served(result: MegaRacAttachResult) -> PresentationStillSe ReadinessConnecting { row: _ } => no_longer_ready ReadinessStartedUnbound { row: _ } => no_longer_ready ReadinessNotPresented { configurations: _ } => - match cd { - CdErrorRead { code: CdErrorStopReason { status: st, class: k } } => PresentationNoLongerReady { reason: concat("the configurations row no longer presents the image: ", stop_reason_text(status: st, class: k)) as NonEmptyStr } - _ => no_longer_ready - } + no_longer_ready ReadinessDuplicate { configurations: _ } => no_longer_ready ReadinessWrongPresentation { image_name: _, configurations: _ } => no_longer_ready } @@ -2104,7 +2033,6 @@ fn presentation_still_served(result: MegaRacAttachResult) -> PresentationStillSe MegaRacMediaStateUnestablished { image_name: _, route: _, look: _, looks: _, cd_before_any_write: _, cd_at_ready: _, general_at_ready: _ } => presentation_unobserved_before_presentation() MegaRacMediaStateUnestablishedBeforeWrite { cd_before_any_write: _, reason: _ } => presentation_unobserved_before_presentation() MegaRacReadinessRefused { image_name: _, route: _, look: _, looks: _, cd_before_any_write: _, cd_at_refusal: _ } => presentation_unobserved_before_presentation() - MegaRacPresentationStopped { image_name: _, route: _, look: _, looks: _, cd_before_any_write: _, stop: _, class: _, general_at_refusal: _ } => presentation_unobserved_before_presentation() MegaRacReadinessClockUnreadable { image_name: _, route: _, detail: _ } => presentation_unobserved_before_presentation() MegaRacReadinessClockIncoherent { image_name: _, route: _, opened_at: _, previous: _, look: _, looks: _ } => presentation_unobserved_before_presentation() MegaRacConfigurationsUnreadable { detail: _ } => presentation_unobserved_before_presentation() @@ -2132,11 +2060,3 @@ fn presentation_still_served(result: MegaRacAttachResult) -> PresentationStillSe fn presentation_unobserved_before_presentation() -> PresentationStillServed { PresentationUnobserved { reason: "the presentation could not be observed (the session was refused or its token unreadable)" } } - -// THE VENDOR'S OWN RENDERING, "Stopped - ", with the code and this repository's class beside it. -fn stop_reason_text(status: MegaRacRmediaStatus, class: MegaRacStopReasonClass) -> String { - join(["Stopped - ", status.name as String, " (cd_error_code ", to_string(status.code), "; ", match class { - StopReasonNoFault => "no fault: the stop was asked for, or the consumer let the medium go" - StopReasonPresentationFailure => "a presentation failure: the controller could not present, or lost, the medium" - }, ")"], "") -} diff --git a/dag/gunbc/machine_intake/megarac_operation_evidence.dag b/dag/gunbc/machine_intake/megarac_operation_evidence.dag index 89207dc9ab1..ded723dcbe8 100644 --- a/dag/gunbc/machine_intake/megarac_operation_evidence.dag +++ b/dag/gunbc/machine_intake/megarac_operation_evidence.dag @@ -1,6 +1,6 @@ module gunbc.machine_intake_megarac_operation_evidence -import std.types { EpochMs, List, NonEmptyStr } +import std.types { EpochMs, List } import std.content_hash { Sha256Digest, Sha256Hash } import std.decl_ref { decl_ref } import extdeps.bmc.endpoint { AmiMegaRac } @@ -8,7 +8,7 @@ import extdeps.bmc.capability { bmc_firmware_release_identity_of_wire } import gunbc.machine_intake_receipt { EvidenceRef } import gunbc.megarac_operation_standing { MegaRacOperationEvidence, megarac_operation_evidence_of, MegaRacVirtualMediaAttach, MegaRacKvmCanvasStill, MegaRacSdrSocketNaming, - megarac_build_reading, megarac_operation_receipt, megarac_firmware_transition, megarac_operation_served_artifact_recovery, ServedArtifactDigestOnly, + megarac_build_reading, megarac_operation_receipt, megarac_firmware_transition, } import gunbc.machine_intake_mtcollins1_access_observation { mtcollins1_endpoint, mtcollins1_firmware, mtcollins1_identity, mtcollins1_evidence_manifest_digest, mtcollins1_observed_at, @@ -17,7 +17,6 @@ import gunbc.machine_intake_mtcollins1_diskless_boot_receipt { mtcollins1_diskless_boot_artifact_path, mtcollins1_diskless_boot_capture_digest, mtcollins1_diskless_boot_observed_at, } import gunbc.machine_intake_mtcollins1_32dimm_bringup_observation { mtcollins1_sdr_capture } -import gunbc.machine_intake_megarac_served_ui_catalog_observation { megarac_source_min_js_sha256_2026_09_27 } // THE COMMITTED EVIDENCE OF WHICH MEGARAC OPERATIONS RAN ON WHICH CONTROLLER BUILD, one row per capture // (gunbc.megarac_operation_standing joins them). Every row cites a committed artifact by path and @@ -27,13 +26,8 @@ import gunbc.machine_intake_megarac_served_ui_catalog_observation { megarac_sour // // MT. COLLINS UNIT 1, AS OF THIS WRITING: // - BMC 0.32 (Foxconn Mt. Collins), read by the 2026-09-05 intake observation. Virtual media ran on it -// (the 2026-09-06 diskless boot) and an SDR read with its CPU_ names ran on it (2026-09-11). The -// KVM canvas still's route was recovered from the web UI bundle that build SERVES to a browser -// (https://192.168.1.228/source.min.js, read 2026-09-27; gunbc.machine_intake_megarac_served_ui_catalog_observation -// megarac_source_min_js_sha256_2026_09_27). That is an observation of what the device serves, not a -// disassembly or a proprietary-notice document. Its BYTES ARE NOT COMMITTED -- the digest is the only -// record, and the row says so (ServedArtifactDigestOnly). No committed still from that build exists, -// so the KVM still is grounded at 0.32 as RECOVERED, never as executed. +// (the 2026-09-06 diskless boot) and an SDR read with its CPU_ names ran on it (2026-09-11). +// The KVM canvas still has no standing at 0.32: no committed still exists from that build. // - Reflashed to BMC 0.45.3 on 2026-10-02 (the firmware converge's recorded credential effect). // - Fleet-converge run 37102391062, 2026-10-03, about 21 h after the reflash, committed its media // attach (diagnostics), its KVM journal with five sha256-bound stills, and a BMC sensor read. Those @@ -64,9 +58,6 @@ data mtcollins1_sdr_32dimm_evidence: EvidenceRef = EvidenceRef { data mtcollins1_sdr_32dimm_read_on: EpochMs = 1789084800000 -// 2026-09-27T00:00:00Z: the bundle read names its day. -data megarac_served_bundle_0_32_read_on: EpochMs = 1790467200000 - data mtcollins1_run_37102391062_diagnostics: EvidenceRef = EvidenceRef { label: "artifacts/bmc/mtcollins1-boot-run-37102391062/mtcollins1-boot-diagnostics.txt", digest: Sha256Hash(Sha256Digest { hex: "d76d874395e4b809a6d70ccbccb4d941012391feeb14e7a8a2241befbf56b2d2" }), @@ -112,15 +103,5 @@ fn mtcollins1_megarac_operation_evidence() -> MegaRacOperationEvidence { record: decl_ref(module_path: "gunbc.fleet.mtcollins_firmware_converge", decl_name: "credential_effect_of"), ), ], - recoveries: [ - megarac_operation_served_artifact_recovery( - operation: MegaRacKvmCanvasStill, - identity: mtcollins1_identity, - served_by: mtcollins1_endpoint, - artifact: "https://192.168.1.228/source.min.js" as NonEmptyStr, - capture: ServedArtifactDigestOnly { digest: Sha256Hash(Sha256Digest { hex: megarac_source_min_js_sha256_2026_09_27 as String }) }, - read_at: megarac_served_bundle_0_32_read_on, - ), - ], ) } diff --git a/dag/gunbc/machine_intake/megarac_served_ui_catalog_observation.dag b/dag/gunbc/machine_intake/megarac_served_ui_catalog_observation.dag deleted file mode 100644 index 2bbb9e9c70a..00000000000 --- a/dag/gunbc/machine_intake/megarac_served_ui_catalog_observation.dag +++ /dev/null @@ -1,20 +0,0 @@ -module gunbc.machine_intake_megarac_served_ui_catalog_observation - -import std.types { NonEmptyStr } - -// RECEIPT OF A REPOSITORY CAPTURE, not an AMI-published catalog identity. The 255 api/ paths -// were extracted from source.min.js served at https://192.168.1.228/source.min.js on -// 2026-09-12 and committed under artifacts/. DESIGN §3: observations produced by this repository -// live in the observing layer. Firmware-keyed facts about what that surface contains stay in -// extdeps.bmc.megarac; this module owns only the capture's path and digest. - -data megarac_spx_ui_surface_path: NonEmptyStr = "artifacts/bmc/mtcollins1-megarac-api-surface.txt" as NonEmptyStr - -data megarac_spx_ui_surface_sha256: NonEmptyStr = "4fc53fea3fe49bc3ed3b967511654223578d59d963546117ab524df11b419bcd" as NonEmptyStr - -// THE 2026-09-27 READ OF THE BUNDLE ITSELF, as its digest. extdeps.bmc.megarac cites this read for the -// screen-capture surfaces and the KVM wire; the bytes were not committed, so the digest is the only -// thing a later read can be compared with. gunbc.machine_intake_mtcollins1_ui_bundle_observe reports -// match or drift against it and does not refuse on drift: a different digest is a finding about the -// firmware's served UI, not a failed read. -data megarac_source_min_js_sha256_2026_09_27: NonEmptyStr = "5029fae278f7fc8f4dd4ad5eb8e4457e49fa25d4d0158d4bf9fbab09f57d4bf3" as NonEmptyStr diff --git a/dag/gunbc/machine_intake/megarac_ui_features.dag b/dag/gunbc/machine_intake/megarac_ui_features.dag deleted file mode 100644 index 510a5efa0ae..00000000000 --- a/dag/gunbc/machine_intake/megarac_ui_features.dag +++ /dev/null @@ -1,403 +0,0 @@ -module gunbc.machine_intake_megarac_ui_features - -import extdeps.tools.sha256sum -import extdeps.tools.gzip -import std.algebra { trim } -import std.types { Bool, FilePath, Int, List, NonEmptyStr, String } -import std.optional { Present, Absent } -import std.process { ExitSuccess, ProcessExit, exit_failure } -import extdeps.filesystem.filesystem_io { Filesystem } -import gunbc.machine_intake_megarac_served_ui_catalog_observation { megarac_source_min_js_sha256_2026_09_27 } -import extdeps.external_authority { CitedFigureStanding, TranscribedUncited } -import extdeps.bmc.capability { BmcFirmwareReleaseIdentity, bmc_firmware_release_identity_eq } -import extdeps.bmc.megarac { megarac_firmware_0_32 } - -// THE FEATURE LIST THE VENDOR UI HANDS ITS KVM VIEWER, READ FROM THE UI'S OWN BYTES (eager-owl-205, -// 2026-09-30; operator rule: derived, never hand-typed). The served viewer.min.js (fleet-converge crawl -// run 36787375313) initialises its KVM view with UNGUARDED sessionStorage.features.indexOf(...) reads -// (KVM_SESSION_RECONNECT, VMEDIA_MAX_COUNT_FOR_KVM, HOST_CURSOR_ENABLED_DEFAULT): with `features` unset -// it throws, the view never initialises, and /kvm is never opened -- KVM probe run 36788605665 journalled -// exactly that page error and no WebSocket. The real UI stores JSON.stringify of its -// project-configurations collection, which is seeded from the STATIC `features` module compiled into -// source.min.js: -// o("features",[],function(){var e=[{feature:"RMEDIA_MULTI_IMAGE_SUPPORT"},...];return e}) -// so the list is parsed out of those bytes, and only out of the build this unit's facts are keyed on: -// the served source.min.js must digest to the 2026-09-27 read's sha256, or nothing is read. -// -// THE LIST IS AN OBSERVATION RECEIPT, NOT A DERIVED FACT IN GIT (review 73613; operator decision via -// eager-owl-205, 2026-10-01). The bundle it is read from is AMI's proprietary MegaRAC UI and is not -// redistributable, so it is not committed and no CI job can reach it: a CI-diffed gate is unavailable, -// and that is a DECLARED BOUNDARY, not a below-ceiling choice (DESIGN 4b, outside the modeled guarantee; -// 4d, the typed bet). The receipt names what was read -- the firmware build, the bundle's sha256, the -// run that retained it -- and carries TranscribedUncited with its read obligation: the route that -// re-derives it. What gates it is that route: megarac_ui_bundle_rows_emit refuses any bundle whose -// digest is not the pinned one, so a later fetch either reproduces these names from these bytes or -// refuses, and never silently diverges from them. -data megarac_ui_features_module_open: String = "o(\"features\",[],function()\{var e=[" - -data megarac_ui_features_module_close: String = "];return e}" - -data megarac_ui_feature_key: String = "feature:\"" - -// THE NAMES IN THE MODULE, IN ITS ORDER: every `feature:"NAME"` between the module's opening and its -// `];return e}`. Absent when the module is not in the text, or holds no name. -fn megarac_ui_features_of_bundle(text: String) -> List? { - match split(s: text, delimiter: megarac_ui_features_module_open).skip(n: 1).first() { - Absent => none - Present { value: after } => { - let body = match split(s: after, delimiter: megarac_ui_features_module_close).first() { Present { value: b } => b Absent => "" } - let names = map(split(s: body, delimiter: megarac_ui_feature_key).skip(n: 1), rest => match split(s: rest, delimiter: "\"").first() { Present { value: n } => n Absent => "" }) - if count(names) == 0 { none } else { Present { value: names } } - } - } -} - -// A FEATURE NAME IS THE VENDOR'S IDENTIFIER SHAPE: upper-case letters, digits and underscores, nonempty. -// Anything else is refused rather than written into a JSON string it could escape from. -data megarac_ui_feature_name_characters: String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" - -fn megarac_ui_feature_name_admitted(name: String) -> Bool { - name != "" && all(split(s: name, delimiter: ""), c => c == "" || string_contains(s: megarac_ui_feature_name_characters, pattern: c)) -} - - -type MegaRacUiFeatures - = MegaRacUiFeaturesRead { names: List, source_sha256: NonEmptyStr } - | MegaRacUiFeaturesUnread { cause: NonEmptyStr } - -// THE READING FROM ALREADY-DECODED BUNDLE TEXT AND ITS DIGEST: refused unless the digest is the pinned -// build's and the module yields only admitted names. -fn megarac_ui_features_reading(observed_sha256: String, text: String) -> MegaRacUiFeatures { - if observed_sha256 != (megarac_source_min_js_sha256_2026_09_27 as String) { - MegaRacUiFeaturesUnread { cause: concat(concat("source.min.js digests to ", observed_sha256), concat(", not the pinned ", megarac_source_min_js_sha256_2026_09_27 as String)) as NonEmptyStr } - } else { - match megarac_ui_features_of_bundle(text: text) { - Absent => MegaRacUiFeaturesUnread { cause: "the features module was not found in source.min.js, or holds no name" } - Present { value: names } => - if all(names, n => megarac_ui_feature_name_admitted(name: n)) { - MegaRacUiFeaturesRead { names: names, source_sha256: megarac_source_min_js_sha256_2026_09_27 } - } else { - MegaRacUiFeaturesUnread { cause: concat("the features module holds a name outside the vendor identifier shape: ", join(filter(names, n => !megarac_ui_feature_name_admitted(name: n)), " ")) as NonEmptyStr } - } - } - } -} - -type MegaRacUiFeaturesReceipt { - firmware: BmcFirmwareReleaseIdentity - bundle_sha256: NonEmptyStr - producing_run: NonEmptyStr - names: List - standing: CitedFigureStanding -} - -// The names, as megarac_ui_bundle_rows_emit wrote them from run 36787375313's retained source.min.js. -data megarac_ui_features_0_32: List = [ - "RMEDIA_MULTI_IMAGE_SUPPORT", - "BIOS_SUPPORT", - "KVM_SESSION_RECONNECT", - "SYSTEM_FIREWALL", - "OEM_PROPRIETARY_LEVEL_SUPPORT", - "HTML5_KVM", - "DUAL_BIOS_FLASH_SUPPORT", - "IPV6_SUPPORT", - "ALLOW_NON_SECURE_COMMUNICATION", - "SET_SENSOR_THRESHOLDS", - "REPLACE_OEM_LOGO", - "EXTENDEDLOG", - "AUTO_RESIZE_KVM_CLIENT_WINDOW", - "SERVICES", - "RMEDIA", - "AUTOVDORECORD_REMOTE", - "CAPTURE_BSOD_JPEG", - "RADIUS_SUPPORT", - "SYSTEM_FIREWALL_TIMEOUT", - "UARTLOG", - "RUNTIME_HOST_LOCK", - "SOL_SUPPORT", - "HPM_SUPPORT", - "ADVISER_SUPPORT", - "AD_SUPPORT", - "MULTIPLE_USER_VMEDIA", - "LDAP_SUPPORT", - "KVM_SESSION_TIMEOUT", - "FIRMWARE_FLASH_SUPPORT", - "CPLD_SUPPORT", - "PTP_SERVER_SUPPORT", - "BOOTFW_SUPPORT", - "HOST_LOCK_AUTO", - "BACKUP_CONFIG", - "SET_SENSOR_THRESHOLDS_RESET", - "SAVE_SELLOG", - "SESSION_MANAGEMENT", - "RMEDIA_MULTI_CD_SUPPORT", - "TSIG", - "IPV6_COMPLIANCE", - "CD_SERVER_APP", - "WEB_DASHBOARD_WIDGETS", - "FOLDER_REDIRECTION", - "CIRCULAR_SEL", - "FW_IMAGE", - "DISABLE_EMPTY_PASSWORD", - "KB_LANG_SELECT_SUPPORT", - "DYNAMIC_KCS_LAN_SUPPORT", - "LMEDIA", - "DUAL_BOOTFW_EEPROM_SUPPORT", - "BLOCK_ICMP_TIMESTAMP", - "SINGLE_PORT_APP", - "MDNS", - "NCSI_SUPPORT", - "IMG_REDIRECTION", - "HD_SERVER_APP", - "PAM_REORDERING", - "EXTENDED_PRIV", - "SNMP", - "WEB_APP_PROXY", - "LMEDIA_MULTI_IMAGE_SUPPORT", - "VMEDIA_MAX_COUNT_FOR_KVM", - "MEDIA_REDIR_READ_WRITE_ONLY", - "RMEDIA_SESSION_RECONNECT", - "PRESERVECONF", - "TIMEZONE_SUPPORT", - "RMEDIA_MULTI_HD_SUPPORT", - "AUTOVDORECORD", - "YES", - "VERSION_CMP_FLASH", - "BMC_RECOVERY", - "NWLINK", - "KVM_OR_MEDIA_AVAILABLE", -] - -data megarac_ui_features_0_32_receipt: MegaRacUiFeaturesReceipt = MegaRacUiFeaturesReceipt { - firmware: megarac_firmware_0_32, - bundle_sha256: megarac_source_min_js_sha256_2026_09_27, - producing_run: "36787375313" as NonEmptyStr, - names: megarac_ui_features_0_32, - standing: TranscribedUncited { read_obligation: "re-derive by the fleet-converge mtcollins1_ui_bundle_observe mode, which retains the served source.min.js with its sha256 as the mtcollins1-ui-bundle artifact, then gunbc.machine_intake_megarac_ui_features megarac_ui_bundle_rows_emit over that file: it refuses unless the bytes digest to megarac_source_min_js_sha256_2026_09_27, and otherwise writes these names" as NonEmptyStr }, -} - -data megarac_ui_features_receipts: List = [megarac_ui_features_0_32_receipt] - -// THE LIST FOR A FIRMWARE BUILD: the receipt observed for that build, or none -- a build no receipt -// covers has no list, and the observer does not drive its viewer. -fn megarac_ui_features(firmware: BmcFirmwareReleaseIdentity) -> List? { - match filter(megarac_ui_features_receipts, r => bmc_firmware_release_identity_eq(a: r.firmware, b: firmware)).first() { - Present { value: r } => Present { value: r.names } - Absent => none - } -} - -// THE REMOTE-MEDIA STOP REASONS, IN THE VENDOR'S OWN NAMES, AS AN OBSERVATION RECEIPT BESIDE THE FEATURE -// LIST'S (same bundle, same boundary: AMI's proprietary UI, not committed, not reachable from CI). The -// SP-X UI's rmedia view renders a stopped CD as its `halted` label ("Stopped") followed by the i18n -// string rmedia:status_, and only for 1 <= code <= 17 (changeInSingleImages: -// `0==r&&l>=1&&17>=l`, r settings/media/general mount_cd, l its cd_error_code). Each row is one -// rmedia:status_ entry of the English dictionary, " - " prefix removed; status_0 ("~") and -// status_100 (" Starting..") are not stop reasons. The receipt CITES the feature receipt's firmware, -// bundle digest and producing run rather than restating them: both lists are written by one emitter -// from one digest-checked read, megarac_ui_bundle_rows_emit. -type MegaRacRmediaStatus { - code: Int - name: NonEmptyStr -} - -type MegaRacRmediaStatusReceipt { - firmware: BmcFirmwareReleaseIdentity - bundle_sha256: NonEmptyStr - producing_run: NonEmptyStr - rows: List - standing: CitedFigureStanding -} - -// The rows, as megarac_ui_bundle_rows_emit wrote them from the same retained source.min.js. -data megarac_rmedia_status_0_32: List = [ - MegaRacRmediaStatus { code: 1, name: "Connection Accepted" }, - MegaRacRmediaStatus { code: 2, name: "Connection Denied" }, - MegaRacRmediaStatus { code: 3, name: "Login Failed" }, - MegaRacRmediaStatus { code: 4, name: "MAX Session Reached" }, - MegaRacRmediaStatus { code: 5, name: "Permission Denied" }, - MegaRacRmediaStatus { code: 6, name: "Unknown Error" }, - MegaRacRmediaStatus { code: 7, name: "Media Detach Stage" }, - MegaRacRmediaStatus { code: 8, name: "All device instances are occupied" }, - MegaRacRmediaStatus { code: 9, name: "Unable to Connect" }, - MegaRacRmediaStatus { code: 10, name: "Invalid Image" }, - MegaRacRmediaStatus { code: 11, name: "Mount Error" }, - MegaRacRmediaStatus { code: 12, name: "Unable to Open" }, - MegaRacRmediaStatus { code: 13, name: "Media License Expired" }, - MegaRacRmediaStatus { code: 14, name: "Connection Lost" }, - MegaRacRmediaStatus { code: 15, name: "Mount Cancelled By User" }, - MegaRacRmediaStatus { code: 16, name: "Device Ejected" }, - MegaRacRmediaStatus { code: 17, name: "Session Terminated" }, -] - -data megarac_rmedia_status_0_32_receipt: MegaRacRmediaStatusReceipt = MegaRacRmediaStatusReceipt { - firmware: megarac_ui_features_0_32_receipt.firmware, - bundle_sha256: megarac_ui_features_0_32_receipt.bundle_sha256, - producing_run: megarac_ui_features_0_32_receipt.producing_run, - rows: megarac_rmedia_status_0_32, - standing: megarac_ui_features_0_32_receipt.standing, -} - -data megarac_rmedia_status_receipts: List = [megarac_rmedia_status_0_32_receipt] - -// A CODE THE BUILD'S TABLE NAMES, A CODE IT DOES NOT, OR A BUILD NO RECEIPT COVERS. The last two are -// different facts: one is this firmware answering outside its own UI's vocabulary, the other is this -// repository not having read that firmware's UI. -type MegaRacRmediaStatusLookup - = RmediaStatusNamed { status: MegaRacRmediaStatus } - | RmediaStatusOutsideTable { firmware: BmcFirmwareReleaseIdentity, wire: Int } - | RmediaStatusTableUnread { firmware: BmcFirmwareReleaseIdentity, wire: Int } - -fn megarac_rmedia_status_of_wire(firmware: BmcFirmwareReleaseIdentity, wire: Int) -> MegaRacRmediaStatusLookup { - match filter(megarac_rmedia_status_receipts, r => bmc_firmware_release_identity_eq(a: r.firmware, b: firmware)).first() { - Absent => RmediaStatusTableUnread { firmware: firmware, wire: wire } - Present { value: r } => - match filter(r.rows, st => st.code == wire).first() { - Present { value: st } => RmediaStatusNamed { status: st } - Absent => RmediaStatusOutsideTable { firmware: firmware, wire: wire } - } - } -} - -// THE REMOTE-MEDIA STOP-REASON TABLE, READ FROM THE SAME BYTES (operator-approved 2026-10-01; rule: -// derived, never hand-typed). The rmedia view names a stopped CD as "Stopped" + rmedia:status_, -// and its range check is compiled in beside the dictionary: -// 0==r&&l>=1&&17>=l?(... a.t("rmedia:halted")+a.t("rmedia:status_"+l) ...) -// The English rmedia dictionary opens once in the bundle -- rmedia:{home:"Home",... -- and a stop reason -// is an entry status_:" - "; status_0 ("~") and status_100 (" Starting..") carry no " - " -// and are not reasons. Two readings of one fact are taken and must agree: the dictionary's entries are -// exactly codes 1..N in order, and N is the upper bound the range check reads. A disagreement is a -// build this parse does not understand, and refuses. -data megarac_rmedia_dictionary_open: String = "rmedia:\{home:\"Home\"," - -data megarac_rmedia_dictionary_close: String = "}" - -data megarac_rmedia_status_key: String = "status_" - -data megarac_rmedia_stop_reason_prefix: String = " - " - -data megarac_rmedia_status_bound_open: String = "0==r&&l>=1&&" - -data megarac_rmedia_status_bound_close: String = ">=l" - -// ONE DICTIONARY ENTRY that names a stop reason: status_:" - ". Anything else in the -// dictionary -- its other keys, status_0, status_100 -- is not a row. -fn megarac_rmedia_status_entry(piece: String) -> MegaRacRmediaStatus? { - match split(s: piece, delimiter: ":\"").first() { - Absent => none - Present { value: key } => - match parse_int(s: key) { - Absent => none - Present { value: code } => { - let value = match split(s: piece, delimiter: ":\"").skip(n: 1).first() { Present { value: v } => match split(s: v, delimiter: "\"").first() { Present { value: q } => q Absent => "" } Absent => "" } - if starts_with(s: value, prefix: megarac_rmedia_stop_reason_prefix) { - let name = join(split(s: value, delimiter: megarac_rmedia_stop_reason_prefix).skip(n: 1), megarac_rmedia_stop_reason_prefix) - if name == "" { none } else { Present { value: MegaRacRmediaStatus { code: code, name: name as NonEmptyStr } } } - } else { none } - } - } - } -} - -// THE STOP REASONS IN THE ENGLISH rmedia DICTIONARY, IN ITS ORDER. Absent when the dictionary is not in -// the text. -fn megarac_rmedia_status_entries_of_bundle(text: String) -> List? { - match split(s: text, delimiter: megarac_rmedia_dictionary_open).skip(n: 1).first() { - Absent => none - Present { value: after } => { - let body = match split(s: after, delimiter: megarac_rmedia_dictionary_close).first() { Present { value: b } => b Absent => "" } - Present { value: fold(split(s: body, delimiter: megarac_rmedia_status_key).skip(n: 1), init: [], f: (acc, p) => match megarac_rmedia_status_entry(piece: p) { Present { value: r } => acc.append(r) Absent => acc }) } - } - } -} - -// THE UPPER BOUND THE VIEW'S OWN RANGE CHECK READS. Absent when the check is not in the text. -fn megarac_rmedia_status_bound_of_bundle(text: String) -> Int? { - match split(s: text, delimiter: megarac_rmedia_status_bound_open).skip(n: 1).first() { - Absent => none - Present { value: after } => - match split(s: after, delimiter: megarac_rmedia_status_bound_close).first() { - Absent => none - Present { value: n } => parse_int(s: n) - } - } -} - -type MegaRacRmediaStatusTable - = MegaRacRmediaStatusTableRead { rows: List, source_sha256: NonEmptyStr } - | MegaRacRmediaStatusTableUnread { cause: NonEmptyStr } - -// A NAME IS WRITTEN INTO A .dag STRING, so a quote or a backslash is refused rather than escaped. -fn megarac_rmedia_status_name_admitted(name: String) -> Bool { - name != "" && !string_contains(s: name, pattern: "\"") && !string_contains(s: name, pattern: "\\") -} - -// THE READING FROM ALREADY-DECODED BUNDLE TEXT AND ITS DIGEST: refused unless the digest is the pinned -// build's, the dictionary and the range check are both present, the entries are exactly 1..bound in -// order, and every name is admitted. -fn megarac_rmedia_status_table_reading(observed_sha256: String, text: String) -> MegaRacRmediaStatusTable { - if observed_sha256 != (megarac_source_min_js_sha256_2026_09_27 as String) { - MegaRacRmediaStatusTableUnread { cause: concat(concat("source.min.js digests to ", observed_sha256), concat(", not the pinned ", megarac_source_min_js_sha256_2026_09_27 as String)) as NonEmptyStr } - } else { - match megarac_rmedia_status_entries_of_bundle(text: text) { - Absent => MegaRacRmediaStatusTableUnread { cause: "the English rmedia dictionary was not found in source.min.js" } - Present { value: rows } => - match megarac_rmedia_status_bound_of_bundle(text: text) { - Absent => MegaRacRmediaStatusTableUnread { cause: "the rmedia view's stop-reason range check was not found in source.min.js" } - Present { value: bound } => - if count(rows) != bound || count(rows) == 0 { - MegaRacRmediaStatusTableUnread { cause: join(["the rmedia dictionary names ", to_string(count(rows)), " stop reasons but the view's range check reads 1..", to_string(bound)], "") as NonEmptyStr } - } else if fold(rows, init: 1, f: (next, r) => if next > 0 && r.code == next { next + 1 } else { 0 }) != count(rows) + 1 { - MegaRacRmediaStatusTableUnread { cause: concat("the rmedia stop reasons are not codes 1..N in order: ", join(map(rows, r => to_string(r.code)), " ")) as NonEmptyStr } - } else if !all(rows, r => megarac_rmedia_status_name_admitted(name: r.name as String)) { - MegaRacRmediaStatusTableUnread { cause: "an rmedia stop-reason name carries a quote or a backslash" } - } else { - MegaRacRmediaStatusTableRead { rows: rows, source_sha256: megarac_source_min_js_sha256_2026_09_27 } - } - } - } - } -} - -fn megarac_rmedia_status_row_text(rows: List) -> String { - concat(concat("data megarac_rmedia_status_0_32: List = [\n", join(map(rows, r => join([" MegaRacRmediaStatus \{ code: ", to_string(r.code), ", name: \"", r.name as String, "\" },"], "")), "\n")), "\n]\n") -} - -// THE ROWS' ONE EMITTER (the entry both receipts' read obligation names): read a RETAINED source.min.js as served -- the -// mtcollins1-ui-bundle artifact of a crawl run -- refuse unless it digests to the pinned 2026-09-27 -// build, decode it ONCE, parse both tables, and write both rows' text to `out`. Either parse refusing writes nothing. Run: -// gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/machine_intake/megarac_ui_features.dag -// --function megarac_ui_bundle_rows_emit --arg bundle= --arg out= -fn megarac_ui_features_row_text(names: List) -> String { - concat(concat("data megarac_ui_features_0_32: List = [\n", join(map(names, n => concat(concat(" \"", n), "\",")), "\n")), "\n]\n") -} - -fn megarac_ui_bundle_rows_emit(bundle: String, out: String) -> ProcessExit { - let digest = sha256sum.Sha256.DigestFile(path: bundle as NonEmptyStr) - let observed = match split(s: trim(s: digest.line), delimiter: " ").first() { Present { value: d } => d Absent => "" } - if !digest.success || observed == "" { - return exit_failure(reason: concat("megarac ui bundle rows: the retained bundle could not be digested: ", digest.line)) - } - let decoded = gzip.Gzip.DecodeToStdout(path: bundle as NonEmptyStr) - if !decoded.success { - return exit_failure(reason: concat("megarac ui bundle rows: the retained bundle could not be decoded: ", trim(s: decoded.stderr))) - } - match megarac_ui_features_reading(observed_sha256: observed, text: decoded.text) { - MegaRacUiFeaturesUnread { cause: c } => exit_failure(reason: concat("megarac ui features: ", c as String)) - MegaRacUiFeaturesRead { names: n, source_sha256: _ } => - match megarac_rmedia_status_table_reading(observed_sha256: observed, text: decoded.text) { - MegaRacRmediaStatusTableUnread { cause: c } => exit_failure(reason: concat("megarac rmedia status table: ", c as String)) - MegaRacRmediaStatusTableRead { rows: r, source_sha256: _ } => { - let w = Filesystem.Write(path: out, content: concat(megarac_ui_features_row_text(names: n), megarac_rmedia_status_row_text(rows: r))) - if w.success { ExitSuccess } else { exit_failure(reason: concat("megarac ui bundle rows: the rows could not be written: ", w.error)) } - } - } - } -} - -fn megarac_ui_features_text(f: MegaRacUiFeatures) -> String { - match f { - MegaRacUiFeaturesRead { names: n, source_sha256: s } => join([to_string(count(n)), " features read from source.min.js sha256 ", s as String], "") - MegaRacUiFeaturesUnread { cause: c } => concat("features UNREAD: ", c as String) - } -} diff --git a/dag/gunbc/machine_intake/mtcollins1_32dimm_bringup_observation.dag b/dag/gunbc/machine_intake/mtcollins1_32dimm_bringup_observation.dag index df349e53ead..f48bac69e0e 100644 --- a/dag/gunbc/machine_intake/mtcollins1_32dimm_bringup_observation.dag +++ b/dag/gunbc/machine_intake/mtcollins1_32dimm_bringup_observation.dag @@ -1,5 +1,6 @@ module gunbc.machine_intake_mtcollins1_32dimm_bringup_observation +import extdeps.ampere.mt_collins_board { mt_collins_board_authority } import gunbc.mtcollins_memory_change { mtcollins_training_capture_path, mtcollins_training_capture_sha256 } import std.types { Bool, EpochMs, Int, List, NonEmptyStr, String } import std.measure { ByteSize, byte_size } @@ -8,7 +9,6 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } import std.process { ExitFailure, ExitSuccess, ProcessExit, exit_failure } import std.scoped_authorization { AttemptIdentity } import std.temporal_effect { LeaseEpoch, LeaseRunningExpected, LeaseRunningStale, held_lease } -import extdeps.boards.types { AmpereMtCollins } import extdeps.bmc.ipmi_sel { FirmwareProgress, InitiatedByPowerUp, @@ -395,7 +395,7 @@ data mtcollins1_sdr_capture: SdrCapture = SdrCapture { // (CPU0_MEM_Pwr / CPU1_MEM_Pwr in Watts, mtcollins1_sdr_capture); managed AC no (the AC cycle was performed // by hand, 10-ac-meta.txt); host callback intended, not demonstrated. data mtcollins1_pre_os_capabilities: PreOsObservationCapabilities = PreOsObservationCapabilities { - baseboard: AmpereMtCollins, + baseboard: mt_collins_board_authority, controller_firmware: mtcollins1_firmware, console: SolPresentEmitting { established_by: DeclarationRef { module_path: "gunbc.machine_intake_mtcollins1_32dimm_bringup_observation", decl_name: "mtcollins1_dram_training_capture", field: WholeDeclaration }, diff --git a/dag/gunbc/machine_intake/mtcollins1_access_observation.dag b/dag/gunbc/machine_intake/mtcollins1_access_observation.dag index 008ec57db3f..47ca3369fe8 100644 --- a/dag/gunbc/machine_intake/mtcollins1_access_observation.dag +++ b/dag/gunbc/machine_intake/mtcollins1_access_observation.dag @@ -1,10 +1,10 @@ module gunbc.machine_intake_mtcollins1_access_observation +import extdeps.board_authority { BoardFruIdentity, BoardFruRead } import std.types { EpochMs, Int, NonEmptyStr, String } import std.measure { Second, second } import extdeps.bmc.ipmi_boot_selection { IpmiBootCdrom, IpmiBootSelection, aarch64_uefi_once_boot_selection } import std.content_hash { ContentHash, Sha256Digest, Sha256Hash } -import extdeps.boards.types { AmpereMtCollins } import extdeps.bmc.endpoint { AmiMegaRac, BmcControllerEndpoint } import extdeps.bmc.capability { BmcFirmwareReleaseCapabilityRow, @@ -110,7 +110,11 @@ data mtcollins1_firmware: BmcFirmwareReleaseIdentity = BmcFirmwareReleaseIdentit // all join on one physical box — that join is what makes this an observation OF A UNIT rather than // of an address. data mtcollins1_identity: BmcObservedIdentity = BmcObservedIdentity { - baseboard: AmpereMtCollins, + board: BoardFruRead { fru: BoardFruIdentity { + manufacturer: "FOXCONN", + product: "Mt. Collins", + part_number: Present { value: "02030A800-600-G" }, + } }, chassis_part_number: Present { value: "1A624RA00-600-G" }, firmware: mtcollins1_firmware, } @@ -295,7 +299,7 @@ data mtcollins1_capability_row: BmcFirmwareReleaseCapabilityRow = BmcFirmwareRel // THE BOUND ON ONE READ-ONLY ipmitool EXCHANGE WITH THIS UNIT'S CONTROLLER, as `timeout --kill-after` // operands. A fact about reaching this BMC, not about any one run that reads it: the boot's diagnostic -// bundle, its media attach and the UI bundle observation all bound their IPMI reads by it. It lives +// bundle and its media attach bound their IPMI reads by it. It lives // here, with the endpoint it bounds, so a read-only mode does not import the boot closure to learn it. data mtcollins1_boot_ipmi_read_deadline: Second = second(count: 20) diff --git a/dag/gunbc/machine_intake/mtcollins1_boot_diagnostic_bundle.dag b/dag/gunbc/machine_intake/mtcollins1_boot_diagnostic_bundle.dag index 3dfb962c66c..3b10898b7b6 100644 --- a/dag/gunbc/machine_intake/mtcollins1_boot_diagnostic_bundle.dag +++ b/dag/gunbc/machine_intake/mtcollins1_boot_diagnostic_bundle.dag @@ -127,10 +127,7 @@ import gunbc.machine_intake_megarac_media_attach { CdErrorRead, CdErrorUnread, CdErrorNone, - CdErrorUncatalogued, - CdErrorStopReason, - MegaRacPresentationStopped, - stop_reason_text, + CdErrorNonzero, megarac_share_type_wire, MegaRacSessionRefused, MegaRacSessionTokenUnreadable, @@ -506,8 +503,7 @@ fn stale_power_off_text(record: StaleMediumPowerOff) -> String { fn cd_reading_text(r: CdErrorReading) -> String { match r { CdErrorRead { code: CdErrorNone } => "0" - CdErrorRead { code: CdErrorStopReason { status: st, class: k } } => concat(to_string(st.code), concat(" ", stop_reason_text(status: st, class: k))) - CdErrorRead { code: CdErrorUncatalogued { wire: w } } => concat(to_string(w), " (uncatalogued)") + CdErrorRead { code: CdErrorNonzero { wire: w, firmware: _ } } => concat(to_string(w), " (observed; no meaning established)") CdErrorUnread { detail: d } => concat("UNREAD: ", d) } } @@ -597,8 +593,6 @@ fn media_attach_outcome_reason(outcome: MegaRacAttachOutcome) -> String? { Present { value: join(["the presentation of '", n as String, "' did not reach Started with a bound session before ", to_string(d), " (", to_string(k), " looks, route: ", attach_route_text(route: r), "); last look ", readiness_look_text(l: l), "; cd_error_code before any write ", cd_reading_text(r: cb), ", at refusal ", cd_reading_text(r: ca), ". No boot-device or power handoff was made"], "") } MegaRacReadinessRefused { image_name: n, route: r, look: l, looks: k, cd_before_any_write: cb, cd_at_refusal: ca } => Present { value: join(["the presentation of '", n as String, "' was refused by the readiness policy after ", to_string(k), " looks (route: ", attach_route_text(route: r), "): ", readiness_look_text(l: l), "; cd_error_code before any write ", cd_reading_text(r: cb), ", at refusal ", cd_reading_text(r: ca), ". No boot-device or power handoff was made"], "") } - MegaRacPresentationStopped { image_name: n, route: r, look: l, looks: k, cd_before_any_write: cb, stop: st, class: c, general_at_refusal: g } => - Present { value: join(["the presentation of '", n as String, "' is ", stop_reason_text(status: st, class: c), " after ", to_string(k), " looks (route: ", attach_route_text(route: r), "): ", readiness_look_text(l: l), "; cd_error_code before any write ", cd_reading_text(r: cb), " (general settings ", raw_read_text(r: g), "). No boot-device or power handoff was made"], "") } MegaRacReadinessClockUnreadable { image_name: n, route: r, detail: d } => Present { value: join(["the clock could not be read, so the readiness window for '", n as String, "' could not be declared or judged: ", d, " (route: ", attach_route_text(route: r), ")"], "") } MegaRacConfigurationsUnreadable { detail: d } => @@ -638,7 +632,7 @@ fn media_attach_outcome_reason(outcome: MegaRacAttachOutcome) -> String? { MegaRacOfferedAmbiguouslyAfterDetach { image_name: n, image_indices: ids, detach: x } => Present { value: join(["after the stale image was stopped (", detach_evidence_text(detach: x), "), the controller offers '", n as String, "' more than once (image_index ", join(ids, ", "), "), so which file would be attached is the controller's enumeration order; nothing further was written"], "") } MegaRacMediaStateUnestablished { image_name: n, route: r, look: l, looks: k, cd_before_any_write: cb, cd_at_ready: ca, general_at_ready: g } => - Present { value: join(["this attempt cannot establish the media state of '", n as String, "': the presentation read Started (", readiness_look_text(l: l), ", ", to_string(k), " looks, route: ", attach_route_text(route: r), ") but cd_error_code before any write was ", cd_reading_text(r: cb), " and at readiness ", cd_reading_text(r: ca), " (general settings ", raw_read_text(r: g), "). A code outside the firmware UI's stop-reason table (extdeps.bmc.megarac megarac_rmedia_status_0_32), or an unread one, has no vendor meaning, so this is a knowledge refusal, not a controller verdict. No boot-device or power handoff was made"], "") } + Present { value: join(["this attempt cannot establish the media state of '", n as String, "': the presentation read Started (", readiness_look_text(l: l), ", ", to_string(k), " looks, route: ", attach_route_text(route: r), ") but cd_error_code before any write was ", cd_reading_text(r: cb), " and at readiness ", cd_reading_text(r: ca), " (general settings ", raw_read_text(r: g), "). A code other than 0, or 16 on the build an executed receipt grounded it for, or an unread one, has no established meaning, so this is a knowledge refusal, not a controller verdict. No boot-device or power handoff was made"], "") } MegaRacMediaStateUnestablishedBeforeWrite { cd_before_any_write: cb, reason: why } => Present { value: join(["this attempt cannot establish the media state, and nothing was written: cd_error_code before any write was ", cd_reading_text(r: cb), "; ", why as String], "") } MegaRacReadinessClockIncoherent { image_name: n, route: rt, opened_at: o, previous: p, look: l, looks: k } => diff --git a/dag/gunbc/machine_intake/mtcollins1_boot_dry_realization.dag b/dag/gunbc/machine_intake/mtcollins1_boot_dry_realization.dag index 65de04f0baa..80d177690bf 100644 --- a/dag/gunbc/machine_intake/mtcollins1_boot_dry_realization.dag +++ b/dag/gunbc/machine_intake/mtcollins1_boot_dry_realization.dag @@ -32,12 +32,6 @@ import extdeps.units.iso8601_calendar { iso8601_utc_text } import std.checked_arithmetic { checked_int_to_nat } import std.measure { millisecond } import v2.std.operation_argv { InputText, InputTextList } -import gunbc.machine_intake_megarac_kvm_still { - KvmInstant, KvmJournalEvent, KvmJournalRendered, KvmJournalRenderRefused, kvm_journal_line, - KvmJournalConnectionRequested, KvmJournalConnectionOpen, KvmJournalEstablished, KvmJournalStill, - KvmJournalStopRequested, KvmJournalSessionRelease, KvmJournalBrowserClose, KvmJournalStopped, - KvmJournalRefused, KvmNoFrame, -} // THE DRY REALIZATION OF ONE mtcollins1 BOOT ATTEMPT'S WHOLE EFFECT DEMAND. The scenario world // composes the one BMC model with what the worker running the attempt has around it: its filesystem @@ -57,7 +51,6 @@ type MtCollins1BootWorld { worker: ModeledWorker console: ModeledHostConsole media: MegaRacMediaWorld - screen: ModeledKvmViewer } // THE WORKER'S SSH AGENT: whether it holds the fleet automation key. Its listing follows @@ -115,23 +108,23 @@ type ModeledHostConsole { } fn with_bmc(w: MtCollins1BootWorld, bmc: BmcWorld) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media, screen: w.screen } + MtCollins1BootWorld { bmc: bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media } } fn with_fs(w: MtCollins1BootWorld, fs: ModeledFilesystem) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: w.bmc, fs: fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media, screen: w.screen } + MtCollins1BootWorld { bmc: w.bmc, fs: fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media } } fn with_worker(w: MtCollins1BootWorld, worker: ModeledWorker) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: worker, console: w.console, media: w.media, screen: w.screen } + MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: worker, console: w.console, media: w.media } } fn with_console(w: MtCollins1BootWorld, console: ModeledHostConsole) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: console, media: w.media, screen: w.screen } + MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: console, media: w.media } } fn with_media(w: MtCollins1BootWorld, media: MegaRacMediaWorld) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: media, screen: w.screen } + MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: media } } fn appended(fs: ModeledFilesystem, path: String, text: String) -> ModeledFilesystem { @@ -247,7 +240,7 @@ fn with_notice_watcher(w: MtCollins1BootWorld, capture_path: String, token: Stri let p = ModeledProcess { pid: w.worker.next_pid, start_time: start_ticks(w: w, now: second(count: 0)), comm: "bash", cmdline: "bash -c gunbc-sol-notice-watch", capture_path: capture_path, diagnostic_path: "", alive: true } let fs = fs_with_file(fs: fs_with_file(fs: w.fs, path: concat(capture_path, mtcollins1_sol_notice_ready_suffix), content: concat(token, "\n")), path: mtcollins1_sol_notice_watcher_path(capture_path: capture_path), content: process_identity(p: p)) let env = list_snoc_item(xs: w.environment, item: ModeledVariable { name: mtcollins1_sol_notice_token_env as String, value: token }) - started(w: MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: env, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media, screen: w.screen }, p: p, fs: fs) + started(w: MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: env, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media }, p: p, fs: fs) } fn exited_step(state: S, stdout: String, exit_code: Int) -> OperationStep { @@ -399,262 +392,7 @@ fn boot_world_idle(w: MtCollins1BootWorld) -> Bool { fn boot_advance(w: MtCollins1BootWorld, now: Second) -> MtCollins1BootWorld { let stepped = if boot_world_idle(w: w) { w } else { with_media(w: with_bmc(w: w, bmc: bmc_advance(world: w.bmc, now: now)), media: megarac_media_advance(world: w.media, now: now)) } - kvm_observers_advance(w: console_advance(w: collectors_follow_session(w: stepped), now: now), now: now) -} - -// THE BMC KVM VIEWER AND THE OBSERVER HELD AGAINST IT (gunbc#12492: the boot's handoff is admitted -// only while the observer it started is established). The observer is the process -// gunbc.owned_process LaunchOwned starts over gunbc.machine_intake_megarac_kvm_still -// kvm_observer_launch_in's node command. The model starts it the way the launch does -- a live process -// in /proc, its " " record at the pid path -- and writes its journal ONLY through -// kvm_journal_line, the writer the production parser is the inverse of, so the dry observer cannot -// speak a grammar the boot does not read. A line kvm_journal_line refuses is a harness fault, never a -// line written anyway. -// -// What it answers, as the observer script does once the viewer's /kvm socket delivers a frame: the -// connection lines, then established with the quoted host:port the base URL names, the viewer -// session, the attempt and the generation; then, while it lives, one still per trigger file (the -// script's 'trigger:'+name reason) and, on the stop file, stop-requested, the session release, the -// browser close and stopped, after which the process has exited. It takes no periodic still: the -// boot's attempt ends well inside megarac_kvm_observer_cadence's first period in every scenario -// the matrix runs, and a longer one reports the gap through kvm_still_gaps as the real journal would. -// -// A still's bytes are one fixed modeled frame, and its digest is supplied beside it (the precedent is -// gunbc.remote_host_model ModeledRemoteFile.sha256): sha256sum answers that digest for exactly those -// bytes, reports a missing file as sha256sum does, and refuses as a harness fault for any other bytes -// rather than inventing a digest. -type ModeledKvmViewer { - session: String - still_bytes: String - still_sha256: String - observers: List -} - -type ModeledKvmObserver { - pid: Int - dir: String - seq: Int - seen_files: Int -} - -// printf '%s' '' | sha256sum -fn healthy_kvm_viewer() -> ModeledKvmViewer { - ModeledKvmViewer { session: "7", still_bytes: "", still_sha256: "550277e71ddf7d978137c4bd140a9b5cfda25500b365a7375fb91db8790f584c", observers: [] } -} - -fn with_screen(w: MtCollins1BootWorld, screen: ModeledKvmViewer) -> MtCollins1BootWorld { - MtCollins1BootWorld { bmc: w.bmc, fs: w.fs, environment: w.environment, agent: w.agent, remote_hosts: w.remote_hosts, clock: w.clock, worker: w.worker, console: w.console, media: w.media, screen: screen } -} - -data kvm_observer_generation: String = "1" - -// The observer script writes Date.now() and its toISOString(); the model's wall clock is whole seconds. -fn kvm_instant(w: MtCollins1BootWorld, now: Second) -> KvmInstant? { - let unix = wall_clock_unix(clock: w.clock, now: now) - match checked_int_to_nat(n: unix * 1000) { - Present { value: ms } => Present { value: KvmInstant { since_epoch: millisecond(count: ms), iso: concat(join(split(s: iso8601_utc_text(unix: unix), delimiter: "Z"), ""), ".000Z") } } - Absent => none - } -} - -// The journal lines for these events, or the first event kvm_journal_line refuses to write. -type KvmModeledLines - = KvmModeledRendered { text: String } - | KvmModeledRefused { event_word: String, would_write: String } - -fn kvm_lines(events: List) -> KvmModeledLines { - fold(events, init: KvmModeledRendered { text: "" }, f: fn(acc, e) { - match acc { - KvmModeledRendered { text: t } => match kvm_journal_line(event: e) { - KvmJournalRendered { line: l } => KvmModeledRendered { text: concat(t, concat(l, "\n")) } - KvmJournalRenderRefused { event_word: word, would_write: line } => KvmModeledRefused { event_word: word, would_write: line } - } - refused => refused - } - }) -} - -fn operation_input_list(call: OperationCall, name: String) -> List? { - fold(call.invocation.bindings, init: none, f: fn(acc, b) { - match acc { - Present { value: v } => Present { value: v } - Absent => if b.name == name { match b.value { InputTextList { items: xs } => Present { value: xs } InputText { text: _ } => none } } else { none } - } - }) -} - -// kvm_observer_launch_in's operands end the node command: base URL, user, credential path, dir, -// attempt, cadence, frame wait, periodic limit, playwright module, viewer feature-list path. -data kvm_observer_operand_count: Int = 10 - -// THE VIEWER READS ITS FEATURE LIST UNGUARDED (KVM probe run 36788605665): the healthy viewer reaches -// established only when the feature-list operand names a file the launch wrote; without one the vendor's -// viewer throws in its KVM view's initialize() and never opens /kvm, which the observer journals as a -// refusal for want of a frame. -data kvm_observer_features_operand: Int = 9 - -fn kvm_operand(command: List, i: Int) -> String { - match command.skip(n: count(command) - kvm_observer_operand_count + i).first() { Present { value: v } => v Absent => "" } -} - -fn url_host(base_url: String) -> String { - match split(s: base_url, delimiter: "://").skip(n: 1).first() { - Present { value: rest } => match split(s: rest, delimiter: "/").first() { Present { value: h } => h Absent => "" } - Absent => "" - } -} - -// gunbc.owned_process LaunchOwned: starts the command detached, publishes " " to the -// pid path, and exits 0. The observer it starts reaches established at once in the healthy viewer. -fn owned_launch_handler(w: MtCollins1BootWorld, call: OperationCall) -> OperationStep { - match operation_input_text(invocation: call.invocation, name: "pid_path") { - Absent => OperationHarnessFault { reason: "LaunchOwned was dispatched without a pid path" as NonEmptyStr } - Present { value: pid_path } => match operation_input_list(call: call, name: "command") { - Absent => OperationHarnessFault { reason: "LaunchOwned was dispatched without a command" as NonEmptyStr } - Present { value: command } => - if count(command) < kvm_observer_operand_count { OperationHarnessFault { reason: "LaunchOwned's command is not the KVM observer's; the dry worker models no other owned process" as NonEmptyStr } } - else { match kvm_instant(w: w, now: call.now) { - Absent => OperationHarnessFault { reason: "the modeled wall clock reads before the epoch" as NonEmptyStr } - Present { value: at } => { - let dir = kvm_operand(command: command, i: 3) - let p = ModeledProcess { pid: w.worker.next_pid, start_time: start_ticks(w: w, now: call.now), comm: "node", cmdline: join(command, " "), capture_path: "", diagnostic_path: "", alive: true } - let features = kvm_operand(command: command, i: kvm_observer_features_operand) - let seeded = features != "" && match fs_file(fs: w.fs, path: features) { Present { value: _ } => true Absent => false } - let journal = if seeded { - kvm_lines(events: [ - KvmJournalConnectionRequested { at: at }, - KvmJournalConnectionOpen { at: at, gen: kvm_observer_generation }, - KvmJournalEstablished { at: at, host: concat("\"", concat(url_host(base_url: kvm_operand(command: command, i: 0)), "\"")), session: w.screen.session, attempt: kvm_operand(command: command, i: 4), gen: kvm_observer_generation }, - ]) - } else { - kvm_lines(events: [KvmJournalRefused { at: at, refusal: KvmNoFrame { detail: "\"the viewer was opened without its feature list\"" } }]) - } - match journal { - KvmModeledRefused { event_word: word, would_write: line } => OperationHarnessFault { reason: concat("kvm_journal_line refused the modeled ", concat(word, concat(" line: ", line))) as NonEmptyStr } - KvmModeledRendered { text: t } => { - let fs = appended(fs: fs_write(fs: w.fs, path: pid_path, content: process_identity(p: p), create_new: false).fs, path: concat(dir, "/journal"), text: t) - let screen = ModeledKvmViewer { session: w.screen.session, still_bytes: w.screen.still_bytes, still_sha256: w.screen.still_sha256, observers: list_snoc_item(xs: w.screen.observers, item: ModeledKvmObserver { pid: p.pid, dir: dir, seq: 0, seen_files: 0 }) } - exited_step(state: with_screen(w: started(w: w, p: p, fs: fs), screen: screen), stdout: "", exit_code: 0) - } - } - } - } } - } - } -} - -fn kvm_observer_alive(w: MtCollins1BootWorld, o: ModeledKvmObserver) -> Bool { - !all(w.worker.processes, p => !(p.alive && p.pid == o.pid)) -} - -// THE OBSERVER'S LOOP, one pass per advance, in the script's order: the stop file ends it; otherwise -// each trigger file, in name order, is consumed and answered with a still. -fn kvm_observers_advance(w: MtCollins1BootWorld, now: Second) -> MtCollins1BootWorld { - if count(w.screen.observers) == 0 { w } else { - fold(w.screen.observers, init: w, f: fn(acc, o) { if kvm_observer_alive(w: acc, o: o) { kvm_observer_pass(w: acc, o: o, now: now) } else { acc } }) - } -} - -// A stop request and a trigger each arrive as a NEW file in the observer's directory, so a pass whose -// filesystem holds the same number of files as the last pass looked at has nothing to answer: nearly -// every dispatch of a boot is that pass, and it neither scans the files nor takes an instant. A pass -// that does look records the count it left behind. -fn kvm_observer_pass(w: MtCollins1BootWorld, o: ModeledKvmObserver, now: Second) -> MtCollins1BootWorld { - let n = count(w.fs.files) - if n == o.seen_files { w } else { - let answered = kvm_observer_look(w: w, o: o, now: now) - with_kvm_seen(w: answered, pid: o.pid, seen: count(answered.fs.files)) - } -} - -fn with_kvm_seen(w: MtCollins1BootWorld, pid: Int, seen: Int) -> MtCollins1BootWorld { - with_screen(w: w, screen: ModeledKvmViewer { session: w.screen.session, still_bytes: w.screen.still_bytes, still_sha256: w.screen.still_sha256, observers: map(w.screen.observers, o => if o.pid == pid { ModeledKvmObserver { pid: o.pid, dir: o.dir, seq: o.seq, seen_files: seen } } else { o }) }) -} - -// The instant is taken only when a line is written. -fn kvm_observer_look(w: MtCollins1BootWorld, o: ModeledKvmObserver, now: Second) -> MtCollins1BootWorld { - match fs_file(fs: w.fs, path: concat(o.dir, "/stop")) { - Present { value: _ } => match kvm_instant(w: w, now: now) { - Present { value: at } => kvm_observer_stopped(w: w, o: o, at: at) - Absent => w - } - Absent => { - let triggers = kvm_trigger_files(w: w, o: o) - if count(triggers) == 0 { w } else { - match kvm_instant(w: w, now: now) { - Present { value: at } => kvm_observer_answer_triggers(w: w, o: o, at: at, triggers: triggers) - Absent => w - } - } - } - } -} - -fn kvm_journal_appended(w: MtCollins1BootWorld, o: ModeledKvmObserver, events: List) -> MtCollins1BootWorld { - match kvm_lines(events: events) { - KvmModeledRendered { text: t } => with_fs(w: w, fs: appended(fs: w.fs, path: concat(o.dir, "/journal"), text: t)) - KvmModeledRefused { event_word: _, would_write: _ } => w - } -} - -fn kvm_observer_stopped(w: MtCollins1BootWorld, o: ModeledKvmObserver, at: KvmInstant) -> MtCollins1BootWorld { - let journalled = kvm_journal_appended(w: w, o: o, events: [ - KvmJournalStopRequested { at: at, by: "stop-file" }, - KvmJournalSessionRelease { at: at, result: "released" }, - KvmJournalBrowserClose { at: at, result: "closed" }, - KvmJournalStopped { at: at, session: "released", browser: "closed" }, - ]) - exit_processes(w: journalled, ending: fn(p) { p.pid == o.pid }) -} - -fn kvm_trigger_files(w: MtCollins1BootWorld, o: ModeledKvmObserver) -> List { - map(filter(w.fs.files, f => starts_with(s: f.path, prefix: concat(o.dir, "/trigger-"))), f => f.path).sort_by(p => p) -} - -fn kvm_observer_answer_triggers(w: MtCollins1BootWorld, o: ModeledKvmObserver, at: KvmInstant, triggers: List) -> MtCollins1BootWorld { - if count(triggers) == 0 { w } else { - let answered = fold(triggers, init: w, f: fn(acc, path) { - let seq = kvm_observer_seq(w: acc, pid: o.pid) + 1 - let file = concat("still-", concat(to_string(seq), ".jpg")) - let name = join(split(s: path, delimiter: concat(o.dir, "/trigger-")), "") - let consumed = with_fs(w: acc, fs: fs_with_file(fs: fs_without_file(fs: acc.fs, path: path), path: concat(o.dir, concat("/", file)), content: acc.screen.still_bytes)) - with_kvm_seq(w: kvm_journal_appended(w: consumed, o: o, events: [KvmJournalStill { at: at, seq: to_string(seq), reason: concat("trigger:", name), file: file, gen: kvm_observer_generation }]), pid: o.pid, seq: seq) - }) - answered - } -} - -fn kvm_observer_seq(w: MtCollins1BootWorld, pid: Int) -> Int { - fold(w.screen.observers, init: 0, f: fn(acc, o) { if o.pid == pid { o.seq } else { acc } }) -} - -fn with_kvm_seq(w: MtCollins1BootWorld, pid: Int, seq: Int) -> MtCollins1BootWorld { - with_screen(w: w, screen: ModeledKvmViewer { session: w.screen.session, still_bytes: w.screen.still_bytes, still_sha256: w.screen.still_sha256, observers: map(w.screen.observers, o => if o.pid == pid { ModeledKvmObserver { pid: o.pid, dir: o.dir, seq: seq, seen_files: o.seen_files } } else { o }) }) -} - -data owned_process_launch_operation: OperationRef = OperationRef { - path: "dag/gunbc/owned_process.dag", - service: "gunbc.owned_process.launch", - operation: "LaunchOwned", -} - -data sha256sum_digest_file_operation: OperationRef = OperationRef { - path: "dag/extdeps/tools/sha256sum.dag", - service: "sha256sum.Sha256", - operation: "DigestFile", -} - -// sha256sum(1): " " and exit 0; a missing file is "No such file or directory", exit 1. -fn digest_file_handler(w: MtCollins1BootWorld, call: OperationCall) -> OperationStep { - match operation_input_text(invocation: call.invocation, name: "path") { - Absent => OperationHarnessFault { reason: "DigestFile was dispatched without a path" as NonEmptyStr } - Present { value: path } => match fs_file(fs: w.fs, path: path) { - Absent => OperationObserved { observation: ShellObserved { observation: ShellProcessExited { exit_code: 1, stdout: "", stderr: concat("sha256sum: ", concat(path, ": No such file or directory\n")) } }, state: w, elapsed: second(count: 0) } - Present { value: f } => - if f.content == w.screen.still_bytes { exited_step(state: w, stdout: concat(w.screen.still_sha256, concat(" ", concat(path, "\n"))), exit_code: 0) } - else { OperationHarnessFault { reason: concat("no digest is modeled for the bytes of ", path) as NonEmptyStr } } - } - } + console_advance(w: collectors_follow_session(w: stepped), now: now) } data sol_hold_activate_held_operation: OperationRef = OperationRef { @@ -712,8 +450,6 @@ fn mtcollins1_boot_dry_realization(identity: NonEmptyStr, initial: MtCollins1Boo OperationBinding { at: sol_hold_release_held_operation, handler: sol_release_held_handler }, OperationBinding { at: linux_procfs_read_uptime_operation, handler: uptime_handler }, OperationBinding { at: os_hostname_read_short_operation, handler: hostname_handler }, - OperationBinding { at: owned_process_launch_operation, handler: owned_launch_handler }, - OperationBinding { at: sha256sum_digest_file_operation, handler: digest_file_handler }, ] OperationRealization { identity: identity, diff --git a/dag/gunbc/machine_intake/mtcollins1_boot_run.dag b/dag/gunbc/machine_intake/mtcollins1_boot_run.dag index 1348fceb4f2..b6d3a2e0c2d 100644 --- a/dag/gunbc/machine_intake/mtcollins1_boot_run.dag +++ b/dag/gunbc/machine_intake/mtcollins1_boot_run.dag @@ -20,7 +20,8 @@ import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } import gunbc.machine_intake_mtcollins1_census_medium_readback { mtcollins1_census_medium_readback, mtcollins1_census_readback_refusal_before_filing, mtcollins1_census_readback_refusal_before_attach, } -import gunbc.boot_medium { BootMedium, BootMediumNoTerminal, BootMediumVolumeLabel, bmc_boot_image_export } +import gunbc.boot_medium { BootMedium, BootMediumNoTerminal, BootMediumVolumeLabel, BootMediumRunnerHostUp, bmc_boot_image_export } +import gunbc.runner.runner_host_medium { RunnerHostUp, RunnerHostUpRefusal, RunnerHostUpPending, RunnerHostUpRefused, RunnerHostUpObserved, runner_host_up_readback, runner_host_up_refusal_text } import gunbc.machine_intake_mtcollins1_census_image { mtcollins1_boot_medium, mtcollins1_census_image_stem } import gunbc.managed_host_boot { AdmittedBootSubject, BootSubjectAdmitted, BootSubjectRefused, @@ -46,7 +47,7 @@ import gunbc.managed_host_boot_phase_timing { BootPhaseRunResult, PhaseVerified, PhaseRefused, BootPhaseStep, PhaseStepRan, PhaseStepSkipped, BootRoute, boot_phase_timing_of, boot_route_sol_refused, boot_smpro_pass_result, } -import extdeps.linux.proc_uptime { proc_uptime_read, UptimeObserved, UptimeRefused } +import extdeps.linux.proc_uptime { ProcUptimeRead, proc_uptime_read, UptimeObserved, UptimeRefused } import gunbc.machine_intake_oob_boot_handoff { oob_power_readback_allowance } import gunbc.machine_intake_mtcollins1_boot_milestone { mtcollins1_boot_topology_expectation } import gunbc.managed_host_boot_milestone { @@ -89,8 +90,10 @@ import gunbc.machine_intake_ampere_smpro_observation { import gunbc.machine_intake_mtcollins1_smpro_observation { mtcollins1_smpro_site } import gunbc.fleet_host_identity { operator_host_mtcollins1 } import gunbc.managed_host { - managed_hosts, managed_host_binding, managed_host_smpro_subject, + ManagedHost, + managed_host_binding, managed_host_smpro_subject, managed_membership_for, ManagedHostBound, ManagedHostAccessUnobserved, ManagedHostNotSecured, ManagedHostEndpointMismatch, + OrdinaryMember, MemberUnderDeclaredDrop, } import extdeps.bmc.ipmi_boot_selection { IpmiBootSelection } import extdeps.bmc.ipmi_chassis_control { ChassisPowerObservation, chassis_observation_text } @@ -119,7 +122,7 @@ import gunbc.owned_process { owned_process_record, OwnedRecorded, OwnedUnrecorde import gunbc.runner_browser_toolchain { runner_browser_toolchain_here_wet, RunnerBrowserToolchainResolution } import gunbc.machine_intake_megarac_kvm_still { KvmObserverStart, KvmObserverStarted, KvmObserverNotStarted, KvmObserverRecord, KvmJournalEvent, - KvmHandoffAdmitted, KvmHandoffRefused, kvm_handoff_admission, kvm_observer_standing_now, kvm_standing_text, + kvm_observer_standing_now, kvm_standing_text, KvmObserverStanding, KvmObserverProcessNotRunning, KvmObserverJournalUnread, KvmObserverStopped, KvmObserverEstablished, KvmObserverLost, KvmObserverMismatch, KvmObserverRefused, KvmObserverFailed, KvmObserverPending, megarac_kvm_observer_start, megarac_kvm_observer_finish, kvm_observer_trigger, kvm_observer_record_not_started, @@ -845,6 +848,7 @@ fn mtcollins1_boot_medium_refusal(medium: BootMedium) -> String? { BootMediumNoTerminal { reason: r } => Present { value: concat("mtcollins1 boot: the boot medium ", medium.image_name as String, " prints no terminal (", r as String, "), so no census terminal can be observed; switch mtcollins1_boot_medium to the published census image (census stage 0D) -- controller untouched") } BootMediumVolumeLabel { label: _ } => none + BootMediumRunnerHostUp { label: _ } => none } } @@ -865,6 +869,7 @@ fn mtcollins1_boot_medium_refusal(medium: BootMedium) -> String? { type MtCollins1BootTerminalCause = TerminalMediumPrintsNoTerminal { image: NonEmptyStr, reason: NonEmptyStr } | TerminalCaptureUnbound { reason: String } + | TerminalRunnerHostNotUp { refusals: List } | TerminalSectionAbsent { name: String } | TerminalSectionUnterminated { name: String } | TerminalSectionUnobservedAtRefusal { name: String } @@ -887,6 +892,7 @@ type MtCollins1BootTerminalVerdict = TerminalPending { reason: String } | TerminalRefused { causes: List } | TerminalAccepted { run: HostCaptureRunIdentity, threads: HardwareThreadCount } + | TerminalRunnerHostAccepted { up: RunnerHostUp } // ONE QUALIFICATION READ AS ONE OF THREE: still being written, refused with a cause, or held. type TerminalQualification @@ -1281,6 +1287,11 @@ fn mtcollins1_boot_terminal_verdict(capture: String, subject: AdmittedBootSubjec HostCaptureOpen { envelope: envelope } => terminal_over_open(envelope: envelope, topology: topology, label: label) HostCaptureBound { envelope: envelope } => terminal_over_closed(envelope: envelope, topology: topology, label: label) } + BootMediumRunnerHostUp { label: _ } => match runner_host_up_readback(capture: capture, subject: subject) { + RunnerHostUpPending { reason: r } => TerminalPending { reason: r } + RunnerHostUpRefused { refusals: rs } => TerminalRefused { causes: [TerminalRunnerHostNotUp { refusals: rs }] } + RunnerHostUpObserved { up: up } => TerminalRunnerHostAccepted { up: up } + } } } @@ -1289,6 +1300,7 @@ fn mtcollins1_boot_terminal_cause_text(cause: MtCollins1BootTerminalCause) -> St TerminalMediumPrintsNoTerminal { image: i, reason: r } => concat("the admitted medium ", i as String, " prints no terminal: ", r as String) TerminalCaptureUnbound { reason: r } => concat("capture does not bind: ", r) + TerminalRunnerHostNotUp { refusals: rs } => concat("runner host not up: ", join(map(rs, x => runner_host_up_refusal_text(refusal: x)), "; ")) TerminalSectionAbsent { name: n } => concat("closed envelope carries no ", n, " section") TerminalSectionUnterminated { name: n } => concat("closed envelope's ", n, " section never printed its EXIT line") TerminalSectionUnobservedAtRefusal { name: n } => concat(n, " not yet observed when the open envelope refused; unqualified") @@ -1438,6 +1450,7 @@ fn sol_client_diagnostics_text(client: SolClientDiagnostics) -> String { // collector is not "still waiting", it is ObservationChannelLost. type WatchTick = WatchAccepted + | WatchRunnerHostAccepted { up: RunnerHostUp } | WatchRefused { causes: List } | WatchChannelLost { collector: SolCollectorObservation, client: SolClientDiagnostics, last: String } | WatchContinue { reason: String } @@ -1450,6 +1463,7 @@ fn watch_tick(verdict: MtCollins1BootTerminalVerdict, channel: SolChannelReading SolChannelNotLive { collector: l, client: c } => WatchChannelLost { collector: l, client: c, last: why } } TerminalAccepted { run: _, threads: _ } => WatchAccepted + TerminalRunnerHostAccepted { up: up } => WatchRunnerHostAccepted { up: up } TerminalRefused { causes: cs } => WatchRefused { causes: cs } } } @@ -1649,6 +1663,24 @@ fn mtcollins1_sol_supervised(stage: SolLossStage, pid_path: String, capture_path } } +// THE HANDOFF'S REQUIRED OBSERVER IS THE SOL COLLECTOR OF THIS RUN (gunbc.rung_drop +// handoff_observer_is_sol_not_kvm_viewer, operator decision msg_bb46c20f over gunbc#12492). The handoff is +// admitted only over the SolHeldAt that mtcollins1_sol_supervised returned for THIS attempt's collector +// (its pid and capture paths are the attempt's), and with no SOL observation the handoff refuses: this is +// a typed admission over a run-bound observation, never an open gate. The KVM viewer that #12492 also +// required is not started in public (megarac_kvm_observer_start answers NotStarted), so it is no longer +// a precondition. +type SolHandoffAdmission + = SolHandoffAdmitted { held_at: NonEmptyStr } + | SolHandoffRefused { cause: String } + +fn sol_handoff_admission(sol: SolSupervision) -> SolHandoffAdmission { + match sol { + SolHeldAt { at: a } => SolHandoffAdmitted { held_at: a } + SolLostAt { outcome: o } => SolHandoffRefused { cause: sol_supervision_refusal_reason(outcome: o) as String } + } +} + // A supervision refusal's reason, as the media record's non-empty reason. fn sol_supervision_refusal_reason(outcome: ProcessExit) -> NonEmptyStr { match outcome { @@ -1767,8 +1799,9 @@ fn mtcollins1_boot_await_capture_terminal( let reported = mtcollins1_boot_screen_boundary_report(screen: screen, already: screen_reported) let ended = match seen.phase { CensusEnded => true _ => false } match watch_tick(verdict: seen.verdict, channel: seen.channel) { - WatchAccepted => TerminalWatch { outcome: ExitSuccess, census_ended: ended, smpro_census: smpro_census, screen_reported: reported } - WatchRefused { causes: causes } => TerminalWatch { outcome: exit_failure(reason: mtcollins1_boot_terminal_refusal_text(causes: causes)), census_ended: ended, smpro_census: smpro_census, screen_reported: reported } + WatchAccepted => TerminalWatch { outcome: ExitSuccess, census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: none } + WatchRunnerHostAccepted { up: up } => TerminalWatch { outcome: ExitSuccess, census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: Present { value: up } } + WatchRefused { causes: causes } => TerminalWatch { outcome: exit_failure(reason: mtcollins1_boot_terminal_refusal_text(causes: causes)), census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: none } WatchChannelLost { collector: l, client: c, last: why } => { let lost = mtcollins1_boot_channel_lost( loss: SolObservationLoss { stage: SolLostDuringWatch, last_live_at: last_live_at, observed_gone_at: seen.at, collector: l, client: c, last_capture_observation: why }, @@ -1776,15 +1809,15 @@ fn mtcollins1_boot_await_capture_terminal( password_file: password_file, screen: Present { value: screen }, ) - TerminalWatch { outcome: exit_failure(reason: observation_channel_lost_text(lost: lost)), census_ended: ended, smpro_census: smpro_census, screen_reported: reported } + TerminalWatch { outcome: exit_failure(reason: observation_channel_lost_text(lost: lost)), census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: none } } WatchContinue { reason: why } => { match mtcollins1_sol_observer_refusal(capture_path: capture_path) { - Present { value: gone } => return TerminalWatch { outcome: exit_failure(reason: concat(gone, "; observed during the capture watch, so the watch stops rather than continuing unreported")), census_ended: ended, smpro_census: smpro_census, screen_reported: reported } + Present { value: gone } => return TerminalWatch { outcome: exit_failure(reason: concat(gone, "; observed during the capture watch, so the watch stops rather than continuing unreported")), census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: none } _ => {} } if remaining <= 0 { - TerminalWatch { outcome: exit_failure(reason: concat("mtcollins1 boot: deadline reached without a host-capture terminal; last observation: ", why, "; capture preserved; claim will abort, not complete")), census_ended: ended, smpro_census: smpro_census, screen_reported: reported } + TerminalWatch { outcome: exit_failure(reason: concat("mtcollins1 boot: deadline reached without a host-capture terminal; last observation: ", why, "; capture preserved; claim will abort, not complete")), census_ended: ended, smpro_census: smpro_census, screen_reported: reported, runner_host: none } } else { let census_pass = match smpro_census { SmproNotTaken { reason: _ } => match seen.phase { @@ -1829,11 +1862,15 @@ fn mtcollins1_census_phase(capture: String, subject: AdmittedBootSubject) -> Cen } } +// runner_host IS THE ONE ACCEPTED RUNNER-HOST READING (gunbc.runner.runner_host_medium RunnerHostUp), +// carried whole so the qualification receipt's GuestIdleMeminfoAndNproc reads it here rather than +// re-parsing the capture; none for every other medium and every non-accepting end. type TerminalWatch { outcome: ProcessExit census_ended: Bool smpro_census: SmproReading screen_reported: List + runner_host: RunnerHostUp? } // THE SCREEN'S BOUNDARY, REPORTED IN THE TICK AND BEFORE ANY OPTIONAL READ (acceptance (b)). Every @@ -1880,12 +1917,8 @@ fn mtcollins1_boot_screen_liveness_lines(standing: KvmObserverStanding) -> List< // the boot parameter 5 read and before the attach, so the phase clock counts its bounded wait // (megarac_kvm_observer_establish_wait) inside media_attach. Bound to the BMC host, the web session // it logs in with (the established line carries its id) and the attempt. The toolchain is resolved, -// never assumed (gunbc.runner_browser_toolchain). FOR THIS DIAGNOSTIC PROFILE THE OBSERVER IS -// REQUIRED: mtcollins1_boot_actuate re-reads it through kvm_handoff_admission immediately before the -// handoff, and issues no handoff unless it is established for this BMC host and this attempt -- a -// runner without the browser toolchain, a busy viewer seat, a mismatched subject or a lost connection -// all stop the boot there, with that standing as the cause. It gates on the witness existing, never on -// what the screen shows. +// never assumed (gunbc.runner_browser_toolchain). // NOT REQUIRED IN PUBLIC: no public KVM client exists, so this answers NotStarted with its typed reason +// and the record carries it; the handoff's required observer is the SOL collector (sol_handoff_admission). fn mtcollins1_boot_screen_start(proof: UnitHoldProof, password_file: String, attempt: String, resolve_toolchain: fn() -> RunnerBrowserToolchainResolution) -> KvmObserverStart { match admit_megarac_host(host: proof.subject.host, operation: MegaRacKvmCanvasStill) { MegaRacHostRefused { cause: c } => KvmObserverNotStarted { reason: megarac_host_refusal_text(cause: c) } @@ -1903,22 +1936,31 @@ fn mtcollins1_boot_screen_start(proof: UnitHoldProof, password_file: String, att // THE PASS READS THROUGH THE MANAGED HOST'S SUBJECT (gunbc.managed_host managed_host_smpro_subject): // the row's bound controller joined to its SMpro site, so the reads go to the controller the site is // evidence about or are not taken, with the refusal as the reason. +fn mtcollins1_boot_smpro_from_row(h: ManagedHost, username: NonEmptyStr, password_file: NonEmptyStr) -> SmproReading { + match managed_host_binding(host: h) { + ManagedHostAccessUnobserved { endpoint: _ } => SmproNotTaken { reason: "mtcollins1's controller access is unobserved, so no SMpro subject binds" } + ManagedHostNotSecured { cause: _ } => SmproNotTaken { reason: "mtcollins1's controller is not secured, so no SMpro subject binds" } + ManagedHostEndpointMismatch { observed: _, secured: _ } => SmproNotTaken { reason: "mtcollins1's access and secure standings name different controllers, so no SMpro subject binds" } + ManagedHostBound { binding: b } => { + let subject = managed_host_smpro_subject(binding: b) + match subject { + SmproPassSubjectBound { subject: s } => SmproProbed { observation: smpro_pass_probes(subject: s, username: username, password_file: password_file, deadline: mtcollins1_boot_ipmi_read_deadline, kill_after: mtcollins1_boot_ipmi_kill_after) } + SmproPassSiteRefused { cause: _ } => SmproNotTaken { reason: match smpro_pass_subject_refusal_text(s: subject) { Present { value: t } => t as NonEmptyStr Absent => "the SMpro site is not admitted" } } + SmproPassEndpointMismatch { bound: _, site: _ } => SmproNotTaken { reason: match smpro_pass_subject_refusal_text(s: subject) { Present { value: t } => t as NonEmptyStr Absent => "the SMpro site names another controller" } } + } + } + } +} + fn mtcollins1_boot_smpro_read(username: NonEmptyStr, password_file: NonEmptyStr) -> SmproReading { - match filter(managed_hosts(), h => (h.host as String) == (operator_host_mtcollins1 as String)).first() { + match managed_membership_for(host: operator_host_mtcollins1) { Absent => SmproNotTaken { reason: "mtcollins1 is not a managed host row: its secure standing is not derived" } - Present { value: h } => match managed_host_binding(host: h) { - ManagedHostAccessUnobserved { endpoint: _ } => SmproNotTaken { reason: "mtcollins1's controller access is unobserved, so no SMpro subject binds" } - ManagedHostNotSecured { cause: _ } => SmproNotTaken { reason: "mtcollins1's controller is not secured, so no SMpro subject binds" } - ManagedHostEndpointMismatch { observed: _, secured: _ } => SmproNotTaken { reason: "mtcollins1's access and secure standings name different controllers, so no SMpro subject binds" } - ManagedHostBound { binding: b } => { - let subject = managed_host_smpro_subject(binding: b) - match subject { - SmproPassSubjectBound { subject: s } => SmproProbed { observation: smpro_pass_probes(subject: s, username: username, password_file: password_file, deadline: mtcollins1_boot_ipmi_read_deadline, kill_after: mtcollins1_boot_ipmi_kill_after) } - SmproPassSiteRefused { cause: _ } => SmproNotTaken { reason: match smpro_pass_subject_refusal_text(s: subject) { Present { value: t } => t as NonEmptyStr Absent => "the SMpro site is not admitted" } } - SmproPassEndpointMismatch { bound: _, site: _ } => SmproNotTaken { reason: match smpro_pass_subject_refusal_text(s: subject) { Present { value: t } => t as NonEmptyStr Absent => "the SMpro site names another controller" } } - } + Present { value: m } => + match m { + OrdinaryMember { host: h } => mtcollins1_boot_smpro_from_row(h: h, username: username, password_file: password_file) + MemberUnderDeclaredDrop { drop: _, standing_subject: _, current_subject: _, host: h } => + mtcollins1_boot_smpro_from_row(h: h, username: username, password_file: password_file) } - } } } @@ -1986,6 +2028,7 @@ type MtCollins1BootActuation { power_after: PowerAfterAttempt timing: BootPhaseTiming screen: KvmObserverRecord + runner_host: RunnerHostUp? } fn actuation_not_powered_on(outcome: ProcessExit, reason: NonEmptyStr) -> MtCollins1BootActuation { @@ -2000,6 +2043,7 @@ fn actuation_not_powered_on(outcome: ProcessExit, reason: NonEmptyStr) -> MtColl power_after: PowerAfterNotTaken { reason: reason }, screen: kvm_observer_record_not_started(attempt: "", reason: reason), timing: boot_phase_timing_not_taken(reason: reason), + runner_host: none, } } @@ -2049,6 +2093,71 @@ type ActuationWatch { stage: MtCollins1BootActuationStage smpro_step: BootPhaseStep terminal_step: BootPhaseStep + runner_host: RunnerHostUp? +} + +// THE ACTUATION AFTER THE WATCH, PURE: the watch's stage and its one accepted runner-host reading are +// taken from the watch itself, so no caller can assemble an actuation that drops the reading the +// terminal accepted. mtcollins1_boot_actuate builds its result only through this. +fn actuation_after_watch( + observed: ActuationWatch, + media: MtCollins1MediaAttachRecord, + handoff_media: MtCollins1HandoffMedia, + end_media: MtCollins1EndMedia, + override_before: BootParam5Reading, + override_after: BootParam5Reading, + power_after: PowerAfterAttempt, + outcome: ProcessExit, + screen: KvmObserverRecord, + timing: BootPhaseTiming, +) -> MtCollins1BootActuation { + MtCollins1BootActuation { + outcome: outcome, + media: media, + handoff_media: handoff_media, + end_media: end_media, + stage: observed.stage, + override_before: override_before, + override_after: override_after, + power_after: power_after, + timing: timing, + screen: screen, + runner_host: observed.runner_host, + } +} + +// THE SAME ACTUATION UNDER A NEW OUTCOME (the unit hold's release), every observation carried. +fn actuation_with_outcome(act: MtCollins1BootActuation, outcome: ProcessExit) -> MtCollins1BootActuation { + MtCollins1BootActuation { + outcome: outcome, + media: act.media, + handoff_media: act.handoff_media, + end_media: act.end_media, + stage: act.stage, + override_before: act.override_before, + override_after: act.override_after, + power_after: act.power_after, + timing: act.timing, + screen: act.screen, + runner_host: act.runner_host, + } +} + +// THE WATCH'S RESULT AS THE ACTUATION SEES IT, PURE: the terminal watch's outcome, stage and the one +// accepted runner-host reading pass through unchanged, so the reading the terminal accepted is the +// reading the attempt result carries (none on every watch that did not accept a runner host). +fn actuation_watch_of(watch: TerminalWatch, selection: IpmiBootSelection, at_power_on: SmproReading, smpro_read: ProcUptimeRead, terminal_read: ProcUptimeRead) -> ActuationWatch { + ActuationWatch { + outcome: watch.outcome, + stage: if watch.census_ended { + ActuationCensusEnded { selection: selection, smpro_power_on: at_power_on, smpro_census: watch.smpro_census } + } else { + ActuationPoweredOn { selection: selection, smpro_power_on: at_power_on, smpro_census: watch.smpro_census } + }, + smpro_step: PhaseStepRan { result: smpro_phase_result(reading: at_power_on), end: smpro_read }, + terminal_step: PhaseStepRan { result: exit_phase_result(outcome: watch.outcome), end: terminal_read }, + runner_host: watch.runner_host, + } } // THE HANDOFF'S RESULT AND ITS ROUTE STEP, made in the one branch that issued (or skipped) it. @@ -2344,6 +2453,7 @@ fn mtcollins1_boot_actuate( start: BootStartUnobserved { reason: "the SOL lease was not acquired, so nothing was written" }, route: boot_route_sol_refused(refused_at: refused_at, cause: r), ), + runner_host: none, } } ExitSuccess => { @@ -2379,13 +2489,13 @@ fn mtcollins1_boot_actuate( MediaGateWithheld { reason: why } => exit_failure(reason: why as String) } let attach_verified = proc_uptime_read() - let gated = match kvm_handoff_admission(start: screen) { - KvmHandoffRefused { cause: why } => MediaGatedHandoff { - result: boot_handoff_not_issued(outcome: exit_failure(reason: concat("mtcollins1 boot: handoff refused: the required KVM observer was not established: ", why))), - media: HandoffMediaNotReached { reason: concat("the required KVM observer was not established: ", why) as NonEmptyStr }, + let gated = match sol_handoff_admission(sol: before_attach) { + SolHandoffRefused { cause: why } => MediaGatedHandoff { + result: boot_handoff_not_issued(outcome: exit_failure(reason: concat("mtcollins1 boot: handoff refused: no SOL observation of this run holds: ", why))), + media: HandoffMediaNotReached { reason: concat("no SOL observation of this run holds: ", why) as NonEmptyStr }, live_at: attach_live_at, } - KvmHandoffAdmitted { session: _ } => mtcollins1_boot_handoff_when_media_ready( + SolHandoffAdmitted { held_at: _ } => mtcollins1_boot_handoff_when_media_ready( proof: proof, media: media, subject: subject, @@ -2421,22 +2531,14 @@ fn mtcollins1_boot_actuate( screen_reported: [], ) let terminal_read = proc_uptime_read() - ActuationWatch { - outcome: watch.outcome, - stage: if watch.census_ended { - ActuationCensusEnded { selection: subject.selection, smpro_power_on: at_power_on, smpro_census: watch.smpro_census } - } else { - ActuationPoweredOn { selection: subject.selection, smpro_power_on: at_power_on, smpro_census: watch.smpro_census } - }, - smpro_step: PhaseStepRan { result: smpro_phase_result(reading: at_power_on), end: smpro_read }, - terminal_step: PhaseStepRan { result: exit_phase_result(outcome: watch.outcome), end: terminal_read }, - } + actuation_watch_of(watch: watch, selection: subject.selection, at_power_on: at_power_on, smpro_read: smpro_read, terminal_read: terminal_read) } other => ActuationWatch { outcome: other, stage: ActuationNotPoweredOn { reason: "the SOL supervision, the attach or the handoff refused, so power on was never observed" }, smpro_step: PhaseStepSkipped { reason: "power on was never verified" }, terminal_step: PhaseStepSkipped { reason: "power on was never verified" }, + runner_host: none, } } let screen_record = megarac_kvm_observer_finish(start: screen, attempt: attempt, terminal_order: 3) @@ -2455,7 +2557,8 @@ fn mtcollins1_boot_actuate( let power_after_read = proc_uptime_read() let teardown = mtcollins1_boot_release_sol(proof: proof, password_file: password_file, pid_path: sol_pid_path, capture_path: sol_capture_path) let released = proc_uptime_read() - MtCollins1BootActuation { + actuation_after_watch( + observed: observed, media: media, handoff_media: gated.media, end_media: end_media, @@ -2467,7 +2570,6 @@ fn mtcollins1_boot_actuate( if sol_release_is_clean(outcome: teardown) { ExitSuccess } else { exit_failure(reason: concat("mtcollins1 boot: ", sol_release_outcome_text(outcome: teardown))) } other => outcome_attributing_media_loss(outcome: other, loss: media_loss_after_handoff(handoff: gated.media, end: end_media)) }, - stage: observed.stage, screen: screen_record, timing: boot_phase_timing_of( hold: hold_acquired, @@ -2484,7 +2586,7 @@ fn mtcollins1_boot_actuate( sol_release: PhaseStepRan { result: sol_release_phase_result(t: teardown), end: released }, }, ), - } + ) } } } @@ -2593,7 +2695,7 @@ fn mtcollins1_boot_actuate_held( sol_capture_path: sol_capture_path, resolve_toolchain: resolve_toolchain, ) - MtCollins1BootActuation { outcome: unit_hold_release(proof: proof, outcome: act.outcome), media: act.media, handoff_media: act.handoff_media, end_media: act.end_media, stage: act.stage, override_before: act.override_before, override_after: act.override_after, power_after: act.power_after, timing: act.timing, screen: act.screen } + actuation_with_outcome(act: act, outcome: unit_hold_release(proof: proof, outcome: act.outcome)) } // EVERY TERMINAL ARM OF A BOOT ATTEMPT GOES THROUGH mtcollins1_boot_conclude (gunbc#12093), which @@ -2623,6 +2725,7 @@ type MtCollins1BootAttempt { power_after: PowerAfterAttempt timing: BootPhaseTiming screen: KvmObserverRecord + runner_host: RunnerHostUp? configuration: AttemptConfigurationReceipt } @@ -2633,6 +2736,26 @@ fn mtcollins1_boot_exit_reason(outcome: ProcessExit) -> String { } } +// THE ACTUATION AS THE ATTEMPT RESULT, PURE: every field the actuation observed, the runner-host +// reading among them, beside the BMC access the attempt held. +fn mtcollins1_boot_attempt_of(act: MtCollins1BootActuation, bmc: MtCollins1BootBmcAccess, configuration: AttemptConfigurationReceipt) -> MtCollins1BootAttempt { + MtCollins1BootAttempt { + outcome: act.outcome, + media: act.media, + handoff_media: act.handoff_media, + end_media: act.end_media, + bmc: bmc, + stage: act.stage, + override_before: act.override_before, + override_after: act.override_after, + power_after: act.power_after, + timing: act.timing, + screen: act.screen, + runner_host: act.runner_host, + configuration: configuration, + } +} + fn mtcollins1_boot_ended_before_bmc(outcome: ProcessExit) -> MtCollins1BootAttempt { MtCollins1BootAttempt { outcome: outcome, @@ -2646,6 +2769,7 @@ fn mtcollins1_boot_ended_before_bmc(outcome: ProcessExit) -> MtCollins1BootAttem power_after: PowerAfterNotTaken { reason: "the attempt ended before the unit hold" }, timing: boot_phase_timing_not_taken(reason: "the attempt ended before the unit hold"), screen: kvm_observer_record_not_started(attempt: "", reason: "the attempt ended before the unit hold"), + runner_host: none, configuration: attempt_configuration_not_recorded(host: operator_host_mtcollins1 as NonEmptyStr, attempt: "not reached", reason: "the attempt ended before the unit hold, so no attempt receipt was admitted"), } } @@ -2861,38 +2985,12 @@ fn mtcollins1_boot_on_srv1_resolving_toolchain(resolve_toolchain: fn() -> Runner Absent => { let held = mtcollins1_boot_under_live_unit_hold(run_id: run_id as NonEmptyStr, subject: subject, password_file: cred, sol_pid_path: sol_pid_path, sol_capture_path: sol_capture_path, resolve_toolchain: resolve_toolchain, baseline: fn() { baseline_not_taken(reason: "mtcollins1's BMC is not BmcSecured, so no account is modeled to read it with") }) let act = held.actuation - MtCollins1BootAttempt { - outcome: act.outcome, - media: act.media, - handoff_media: act.handoff_media, - end_media: act.end_media, - bmc: BmcAccessNotReached { reason: "mtcollins1's BMC is not BmcSecured, so no account is modeled to read it with" }, - stage: act.stage, - override_before: act.override_before, - override_after: act.override_after, - power_after: act.power_after, - timing: act.timing, - screen: act.screen, - configuration: held.configuration, - } + mtcollins1_boot_attempt_of(act: act, bmc: BmcAccessNotReached { reason: "mtcollins1's BMC is not BmcSecured, so no account is modeled to read it with" }, configuration: held.configuration) } Present { value: user } => { let held = mtcollins1_boot_under_live_unit_hold(run_id: run_id as NonEmptyStr, subject: subject, password_file: cred, sol_pid_path: sol_pid_path, sol_capture_path: sol_capture_path, resolve_toolchain: resolve_toolchain, baseline: fn() { mtcollins1_boot_baseline(username: user, password_file: cred as NonEmptyStr) }) let act = held.actuation - MtCollins1BootAttempt { - outcome: act.outcome, - media: act.media, - handoff_media: act.handoff_media, - end_media: act.end_media, - bmc: BmcAccessHeld { username: user, password_file: cred as NonEmptyStr, password: pw_read.content, sdr_cache: held.baseline.sdr, sel_before: held.baseline.sel_before }, - stage: act.stage, - override_before: act.override_before, - override_after: act.override_after, - power_after: act.power_after, - timing: act.timing, - screen: act.screen, - configuration: held.configuration, - } + mtcollins1_boot_attempt_of(act: act, bmc: BmcAccessHeld { username: user, password_file: cred as NonEmptyStr, password: pw_read.content, sdr_cache: held.baseline.sdr, sel_before: held.baseline.sel_before }, configuration: held.configuration) } } } diff --git a/dag/gunbc/machine_intake/mtcollins1_census_image.dag b/dag/gunbc/machine_intake/mtcollins1_census_image.dag index f3d56548f12..d8133e6c26f 100644 --- a/dag/gunbc/machine_intake/mtcollins1_census_image.dag +++ b/dag/gunbc/machine_intake/mtcollins1_census_image.dag @@ -16,7 +16,7 @@ import extdeps.os.ubuntu_autoinstall { UbuntuAutoinstallLiveCommands } import gunbc.os_install_emit { autoinstall_live_commands_user_data, AutoinstallUserData, AutoinstallUserDataRendered, AutoinstallUserDataRefused } import gunbc.seeded_install_media_policy { seeded_install_media_pinned_dates, seeded_install_media_builder_revision } import std.optional { Present } -import gunbc.boot_medium { BootImageExport, BootMedium, BootMediumNoTerminal, BootMediumVolumeLabel, bmc_boot_image_export, boot_image_export_eq, stock_installer_boot_medium } +import gunbc.boot_medium { BootImageExport, BootMedium, BootMediumNoTerminal, BootMediumVolumeLabel, BootMediumRunnerHostUp, bmc_boot_image_export, boot_image_export_eq, stock_installer_boot_medium } import gunbc.machine_intake_boot_image_fetch { boot_image_published_path } import gunbc.machine_intake_host_capture_envelope { HostCaptureStage, @@ -167,6 +167,7 @@ fn mtcollins1_seeded_image_input_with_cmdline( content: mtcollins1_census_capture_program(stages: stages), }, ], + seed_artifacts: [], grub_kernel_cmdline: grub_kernel_cmdline, volume_id: volume_id, pinned_dates: seeded_install_media_pinned_dates, @@ -255,6 +256,7 @@ fn mtcollins1_census_medium_qualification(medium: BootMedium) -> CensusMediumQua let labelled = match medium.terminal { BootMediumVolumeLabel { label: l } => (l as String) == (mtcollins1_census_volume_id as String) BootMediumNoTerminal { reason: _ } => false + BootMediumRunnerHostUp { label: _ } => false } if !(labelled && (medium.image_name as String) == (census.image_name as String)) { CensusMediumNotCensus diff --git a/dag/gunbc/machine_intake/mtcollins1_managed_host_admission.dag b/dag/gunbc/machine_intake/mtcollins1_managed_host_admission.dag new file mode 100644 index 00000000000..0c29ccff7c0 --- /dev/null +++ b/dag/gunbc/machine_intake/mtcollins1_managed_host_admission.dag @@ -0,0 +1,71 @@ +module gunbc.machine_intake_mtcollins1_managed_host_admission + +import std.types { List, NonEmptyStr } +import extdeps.bmc.capability { BmcFirmwareReleaseIdentity } +import gunbc.machine_intake_subject { + FirmwareEntry, + FirmwareManifest, + MachineIntakeSubject, + qualification_subject_of, +} +import gunbc.machine_intake_mtcollins1_access_observation { mtcollins1_endpoint } +import gunbc.machine_intake_mtcollins1_bmc_secure_observation { + mtcollins1_assembly, + mtcollins1_intake_subject, +} +import gunbc.megarac_operation_standing { + CurrentBuildKnownUnread, + CurrentBuildRead, + CurrentBuildUnobserved, + megarac_current_build, +} +import gunbc.machine_intake_megarac_operation_evidence { mtcollins1_megarac_operation_evidence } + +// THE UNIT'S CURRENT INTAKE SUBJECT, FROM COMMITTED BUILD EVIDENCE, NEVER INFERRED. +// gunbc.megarac_operation_standing megarac_current_build reads the committed readings and the +// recorded 2026-10-02 reflash. A later reading of 0.45.3 supersedes the transition's `to` the +// moment it lands. Unobserved current firmware yields Absent: the admission projection then +// keeps the unit out rather than guessing a subject. +// +// This is not a ManagedHostAdmission record. That record is assembled in gunbc.managed_host from +// this subject, the frozen BmcSecureStanding, and the unit's other arrival observations, so this +// module does not import the population type (no cycle, and no second roster). + +fn bmc_firmware_manifest(firmware: BmcFirmwareReleaseIdentity) -> FirmwareManifest { + FirmwareManifest { + entries: [FirmwareEntry { component: "bmc", version: firmware.raw_version }], + configuration_digests: [], + } +} + +fn mtcollins1_current_intake_subject() -> MachineIntakeSubject? { + match mtcollins1_intake_subject() { + Absent => none + Present { value: standing } => + match megarac_current_build(evidence: mtcollins1_megarac_operation_evidence(), endpoint: mtcollins1_endpoint) { + CurrentBuildUnobserved => none + CurrentBuildRead { reading: r } => + Present { + value: MachineIntakeSubject { + subject: qualification_subject_of( + unit_key: standing.subject.unit_key, + assembly: mtcollins1_assembly, + firmware: bmc_firmware_manifest(firmware: r.identity.firmware), + ), + attempt_id: standing.attempt_id, + }, + } + CurrentBuildKnownUnread { transition: t } => + Present { + value: MachineIntakeSubject { + subject: qualification_subject_of( + unit_key: standing.subject.unit_key, + assembly: mtcollins1_assembly, + firmware: bmc_firmware_manifest(firmware: t.to), + ), + attempt_id: standing.attempt_id, + }, + } + } + } +} diff --git a/dag/gunbc/machine_intake/mtcollins1_memory_census_observation.dag b/dag/gunbc/machine_intake/mtcollins1_memory_census_observation.dag index dcd17cd7d5e..3bd1db95693 100644 --- a/dag/gunbc/machine_intake/mtcollins1_memory_census_observation.dag +++ b/dag/gunbc/machine_intake/mtcollins1_memory_census_observation.dag @@ -562,11 +562,6 @@ data mtcollins1_host_memory_population: HostMemoryPopulation = HostMemoryPopulat // identity, and a caller policy on top of it. Leaving it here would have made this receipt a // second authority for a question the migration is modelling properly. // -// AND ITS PROVENANCE WAS PROSE. The mapping was read from source.min.js as served by this -// controller, and NO SUCH ARTIFACT IS COMMITTED. A sentence naming the file it came from is not a -// resolvable receipt; a reader cannot check it, which is the same defect as a hand-transcribed -// number. The observation therefore does not belong in an evidence PR without its bytes. -// // WHAT SURVIVES HERE IS THE MEASUREMENT ALONE, because the transition itself was observed against // the live controller and is evidence about this unit. It is carried as SAMPLES rather than a // sentence: the reading is a timestamped sequence of two raw controller fields, and a prose @@ -575,9 +570,7 @@ data mtcollins1_host_memory_population: HostMemoryPopulation = HostMemoryPopulat // // THE STATUS AND INDEX ARE CARRIED RAW AND UNDECODED, DELIBERATELY. What any particular value // MEANS is an extdeps.bmc.megarac catalog keyed to a firmware identity, and the policy over it is -// the boot-path migration's; this module draws no readiness conclusion. The decoder that used to -// live here was removed because its source (source.min.js as served by this controller) is not a -// committed artifact, and a sentence naming the file it came from is not a resolvable receipt. +// the boot-path migration's; this module draws no readiness conclusion. type MtCollins1MediaRedirectionSample { offset_from_attach: Second redirection_status: Nat diff --git a/dag/gunbc/machine_intake/mtcollins1_ui_bundle_observe.dag b/dag/gunbc/machine_intake/mtcollins1_ui_bundle_observe.dag deleted file mode 100644 index 800a5724def..00000000000 --- a/dag/gunbc/machine_intake/mtcollins1_ui_bundle_observe.dag +++ /dev/null @@ -1,370 +0,0 @@ -module gunbc.machine_intake_mtcollins1_ui_bundle_observe - -import std.decl_ref { decl_ref } -import extdeps.shell -import extdeps.bmc.ipmi -import extdeps.bmc.megarac -import extdeps.tools.sha256sum -import extdeps.tools.gzip -import std.algebra { trim } -import std.types { Bool, FilePath, Int, List, NonEmptyStr, String } -import std.process { ExitSuccess, ProcessExit, exit_failure } -import extdeps.filesystem.filesystem_io { Filesystem } -import extdeps.tools.gnu_coreutils { coreutils_duration_operand } -import extdeps.bmc.megarac { megarac_ui_bundle_path } -import gunbc.machine_intake_mtcollins1_access_observation { mtcollins1_endpoint, mtcollins1_firmware, mtcollins1_boot_ipmi_read_deadline, mtcollins1_boot_ipmi_kill_after } -import gunbc.managed_host { managed_host_account_driving, managed_host_binding_for } -import gunbc.fleet_host_identity { operator_host_mtcollins1 } -import gunbc.machine_intake_mtcollins1_fan_observe { FanCredentialPathReady, FanCredentialPathUnset, FanCredentialPathEmpty, mtcollins1_fan_credential_path, mtcollins1_fan_credential_path_env } -import gunbc.machine_intake_megarac_media_attach { megarac_media_read_max_time, megarac_media_read_max_time_operand } -import std.measure { Second, second, second_count } -import std.nat { Nat } -import std.optional { Present, Absent } -import v2.std.algebra { any } -import gunbc.machine_intake_megarac_served_ui_catalog_observation { megarac_source_min_js_sha256_2026_09_27 } - -// ONE READ OF MT. COLLINS' OWN UI BUNDLE, SO A VENDOR CODE IS INTERPRETED FROM THE VENDOR'S SOURCE -// (operator, amended brief msg_20e45239 and decision msg_b3c77f62, 2026-09-30). The settings route -// answers cd_error_code 16 and nothing in the tree says what 16 names; the firmware's UI names its -// own codes in source.min.js, which is how start-media's 13410/13460 were recovered. This mode is the -// modeled route to those bytes: the controller's firmware revision is read over IPMI (`mc info`, -// read-only), the bundle is fetched only when that revision is the one this unit's facts are keyed -// on, and the served bytes, their sha256 and the comparison with the 2026-09-27 read are the receipt. -// -// ONE READ-ONLY SERVED-FILE OBSERVATION, NOT ONE FILE (eager-owl-205, 2026-09-30). The KVM viewer's -// /kvm video socket is not in source.min.js -- that bundle's one WebSocket is SOL -- so the mode also -// reads viewer.html and every script it loads, derived by parsing what was served and followed to -// closure within a declared budget (served_crawl). Each file is its own receipt row. -// -// NOTHING IS WRITTEN TO THE CONTROLLER AND NO WEB SESSION IS OPENED. Every file read is served to the -// login page's origin before a session exists, so each GET carries no credential (extdeps.bmc.megarac -// megarac.Ui.GetServedBundle); the only credential this run materializes is the IPMI one `mc info` -// needs, through the same prelude the fan observation uses. No unit hold is taken: like the fan -// series, nothing here changes chassis, media or boot state. - -// EVERYTHING THE MODE KEEPS SHARES ONE PREFIX, so the upload is one glob: the receipt, and one directory -// holding every served file, named by its served path with "/" read as "__". -data mtcollins1_ui_bundle_prefix: String = "target/mtcollins1-ui-bundle-" - -data mtcollins1_ui_bundle_artifact_glob: String = concat(mtcollins1_ui_bundle_prefix, "*") - -data mtcollins1_ui_bundle_dir: String = concat(mtcollins1_ui_bundle_prefix, "files") - -fn mtcollins1_ui_bundle_file_path(served: String) -> String { - concat(concat(mtcollins1_ui_bundle_dir, "/"), join(split(s: served, delimiter: "/"), "__")) -} - -data mtcollins1_ui_bundle_receipt_path: String = concat(mtcollins1_ui_bundle_prefix, "receipt.txt") - -// THE CRAWL'S BUDGET, A DECLARED POLICY BOUND (DESIGN §5: an explicit budget, never a silent cut): at -// most this many served files are fetched in one run. The require config the 2026-09-30 read of -// source.min.js carries names 23 ./libs/kvm/ modules; the budget is room for roughly twice that plus the -// two seeds. A run that reaches it records a ServedFileBudgetReached row naming every path left unvisited. -data mtcollins1_ui_bundle_file_budget: Nat = 48 - -// WHAT THE PROBE'S OWN READS CAN COST AT MOST: the one `mc info` under its timeout and kill-after, then -// every served GET the budget admits under curl --max-time. The step budget adds gunbc run's load and the -// credential prelude on the workflow side (gunbc.fleet_converge_workflow), which owns those authorities. -fn mtcollins1_ui_bundle_read_bound() -> Second { - second(count: second_count(s: mtcollins1_boot_ipmi_read_deadline) + second_count(s: mtcollins1_boot_ipmi_kill_after) - + mtcollins1_ui_bundle_file_budget * second_count(s: megarac_media_read_max_time)) -} - -// THE REVISION AS `mc info` PRINTS IT: the value after the colon on the `Firmware Revision` line. -fn firmware_revision_of_mc_info(stdout: String) -> String? { - let lines = filter(split(s: stdout, delimiter: "\n"), l => starts_with(s: trim(s: l), prefix: "Firmware Revision")) - match lines.first() { - Absent => none - Present { value: line } => { - let after = split(s: line, delimiter: ":").skip(n: 1) - let v = trim(s: join(after, ":")) - if v == "" { none } else { Present { value: v } } - } - } -} - -// THE BUNDLE IS FETCHED ONLY UNDER THE REVISION THE UNIT'S FACTS ARE KEYED ON. Every code this -// module exists to recover is a 0.32 fact (mtcollins1_firmware); bytes from another build would be -// read as 0.32's table. -type UiBundleFirmwareStanding - = UiBundleFirmwareExpected { revision: NonEmptyStr } - | UiBundleFirmwareDiffers { revision: NonEmptyStr, expected: NonEmptyStr } - | UiBundleFirmwareUnread { detail: String } - -fn ui_bundle_firmware_standing(success: Bool, stdout: String, exit_code: Int, stderr: String) -> UiBundleFirmwareStanding { - if !success { - UiBundleFirmwareUnread { detail: join(["ipmitool mc info exited ", to_string(exit_code), ": ", trim(s: stderr)], "") } - } else { - match firmware_revision_of_mc_info(stdout: stdout) { - Absent => UiBundleFirmwareUnread { detail: concat("mc info printed no Firmware Revision line: ", stdout) } - Present { value: r } => - if r == (mtcollins1_firmware.raw_version as String) { UiBundleFirmwareExpected { revision: r as NonEmptyStr } } - else { UiBundleFirmwareDiffers { revision: r as NonEmptyStr, expected: mtcollins1_firmware.raw_version } } - } - } -} - -// MATCH OR DRIFT AGAINST THE 2026-09-27 READ, REPORTED AND NEVER A REFUSAL: a different digest is a -// finding about the served UI, not a failed read. -type UiBundleDigestComparison - = UiBundleDigestMatches - | UiBundleDigestDrifted { cited: NonEmptyStr } - -fn ui_bundle_digest_comparison(observed: String) -> UiBundleDigestComparison { - if observed == (megarac_source_min_js_sha256_2026_09_27 as String) { UiBundleDigestMatches } - else { UiBundleDigestDrifted { cited: megarac_source_min_js_sha256_2026_09_27 } } -} - -fn ui_bundle_digest_comparison_text(c: UiBundleDigestComparison) -> String { - match c { - UiBundleDigestMatches => "MATCH with the 2026-09-27 read" - UiBundleDigestDrifted { cited: d } => concat("DRIFT from the 2026-09-27 read, which was ", d as String) - } -} - -fn ui_bundle_firmware_text(f: UiBundleFirmwareStanding) -> String { - match f { - UiBundleFirmwareExpected { revision: r } => concat("firmware revision ", concat(r as String, " (expected)")) - UiBundleFirmwareDiffers { revision: r, expected: e } => join(["firmware revision ", r as String, " is NOT the expected ", e as String, "; the bundle was not fetched"], "") - UiBundleFirmwareUnread { detail: d } => concat("firmware revision UNREAD, so the bundle was not fetched: ", d) - } -} - -// THE RECEIPT IS WRITTEN ON EVERY PATH THAT REACHED THE CONTROLLER, and the exit carries the same -// sentence, so a refused run's artifact says why it holds no bytes. -fn ui_bundle_conclude(lines: List, outcome: ProcessExit) -> ProcessExit { - let body = join(concat([concat("subject=", mtcollins1_endpoint.host as String), concat("bundle_path=", megarac_ui_bundle_path as String)], lines), "\n") - let kept = Filesystem.Write(path: mtcollins1_ui_bundle_receipt_path, content: concat(body, "\n")) - if kept.success { outcome } else { - match outcome { - ExitSuccess => exit_failure(reason: concat("mtcollins1 ui bundle: the bundle was read and its receipt could not be written: ", kept.error)) - other => other - } - } -} - -fn ui_bundle_firmware_refused(firmware: UiBundleFirmwareStanding) -> ProcessExit { - ui_bundle_conclude(lines: [concat("firmware=", ui_bundle_firmware_text(f: firmware))], outcome: exit_failure(reason: concat("mtcollins1 ui bundle: ", ui_bundle_firmware_text(f: firmware)))) -} - -// ── THE SERVED FILES, DERIVED RATHER THAN LISTED (operator rule: no hardcoded lists) ───────────────── -// Two seeds: source.min.js, the UI bundle (still compared with the 2026-09-27 read), and viewer.html, -// the H5 viewer the UI opens with window.open. Every further path is PARSED out of a file already -// fetched: an HTML file's " - served_html_references(html: html) == ["libs/require.js", "viewer-main.js", "./js/viewer.min.js?v=12"] -} - -// A script's KVM libraries, as the 2026-09-30 source.min.js spells them: a require-config path (a module -// id, loaded as .js) and a Worker constructed from a file name, in either quote. -test fn a_script_yields_its_kvm_libraries_as_files() -> Bool { - let js = "paths:\{videosocket:\"./libs/kvm/videosocket\",decode_worker:\"./libs/kvm/ast/decode_worker.min\"},this.decodeWorker=new Worker('./libs/kvm/ast/decode_worker.js');var s=\"./libs/media/cdimage\"" - served_js_references(js: js) == ["./libs/kvm/videosocket.js", "./libs/kvm/ast/decode_worker.min.js", "./libs/kvm/ast/decode_worker.js"] -} - -// Only HTML and scripts are parsed; anything else yields no references. -test fn only_pages_and_scripts_are_parsed_for_references() -> Bool { - served_references(path: "viewer.html", text: "") == ["a.js"] - && served_references(path: "libs/kvm/x.js", text: "\"./libs/kvm/y\"") == ["./libs/kvm/y.js"] - && served_references(path: "images/logo.png", text: "\"./libs/kvm/y\"") == [] -} - -// A reference becomes a path under the controller's root; another origin is kept, never fetched. -test fn a_reference_reads_as_a_root_path_or_another_origin() -> Bool { - (match served_ref_reading(reference: "./js/viewer.min.js?v=12#x") { ServedRefLocal { path: p } => p == "js/viewer.min.js" _ => false }) - && (match served_ref_reading(reference: "/libs/require.js") { ServedRefLocal { path: p } => p == "libs/require.js" _ => false }) - && (match served_ref_reading(reference: "https://cdn.example/jquery.js") { ServedRefOtherOrigin { reference: r } => r == "https://cdn.example/jquery.js" _ => false }) - && (match served_ref_reading(reference: "//cdn.example/x.js") { ServedRefOtherOrigin { reference: _ } => true _ => false }) - && (match served_ref_reading(reference: "?v=1") { ServedRefEmpty => true _ => false }) -} - -// THE BUDGET'S EDGE IS A ROW, NOT A SILENT CUT: with nothing left to spend, the crawl fetches nothing and -// records every distinct unvisited path. (No GET is made on this path, so it runs without a controller.) -test fn the_budget_edge_names_every_unvisited_path() -> Bool { - let queue = [ - ServedRef { path: "viewer.html", found_by: "seed" }, - ServedRef { path: "libs/kvm/videosocket.js", found_by: "viewer.html" }, - ServedRef { path: "viewer.html", found_by: "libs/kvm/x.js" }, - ] - match served_crawl(queue: queue, seen: [], rows: [], remaining: 0).first() { - Present { value: row } => - match row { - ServedFileBudgetReached { budget: b, unvisited: u } => b == (mtcollins1_ui_bundle_file_budget as Int) && u == ["viewer.html", "libs/kvm/videosocket.js"] - _ => false - } - Absent => false - } -} - -// ONE GLOB CARRIES THE RECEIPT AND EVERY KEPT FILE. -test fn the_upload_glob_covers_the_receipt_and_every_kept_file() -> Bool { - let prefix = substring(s: mtcollins1_ui_bundle_artifact_glob, start: 0, end: string_length(mtcollins1_ui_bundle_artifact_glob) - 1) - starts_with(s: mtcollins1_ui_bundle_receipt_path, prefix: prefix) - && starts_with(s: mtcollins1_ui_bundle_file_path(served: "libs/kvm/ast/decode_worker.js"), prefix: concat(mtcollins1_ui_bundle_dir, "/")) - && starts_with(s: mtcollins1_ui_bundle_dir, prefix: prefix) - && mtcollins1_ui_bundle_file_path(served: "libs/kvm/ast/decode_worker.js") == concat(mtcollins1_ui_bundle_dir, "/libs__kvm__ast__decode_worker.js") -} diff --git a/dag/test/claim/machine_intake/mtjade1_arrival_principal_witness_test.dag b/dag/test/claim/machine_intake/mtjade1_arrival_principal_witness_test.dag new file mode 100644 index 00000000000..bf2ddf05d3b --- /dev/null +++ b/dag/test/claim/machine_intake/mtjade1_arrival_principal_witness_test.dag @@ -0,0 +1,224 @@ +module test.claim.machine_intake.mtjade1_arrival_principal_witness + +import std.logic { Bool } +import std.types { List, NonEmptyStr, String } +import std.durable_exclusive_hold { DurableHoldOwnerRef } +import gunbc.machine_intake_arrival_subject { + arrival_converge_subject_options, admit_arrival_subject, + ArrivalSubjectInPopulation, ArrivalSubjectUnknown, ArrivalSubjectIsManagedHostExecutorConfusion, +} +import gunbc.machine_intake_arrival_interlock { + ArrivalSlotHeld, ArrivalSlotNotHeld, + admit_arrival_archive_interlock, admit_arrival_bmc_secure_interlock, + ArrivalArchiveInterlockAdmitted, ArrivalArchiveInterlockRefused, + ArrivalBmcSecureInterlockAdmitted, ArrivalBmcSecureInterlockRefused, + arrival_combine_prefix_and_release, arrival_after_acquire, arrival_archive_slot_held, + ArrivalArchiveSlotRelease, ArrivalArchiveSlotReleased, ArrivalArchiveSlotReleaseRefused, + ArrivalArchiveSlotAcquire, ArrivalArchiveSlotAcquireRefused, ArrivalArchiveSlotAcquired, ArrivalArchiveSlotHeld, +} +import std.process { ProcessExit, ExitSuccess, ExitFailure } +import std.durable_compare_and_set { cas_generation_first } +import gunbc.machine_intake_bmc_rotation_route { rotation_route_ungrounded } +import extdeps.bmc.endpoint { AmiMegaRac, BmcControllerEndpoint } +import extdeps.bmc.capability { bmc_firmware_release_identity_of_wire } +import gunbc.auth.mtjade1_arrival_pair { mtjade1_arrival_principal_member, mtjade1_arrival_secret_grants } +import gunbc.auth.mtjade1_arrival_federation { + mtjade1_arrival_archive_effect, mtjade1_arrival_firmware_qualification_effect, +} +import gunbc.auth.authorization_pattern_selection { + FederatedScopedGrant, OperatorApprovedCapability, + PatternSelected, AuthorizationPatternSelection, + InterlockPending, InterlockedBy, + select_authorization_pattern, +} +import gunbc.auth.privileged_effect_census { + rulings_without_a_rostered_interlock, privileged_effect_census, + RealizedUnauthorized, RealizedFederatedGrant, RealizedRunSelected, + RealizedApprovalCapability, RealizedHumanStep, RealizedOperatorSession, +} +import gunbc.machine_intake_arrival_converge { grant_discharges_bmc_secure_account_write } +import gunbc.machine_intake_arrival_run { arrival_wet_prefix_from_unobserved_world } +import gunbc.auth.standing_operator_grant { + mtjade1_live_standing_grant, standing_grant_covers, standing_grant_arm_interlock, StandingGrantHost, + StandingJadeArrivalArchive, StandingJadeFirmwareQualification, StandingBmcSecureAccountWrite, StandingD0ReleaseToFleet, +} +import std.decl_ref { declaration_ref_display_key } +import gunbc.fleet_host_identity { operator_host_mtjade1, operator_host_mtcollins1 } + +fn selected_federated(s: AuthorizationPatternSelection) -> Bool { + match s { + PatternSelected { pattern: FederatedScopedGrant, receipt: _ } => true + _ => false + } +} + +fn selected_approved(s: AuthorizationPatternSelection) -> Bool { + match s { + PatternSelected { pattern: OperatorApprovedCapability, receipt: _ } => true + _ => false + } +} + +test fn w_arrival_options_are_the_population_projection() -> Bool { + arrival_converge_subject_options() == ["mtjade1"] +} + +test fn w_mtjade1_is_admitted_and_mtcollins1_and_outsiders_refuse_before_effect() -> Bool { + match admit_arrival_subject(wire: "mtjade1") { + ArrivalSubjectInPopulation { subject: s } => (s.wire as String) == "mtjade1" + _ => false + } + && match admit_arrival_subject(wire: operator_host_mtcollins1 as String) { + ArrivalSubjectIsManagedHostExecutorConfusion { wire: _ } => true + _ => false + } + && match admit_arrival_subject(wire: "not-a-unit") { + ArrivalSubjectUnknown { wire: w } => w == "not-a-unit" + _ => false + } +} + +test fn w_grant_covers_arrival_arms_on_mtjade1_only() -> Bool { + standing_grant_covers(g: mtjade1_live_standing_grant, scope: StandingGrantHost { host: operator_host_mtjade1 }, effect: StandingJadeArrivalArchive) + && standing_grant_covers(g: mtjade1_live_standing_grant, scope: StandingGrantHost { host: operator_host_mtjade1 }, effect: StandingJadeFirmwareQualification) + && standing_grant_covers(g: mtjade1_live_standing_grant, scope: StandingGrantHost { host: operator_host_mtjade1 }, effect: StandingBmcSecureAccountWrite) + && grant_discharges_bmc_secure_account_write(g: mtjade1_live_standing_grant, host: operator_host_mtjade1) + && !grant_discharges_bmc_secure_account_write(g: mtjade1_live_standing_grant, host: operator_host_mtcollins1) + && !standing_grant_covers(g: mtjade1_live_standing_grant, scope: StandingGrantHost { host: operator_host_mtcollins1 }, effect: StandingBmcSecureAccountWrite) + && !standing_grant_covers(g: mtjade1_live_standing_grant, scope: StandingGrantHost { host: operator_host_mtjade1 }, effect: StandingD0ReleaseToFleet) +} + +test fn w_ungrounded_route_refuses_bmc_secure_interlock() -> Bool { + match admit_arrival_subject(wire: "mtjade1") { + ArrivalSubjectInPopulation { subject: s } => + match admit_arrival_archive_interlock(subject: s, slot: ArrivalSlotNotHeld { cause: "slot free" as NonEmptyStr }) { + ArrivalArchiveInterlockRefused { cause: _ } => + match admit_arrival_bmc_secure_interlock( + subject: s, + slot: ArrivalSlotHeld { subject: s }, + route: rotation_route_ungrounded( + endpoint: BmcControllerEndpoint { host: "192.168.1.246" }, + firmware: bmc_firmware_release_identity_of_wire(family: AmiMegaRac, wire: "2.11.104000" as NonEmptyStr), + ), + ) { + ArrivalBmcSecureInterlockRefused { cause: _ } => true + ArrivalBmcSecureInterlockAdmitted { subject: _ } => false + } + ArrivalArchiveInterlockAdmitted { subject: _ } => false + } + _ => false + } +} + +test fn w_release_failure_after_success_is_visible() -> Bool { + match arrival_combine_prefix_and_release( + prefix: ExitSuccess, + slot: ArrivalArchiveSlotReleaseRefused { cause: "arrival slot release was not eligible" as NonEmptyStr }, + ) { + ExitSuccess => false + ExitFailure { code: _, reason: r } => string_contains(s: r, pattern: "slot release refused") + } +} + +test fn w_release_failure_after_prefix_failure_is_concatenated() -> Bool { + match arrival_combine_prefix_and_release( + prefix: ExitFailure { code: 2, reason: "prefix refused" }, + slot: ArrivalArchiveSlotReleaseRefused { cause: "arrival slot release was not eligible" as NonEmptyStr }, + ) { + ExitSuccess => false + ExitFailure { code: c, reason: r } => + c == 2 && string_contains(s: r, pattern: "prefix refused") && string_contains(s: r, pattern: "slot release refused") + } +} + +fn body_that_refuses() -> ProcessExit { + ExitFailure { code: 9, reason: "BODY_RAN" } +} + +fn release_that_marks(held: ArrivalArchiveSlotHeld) -> ArrivalArchiveSlotRelease { + match held { + ArrivalArchiveSlotHeld { subject: _, slot_key: _, owner: _, generation: _ } => + ArrivalArchiveSlotReleaseRefused { cause: "RELEASE_RAN" as NonEmptyStr } + } +} + +fn fixture_acquired_slot() -> ArrivalArchiveSlotAcquire { + match admit_arrival_subject(wire: "mtjade1") { + ArrivalSubjectInPopulation { subject: s } => + ArrivalArchiveSlotAcquired { + held: arrival_archive_slot_held( + subject: s, + slot_key: "arrival:mtjade1" as NonEmptyStr, + owner: "arrival-principal-witness" as DurableHoldOwnerRef, + generation: cas_generation_first(), + ), + } + _ => ArrivalArchiveSlotAcquireRefused { cause: "fixture subject was not admitted" as NonEmptyStr } + } +} + +test fn w_acquired_runs_body_and_release_when_body_refuses() -> Bool { + match arrival_after_acquire( + outcome: fixture_acquired_slot(), + body: fn() { body_that_refuses() }, + release_fn: fn(h) { release_that_marks(held: h) }, + ) { + ExitSuccess => false + ExitFailure { code: c, reason: r } => + c == 9 && string_contains(s: r, pattern: "BODY_RAN") && string_contains(s: r, pattern: "RELEASE_RAN") + } +} + +test fn w_successful_release_keeps_the_prefix() -> Bool { + match arrival_combine_prefix_and_release( + prefix: ExitSuccess, + slot: ArrivalArchiveSlotReleased { generation: cas_generation_first() }, + ) { + ExitSuccess => true + ExitFailure { code: _, reason: _ } => false + } +} + +test fn w_bound_principal_selects_federated_scoped_grant() -> Bool { + selected_federated(s: select_authorization_pattern(e: mtjade1_arrival_archive_effect())) + && selected_approved(s: select_authorization_pattern(e: mtjade1_arrival_firmware_qualification_effect())) + && !selected_federated(s: select_authorization_pattern(e: mtjade1_arrival_firmware_qualification_effect())) + && !selected_approved(s: select_authorization_pattern(e: mtjade1_arrival_archive_effect())) + && mtjade1_arrival_principal_member() == "serviceAccount:mtjade1-arrival@gunbai-secrets.iam.gserviceaccount.com" + && mtjade1_arrival_secret_grants().length() == 1 + && rulings_without_a_rostered_interlock().length() == 1 + && match rulings_without_a_rostered_interlock().first() { + Present { value: s } => + declaration_ref_display_key(ref: s) == "gunbc.auth.mtjade1_arrival_federation::mtjade1_arrival_firmware_qualification_effect" + Absent => false + } + && match standing_grant_arm_interlock(g: mtjade1_live_standing_grant, e: StandingJadeFirmwareQualification) { + InterlockPending { obligation: _ } => true + InterlockedBy { hold: _ } => false + } + && match standing_grant_arm_interlock(g: mtjade1_live_standing_grant, e: StandingBmcSecureAccountWrite) { + InterlockedBy { hold: h } => (h.decl_name as String) == "admit_rotation_apply" + InterlockPending { obligation: _ } => false + } +} + +test fn w_wet_door_refuses_unobserved_prior_life_world() -> Bool { + match arrival_wet_prefix_from_unobserved_world() { + ExitSuccess => false + ExitFailure { code: _, reason: r } => + string_contains(s: r, pattern: "unobserved PriorLifeBoundary") + } +} + +test fn w_archive_census_does_not_claim_a_write_the_wet_door_refuses() -> Bool { + any(privileged_effect_census, row => + (row.site.decl_name as String) == "mtjade1_arrival_archive_effect" + && match row.realized { + RealizedUnauthorized => true + RealizedFederatedGrant => false + RealizedRunSelected => false + RealizedApprovalCapability => false + RealizedHumanStep => false + RealizedOperatorSession { arm: _ } => false + }) +} diff --git a/dag/test/claim/machine_shape_catalog_integration_witness_test.dag b/dag/test/claim/machine_shape_catalog_integration_witness_test.dag index 79cbcbc5157..cc1a1cd4473 100644 --- a/dag/test/claim/machine_shape_catalog_integration_witness_test.dag +++ b/dag/test/claim/machine_shape_catalog_integration_witness_test.dag @@ -48,23 +48,20 @@ test fn w_cpu_cache_levels_integrated_into_memory_shape() -> Bool { Present { value: domain } => { let memory = domain.memory list_length(items: memory) == 3 - && memory.first().id == ampere_l1_level && match memory.first() { - Present { value: level } => byte_size_count(b: level.capacity) == ampere_l1_bytes_per_core + Present { value: level } => level.id == ampere_l1_level && byte_size_count(b: level.capacity) == ampere_l1_bytes_per_core Absent => false } && match memory.first().transfer_grain { Present { value: grain } => byte_size_count(b: grain) == ampere_cache_line_bytes Absent => false } - && memory.skip(n: 1).first().id == ampere_l2_level && match memory.skip(n: 1).first() { - Present { value: level } => byte_size_count(b: level.capacity) == ampere_l2_bytes_per_core + Present { value: level } => level.id == ampere_l2_level && byte_size_count(b: level.capacity) == ampere_l2_bytes_per_core Absent => false } - && memory.skip(n: 2).first().id == ampere_l3_level && match memory.skip(n: 2).first() { - Present { value: level } => byte_size_count(b: level.capacity) == ampere_l3_bytes + Present { value: level } => level.id == ampere_l3_level && byte_size_count(b: level.capacity) == ampere_l3_bytes Absent => false } } diff --git a/dag/test/claim/map_lookup_dual_dispatch_witness_test.dag b/dag/test/claim/map_lookup_dual_dispatch_witness_test.dag index 6aae645207a..3febbb72a1e 100644 --- a/dag/test/claim/map_lookup_dual_dispatch_witness_test.dag +++ b/dag/test/claim/map_lookup_dual_dispatch_witness_test.dag @@ -4,12 +4,20 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly test fn map_get_method_routes_through_dual_dispatch_chokepoint() -> Bool { let m = empty_map() |> map_insert("a", 10) - (m |> get("a")) == 10 + let o = m |> get("a") + match o { + Present { value: v } => v == 10 + Absent => false + } } test fn map_index_routes_through_dual_dispatch_chokepoint() -> Bool { let m = empty_map() |> map_insert("b", 42) - m["b"] == 42 + let o = m["b"] + match o { + Present { value: v } => v == 42 + Absent => false + } } test fn lookup_builtin_routes_through_dual_dispatch_chokepoint() -> Bool { diff --git a/dag/test/claim/materialization_provider_witness_test.dag b/dag/test/claim/materialization_provider_witness_test.dag index 0c190dcff3f..18a721ed4e8 100644 --- a/dag/test/claim/materialization_provider_witness_test.dag +++ b/dag/test/claim/materialization_provider_witness_test.dag @@ -709,8 +709,8 @@ test fn completeness_holds_when_all_required_outputs_are_carried() -> Bool { )) } -test fn consumer_target_frontier_names_six_caches_with_typed_binds() -> Bool { - (materialization_provider_target_count() == 6) +test fn consumer_target_frontier_names_four_caches_with_typed_binds() -> Bool { + (materialization_provider_target_count() == 4) && materialization_provider_targets_contain(id: typed_module_cache_id) && materialization_provider_targets_all_bound() } diff --git a/dag/test/claim/materialization_store_local_immutable_pin_wet_witness_test.dag b/dag/test/claim/materialization_store_local_immutable_pin_wet_witness_test.dag new file mode 100644 index 00000000000..6940a163ce4 --- /dev/null +++ b/dag/test/claim/materialization_store_local_immutable_pin_wet_witness_test.dag @@ -0,0 +1,158 @@ +module test.claim.materialization_store_local_immutable_pin_wet_witness + +import extdeps.shell +import extdeps.filesystem.filesystem_io { Filesystem } +import std.types { Bool, List, String, NonEmptyStr } +import std.content_hash { Fnv1a64Structural, content_hash_atom } +import std.materialization_provider { + ArtifactRequest, NativeModuleVerdictBundleRequest, NativeModuleVerdictBundleSubject, + evaluation_store_address, evaluation_store_admitted_address, + output_module_test_verdicts, output_module_preparation_diagnostics, +} +import std.materialization_store_grant { MaterializationStoreRoot, WitnessScratchRoot } +import std.materialization_object { + ArtifactPayload, + StoreCommit, + store_object_name, + store_lookup_is_miss, + store_commit_refusal_tag, +} +import extdeps.realization.materialization_store_local { + LocalStoreCapability, + LocalStoreOpened, LocalStoreUnavailable, + LocalStoreInitialization, LocalStoreInitialized, LocalStoreInitializationRefused, + local_store_initialize, local_store_lookup, store_path, + LocalStoreFamilyBudget, + local_store_with_family, LocalStoreBracket, LocalStoreBracketRan, LocalStoreBracketRefused, local_store_commit_under, + LocalStoreHoldStale, LocalStoreHoldNamedWindowHeadMoved, LocalStoreHoldWrongFamily, + local_store_hold_key, + LocalStoreHoldRefusal, LocalStoreFamilyBusy, + LocalStoreHolderUnobservable, LocalStoreHoldRecoveryRefused, LocalStoreHoldContended, LocalStoreHoldStoreUnavailable, + LocalStoreHoldSlotUndecodable, LocalStoreHoldKeyNotAddressable, LocalStoreHoldSelfUnidentified, LocalStoreHoldWindowTooNarrow, LocalStoreHoldUnsettled, +} +import gunbc.durable_cas_file_store { cas_file_slot_path } +import std.cache_identity { native_module_verdict_bundle_artifact_kind, typed_module_artifact_kind } +import std.measure { byte_size } + +// PARTIAL RECLAIM MUST NOT RE-AUTHORIZE A LEAKED HOLD. Independent deletes could remove G+1 while +// G survived, so named verify would treat leaked G as head. Pin G against unlink, run another +// acquire/release (the reclaim that used to punch that hole), then reuse G. commit_under observes +// the hold first: named verify must answer NamedWindowHeadMoved (G present, G+1 still there). +// +// DECLARED NO-CI-WET-LANE DEBT. ioctl-set IMMUTABLE needs CAP_LINUX_IMMUTABLE; no required wet +// lane grants it. This file is BinWitnessWet in gunbc.ci_layer_roots and named in +// gunbc.rung_drop.edited_bin_witness_wet_rows_not_executed_by_ci_population, so a changed +// identity is DeclinedNoCiWetLane rather than PlannedAsChangedWitness without a terminal. +// Restoration: a wet lane that grants CAP_LINUX_IMMUTABLE, or an unlink-refusal construction +// that needs none. Floor wall for the invariant: +// reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution. +// Integration evidence (not CI): invocation bbdf835a-b6fa-4a51-a93b-26b140398306, FAIL then PASS +// with reclaim-stop reverted. + +data witness_family_budgets: List = [ + LocalStoreFamilyBudget { family: native_module_verdict_bundle_artifact_kind, budget: byte_size(count: 4096) }, + LocalStoreFamilyBudget { family: typed_module_artifact_kind, budget: byte_size(count: 4096) }, +] + +fn h(v: String) -> Fnv1a64Structural { + content_hash_atom(value: v as NonEmptyStr) +} + +fn bundle_req(module: String) -> ArtifactRequest { + NativeModuleVerdictBundleRequest(NativeModuleVerdictBundleSubject { + route_binary_identity: h(v: "route-binary-1"), + module_identity: h(v: module), + canonical_module_import_closure_identity: h(v: "closure-1"), + ordered_test_identity_population: h(v: "t1,t2"), + eval_interpretation_identity: h(v: "eval-1") + }) +} + +fn verdicts(text: String) -> List { + [ + ArtifactPayload { id: output_module_test_verdicts, content: text }, + ArtifactPayload { id: output_module_preparation_diagnostics, content: "" }, + ] +} + +fn object_path(root_path: String, req: ArtifactRequest) -> String { + match evaluation_store_admitted_address(admission: evaluation_store_address(req: req)) { + Absent => "" + Present { value: addr } => store_path(root_path: root_path, name: store_object_name(address: addr)) + } +} + +fn commit_tag(c: StoreCommit) -> String { + store_commit_refusal_tag(c: c) +} + +fn fresh_root_path() -> String { + shell.Mktemp.DirWithTemplate(template: "/tmp/gunbc_matstore.XXXXXX").path +} + +fn opened(root: MaterializationStoreRoot) -> LocalStoreCapability? { + match local_store_initialize(root: root, instance_label: "wet-witness", budgets: witness_family_budgets) { + LocalStoreInitialized { opening: o } => match o { + LocalStoreOpened { capability: c, durability: _ } => Present { value: c } + LocalStoreUnavailable { root_path: _, cause: _ } => none + } + LocalStoreInitializationRefused { root_path: _, cause: _ } => none + } +} + +fn hold_refusal_is_named_window_head_moved(r: LocalStoreHoldRefusal?) -> Bool { + match r { + Present { value: x } => match x { + LocalStoreHoldNamedWindowHeadMoved { named: _ } => true + LocalStoreHoldStale { observed: _ } => false + LocalStoreFamilyBusy { holder: _, evidence: _ } => false + LocalStoreHolderUnobservable { holder: _, cause: _ } => false + LocalStoreHoldRecoveryRefused { refusal: _ } => false + LocalStoreHoldContended => false + LocalStoreHoldStoreUnavailable { cause: _ } => false + LocalStoreHoldSlotUndecodable { detail: _ } => false + LocalStoreHoldKeyNotAddressable => false + LocalStoreHoldSelfUnidentified { cause: _ } => false + LocalStoreHoldWindowTooNarrow { k: _ } => false + LocalStoreHoldUnsettled { settlement: _ } => false + LocalStoreHoldWrongFamily { held: _, requested: _ } => false + } + Absent => false + } +} + +data pin_ext2_immutable_script: String = "import fcntl, os, struct, sys\nGET = 0x80086601\nSET = 0x40086602\nIMM = 0x10\npath = sys.argv[1]\non = sys.argv[2] == \"1\"\nfd = os.open(path, os.O_RDONLY)\ncur = struct.unpack(\"I\", fcntl.ioctl(fd, GET, struct.pack(\"I\", 0)))[0]\nfcntl.ioctl(fd, SET, struct.pack(\"I\", (cur | IMM) if on else (cur & ~IMM)))\nos.close(fd)\n" + +fn pin_generation_unlink(path: String, on: Bool) -> Bool { + shell.Exec.RunArgv(program: "python3", arguments: ["-c", pin_ext2_immutable_script, path, if on { "1" } else { "0" }]).exit_code == 0 +} + +test fn a_leaked_hold_stays_stale_when_reclaim_cannot_remove_its_generation_by_real_execution() -> Bool { + let root_path = fresh_root_path() + let root = WitnessScratchRoot { root: root_path } + let rs = bundle_req(module: "s") + let verdict = match opened(root: root) { + Absent => false + Present { value: store } => + match local_store_with_family(capability: store, of: rs, run: h => h) { + LocalStoreBracketRefused { refusal: _ } => false + LocalStoreBracketRan { value: leaked, hold_release: _ } => { + let pinned = cas_file_slot_path(root: root_path as NonEmptyStr, key: local_store_hold_key(family: leaked.family), generation: leaked.generation) + let immutable = pin_generation_unlink(path: pinned, on: true) + let advanced = match local_store_with_family(capability: store, of: rs, run: _ => true) { + LocalStoreBracketRefused { refusal: _ } => false + LocalStoreBracketRan { value: ok, hold_release: _ } => ok + } + let c = local_store_commit_under(held: leaked, req: rs, payloads: verdicts(text: "s pass"), parts: []) + let _unlock = pin_generation_unlink(path: pinned, on: false) + immutable && advanced + && commit_tag(c: c.commit) == "occupancy_unavailable" + && hold_refusal_is_named_window_head_moved(r: c.hold_refusal) + && !Filesystem.Read(path: object_path(root_path: root_path, req: rs)).success + && store_lookup_is_miss(l: local_store_lookup(capability: store, req: rs)) + } + } + } + let removed = shell.Remove.RecursiveForce(path: root_path) + verdict && removed.success +} diff --git a/dag/test/claim/materialization_store_local_seal_witness_test.dag b/dag/test/claim/materialization_store_local_seal_witness_test.dag index d8411bace7a..1ce58e244b0 100644 --- a/dag/test/claim/materialization_store_local_seal_witness_test.dag +++ b/dag/test/claim/materialization_store_local_seal_witness_test.dag @@ -142,3 +142,15 @@ test fn a_direct_raw_observe_call_is_an_admission_refusal() -> Bool { test fn a_direct_raw_reclaim_call_is_an_admission_refusal() -> Bool { admission_refusals(source: direct_raw_reclaim_source, callee: "cas_reclaim_below") == 1 } + +data direct_held_session_source: String = "module probe_direct_held_session\nimport std.cache_identity { ArtifactKindId }\nimport extdeps.realization.materialization_store_local { LocalStoreCapability, LocalStoreFamilyHeld, LocalStoreBracket, local_store_held_session }\nfn direct(c: LocalStoreCapability, f: ArtifactKindId) -> LocalStoreBracket {\n local_store_held_session(capability: c, family: f, run: h => h)\n}\n" + +data direct_named_window_observe_source: String = "module probe_direct_named_window_observe\nimport std.types { NonEmptyStr }\nimport std.durable_compare_and_set { CasGeneration }\nimport gunbc.durable_cas_file_store { cas_verify_named_window_head, CasNamedWindowVerify }\nfn direct(r: NonEmptyStr, k: NonEmptyStr, g: CasGeneration) -> CasNamedWindowVerify {\n cas_verify_named_window_head(root: r, key: k, generation: g)\n}\n" + +test fn a_direct_held_session_call_is_an_admission_refusal() -> Bool { + admission_refusals(source: direct_held_session_source, callee: "local_store_held_session") == 1 +} + +test fn a_direct_named_window_observe_call_is_an_admission_refusal() -> Bool { + admission_refusals(source: direct_named_window_observe_source, callee: "cas_verify_named_window_head") == 1 +} diff --git a/dag/test/claim/materialization_store_local_wet_witness_test.dag b/dag/test/claim/materialization_store_local_wet_witness_test.dag index 3390043414a..04c22fa8321 100644 --- a/dag/test/claim/materialization_store_local_wet_witness_test.dag +++ b/dag/test/claim/materialization_store_local_wet_witness_test.dag @@ -39,19 +39,21 @@ import extdeps.realization.materialization_store_local { local_store_record_recency, local_store_is_object_name, local_store_is_store_file_name, local_store_object_bytes, local_store_family_budget, LocalStoreFamilyBudget, local_store_budget_sum, LocalStoreRecencyHoldRefused, LocalStoreRecencyReceipt, - local_store_with_family, LocalStoreBracket, LocalStoreBracketRan, LocalStoreBracketRefused, local_store_commit_under, LocalStoreHoldStale, LocalStoreHoldWrongFamily, + local_store_with_family, LocalStoreBracket, LocalStoreBracketRan, LocalStoreBracketRefused, local_store_commit_under, local_store_held_session, LocalStoreHoldStale, LocalStoreHoldNamedWindowHeadMoved, LocalStoreHoldWrongFamily, local_store_hold_key, local_store_hold_owner_prefix, LocalStoreHoldAcquisition, LocalStoreHoldAcquired, LocalStoreHoldRefused, LocalStoreHoldRefusal, LocalStoreFamilyBusy, LocalStoreHolderUnobservable, LocalStoreHoldRecoveryRefused, LocalStoreHoldContended, LocalStoreHoldStoreUnavailable, LocalStoreHoldSlotUndecodable, LocalStoreHoldKeyNotAddressable, LocalStoreHoldSelfUnidentified, LocalStoreHoldWindowTooNarrow, LocalStoreHoldUnsettled, LocalStoreCatalogMismatch, LocalStoreCatalogInadmissible, LocalStoreSweepIncomplete, LocalStoreUnavailableCause, - local_store_index_window, + local_store_index_window, local_store_digits_int, LocalStoreRecordDeleteAttempt, LocalStoreRecordGone, LocalStoreRecordFileRefused, LocalStoreRecordPartsRefused, LocalStoreRecordPartsUnlisted, LocalStoreStagedPart, LocalStorePartsLookup, local_store_lookup_parts, local_store_part_name, } import gunbc.process_hold_identity { self_process_identity, SelfIdentified, SelfUnidentified, process_hold_text } import gunbc.build_cache_instance { ProcessIdentity } import gunbc.durable_exclusive_hold_file_store { file_hold_acquire_windowed, FileHoldAcquired, FileHoldOccupied, FileHoldAcquireLost, FileHoldAcquireStoreUnavailable, FileHoldSlotUndecodable, FileHoldKeyNotSlotAddressable } -import std.durable_exclusive_hold { DurableHoldOwnerRef } +import std.durable_exclusive_hold { + DurableHoldOwnerRef, +} import std.fabric_blob { fabric_blob_reading_path } import std.cache_identity { native_module_verdict_bundle_artifact_kind, typed_module_artifact_kind } import gunbc.materialization_store_budgets { @@ -627,6 +629,7 @@ fn hold_refusal_is_busy(r: LocalStoreHoldRefusal?) -> Bool { LocalStoreHoldWindowTooNarrow { k: _ } => false LocalStoreHoldUnsettled { settlement: _ } => false LocalStoreHoldStale { observed: _ } => false + LocalStoreHoldNamedWindowHeadMoved { named: _ } => false LocalStoreHoldWrongFamily { held: _, requested: _ } => false } Absent => false @@ -661,10 +664,11 @@ test fn a_commit_while_the_family_is_held_refuses_and_writes_nothing_by_real_exe verdict && removed.success } -fn hold_refusal_is_stale(r: LocalStoreHoldRefusal?) -> Bool { +fn hold_refusal_is_named_window_head_moved(r: LocalStoreHoldRefusal?) -> Bool { match r { Present { value: x } => match x { - LocalStoreHoldStale { observed: _ } => true + LocalStoreHoldNamedWindowHeadMoved { named: _ } => true + LocalStoreHoldStale { observed: _ } => false LocalStoreFamilyBusy { holder: _, evidence: _ } => false LocalStoreHolderUnobservable { holder: _, cause: _ } => false LocalStoreHoldRecoveryRefused { refusal: _ } => false @@ -682,8 +686,9 @@ fn hold_refusal_is_stale(r: LocalStoreHoldRefusal?) -> Bool { } // RELEASE-THEN-MUTATE REFUSES. A bracket body returns the held value it was given, so it outlives its -// release. Committing under that leaked value re-observes the family's hold slot, finds it no longer -// held at that generation by that owner, and refuses as a stale hold; nothing is written. +// release. Committing under that leaked value named-verifies G with G+1 present (the free successor +// the release published) and refuses as NamedWindowHeadMoved; nothing is written. The listing path +// used to decode the new free head as LocalStoreHoldStale; named verify does not observe that head. test fn a_hold_leaked_out_of_its_bracket_is_refused_as_stale_by_real_execution() -> Bool { let root_path = fresh_root_path() let root = WitnessScratchRoot { root: root_path } @@ -695,7 +700,7 @@ test fn a_hold_leaked_out_of_its_bracket_is_refused_as_stale_by_real_execution() LocalStoreBracketRan { value: leaked, hold_release: _ } => { let rs = bundle_req(module: "s") let c = local_store_commit_under(held: leaked, req: rs, payloads: verdicts(text: "s pass"), parts: []) - commit_tag(c: c.commit) == "occupancy_unavailable" && hold_refusal_is_stale(r: c.hold_refusal) + commit_tag(c: c.commit) == "occupancy_unavailable" && hold_refusal_is_named_window_head_moved(r: c.hold_refusal) && !Filesystem.Read(path: object_path(root_path: root_path, req: rs)).success && store_lookup_is_miss(l: local_store_lookup(capability: store, req: rs)) } @@ -1339,3 +1344,66 @@ test fn a_leftover_copy_that_cannot_be_removed_leaves_the_store_unwritable_by_re let removed = shell.Remove.RecursiveForce(path: root_path) verdict && removed.success } + +fn hold_slot_head_generation(root_path: String) -> Int { + let prefix = concat(local_store_hold_key(family: native_module_verdict_bundle_artifact_kind) as String, ".") + let listing = Filesystem.List(path: root_path) + match filesystem_listing_observation(directory: root_path, success: listing.success, entries: listing.entries, error: listing.error) { + FilesystemDirectoryListed(listed) => + fold(filesystem_listing_entry_names(listing: listed), init: 0, f: (best, n) => + if !starts_with(s: n, prefix: prefix) { best } else { + match local_store_digits_int(s: substring(s: n, start: string_length(s: prefix), end: string_length(s: n))) { + Absent => best + Present { value: g } => if g > best { g } else { best } + } + } + ) + FilesystemDirectoryListingRefused { directory: _, error: _ } => 0 + FilesystemDirectorySubjectRefused { directory: _, cause: _ } => 0 + } +} + +fn session_commit_ok(c: LocalStoreCommit) -> Bool { + commit_tag(c: c.commit) == "committed" +} + +// N COMMITS UNDER ONE SESSION ACQUIRE THE HOLD ONCE. Open already takes and releases the family +// hold (the sweep), so the discriminator is the DELTA after open: one session acquire+release +// advances the slot by 2; N per-commit brackets advance it by 2N. If the session were secretly +// the per-commit path, the two deltas would match and this claim would fail. Lookups run under +// the same session. This measures hold-acquire count, not root-List count: occupancy writes still +// list the store. +test fn n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution() -> Bool { + let n = 6 + let session_root_path = fresh_root_path() + let bracket_root_path = fresh_root_path() + let session_root = WitnessScratchRoot { root: session_root_path } + let bracket_root = WitnessScratchRoot { root: bracket_root_path } + let session_ok = match opened(root: session_root) { + Absent => false + Present { value: store } => { + let before = hold_slot_head_generation(root_path: session_root_path) + match local_store_held_session(capability: store, family: native_module_verdict_bundle_artifact_kind, run: h => { + let writes = [1, 2, 3, 4, 5, 6] |> map(i => local_store_commit_under(held: h, req: bundle_req(module: concat("s", i as String)), payloads: verdicts(text: "s pass"), parts: [])) + let hits = [1, 2, 3, 4, 5, 6] |> map(i => store_lookup_is_hit(l: local_store_lookup(capability: store, req: bundle_req(module: concat("s", i as String))))) + writes |> all(c => session_commit_ok(c: c)) && hits |> all(x => x) + }) { + LocalStoreBracketRefused { refusal: _ } => false + LocalStoreBracketRan { value: body_ok, hold_release: _ } => + body_ok && (hold_slot_head_generation(root_path: session_root_path) - before) == 2 + } + } + } + let bracket_ok = match opened(root: bracket_root) { + Absent => false + Present { value: store } => { + let before = hold_slot_head_generation(root_path: bracket_root_path) + let writes = [1, 2, 3, 4, 5, 6] |> map(i => local_store_commit(capability: store, req: bundle_req(module: concat("b", i as String)), payloads: verdicts(text: "b pass"), parts: [])) + writes |> all(c => session_commit_ok(c: c)) && (hold_slot_head_generation(root_path: bracket_root_path) - before) == (2 * n) + } + } + let removed_s = shell.Remove.RecursiveForce(path: session_root_path) + let removed_b = shell.Remove.RecursiveForce(path: bracket_root_path) + session_ok && bracket_ok && removed_s.success && removed_b.success +} + diff --git a/dag/test/claim/merge_admission_witness_test.dag b/dag/test/claim/merge_admission_witness_test.dag index e19c4517a3e..65afe2d23d5 100644 --- a/dag/test/claim/merge_admission_witness_test.dag +++ b/dag/test/claim/merge_admission_witness_test.dag @@ -84,7 +84,10 @@ test fn gate_roster_hash_unchanged_on_surface_only_move() -> Bool { test fn gunbc_repo_standard_is_squash_only() -> Bool { gunbc_repo_standard.default_branch == "main" && length(gunbc_repo_standard.allowed_strategies) == 1 - && gunbc_repo_standard.allowed_strategies.first() == SquashMerge + && match gunbc_repo_standard.allowed_strategies.first() { + Present { value: strategy } => strategy == SquashMerge + Absent => false + } && gunbc_repo_standard.require_up_to_date == false } diff --git a/dag/test/claim/mt_collins_dimm_physical_identity_witness_test.dag b/dag/test/claim/mt_collins_dimm_physical_identity_witness_test.dag index de4e63794a6..54bc5ed7652 100644 --- a/dag/test/claim/mt_collins_dimm_physical_identity_witness_test.dag +++ b/dag/test/claim/mt_collins_dimm_physical_identity_witness_test.dag @@ -86,7 +86,13 @@ test fn w_every_figure_connector_resolves() -> Bool { DimmDiagramIdentityRefused { connector: _, cause: _ } => false })) && match resolve_dimm_diagram_identity(connector: 16, banks: dimm_figure_banks, pairs: mt_collins_channel_connector_pairs) { - DimmDiagramIdentityResolved { identity: i } => i.diagram_label == "J16" && i.bank.connectors_left_to_right.first() == 16 && i.bank.connectors_left_to_right.last() == 9 && i.channel.memory_controller == 7 && i.legend.slot_number == 1 && i.legend.drawing_colour == "blue" && i.bank.side == RightOfProcessor + DimmDiagramIdentityResolved { identity: i } => i.diagram_label == "J16" && match i.bank.connectors_left_to_right.first() { + Present { value: c } => c == 16 + Absent => false + } && match i.bank.connectors_left_to_right.last() { + Present { value: c } => c == 9 + Absent => false + } && i.channel.memory_controller == 7 && i.legend.slot_number == 1 && i.legend.drawing_colour == "blue" && i.bank.side == RightOfProcessor DimmDiagramIdentityRefused { connector: _, cause: _ } => false } } @@ -97,7 +103,14 @@ test fn w_sixteen_dimm_join_keeps_orientation_and_physical_gaps() -> Bool { list_length(items: slots) == p.dimm_count && p.dimm_count == 16 && o.left_processor_socket == 0 && o.right_processor_socket == 1 && list_length(items: gaps) == 2 - && colours.first() == "blue" && colours.last() == "black" + && match colours.first() { + Present { value: c } => c == "blue" + Absent => false + } + && match colours.last() { + Present { value: c } => c == "black" + Absent => false + } && citation.figure == 19 && citation.page == 52 && mirror.standing == RecordedReadBytesAgree && fold(slots, init: true, f: (acc, i) => acc && i.legend.slot_number == 0 && i.legend.drawing_colour == "yellow") PopulationDiagramRefused { unresolved: _ } => false @@ -120,7 +133,11 @@ test fn w_bad_population_never_returns_partial_slots() -> Bool { PopulationConnectorRepeated { connectors: _ } => false } let repeated = match population_diagram_identity(population: MtCollinsDimmPopulationRow { dimm_count: 2, connectors: [1, 1] }) { - PopulationConnectorRepeated { connectors: cs } => cs.first() == 1 + PopulationConnectorRepeated { connectors: cs } => + match cs.first() { + Present { value: c } => c == 1 + Absent => false + } PopulationDiagramResolved { population: _, slots: _, orientation: _, physical_gaps: _, photographed_colours: _, user_guide_citation: _, mirror_read: _ } => false PopulationDiagramRefused { unresolved: _ } => false PopulationCountDisagrees { declared: _, listed: _ } => false diff --git a/dag/test/claim/mt_collins_population_role_witness_test.dag b/dag/test/claim/mt_collins_population_role_witness_test.dag index cdc0699d8dc..03686ea0956 100644 --- a/dag/test/claim/mt_collins_population_role_witness_test.dag +++ b/dag/test/claim/mt_collins_population_role_witness_test.dag @@ -81,10 +81,14 @@ test fn w_the_source_pairs_all_resolve() -> Bool { match mt_collins_channel_population_roles { PopulationRolesResolved { roles: rs } => list_length(items: rs) == 16 - && rs.first().connector_used_at_one_dpc == 1 - && rs.first().additional_connector_at_two_dpc == 2 - && rs.last().connector_used_at_one_dpc == 25 - && rs.last().additional_connector_at_two_dpc == 26 + && match rs.first() { + Present { value: r } => r.connector_used_at_one_dpc == 1 && r.additional_connector_at_two_dpc == 2 + Absent => false + } + && match rs.last() { + Present { value: r } => r.connector_used_at_one_dpc == 25 && r.additional_connector_at_two_dpc == 26 + Absent => false + } && fold(rs, init: true, f: fn(acc, r) { acc && (r.connector_used_at_one_dpc < r.additional_connector_at_two_dpc) }) diff --git a/dag/test/claim/native_effect_realization_admission_witness_test.dag b/dag/test/claim/native_effect_realization_admission_witness_test.dag new file mode 100644 index 00000000000..09bcac85a91 --- /dev/null +++ b/dag/test/claim/native_effect_realization_admission_witness_test.dag @@ -0,0 +1,136 @@ +module test.claim.native_effect_realization_admission_witness + +import std.types { String, Bool, List } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.compile_diagnostic_census { CompileDiagnosticCensusRow } +import tools.multi_module_compile_fixture { + MultiModuleCompileFixture, + MultiModuleCompileFixtureOutcome, + FixtureInstrumentRefused, + FixtureCompileRefused, + FixtureCompileCompleted, + FixtureSource, + fixture_source, + compile_fixture, + fixture_rows_of_class +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE SUBJECT: v1.compiler.emit NativeEffectRealizationAdmission, computed by v1.compiler.emit_rust +// native_entry_effects for a native entry's function. An effect it does not admit REFUSES EMISSION +// with NativeEffectRealizationRefused, typed and naming the arm; before it, the rendered main wrote +// its handles by hand, and an effect it did not write surfaced only as a rustc error at the handler +// call, or for a resource trait with no implementor as a link failure. The executed half -- that +// the handlers it admits are bound and answer -- is gunbc test //gunbc/instruments:native-serve. +// +// The fixtures are isolated (tools.multi_module_compile_fixture), so each declares the vocabulary +// it needs. The denominators (std.compiler_entry native_*_route_denominator) name effects by +// DECLARATION IDENTITY, so a fixture that means the real clock or the real Entropy declares it in the +// module that owns it (extdeps.clock, std.resources), and the homonym case declares a Clock elsewhere +// to show a spelling alone decides nothing. + +data fx_entry_driver_source: String = "module nefx.entry_driver\ntype CompilerEntryDriver = NativeServeDriver | NativeCliDriver | NativeClaimDriver\ntype ServeWireResponse { status: Int, body: String }\n" + +// The real clock: extdeps.clock Clock, a service on the shell transport. +data fx_extdeps_clock_source: String = "module extdeps.clock\nservice Clock {\n operation Now {\n requires none\n input {}\n output { timestamp: String from \"stdout\" }\n readonly\n transport shell { argv: [\"date\", \"-u\"] }\n exit {\n 0 => Unit\n nonzero => String \"date failed\"\n }\n }\n}\n" + +// A HOMONYM: a service spelled Clock that is not extdeps.clock Clock. +data fx_homonym_clock_source: String = "module nefx.other\nservice Clock {\n operation Now {\n requires none\n input {}\n output { timestamp: String from \"stdout\" }\n readonly\n transport shell { argv: [\"date\", \"-u\"] }\n exit {\n 0 => Unit\n nonzero => String \"date failed\"\n }\n }\n}\n" + +// std.resources Entropy, declared where it lives (no capability, so only the ruling can refuse it). +data fx_std_resources_source: String = "module std.resources\nresource Entropy {\n kind: Observation\n mode: Read\n acquire {}\n release {}\n}\n" + +// A REST transport under an innocuous name: outgoing HTTP by what it binds, not by what it is called. +data fx_rest_result_source: String = "module extdeps.transports.rest\ntype RestResult = RestAnswered { answer: Body } | RestRefused { refusal: String }\n" +data fx_fetch_service_source: String = "module nefx.fetch\nimport extdeps.transports.rest { RestResult }\nservice Fetcher {\n config {\n endpoint: \"http://127.0.0.1\"\n }\n operation Get {\n requires none\n input {}\n output { live: Bool from \"live\" }\n readonly\n transport rest { method: GET, path: \"/livez\" }\n response {\n 200 => Bool\n }\n }\n}\n" + +// A resource with a capability renders as a trait with no implementor: Unavailable. +data fx_serve_resource_trait_source: String = "module nefx.serve\nimport nefx.entry_driver { CompilerEntryDriver, NativeServeDriver, ServeWireResponse }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeServeDriver\nresource Clock {\n kind: Observation\n mode: Read\n acquire {}\n release {}\n capability now {\n input {}\n output { timestamp: String }\n }\n}\nfn native_serve_handle(method: String, path: String, body: String, tailscale_identity: String, release_revision: String) -> ServeWireResponse\n uses clk: Clock\n{\n ServeWireResponse { status: 200, body: \"\" }\n}\n" + +data fx_serve_entropy_source: String = "module nefx.serve\nimport nefx.entry_driver { CompilerEntryDriver, NativeServeDriver, ServeWireResponse }\nimport std.resources { Entropy }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeServeDriver\nfn native_serve_handle(method: String, path: String, body: String, tailscale_identity: String, release_revision: String) -> ServeWireResponse\n uses rng: Entropy\n{\n ServeWireResponse { status: 200, body: \"\" }\n}\n" + +data fx_cli_clock_source: String = "module nefx.cli\nimport nefx.entry_driver { CompilerEntryDriver, NativeCliDriver }\nimport extdeps.clock { Clock }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver\nfn v2_cli_main(argv: List) -> String {\n let r = Clock.Now()\n r.timestamp\n}\n" + +data fx_cli_homonym_source: String = "module nefx.cli\nimport nefx.entry_driver { CompilerEntryDriver, NativeCliDriver }\nimport nefx.other { Clock }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver\nfn v2_cli_main(argv: List) -> String {\n let r = Clock.Now()\n r.timestamp\n}\n" + +data fx_cli_fetch_source: String = "module nefx.cli\nimport nefx.entry_driver { CompilerEntryDriver, NativeCliDriver }\nimport nefx.fetch { Fetcher }\nimport extdeps.transports.rest { RestResult, RestAnswered, RestRefused }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver\nfn v2_cli_main(argv: List) -> Bool {\n match Fetcher.Get() {\n RestAnswered { answer } => answer\n RestRefused { refusal } => false\n }\n}\n" + +data fx_claim_clock_source: String = "module nefx.claim\nimport nefx.entry_driver { CompilerEntryDriver, NativeClaimDriver }\nimport extdeps.clock { Clock }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeClaimDriver\nfn native_claim_report() -> String {\n let r = Clock.Now()\n r.timestamp\n}\n" + +data fx_serve_clock_source: String = "module nefx.serve\nimport nefx.entry_driver { CompilerEntryDriver, NativeServeDriver, ServeWireResponse }\nimport extdeps.clock { Clock }\ndata compiler_pipeline_entry: CompilerEntryDriver = NativeServeDriver\nfn native_serve_handle(method: String, path: String, body: String, tailscale_identity: String, release_revision: String) -> ServeWireResponse {\n let r = Clock.Now()\n ServeWireResponse { status: 200, body: r.timestamp }\n}\n" + +fn fx_run(entry_path: String, entry_source: String, peers: List) -> MultiModuleCompileFixtureOutcome { + compile_fixture(MultiModuleCompileFixture { + sources: concat([fixture_source("nefx/entry_driver.dag", fx_entry_driver_source), fixture_source(entry_path, entry_source)], peers), + entry: entry_path + }) +} + +fn fx_extdeps_clock() -> FixtureSource { fixture_source("extdeps/clock.dag", fx_extdeps_clock_source) } + +fn refusal_rows(o: MultiModuleCompileFixtureOutcome) -> List { + match o { + FixtureCompileRefused { module_count: _, diagnostics: rows, source_digest: _, compiler_digest: _ } => + fixture_rows_of_class(rows, "NativeEffectRealizationRefused") + FixtureCompileCompleted { module_count: _, emitted_files: _, resolved_rust_functions: _, diagnostics: rows, source_digest: _, compiler_digest: _ } => + fixture_rows_of_class(rows, "NativeEffectRealizationRefused") + FixtureInstrumentRefused { cause: _ } => [] + } +} + +// Refused by the admission, on the arm named by `marker` -- not merely "something refused", which a +// typo in the fixture would also satisfy. +fn refused_naming(o: MultiModuleCompileFixtureOutcome, marker: String) -> Bool { + let rows = refusal_rows(o: o) + match o { + FixtureCompileRefused { module_count: _, diagnostics: _, source_digest: _, compiler_digest: _ } => + (rows |> filter(r => r.blocking && string_contains(s: r.subject_name, pattern: marker)) |> count) == 1 + _ => false + } +} + +fn admitted(o: MultiModuleCompileFixtureOutcome) -> Bool { + match o { + FixtureCompileCompleted { module_count: _, emitted_files: files, resolved_rust_functions: _, diagnostics: _, source_digest: _, compiler_digest: _ } => + (refusal_rows(o: o) |> count) == 0 && (files |> count) > 0 + _ => false + } +} + +test fn serve_entry_demanding_a_resource_trait_refuses_as_unavailable() -> Bool { + refused_naming(o: fx_run(entry_path: "nefx/serve.dag", entry_source: fx_serve_resource_trait_source, peers: []), + marker: "resource nefx.serve Clock (uses clk) has no native realization: it is a resource trait with 1 capabilities and no native implementor") +} + +test fn serve_entry_demanding_entropy_refuses_as_outside_the_denominator() -> Bool { + refused_naming(o: fx_run(entry_path: "nefx/serve.dag", entry_source: fx_serve_entropy_source, peers: [fixture_source("std/resources.dag", fx_std_resources_source)]), + marker: "resource std.resources Entropy (uses rng) is outside the NativeServeDriver denominator (ruling: gunbc.plans.demand_engine_program demand_engine_program_plan)") +} + +test fn cli_entry_reaching_a_clock_service_refuses_as_outside_the_compiler_route() -> Bool { + refused_naming(o: fx_run(entry_path: "nefx/cli.dag", entry_source: fx_cli_clock_source, peers: [fx_extdeps_clock()]), + marker: "service extdeps.clock Clock is outside the NativeCliDriver denominator (ruling: gunbc.plans.demand_engine_program demand_engine_program_plan)") +} + +test fn cli_entry_reaching_a_rest_service_refuses_as_outgoing_http() -> Bool { + refused_naming(o: fx_run(entry_path: "nefx/cli.dag", entry_source: fx_cli_fetch_source, peers: [fixture_source("nefx/fetch.dag", fx_fetch_service_source), fixture_source("extdeps/transports/rest.dag", fx_rest_result_source)]), + marker: "service nefx.fetch Fetcher is outside the NativeCliDriver denominator") +} + +test fn claim_entry_reaching_a_clock_service_refuses_as_outside_its_declared_set() -> Bool { + refused_naming(o: fx_run(entry_path: "nefx/claim.dag", entry_source: fx_claim_clock_source, peers: [fx_extdeps_clock()]), + marker: "service extdeps.clock Clock is outside the NativeClaimDriver denominator (ruling: std.compiler_entry NativeClaimDriver)") +} + +// THE IDENTITY CONTROL: a service SPELLED Clock that is not extdeps.clock Clock is not excluded from +// the compiler route. Matching by spelling would refuse it; matching by declaration does not. +test fn cli_entry_reaching_a_homonym_clock_is_judged_by_identity() -> Bool { + admitted(o: fx_run(entry_path: "nefx/cli.dag", entry_source: fx_cli_homonym_source, peers: [fixture_source("nefx/other.dag", fx_homonym_clock_source)])) +} + +// THE POSITIVE CONTROL. Without it every RED above is satisfied by an admission that refuses +// everything: the real clock under a serve entry is admitted. +test fn serve_entry_reaching_a_clock_service_is_admitted() -> Bool { + admitted(o: fx_run(entry_path: "nefx/serve.dag", entry_source: fx_serve_clock_source, peers: [fx_extdeps_clock()])) +} diff --git a/dag/test/claim/nbd_proxy_endpoint_witness_test.dag b/dag/test/claim/nbd_proxy_endpoint_witness_test.dag index ea040d2a257..a300f279eae 100644 --- a/dag/test/claim/nbd_proxy_endpoint_witness_test.dag +++ b/dag/test/claim/nbd_proxy_endpoint_witness_test.dag @@ -9,5 +9,8 @@ test fn altrad8ud_nbd_proxy_primary_endpoint_is_vm_slot_index() -> Bool { test fn altrad8ud_nbd_proxy_nbd_slot_absent_on_2_07_00() -> Bool { list_length(items: asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_absent_endpoints) == 1 - && asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_absent_endpoints.first() == NbdSlotEndpoint + && match asrock_altrad8ud_openbmc_2_07_00_nbd_proxy_absent_endpoints.first() { + Present { value: e } => e == NbdSlotEndpoint + Absent => false + } } diff --git a/dag/test/claim/openrouter_hosted_route_witness_test.dag b/dag/test/claim/openrouter_hosted_route_witness_test.dag new file mode 100644 index 00000000000..338e194efb5 --- /dev/null +++ b/dag/test/claim/openrouter_hosted_route_witness_test.dag @@ -0,0 +1,703 @@ +module test.claim.openrouter_hosted_route_witness_test + +import std.types { Bool, String, Int, List, NonEmptyStr } +import std.nat { Nat } +import std.optional { Present, Absent } +import extdeps.languages.json.emit { serialize_json, json_object } +import extdeps.openrouter.openrouter { + openrouter_decode_error_body, OpenRouterErrorDecoded, OpenRouterErrorUndecodable, + OpenRouterPaymentRequired, OpenRouterRateLimited, OpenRouterOtherError, + KeySpendLimit, InFlightBudgetLimit, CreditsLimit, WeightExceedsBudget, + openrouter_decode_key_read, OpenRouterKeyDecoded, OpenRouterKeyUndecodable, + KeySpendCappedAtZero, KeySpendUncapped, KeySpendCappedAbove, + openrouter_credits_from_daily_limit, CreditsTierObserved, CreditsTierUnrecognized, + CreditsPurchasedAtLeastTen, CreditsPurchasedBelowTen, openrouter_free_models_per_day, openrouter_free_models_per_minute, + EffortHigh, EffortMedium, + openrouter_decode_rate_limited, OpenRouterRateLimitRefusal, RetryAfterSeconds, RetryAfterUnparsed, RetryAfterAbsent, RetryAfterUnread, + openrouter_rate_limit_wire, openrouter_rate_limit_hints, HeaderDumpRead, HeaderDumpUnread, +} +import gunbc.harness.harness_reasoning_wire { + NemotronUltraFreeOnOpenRouter, ReasoningOff, ReasoningStandard, ReasoningMaximum, + harness_openrouter_reasoning_kwargs, harness_reasoning_kwargs_for, ReasoningKwargsAdmitted, ReasoningLevelUnsupported, + harness_unit_model_id, harness_unit_placement, LeasedFromHostedQuota, PlacedOnFleetSeat, +} +import gunbc.harness.harness_hosted_route { + hosted_round_decision, RoundBothLeased, RoundRefusedFull, RoundRefusedUnleasable, + harness_hosted_route_for, harness_hosted_level_choice, HostedLevelSelected, HostedLevelUnselected, + hosted_key_refusal, hosted_day_remaining, hosted_access_from_key, harness_hosted_round_charge, HostedRoundCharged, HostedRoundQuotaFull, HostedRoundUnleasable, +} +import product.capacity.pool_events { PoolEvent, PoolAcquired, PoolSettled, PoolReleased, PoolUpstreamObserved, pool_apply_payload, pool_fold, PoolFolded, PoolFoldRefused } +import product.capacity.event_chain { ChainEnvelope, ChainEvent, EventId } +import gunbc.fabric_quota { + quota_term_to_window_end, quota_window_start, fabric_quota_lease_for, fabric_quota_settle_at, QuotaTermToWindowEnd, + QuotaSettled, QuotaSettleRefused, +} +import gunbc.harness.harness_seat { harness_seat_pool_root, hosted_inflight_carried, hosted_inflight_release_hint, hosted_inflight_partition } +import gunbc.harness.harness_hosted_route { HarnessHostedQuota } +import product.capacity.event_chain { ChainWalked } +import std.nat { nat_range_inclusive } +import product.capacity.quota { quota_root_pool } +import product.capacity.pool { + Pool, pool_acquire, pool_settle, observe_upstream_remaining, PoolOutcome, PoolAdvanced, PoolRefused, + LeaseCrossesLapse, LeaseAlreadyLapsed, LedgerRefusal, pool_reading_at, PoolRead, PoolReadingRefused, +} +import std.measure { Measure, Count, One, second_count } +import extdeps.api_rate_limit { UpstreamRateLimit } +import gunbc.fabric_quota { QuotaLease, QuotaLeased, QuotaFull, QuotaLeaseRefused } +import product.capacity.lease { LeaseGrant, LeaseFence, GrantIdentity } +import product.capacity.event_chain { PartitionId } +import gunbc.fabric_storage_client { FabricStorageServed } +import gunbc.harness.harness_turn { TurnHostedQuotaFull, TurnHostedRateLimited, harness_outcome_label, harness_outcome_detail } +import gunbc.harness.harness_cli { harness_turn_exit, harness_default_max_token_count, harness_request_deadline } +import std.process { ExitSuccess, ExitFailure } +import std.measure { token_count } + +// ── THE QUOTA REFUSAL: a full pool stops the request before it is sent ────────────────────── +// +// Supplied lease answers at the decision's own interface (DESIGN section 3, a witness discriminates at one +// interface): the leases are gunbc.fabric_quota's subject and are witnessed there. + +fn w_leased(reference: String) -> QuotaLease { + QuotaLeased { + grant: LeaseGrant { + reference: reference as NonEmptyStr, + fence: LeaseFence { grant: join(["g-", reference], "") as GrantIdentity, generation: 1 }, + granted_at: 100, + maximum_duration_seconds: 60, + expires_at: 160, + }, + partition: join(["quota-openrouter-", reference], "") as PartitionId, + store: FabricStorageServed { endpoint: "witness" as NonEmptyStr }, + limit: openrouter_free_models_per_minute, + } +} + +// RED: the day pool is full after the minute pool leased. The round refuses as FULL, and the minute +// lease is named as owed back -- a refusal that kept it would hold a slot for a request never sent. +test fn hosted_round_refuses_full_when_the_day_pool_is_full_and_returns_the_minute_lease() -> Bool { + match hosted_round_decision(minute: w_leased(reference: "m"), day: Present { value: QuotaFull { wire: "openrouter-free-models-day: 1000/1000" } }) { + RoundRefusedFull { wire: w, minute_to_return: Present { value: QuotaLeased { grant: g, partition: _, store: _, limit: _ } } } => + w.contains("free-models-day") && (g.reference as String) == "m" + RoundRefusedFull { wire: _, minute_to_return: _ } => false + RoundBothLeased { minute: _, day: _ } => false + RoundRefusedUnleasable { detail: _, minute_to_return: _ } => false + } +} + +// RED: the minute pool is full, so the day pool is never asked and nothing is owed back. +test fn hosted_round_refuses_full_when_the_minute_pool_is_full_and_asks_nothing_else() -> Bool { + match hosted_round_decision(minute: QuotaFull { wire: "openrouter-free-models-minute: 20/20" }, day: none) { + RoundRefusedFull { wire: w, minute_to_return: Absent } => w.contains("free-models-minute") + RoundRefusedFull { wire: _, minute_to_return: Present { value: _ } } => false + RoundBothLeased { minute: _, day: _ } => false + RoundRefusedUnleasable { detail: _, minute_to_return: _ } => false + } +} + +// CONTROL: both pools leased is the only arm that posts. +test fn hosted_round_posts_only_when_both_pools_leased() -> Bool { + match hosted_round_decision(minute: w_leased(reference: "m"), day: Present { value: w_leased(reference: "d") }) { + RoundBothLeased { minute: _, day: _ } => true + RoundRefusedFull { wire: _, minute_to_return: _ } => false + RoundRefusedUnleasable { detail: _, minute_to_return: _ } => false + } +} + +// A FULL ROUND IS A REFUSAL EXIT, NOT A COMPLETED TURN. +test fn a_hosted_quota_full_turn_exits_as_a_refusal() -> Bool { + let o = TurnHostedQuotaFull { wire: "openrouter-free-models-minute: 20/20", steps_taken: 2 } + harness_outcome_label(outcome: o) == "hosted-quota-full" + && (match harness_turn_exit(outcome: o) { ExitSuccess => false ExitFailure { code, reason: _ } => code == 1 }) +} + +// ── THE 402 DECODE: limit_source is a closed set and names which budget refused ───────────── + +test fn a_402_from_the_key_limit_decodes_to_the_key_spend_limit() -> Bool { + match openrouter_decode_error_body(body: "{\"error\":{\"code\":402,\"message\":\"Key limit exceeded\",\"metadata\":{\"limit_source\":\"openrouter_key_limit\",\"remedy_hint\":\"raise the key limit\"}}}") { + OpenRouterErrorDecoded { error: OpenRouterPaymentRequired { limit_source: KeySpendLimit, reason: Absent, message: m, remedy_hint: Present { value: _ } } } => m == "Key limit exceeded" + OpenRouterErrorDecoded { error: _ } => false + OpenRouterErrorUndecodable { cause: _ } => false + } +} + +test fn a_402_from_the_in_flight_budget_carries_its_reason() -> Bool { + match openrouter_decode_error_body(body: "{\"error\":{\"code\":402,\"message\":\"busy\",\"metadata\":{\"limit_source\":\"openrouter_in_flight_budget\",\"reason\":\"weight_exceeds_budget\"}}}") { + OpenRouterErrorDecoded { error: OpenRouterPaymentRequired { limit_source: InFlightBudgetLimit, reason: Present { value: WeightExceedsBudget }, message: _, remedy_hint: Absent } } => true + OpenRouterErrorDecoded { error: _ } => false + OpenRouterErrorUndecodable { cause: _ } => false + } +} + +// RED: an unpublished limit_source refuses rather than decoding as the nearest of three. +test fn a_402_with_an_unpublished_limit_source_refuses_to_decode() -> Bool { + match openrouter_decode_error_body(body: "{\"error\":{\"code\":402,\"message\":\"x\",\"metadata\":{\"limit_source\":\"openrouter_mystery_budget\"}}}") { + OpenRouterErrorUndecodable { cause: c } => c.contains("openrouter_mystery_budget") + OpenRouterErrorDecoded { error: _ } => false + } +} + +// RED: a 402 naming no budget is not decodable as a payment refusal. +test fn a_402_without_a_limit_source_refuses_to_decode() -> Bool { + match openrouter_decode_error_body(body: "{\"error\":{\"code\":402,\"message\":\"x\"}}") { + OpenRouterErrorUndecodable { cause: c } => c.contains("limit_source") + OpenRouterErrorDecoded { error: _ } => false + } +} + +test fn a_429_names_whose_limit_it_was() -> Bool { + match openrouter_decode_error_body(body: "{\"error\":{\"code\":429,\"message\":\"slow down\",\"metadata\":{\"error_type\":\"rate_limit_exceeded\",\"provider_code\":\"nvidia\"}}}") { + OpenRouterErrorDecoded { error: OpenRouterRateLimited { message: _, provider_code: Present { value: p } } } => p == "nvidia" + OpenRouterErrorDecoded { error: _ } => false + OpenRouterErrorUndecodable { cause: _ } => false + } +} + +// ── THE REASONING LEVEL: mapped to a listed effort or refused ─────────────────────────────── + +// RED: a listing whose supported efforts lack medium refuses the standard level instead of sending it. +test fn a_level_mapping_to_an_unlisted_effort_is_refused() -> Bool { + match harness_openrouter_reasoning_kwargs(unit: NemotronUltraFreeOnOpenRouter, level: ReasoningStandard, supported: [EffortHigh], mandatory: false) { + ReasoningLevelUnsupported { unit: _, level: _, cause: c } => (c as String).contains("medium") + ReasoningKwargsAdmitted { kwargs: _ } => false + } +} + +// RED: a listing that makes reasoning mandatory refuses the off level. +test fn reasoning_off_is_refused_where_the_listing_makes_reasoning_mandatory() -> Bool { + match harness_openrouter_reasoning_kwargs(unit: NemotronUltraFreeOnOpenRouter, level: ReasoningOff, supported: [EffortHigh, EffortMedium], mandatory: true) { + ReasoningLevelUnsupported { unit: _, level: _, cause: _ } => true + ReasoningKwargsAdmitted { kwargs: _ } => false + } +} + +// CONTROL: the same mapping against the real listing's row sends OpenRouter's reasoning object, never the +// model's chat-template keys. +test fn the_standard_level_sends_effort_medium_on_the_real_listing() -> Bool { + match harness_reasoning_kwargs_for(unit: NemotronUltraFreeOnOpenRouter, level: ReasoningStandard) { + ReasoningKwargsAdmitted { kwargs: k } => { + let wire = serialize_json(v: json_object(k)) + wire.contains("\"effort\": \"medium\"") && !wire.contains("chat_template_kwargs") + } + ReasoningLevelUnsupported { unit: _, level: _, cause: _ } => false + } +} + +// ── THE KEY READ: a key that can spend money is refused ───────────────────────────────────── + +test fn a_key_capped_at_zero_on_the_thousand_tier_decodes_both_facts() -> Bool { + match openrouter_decode_key_read(body: "{\"data\":{\"limit\":0,\"limit_remaining\":0,\"usage\":0,\"is_free_tier\":false,\"free_model_daily_requests\":{\"used\":3,\"limit\":1000,\"remaining\":997}}}") { + OpenRouterKeyDecoded { key: k } => + (match k.spend_limit { KeySpendCappedAtZero => true KeySpendUncapped => false KeySpendCappedAbove { lexeme: _ } => false }) + && (match openrouter_credits_from_daily_limit(daily_limit: k.free_daily_limit) { + CreditsTierObserved { credits: CreditsPurchasedAtLeastTen } => true + CreditsTierObserved { credits: CreditsPurchasedBelowTen } => false + CreditsTierUnrecognized { daily_limit: _ } => false + }) + OpenRouterKeyUndecodable { cause: _ } => false + } +} + +// RED: an uncapped key is refused by name. +test fn an_uncapped_key_is_refused_as_able_to_spend() -> Bool { + match openrouter_decode_key_read(body: "{\"data\":{\"limit\":null,\"is_free_tier\":false,\"free_model_daily_requests\":{\"used\":0,\"limit\":1000,\"remaining\":1000}}}") { + OpenRouterKeyDecoded { key: k } => hosted_key_refusal(key: k).contains("no credit limit") + OpenRouterKeyUndecodable { cause: _ } => false + } +} + +// THE TIER SETS THE DAY POOL'S CEILING, and an unpublished ceiling is not a tier. +test fn the_daily_tier_selects_the_day_pool_ceiling_and_an_unpublished_one_refuses() -> Bool { + openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen).requests == 1000 + && openrouter_free_models_per_day(credits: CreditsPurchasedBelowTen).requests == 50 + && (match openrouter_credits_from_daily_limit(daily_limit: 200) { CreditsTierUnrecognized { daily_limit: n } => n == 200 CreditsTierObserved { credits: _ } => false }) +} + +// ── INHABITANCE ON THE REAL PATH ───────────────────────────────────────────────────────────── +// +// The real unit's real route, joined from the real listing rows, reaches the real decisions: it is placed +// by a hosted quota lease rather than a seat; its route is HTTPS and names the listing's model id; the +// selection over the real field chooses the maximum level for the harness's real max_tokens; and that level +// renders effort high. Deleting the hosted arm from any of harness_unit_placement, harness_hosted_route_for, +// the selection or the reasoning seam turns this red. +test fn the_hosted_nemotron_route_is_quota_leased_https_and_selects_effort_high() -> Bool { + match harness_hosted_route_for(unit: NemotronUltraFreeOnOpenRouter) { + Absent => false + Present { value: route } => + (match harness_unit_placement(unit: NemotronUltraFreeOnOpenRouter) { LeasedFromHostedQuota => true PlacedOnFleetSeat => false }) + && (route.url as String).starts_with("https://") + && (harness_unit_model_id(unit: NemotronUltraFreeOnOpenRouter) as String) == "nvidia/nemotron-3-ultra-550b-a55b:free" + && (match harness_hosted_level_choice(route: route, max_tokens: harness_default_max_token_count) { + HostedLevelSelected { level: ReasoningMaximum } => + (match harness_reasoning_kwargs_for(unit: NemotronUltraFreeOnOpenRouter, level: ReasoningMaximum) { + ReasoningKwargsAdmitted { kwargs: k } => serialize_json(v: json_object(k)).contains("\"effort\": \"high\"") + ReasoningLevelUnsupported { unit: _, level: _, cause: _ } => false + }) + HostedLevelSelected { level: _ } => false + HostedLevelUnselected { cause: _ } => false + }) + } +} + +// RED: a max_tokens above the listing's completion ceiling leaves no admissible level, and the selection +// refuses rather than choosing one. +test fn a_request_above_the_completion_ceiling_selects_no_level() -> Bool { + match harness_hosted_route_for(unit: NemotronUltraFreeOnOpenRouter) { + Absent => false + Present { value: route } => + match harness_hosted_level_choice(route: route, max_tokens: token_count(count: 70000)) { + HostedLevelUnselected { cause: _ } => true + HostedLevelSelected { level: _ } => false + } + } +} + +// ── THE CHARGE FITS ITS WINDOW AT EVERY INSTANT, THROUGH THE REAL POOL ADMISSION ────────────── +// +// The pools are the production pools (product.capacity.quota quota_root_pool over the real OpenRouter +// rows the hosted route carries), the term is the production derivation (gunbc.fabric_quota +// quota_term_to_window_end), and admission is product.capacity.pool pool_acquire -- the function a +// partition append adjudicates with. Only the clock is supplied: an instant one second into a UTC day +// that is also one second into its minute, and instants one second before the minute and the day end. +data w_day_start: Int = 1780531200 + +fn w_charge_admitted(limit: UpstreamRateLimit, at: Int) -> Bool { + match quota_term_to_window_end(limit: limit, at: at) { + Absent => false + Present { value: term } => + match pool_acquire(pool: quota_root_pool(limit: limit), reference: "charge" as NonEmptyStr, amount: Measure { count: 1 }, at: at, term_seconds: second_count(s: term)) { + PoolAdvanced { pool: _ } => true + PoolRefused { refusal: _ } => false + } + } +} + +test fn a_hosted_charge_fits_both_real_pools_early_and_late_in_their_windows() -> Bool { + match harness_hosted_route_for(unit: NemotronUltraFreeOnOpenRouter) { + Absent => false + Present { value: route } => { + let day = openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) + w_charge_admitted(limit: route.per_minute, at: w_day_start + 1) + && w_charge_admitted(limit: route.per_minute, at: w_day_start + 59) + && w_charge_admitted(limit: day, at: w_day_start + 1) + && w_charge_admitted(limit: day, at: w_day_start + 86399) + } + } +} + +// RED, THE DEFECT THIS REPLACED: a lease held for the response deadline cannot fit the minute window at +// any instant, so the pool refuses it as crossing the lapse. +test fn a_lease_for_the_response_deadline_crosses_the_minute_lapse() -> Bool { + match harness_hosted_route_for(unit: NemotronUltraFreeOnOpenRouter) { + Absent => false + Present { value: route } => + match pool_acquire(pool: quota_root_pool(limit: route.per_minute), reference: "held" as NonEmptyStr, amount: Measure { count: 1 }, at: w_day_start + 1, term_seconds: second_count(s: harness_request_deadline)) { + PoolRefused { refusal: LeaseCrossesLapse { reference: _, expires_at: _, window_end: _ } } => true + PoolRefused { refusal: _ } => false + PoolAdvanced { pool: _ } => false + } + } +} + +// ── THE DAY POOL STARTS FROM WHAT THE UPSTREAM SAYS REMAINS ──────────────────────────────────── +// +// The remaining count is read by the production reader off a decoded key read, recorded through the +// fold's own observe_upstream_remaining, and then asked for leases through pool_acquire. +fn w_key_remaining(used: Int, remaining: Int) -> Nat? { + match openrouter_decode_key_read(body: join(["{\"data\":{\"limit\":0,\"is_free_tier\":false,\"free_model_daily_requests\":{\"used\":", to_string(used), ",\"limit\":1000,\"remaining\":", to_string(remaining), "}}}"], "")) { + OpenRouterKeyDecoded { key: k } => + hosted_day_remaining(key: k) + OpenRouterKeyUndecodable { cause: _ } => none + } +} + +fn w_observed(pool: Pool, remaining: Nat, at: Int) -> Pool? { + let day = openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) + match quota_term_to_window_end(limit: day, at: at) { + Absent => none + Present { value: term } => + match observe_upstream_remaining(pool: pool, remaining: Measure { count: remaining }, at: at, term_seconds: second_count(s: term), reading: join(["witness@", to_string(at)], "") as NonEmptyStr) { + PoolAdvanced { pool: p } => Present { value: p } + PoolRefused { refusal: _ } => none + } + } +} + +fn w_day_headroom(pool: Pool, at: Int) -> Int { + match pool_reading_at(pool: pool, at: at) { + PoolRead { committed: _, ceiling: _, headroom: h, over_committed: _ } => h.count as Int + PoolReadingRefused { at: _, anchor: _ } => -1 + } +} + +// RED: the upstream reports every free request spent (used = limit, remaining = 0) and the local ledger +// is fresh -- the day pool admits nothing, so no request is ever sent. +test fn an_exhausted_upstream_day_leaves_a_fresh_day_pool_no_headroom() -> Bool { + let day = openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) + match w_key_remaining(used: 1000, remaining: 0) { + Absent => false + Present { value: r } => + match w_observed(pool: quota_root_pool(limit: day), remaining: r, at: w_day_start + 100) { + Absent => false + Present { value: p } => w_day_headroom(pool: p, at: w_day_start + 101) == 0 + } + } +} + +// A PARTIAL REMAINDER IS RESPECTED EXACTLY, and a later reading claiming more never gives any back. +test fn a_partial_upstream_remainder_is_the_headroom_and_later_readings_never_replenish() -> Bool { + let day = openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) + match w_key_remaining(used: 997, remaining: 3) { + Absent => false + Present { value: r } => + match w_observed(pool: quota_root_pool(limit: day), remaining: r, at: w_day_start + 100) { + Absent => false + Present { value: p } => + w_day_headroom(pool: p, at: w_day_start + 101) == 3 + && (match w_observed(pool: p, remaining: 1000, at: w_day_start + 102) { + Absent => false + Present { value: p2 } => w_day_headroom(pool: p2, at: w_day_start + 103) == 3 + }) + } + } +} + +// ── A TRANSITION IS CHECKED AGAINST THE FOLDED POOL BEFORE IT IS WRITTEN ───────────────────────── +// +// gunbc.fabric_event_log fabric_pool_transition appends only what product.capacity.pool_events +// pool_apply_payload advances on the folded pool -- the same function replay applies. These supply the +// folded pool and ask that function, which is the decision the carrier makes before any write. +fn w_env(id: String, at: Int, payload: PoolEvent) -> ChainEnvelope { + ChainEnvelope { + id: id as EventId, + event: ChainEvent { partition: "quota-openrouter-free-models-minute" as PartitionId, parent: none, recorded_at: at, actor: "witness" as NonEmptyStr, payload: payload }, + } +} + +// RED: the minute lease is taken one second before rollover (its term runs to the window end) and the +// settlement arrives after it. The transition is refused -- so the checked carrier writes nothing and +// reports no settlement -- and the CONTROL shows why that matters: the same settlement appended blind +// makes the whole partition unfoldable. +test fn a_settlement_after_rollover_is_refused_before_it_could_poison_the_partition() -> Bool { + let minute = openrouter_free_models_per_minute + let at = w_day_start + 59 + match quota_term_to_window_end(limit: minute, at: at) { + Absent => false + Present { value: term } => { + let acquired = w_env(id: "e1", at: at, payload: PoolAcquired { reference: "charge" as NonEmptyStr, amount: 1, term_seconds: second_count(s: term) }) + match pool_fold(root: quota_root_pool(limit: minute), oldest_first: [acquired]) { + PoolFoldRefused { at_event: _, wire: _ } => false + PoolFolded { pool: p, generation: _ } => + (match pool_apply_payload(pool: p, payload: PoolSettled { reference: "charge" as NonEmptyStr, actual: 1 }, at: w_day_start + 60, identity: "pending:e1" as NonEmptyStr) { + PoolRefused { refusal: LeaseAlreadyLapsed { reference: _, lapsed_at: _, at: _ } } => true + PoolRefused { refusal: _ } => false + PoolAdvanced { pool: _ } => false + }) + && (match pool_apply_payload(pool: p, payload: PoolSettled { reference: "charge" as NonEmptyStr, actual: 1 }, at: at, identity: "pending:e1" as NonEmptyStr) { + PoolAdvanced { pool: _ } => true + PoolRefused { refusal: _ } => false + }) + && (match pool_fold(root: quota_root_pool(limit: minute), oldest_first: [acquired, w_env(id: "e2", at: w_day_start + 60, payload: PoolSettled { reference: "charge" as NonEmptyStr, actual: 1 })]) { + PoolFoldRefused { at_event: e, wire: _ } => (e as String) == "e2" + PoolFolded { pool: _, generation: _ } => false + }) + } + } + } +} + +// TWO DECREASING UPSTREAM READINGS IN ONE SECOND STAY REPLAYABLE: each hold is named by its own event, +// so the second is not a duplicate of the first, and the pool ends at the lower reading. +test fn two_decreasing_upstream_readings_in_one_second_fold_to_the_lower() -> Bool { + let day = openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) + let at = w_day_start + 100 + match quota_term_to_window_end(limit: day, at: at) { + Absent => false + Present { value: term } => + match pool_fold(root: quota_root_pool(limit: day), oldest_first: [ + w_env(id: "o1", at: at, payload: PoolUpstreamObserved { remaining: 3, term_seconds: second_count(s: term) }), + w_env(id: "o2", at: at, payload: PoolUpstreamObserved { remaining: 1, term_seconds: second_count(s: term) }), + ]) { + PoolFoldRefused { at_event: _, wire: _ } => false + PoolFolded { pool: p, generation: g } => g == 2 && w_day_headroom(pool: p, at: at) == 1 + } + } +} + +// ── A SEND IN FLIGHT ACROSS THE ROLLOVER IS CHARGED IN THE WINDOW IT MAY ARRIVE IN ─────────────── +// +// The in-flight partition is the production seat-pool shape (gunbc.harness.harness_seat +// harness_seat_pool_root) under the production partition name; the carried count is the production +// hosted_inflight_carried; the charge is the real minute pool's pool_acquire at the amount the round asks +// for (its own send plus the carried ones). Only the clock and the events are supplied. +fn w_quota() -> HarnessHostedQuota { + HarnessHostedQuota { short_hostname: "witness", actor: "hosted-witness" as NonEmptyStr, per_minute: openrouter_free_models_per_minute, per_day: openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) } +} + +fn w_inflight(events: List>) -> Pool? { + match pool_fold(root: harness_seat_pool_root(partition: hosted_inflight_partition(quota: w_quota()), ceiling: 20), oldest_first: events) { + PoolFolded { pool: p, generation: _ } => Present { value: p } + PoolFoldRefused { at_event: _, wire: _ } => none + } +} + +// The minute after the rollover, with `n` sends already charged and settled in it. +fn w_minute_with(n: Nat, at: Int) -> Pool? { + fold(nat_range_inclusive(lo: 1, hi: n), init: Present { value: quota_root_pool(limit: openrouter_free_models_per_minute) }, f: (acc, i) => + match acc { + Absent => none + Present { value: p } => { + let r = join(["charged-", to_string(i)], "") as NonEmptyStr + match pool_acquire(pool: p, reference: r, amount: Measure { count: 1 }, at: at, term_seconds: 1) { + PoolRefused { refusal: _ } => none + PoolAdvanced { pool: q } => + match pool_settle(pool: q, reference: r, actual: Measure { count: 1 }, at: at) { + PoolRefused { refusal: _ } => none + PoolAdvanced { pool: settled } => Present { value: settled } + } + } + } + }) +} + +fn w_round_admitted(minute: Pool, carried: Nat, at: Int) -> Bool { + match quota_term_to_window_end(limit: openrouter_free_models_per_minute, at: at) { + Absent => false + Present { value: term } => + match pool_acquire(pool: minute, reference: "this-round" as NonEmptyStr, amount: Measure { count: carried + 1 }, at: at, term_seconds: second_count(s: term)) { + PoolAdvanced { pool: _ } => true + PoolRefused { refusal: _ } => false + } + } +} + +// RED: a send admitted at T+58 pauses across the rollover and is still in flight at T+61. With 19 sends +// already charged in the new minute, the next round must cover 2 -- its own and the carried one -- so it +// refuses: no request goes out in a window whose quota the paused send may already have spent. The same +// round with nothing carried is admitted, so the refusal is the carried send's. +test fn a_send_paused_across_the_rollover_is_charged_in_the_next_window() -> Bool { + match w_inflight(events: [w_env(id: "s1", at: w_day_start + 58, payload: PoolAcquired { reference: "paused" as NonEmptyStr, amount: 1, term_seconds: 2348 })]) { + Absent => false + Present { value: inflight } => + match quota_window_start(limit: openrouter_free_models_per_minute, at: w_day_start + 61) { + Absent => false + Present { value: start } => + match w_minute_with(n: 19, at: w_day_start + 60) { + Absent => false + Present { value: minute } => { + let carried = hosted_inflight_carried(pool: inflight, window_start: start) + carried == 1 + && !w_round_admitted(minute: minute, carried: carried, at: w_day_start + 61) + && w_round_admitted(minute: minute, carried: 0, at: w_day_start + 61) + } + } + } + } +} + +// CONTROL: a send made inside its window whose answer arrives much later carries nothing into later +// windows once its transport returns and its seat is released -- and the send is counted in its own window +// only once, by its own charge, not by the in-flight count. +test fn a_send_released_after_a_late_response_carries_nothing_forward() -> Bool { + match w_inflight(events: [ + w_env(id: "s1", at: w_day_start + 10, payload: PoolAcquired { reference: "slow-answer" as NonEmptyStr, amount: 1, term_seconds: 2348 }), + w_env(id: "s2", at: w_day_start + 300, payload: PoolReleased { reference: "slow-answer" as NonEmptyStr, reason: "the transport returned" as NonEmptyStr }), + ]) { + Absent => false + Present { value: inflight } => + hosted_inflight_carried(pool: inflight, window_start: w_day_start + 300) == 0 + && hosted_inflight_carried(pool: inflight, window_start: w_day_start) == 0 + } +} + +// A SEAT HELD BY A HOLDER THAT DIED IS NAMED WITH ITS RELEASE, so a starved route says what to free. +test fn a_held_inflight_seat_is_named_with_its_release_in_the_refusal_hint() -> Bool { + let events = [w_env(id: "s1", at: w_day_start + 58, payload: PoolAcquired { reference: "worker-7@1780531258#round3" as NonEmptyStr, amount: 1, term_seconds: 2348 })] + match w_inflight(events: events) { + Absent => false + Present { value: inflight } => { + let hint = hosted_inflight_release_hint(quota: w_quota(), pool: inflight, generation: 1, walk: ChainWalked { oldest_first: events }) + hint.contains("worker-7@1780531258#round3") && hint.contains("grant=s1") && hint.contains("harness_release_cli") + && hint.contains(hosted_inflight_partition(quota: w_quota()) as String) + } + } +} + +// ── A 429 FROM THE UPSTREAM IS THE TYPED BACKSTOP, AND IT IS NEVER RETRIED ─────────────────────────── +// +// The decode is the production decode over the shapes the limits page documents: the error body, and the +// header dump curl writes (the status line starts a block; only the last block is the answer). +data w_429_body: String = "{\"error\":{\"code\":429,\"message\":\"Rate limit exceeded: free-models-per-min\",\"metadata\":{\"error_type\":\"rate_limit_exceeded\"}}}" + +data w_429_headers: String = "HTTP/2 429\r\ncontent-type: application/json\r\nretry-after: 30\r\nx-ratelimit-limit: 20\r\nx-ratelimit-remaining: 0\r\nx-ratelimit-reset: 1791265800000\r\n\r\n" + +test fn a_429_decodes_to_the_typed_refusal_with_the_upstreams_retry_hints() -> Bool { + match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: w_429_headers }) { + Absent => false + Present { value: r } => + (match r.hints.retry_after { RetryAfterSeconds { seconds: n } => second_count(s: n) == 30 RetryAfterUnparsed { text: _ } => false RetryAfterAbsent => false RetryAfterUnread { cause: _ } => false }) + && (match r.hints.reset { Present { value: v } => v == "1791265800000" Absent => false }) + && (match r.hints.remaining { Present { value: v } => v == "0" Absent => false }) + && (match r.provider_code { Absent => true Present { value: _ } => false }) + && openrouter_rate_limit_wire(r: r).contains("Retry-After 30 s") + && openrouter_rate_limit_wire(r: r).contains("unit not documented") + && openrouter_rate_limit_wire(r: r).contains("NOT retried") + } +} + +// A RETRY-AFTER THAT IS NOT DELTA-SECONDS IS CARRIED AS TEXT, never read as a number it is not; and a +// refusal with no hint headers is still a typed refusal, saying so. The LAST block of a dump is the answer. +test fn a_retry_after_that_is_not_seconds_and_a_missing_hint_are_stated_not_guessed() -> Bool { + (match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: "HTTP/2 429\r\nretry-after: Wed, 21 Oct 2026 07:28:00 GMT\r\n\r\n" }) { + Present { value: r } => (match r.hints.retry_after { RetryAfterUnparsed { text: t } => t.contains("21 Oct 2026") RetryAfterSeconds { seconds: _ } => false RetryAfterAbsent => false RetryAfterUnread { cause: _ } => false }) + Absent => false + }) + && (match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: "HTTP/1.1 429 Too Many Requests\r\nRetry-After: 12\r\nX-RateLimit-Reset: 5\r\n\r\n" }) { + Present { value: r } => (match r.hints.retry_after { RetryAfterSeconds { seconds: n } => second_count(s: n) == 12 RetryAfterUnparsed { text: _ } => false RetryAfterAbsent => false RetryAfterUnread { cause: _ } => false }) + && (match r.hints.reset { Present { value: v } => v == "5" Absent => false }) + Absent => false + }) + && (match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: "" }) { + Present { value: r } => (match r.hints.retry_after { RetryAfterAbsent => true RetryAfterSeconds { seconds: _ } => false RetryAfterUnparsed { text: _ } => false RetryAfterUnread { cause: _ } => false }) && openrouter_rate_limit_wire(r: r).contains("no Retry-After") + Absent => false + }) + && (match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: "HTTP/1.1 100 Continue\r\nretry-after: 99\r\n\r\nHTTP/2 429\r\nretry-after: 7\r\n\r\n" }) { + Present { value: r } => (match r.hints.retry_after { RetryAfterSeconds { seconds: n } => second_count(s: n) == 7 RetryAfterUnparsed { text: _ } => false RetryAfterAbsent => false RetryAfterUnread { cause: _ } => false }) + Absent => false + }) +} + +// RED: a header dump that could not be read is NOT "no Retry-After". The upstream may have sent a hint that +// this process failed to read, so the refusal says the hint is unknown and why, and carries no reset or counters. +test fn an_unread_header_dump_is_stated_unknown_not_reported_as_no_retry_after() -> Bool { + match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpUnread { cause: "the dump file is absent" }) { + Absent => false + Present { value: r } => + (match r.hints.retry_after { RetryAfterUnread { cause: c } => c.contains("dump file is absent") RetryAfterAbsent => false RetryAfterSeconds { seconds: _ } => false RetryAfterUnparsed { text: _ } => false }) + && (match r.hints.reset { Absent => true Present { value: _ } => false }) + && openrouter_rate_limit_wire(r: r).contains("Retry-After unknown") + && !openrouter_rate_limit_wire(r: r).contains("no Retry-After") + && openrouter_rate_limit_wire(r: r).contains("NOT retried") + } +} + +// RED: delay-seconds is 1*DIGIT, and the integer parser would accept a sign, so a signed, fractional or +// unit-suffixed value is carried as text and never read as a number of seconds. +fn w_retry_after_of(value: String) -> OpenRouterRetryAfter? { + match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: join(["HTTP/2 429\r\nretry-after: ", value, "\r\n\r\n"], "") }) { + Present { value: r } => Present { value: r.hints.retry_after } + Absent => none + } +} + +fn w_is_unparsed(r: OpenRouterRetryAfter?) -> Bool { + match r { + Present { value: RetryAfterUnparsed { text: _ } } => true + Present { value: RetryAfterSeconds { seconds: _ } } => false + Present { value: RetryAfterAbsent } => false + Present { value: RetryAfterUnread { cause: _ } } => false + Absent => false + } +} + +test fn a_signed_fractional_or_suffixed_retry_after_is_not_read_as_seconds() -> Bool { + w_is_unparsed(r: w_retry_after_of(value: "+30")) + && w_is_unparsed(r: w_retry_after_of(value: "-0")) + && w_is_unparsed(r: w_retry_after_of(value: "-30")) + && w_is_unparsed(r: w_retry_after_of(value: "3.5")) + && w_is_unparsed(r: w_retry_after_of(value: "30s")) + && (match w_retry_after_of(value: "30") { Present { value: RetryAfterSeconds { seconds: n } } => second_count(s: n) == 30 Present { value: _ } => false Absent => false }) +} + +// RED: a 402, or any body that is not a 429, is NOT a rate-limit refusal -- it keeps its own typed error +// (the 402 limit_source decode above), so a paid-model refusal is never read as a wait-and-retry. +test fn only_a_429_is_a_rate_limit_refusal() -> Bool { + (match openrouter_decode_rate_limited(body: "{\"error\":{\"code\":402,\"message\":\"x\",\"metadata\":{\"limit_source\":\"openrouter_key_limit\"}}}", header_dump: HeaderDumpRead { text: w_429_headers }) { + Absent => true + Present { value: _ } => false + }) + && (match openrouter_decode_rate_limited(body: "not json", header_dump: HeaderDumpRead { text: w_429_headers }) { Absent => true Present { value: _ } => false }) +} + +// THE TURN ENDS ON IT. It is a terminal outcome of the turn -- there is no retry arm in the loop to take -- +// and a refusal exit that says it was not retried and carries the hint into the operator's line. +test fn a_hosted_rate_limit_ends_the_turn_as_a_refusal_naming_the_hint_and_no_retry() -> Bool { + match openrouter_decode_rate_limited(body: w_429_body, header_dump: HeaderDumpRead { text: w_429_headers }) { + Absent => false + Present { value: r } => { + let o = TurnHostedRateLimited { refusal: r, steps_taken: 3 } + harness_outcome_label(outcome: o) == "hosted-rate-limited" + && harness_outcome_detail(outcome: o).contains("Retry-After 30 s") + && harness_outcome_detail(outcome: o).contains("rather than retrying") + && (match harness_turn_exit(outcome: o) { ExitSuccess => false ExitFailure { code, reason: _ } => code == 1 }) + } + } +} + +// ── THE SPENDABLE-KEY WALL, AT THE FUNCTION THAT ENFORCES IT ───────────────────────────────────── +// +// hosted_access_from_key is what refuses to build a binding; a witness over the rendering helper beside it +// would stay green if the wall were removed. These decode real key reads and ask the wall itself. +fn w_bind_from_key(limit_json: String, daily_limit: Int) -> Bool? { + match openrouter_decode_key_read(body: join(["{\"data\":{\"limit\":", limit_json, ",\"is_free_tier\":false,\"free_model_daily_requests\":{\"used\":0,\"limit\":", to_string(daily_limit), ",\"remaining\":", to_string(daily_limit), "}}}"], "")) { + OpenRouterKeyUndecodable { cause: _ } => none + OpenRouterKeyDecoded { key: k } => + match harness_hosted_route_for(unit: NemotronUltraFreeOnOpenRouter) { + Absent => none + Present { value: route } => + match hosted_access_from_key(route: route, key: k, level: ReasoningMaximum, header_dir: "/tmp/witness" as NonEmptyStr, header_file: "/tmp/witness/authorization" as NonEmptyStr, short_hostname: "witness", actor: "witness" as NonEmptyStr) { + Absent => Present { value: false } + Present { value: _ } => Present { value: true } + } + } + } +} + +// RED: a key with no credit limit, and one with a positive limit, can spend money on a priced model, so no +// binding is built; the zero-credit key on a published tier is the only one that is. A tier that is neither +// published value refuses even on the safe key. +test fn only_a_zero_credit_key_on_a_published_tier_builds_a_binding() -> Bool { + (match w_bind_from_key(limit_json: "null", daily_limit: 1000) { Present { value: b } => !b Absent => false }) + && (match w_bind_from_key(limit_json: "5", daily_limit: 1000) { Present { value: b } => !b Absent => false }) + && (match w_bind_from_key(limit_json: "0", daily_limit: 200) { Present { value: b } => !b Absent => false }) + && (match w_bind_from_key(limit_json: "0", daily_limit: 1000) { Present { value: b } => b Absent => false }) + && (match w_bind_from_key(limit_json: "0.0", daily_limit: 50) { Present { value: b } => b Absent => false }) +} + +// ── THE REAL LEASE PRODUCER IS EXECUTED, THROUGH THE PRODUCTION CHARGE ─────────────────────────── +// +// The constructed leases above are for the decision's own boundary. This runs the PRODUCTION charge +// (harness_hosted_round_charge -> gunbc.fabric_quota fabric_quota_lease_for) for a host the fleet declares no +// dashboard instance for: the real producer answers with its typed refusal, the charge turns it into +// HostedRoundUnleasable naming the host, and nothing is leased, charged or sent. Deleting the producer, or +// the charge's call to it, turns this red. +// +// WHAT THIS DOES NOT EXECUTE, STATED: the producer's success arm -- a seat acquired and settled on a real +// event-log store -- needs a store, and the floor refuses a new real-execution witness +// (v2.workflow.floor_changed_witness), so no claim here can stand one up. Its pieces are witnessed at their +// own interfaces above: the term derivation and admission through the production pool_acquire, and the +// checked settlement through the replay's own pool_apply_payload. The joined route runs in the post-merge +// live exercise. +fn w_unplaced_quota() -> HarnessHostedQuota { + HarnessHostedQuota { short_hostname: "no-such-host-witness", actor: "hosted-witness" as NonEmptyStr, per_minute: openrouter_free_models_per_minute, per_day: openrouter_free_models_per_day(credits: CreditsPurchasedAtLeastTen) } +} + +test fn the_production_charge_refuses_through_the_real_lease_producer_for_an_undeclared_host() -> Bool { + (match harness_hosted_round_charge(quota: w_unplaced_quota(), carried_minute: 0, carried_day: 0, release_hint: "") { + HostedRoundUnleasable { detail: d } => d.contains("no dashboard instance is declared for host no-such-host-witness") + HostedRoundCharged => false + HostedRoundQuotaFull { wire: _ } => false + }) + && (match fabric_quota_lease_for(short_hostname: "no-such-host-witness", limit: openrouter_free_models_per_minute, amount: 1, actor: "hosted-witness" as NonEmptyStr, term: QuotaTermToWindowEnd) { + QuotaLeaseRefused { detail: d } => d.contains("no dashboard instance is declared") + QuotaLeased { grant: _, partition: _, store: _, limit: _ } => false + QuotaFull { wire: _ } => false + }) +} + +// A SETTLEMENT OF A LEASE THAT NEVER EXISTED IS A TYPED REFUSAL, not a recorded charge. +test fn settling_a_refused_lease_records_nothing() -> Bool { + match fabric_quota_settle_at(lease: QuotaLeaseRefused { detail: "no dashboard instance is declared for host x" }, actual: 1, now: w_day_start + 59) { + QuotaSettleRefused { detail: d } => d.contains("nothing to settle") + QuotaSettled { partition: _, actual: _ } => false + } +} diff --git a/dag/test/claim/owned_directory_witness_test.dag b/dag/test/claim/owned_directory_witness_test.dag new file mode 100644 index 00000000000..5f1546fb529 --- /dev/null +++ b/dag/test/claim/owned_directory_witness_test.dag @@ -0,0 +1,133 @@ +module test.claim.owned_directory_witness_test + +import std.types { Bool, String, NonEmptyStr, FilePath } +import extdeps.posix.identity { PosixOwnerNames } +import gunbc.managed_directory { DirectoryOwnerOnly } +import gunbc.ensure { EnsureMayApply, EnsureObserveOnly, EnsureObservation, EnsureObserved, EnsureAbsent, EnsureInaccessible } +import std.upsert_decision { Noop, Apply, Refuse } +import gunbc.owned_directory { + OwnedDirectoryTarget, OwnedDirectoryDeclared, OwnedDirectoryReading, + owned_directory_decide, owned_directory_reading_of_stat_line, owned_directory_observe, + ObserverRead, ObserverReadOut, ObserverReadFailed, ObserverFailure, ObserverNotSpawned, ObserverExited, ObserverTransportRefused, + owned_directory_ensure, OwnedDirectoryEnsured, OwnedDirectoryEnsureRefused, +} + +// THE PROVIDER'S DECISION TABLE, over supplied observations (DESIGN 3: one interface, supplied values). +// The real path -- a stat line read off a host and parsed -- is inhabited by the parse claims below and +// by the live_deploy route claims in test.claim.live_deploy.owned_directory_route_witness_test. + +fn target() -> OwnedDirectoryTarget { + OwnedDirectoryDeclared { + member: "auth-sessions" as NonEmptyStr, + path: "/srv/instance/auth-sessions" as FilePath, + owner: PosixOwnerNames { user: "svc" as NonEmptyStr, group: "svc" as NonEmptyStr }, + mode: DirectoryOwnerOnly, + } +} + +fn observed(line: String) -> EnsureObservation { + owned_directory_observe(path: "/srv/instance/auth-sessions" as FilePath, list: p => ObserverReadOut { stdout: "auth-sessions\n" }, stat: p => ObserverReadOut { stdout: line }) +} + +fn decision_label(o: EnsureObservation, may_apply: Bool) -> String { + match owned_directory_decide(policy: if may_apply { EnsureMayApply } else { EnsureObserveOnly }, target: target(), observation: o) { + Noop => "noop" + Apply { plan: _ } => "apply" + Refuse { reason } => join(["refuse:", reason as String], "") + } +} + +// The stat line parses into owner, group and mode; a malformed line is no reading at all. +test fn the_stat_identity_line_parses_and_a_malformed_line_does_not() -> Bool { + let parsed = match owned_directory_reading_of_stat_line(line: "directory|svc|svc|700\n") { + Present { value: r } => r.is_directory && (r.owner.user as String) == "svc" + Absent => false + } + let malformed = match owned_directory_reading_of_stat_line(line: "directory|svc|700") { + Present { value: _ } => false + Absent => true + } + parsed && malformed +} + +// CONVERGED -> NOOP: a second apply over a converged directory does nothing. +test fn a_converged_directory_is_noop() -> Bool { + decision_label(o: observed(line: "directory|svc|svc|700"), may_apply: true) == "noop" +} + +// FOREIGN OWNER -> REFUSE, NEVER ADOPTED, under May-apply. The red control is the same directory under +// the demanded owner, which is Noop. +test fn a_foreign_owner_refuses_and_is_never_adopted() -> Bool { + starts_with(s: decision_label(o: observed(line: "directory|root|root|700"), may_apply: true), prefix: "refuse:") + && string_contains(s: decision_label(o: observed(line: "directory|root|root|700"), may_apply: true), pattern: "never adopted") +} + +// A NON-DIRECTORY IS NEVER REPLACED. +test fn a_non_directory_refuses() -> Bool { + starts_with(s: decision_label(o: observed(line: "regular file|svc|svc|700"), may_apply: true), prefix: "refuse:") +} + +// ── THE OBSERVATION CONTRACT: ABSENCE IS ESTABLISHED BY A READ, NEVER INFERRED FROM A FAILURE ──── +// +// Routes are stubbed at the contract (DESIGN 3: supplied values at one interface): `list` and `stat` +// return what a read of one directory returned, or the typed origin of why nothing was read. +fn listing(stdout: String) -> ObserverRead { ObserverReadOut { stdout: stdout } } + +fn observe_with(list: fn(FilePath) -> ObserverRead, stat: fn(FilePath) -> ObserverRead) -> EnsureObservation { + owned_directory_observe(path: "/srv/instance/auth-sessions" as FilePath, list: list, stat: stat) +} + +data missing_observer: ObserverFailure = ObserverNotSpawned { cause: "No such file or directory" } + +// GREEN POSITIVE: the parent's listing was read and does not name the directory -> absent -> May-apply +// installs. And a parent that is itself not listed in ITS parent makes the child absent too. +test fn a_parent_listing_that_omits_the_directory_establishes_absence() -> Bool { + decision_label(o: observe_with(list: p => listing(stdout: "other\nauth-jwks\n"), stat: p => listing(stdout: "")), may_apply: true) == "apply" + && decision_label(o: observe_with( + list: p => if (p as String) == "/srv/instance" { ObserverReadFailed { origin: ObserverExited { exit_code: 2, stderr: "" } } } else { listing(stdout: "bin\n") }, + stat: p => listing(stdout: ""), + ), may_apply: true) == "apply" +} + +// LISTED, THEN STAT: a listed directory at the demanded identity is converged. +test fn a_listed_directory_reads_its_identity_from_stat() -> Bool { + decision_label(o: observe_with(list: p => listing(stdout: "auth-sessions\n"), stat: p => listing(stdout: "directory|svc|svc|700")), may_apply: true) == "noop" +} + +// A MISSING OBSERVER EXECUTABLE IS NOT AN ABSENT DIRECTORY, whatever text it carries: the listing was +// never read, so presence is unestablished -> refuse. ZERO INSTALL CALLS: the provider is driven with a +// `run` that would report failure; had it been called, the receipt would carry "Apply was paired with +// NotApplied". Its GREEN POSITIVE is the same provider over an established absence, where run IS called. +test fn a_missing_observer_executable_is_inaccessible_and_installs_nothing() -> Bool { + let unread = p => ObserverReadFailed { origin: missing_observer } + let exited = p => ObserverReadFailed { origin: ObserverExited { exit_code: 127, stderr: "env: '/usr/bin/ls': No such file or directory" } } + starts_with(s: decision_label(o: observe_with(list: unread, stat: unread), may_apply: true), prefix: "refuse:") + && starts_with(s: decision_label(o: observe_with(list: exited, stat: exited), may_apply: true), prefix: "refuse:") + && match owned_directory_ensure(policy: EnsureMayApply, target: target(), observe: p => observe_with(list: unread, stat: unread), run: plan => false) { + OwnedDirectoryEnsured { target: _, converged: _ } => false + OwnedDirectoryEnsureRefused { target: _, reason } => !string_contains(s: reason, pattern: "Apply was paired with NotApplied") + } + && match owned_directory_ensure(policy: EnsureMayApply, target: target(), observe: p => observe_with(list: q => listing(stdout: ""), stat: q => listing(stdout: "")), run: plan => false) { + OwnedDirectoryEnsured { target: _, converged: _ } => false + OwnedDirectoryEnsureRefused { target: _, reason } => string_contains(s: reason, pattern: "Apply was paired with NotApplied") + } +} + +// A TRANSPORT FAILURE CARRYING THE SAME TEXT IS INACCESSIBLE, and so is a refused grant (an observer +// that ran and exited nonzero): the origin is typed, and no failed read establishes anything. +test fn a_transport_or_grant_failure_is_inaccessible() -> Bool { + starts_with(s: decision_label(o: observe_with(list: p => ObserverReadFailed { origin: ObserverTransportRefused { cause: "ssh: /srv/x: No such file or directory" } }, stat: p => listing(stdout: "")), may_apply: true), prefix: "refuse:") + && starts_with(s: decision_label(o: observe_with(list: p => ObserverReadFailed { origin: ObserverExited { exit_code: 1, stderr: "sudo: a password is required" } }, stat: p => listing(stdout: "")), may_apply: true), prefix: "refuse:") + && starts_with(s: decision_label(o: observe_with(list: p => listing(stdout: "auth-sessions\n"), stat: p => ObserverReadFailed { origin: ObserverExited { exit_code: 1, stderr: "Permission denied" } }), may_apply: true), prefix: "refuse:") +} +// OBSERVE-ONLY AND ABSENT -> REFUSE; May-apply on the same observation applies. +test fn observe_only_absent_refuses() -> Bool { + let absent = EnsureAbsent { reason: "absent" } + starts_with(s: decision_label(o: absent, may_apply: false), prefix: "refuse:") + && decision_label(o: absent, may_apply: true) == "apply" +} + +// A MODE DRIFT ON THE DEMANDED OWNER'S DIRECTORY IS REPAIRED, not refused. +test fn a_mode_drift_on_the_owners_directory_applies() -> Bool { + decision_label(o: observed(line: "directory|svc|svc|755"), may_apply: true) == "apply" +} diff --git a/dag/test/claim/parse_test_alias_decl_carry_test.dag b/dag/test/claim/parse_test_alias_decl_carry_test.dag new file mode 100644 index 00000000000..f00a4e11ec6 --- /dev/null +++ b/dag/test/claim/parse_test_alias_decl_carry_test.dag @@ -0,0 +1,173 @@ +module test.claim.parse_test_alias_decl_carry + +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.node { Authored, Edge, Node, Positional, StructuralLabel, node_subtree_nodes } +import v2.std.namespace_alias { AliasBindingRow } +import v2.std.qualified_name { qualified_name_from_dotted_string } +import v2.compiler.normalized_tree { NormalizedTree } +import v2.std.symbol_index { + LexicalAmbiguous, + LexicalHit, + LexicalUnbound, + SymbolIndex, + symbol_index_lexical_lookup +} +import v2.test.parse.alias_decl_parse { + alias_amb_a_source, + alias_amb_b_source, + alias_amb_subject_source, + alias_decl_fill_parse_source, + alias_decl_index_of, + alias_decl_lookup_hits_target, + alias_decl_normalized, + alias_decl_resolve_verdict, + alias_decl_unresolved_source +} + +// THE `alias Name = a.b.C` CARRY, CLAIMED ON THE PER-PR PATH (gunbc#13428; ruling B, the grammar +// program). Each subject is produced ONCE from real source by a NULLARY producer below, and every +// producer is read by at least two claims, so the required floor fills it once and nets the fill from +// the claim that pays it (v2.workflow.floor_pure_producer_share): each claim is charged only for its +// own assertion. Producing from source rather than supplying a built tree is deliberate -- the +// subject IS the route (graft dissolves the declaration, normalize captures its row, the fill binds +// it), and a supplied tree would assert a shape nothing produced (DESIGN section 3, the pairing rule). +// The discriminating red is enrolled, not transcribed: alias_decl_graft_dissolves_the_declaration_holds +// asserts the absence of exactly the self-binding member this class names, so it is false whenever +// v2.compiler.namespace_graft grafts the alias as a member again. + +data acd_fill_module: String = "v2.test.parse.alias_decl_fill_probe" + +data acd_fill_target: String = "v2.test.parse.alias_decl_fill_probe.AliasTargetType" + +fn acd_fill_tree() -> Outcome { + alias_decl_normalized(text: alias_decl_fill_parse_source, file: ^alias_decl_fill_probe) +} + +fn acd_edge_names(e: Edge, name: Symbol) -> Bool { + match e.label { + Authored { name: s } => s == name + StructuralLabel { label: _ } => false + Positional => false + } +} + +fn acd_names_a_member(root: Node, name: Symbol) -> Bool { + fold(node_subtree_nodes(root: root), init: false, f: fn(acc, n) { + acc || fold(n.children, init: false, f: fn(a, e) { a || acd_edge_names(e: e, name: name) }) + }) +} + +// ROUTE, GRAFT: the declaration dissolved, so the normalized root names no member AliasBinding. +test fn alias_decl_graft_dissolves_the_declaration_holds() -> Bool { + match acd_fill_tree() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + !acd_names_a_member(root: t.root, name: ^AliasBinding) + && acd_names_a_member(root: t.root, name: ^AliasTargetType) + } +} + +// ROUTE, CAPTURE: normalize carried exactly the one row, binding AliasBinding to its target. +test fn alias_decl_normalize_captures_the_binding_row_holds() -> Bool { + match acd_fill_tree() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + t.import_bindings == [ + AliasBindingRow { binding: ^AliasBinding, target: qualified_name_from_dotted_string(dotted: acd_fill_target) } + ] + } +} + +// ROUTE, BIND: the alias name's lexical hit at its module IS the target's node (the #13375 control, +// flipped: before the interim refusal the hit was the alias's own grafted node). +test fn alias_decl_name_resolves_to_its_target_holds() -> Bool { + match acd_fill_tree() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + alias_decl_lookup_hits_target( + idx: alias_decl_index_of(trees: [t]), + position: acd_fill_module, + name: ^AliasBinding, + target: acd_fill_target + ) + } +} + +// AN ALIAS TO A TARGET NOTHING DECLARES: the row stays deferred at the fill's fixed point, so the name +// is UNBOUND (never a hit on its own node), and resolution refuses the reference that wanted it, +// located at that reference (docs/plans/namespace-resolution-design.md section 13). +fn acd_unresolved_tree() -> Outcome { + alias_decl_normalized(text: alias_decl_unresolved_source, file: ^alias_decl_unresolved_probe) +} + +test fn alias_decl_to_an_undeclared_target_is_unbound_holds() -> Bool { + match acd_unresolved_tree() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + match symbol_index_lexical_lookup( + index: alias_decl_index_of(trees: [t]), + position: qualified_name_from_dotted_string(dotted: "v2.test.parse.alias_decl_unresolved_probe"), + name: ^Missing + ) { + LexicalUnbound => true + LexicalHit { node: _, path: _ } => false + LexicalAmbiguous { candidates: _ } => false + } + } +} + +test fn alias_decl_to_an_undeclared_target_refuses_at_the_reference_holds() -> Bool { + match acd_unresolved_tree() { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + alias_decl_resolve_verdict(t: t, module: "v2.test.parse.alias_decl_unresolved_probe") + == "resolve_reason_unbound_symbol@Missing" + } +} + +// AN ALIAS DISAMBIGUATES WHAT UNIQUE-ON-CHAIN REFUSES: two imports bind X at the subject, so X is +// Ambiguous there; `alias AX = ...alias_amb_a.X` names one of them under a name only it binds. +fn acd_amb_index() -> Outcome { + match alias_decl_normalized(text: alias_amb_a_source, file: ^alias_amb_a_probe) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: a, diagnostics: _ } => + match alias_decl_normalized(text: alias_amb_b_source, file: ^alias_amb_b_probe) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: b, diagnostics: _ } => + match alias_decl_normalized(text: alias_amb_subject_source, file: ^alias_amb_s_probe) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: s, diagnostics: _ } => + Accepted { value: alias_decl_index_of(trees: [a, b, s]), diagnostics: v2.std.diagnostic.None } + } + } + } +} + +test fn two_imports_of_one_name_stay_ambiguous_beside_an_alias_holds() -> Bool { + match acd_amb_index() { + Rejected { diagnostics: _ } => false + Accepted { value: idx, diagnostics: _ } => + match symbol_index_lexical_lookup( + index: idx, + position: qualified_name_from_dotted_string(dotted: "v2.test.parse.alias_amb_s"), + name: ^X + ) { + LexicalAmbiguous { candidates: _ } => true + LexicalHit { node: _, path: _ } => false + LexicalUnbound => false + } + } +} + +test fn an_alias_resolves_one_of_two_ambiguous_imports_holds() -> Bool { + match acd_amb_index() { + Rejected { diagnostics: _ } => false + Accepted { value: idx, diagnostics: _ } => + alias_decl_lookup_hits_target( + idx: idx, + position: "v2.test.parse.alias_amb_s", + name: ^AX, + target: "v2.test.parse.alias_amb_a.X" + ) + } +} diff --git a/dag/test/claim/parse_test_choice_lookahead_guard_test.dag b/dag/test/claim/parse_test_choice_lookahead_guard_test.dag index f41209c15b2..9efc6ac091c 100644 --- a/dag/test/claim/parse_test_choice_lookahead_guard_test.dag +++ b/dag/test/claim/parse_test_choice_lookahead_guard_test.dag @@ -17,9 +17,8 @@ import v2.std.grammar { node_atom_identity_optional } import v2.compiler.parse { grammar_choice_ambiguity_count, parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node, Symbol } @@ -163,11 +162,11 @@ test fn guard_missing_one_word_keeps_that_row() -> Bool { fn clg_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } diff --git a/dag/test/claim/parse_test_fn_decl_return_clause_test.dag b/dag/test/claim/parse_test_fn_decl_return_clause_test.dag index fad8326a214..042944078b5 100644 --- a/dag/test/claim/parse_test_fn_decl_return_clause_test.dag +++ b/dag/test/claim/parse_test_fn_decl_return_clause_test.dag @@ -88,7 +88,7 @@ fn nfbcp_parsed(text: String) -> Optional { match dag_prepared_grammar() { Rejected { diagnostics: _ } => Absent Accepted { value: prepared, diagnostics: _ } => - match parse_module_prepared(tokens: ts, prepared: prepared, validation_residue: None) { + match parse_module_prepared(tokens: ts, prepared: prepared) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => Present { value: artifact.tree } } diff --git a/dag/test/claim/parse_test_grammar_choice_overlap_test.dag b/dag/test/claim/parse_test_grammar_choice_overlap_test.dag new file mode 100644 index 00000000000..c1930c45115 --- /dev/null +++ b/dag/test/claim/parse_test_grammar_choice_overlap_test.dag @@ -0,0 +1,89 @@ +module test.claim.parse_test_grammar_choice_overlap + +import std.algebra { Cons, Empty } +import v2.compiler.parse { + PreparedModeled, + PreparedVoidGrammar, + grammar_choice_ambiguity_roster_from_analysis, + grammar_validate_for_parse +} +import v2.compiler.program_assembly { dag_prepared_grammar } +import v2.std.algebra { length } +import v2.std.diagnostic { Accepted, Rejected, declaration_locus, invariant_port_locus, locus_eq } +import v2.std.grammar { + Choice, + GrammarProduction, + GrammarRoot, + Nonterminal, + StampClass, + Terminal, + grammar_atom, + grammar_empty_sync_tokens +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE DAG GRAMMAR CARRIES NO CHOICE OVERLAP, AND THE PARSER REFUSES ONE THAT IT WOULD. +// +// Grammar validation (v2.compiler.parse grammar_validate_and_analyze) refuses every overlap row, +// located at the production that owns the choice; there is no accept-with-residue arm. These two +// claims are that wall's evidence on the merge path, which is why the module is named into the +// admitted `test.claim.parse_test` family of v2.workflow.required_floor `required_gate_prefixes`. +// +// THE COUNT IS READ FROM THE ANALYSIS THE PARSER RUNS ON. dag_prepared_grammar is the warm-shared +// producer (v2.workflow.floor_pure_producer_share), so this claim pays for one roster read, not a +// second FIRST analysis: the roster is grammar_choice_ambiguity_roster_from_analysis over the +// prepared grammar's own analysis, the same value grammar_choice_ambiguity_count folds. A +// regression reds here twice over: a new overlap row makes the prepare itself refuse, and every +// parse_test claim that consumes the prepared grammar refuses with it. +test fn dag_grammar_choice_ambiguity_count_is_zero() -> Bool { + match dag_prepared_grammar() { + Rejected { diagnostics: _ } => false + Accepted { value: prepared, diagnostics: _ } => + match prepared { + PreparedVoidGrammar => false + PreparedModeled { modeled: m, materialization: _ } => + length(xs: grammar_choice_ambiguity_roster_from_analysis(root: m.validated.root, analysis: m.analysis)) == 0 + } + } +} + +// THE DISCRIMINATING RED, SUPPLIED. One overlapping choice, owned by a production that is NOT the +// start, so a refusal located at the start or at the stage port fails this control: only the +// owning production is the right answer. +data overlap_fixture_root: GrammarRoot = GrammarRoot { + start: ^overlap_fixture_start, + productions: Cons { + head: GrammarProduction { + name: ^overlap_fixture_start, + expression: Nonterminal { production: ^overlap_fixture_owner }, + emitted: grammar_atom(identity: ^overlap_fixture_start) + }, + tail: Cons { + head: GrammarProduction { + name: ^overlap_fixture_owner, + expression: Choice { + left: Terminal { token_class: ^overlap_fixture_tok, stamp: StampClass }, + right: Terminal { token_class: ^overlap_fixture_tok, stamp: StampClass } + }, + emitted: grammar_atom(identity: ^overlap_fixture_owner) + }, + tail: Empty + } + }, + sync_tokens: grammar_empty_sync_tokens() +} + +test fn a_grammar_with_one_overlapping_choice_is_refused_at_its_production() -> Bool { + match grammar_validate_for_parse( + root: overlap_fixture_root, + at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_choice_overlap_fixture_contract) + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => + (d.head.reason == ^parse_grammar_choice_ambiguity) + && locus_eq(a: d.head.at, b: declaration_locus(declaration: Cons { head: ^overlap_fixture_owner, tail: Empty })) + && (length(xs: d.tail) == 0) + } +} diff --git a/dag/test/claim/parse_test_grammar_program_routes_test.dag b/dag/test/claim/parse_test_grammar_program_routes_test.dag new file mode 100644 index 00000000000..1347b62851d --- /dev/null +++ b/dag/test/claim/parse_test_grammar_program_routes_test.dag @@ -0,0 +1,63 @@ +module test.claim.parse_test_grammar_program_routes + +import v2.compiler.body_lowering_fold { body_lower_call_arg_capture_is_named, body_lower_find_captured } +import test.claim.parse_test_declaration_choice_route { route_parse_at, route_shells } +import v2.extdeps.languages.dag { ParseSubtreeAbsent, ParseSubtreeFound } +import v2.std.algebra { length } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { Symbol } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE ROUTE CONTROLS THE ZERO-OVERLAP GRAMMAR PROGRAM LACKED ANYWHERE. Every other production the +// program changed (gunbc#13034, #13039, #13056, #13087, #13133) already had a route control, and +// gunbc#13126 moved those modules into the admitted `test.claim.parse_test` family so they gate. These +// three productions had none: each claim parses ONE supplied source at the production itself (not a +// whole module) and asserts which alternative the parse took, read by the reader that consumes it. +// The whole-file gap these do not close is declared at gunbc.guarantee_stall +// grammar_change_parses_no_real_corpus_file_stall. + +// `arg` (gunbc#13056): the positional arm is guarded by !(name :). The route is read by +// v2.compiler.body_lowering_fold body_lower_call_arg_capture_is_named, the reader the call lowering uses. +fn gpr_arg_is_named(text: String) -> Symbol { + match route_parse_at(text: text, production: ^dag_production_arg) { + Rejected { diagnostics: _ } => ^gpr_refused + Accepted { value: tree, diagnostics: _ } => + match body_lower_find_captured(root: tree, emitted: ^dag_surface_arg) { + ParseSubtreeAbsent => ^gpr_unfound + ParseSubtreeFound { captured: arg } => + if body_lower_call_arg_capture_is_named(arg_capture: arg) { ^gpr_named } else { ^gpr_positional } + } + } +} + +test fn a_name_colon_argument_takes_the_named_route_holds() -> Bool { + gpr_arg_is_named(text: "x: 1") == ^gpr_named +} + +test fn a_bare_argument_takes_the_positional_route_holds() -> Bool { + gpr_arg_is_named(text: "x") == ^gpr_positional +} + +// `match_arm` (gunbc#13056): the expression alternative was removed, so an expression arm body now +// takes the guarded `match_arm_stmt_body` route, and never the `empty_block_body` one. +test fn an_expression_arm_body_takes_the_statement_body_route_holds() -> Bool { + match route_parse_at(text: "S { node: node } => node", production: ^dag_production_match_arm) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + (length(xs: route_shells(n: tree, emitted: ^dag_surface_match_arm_stmt_body, acc: [])) == 1) + && (length(xs: route_shells(n: tree, emitted: ^dag_surface_empty_block_body, acc: [])) == 0) + } +} + +// `if_expr` (gunbc#13056): one `if cond` head, then the then-form or the block form. The then-form +// carries no block body, so it is told from the block form by the absence of an fn_body shell. +test fn if_then_else_takes_the_then_form_route_holds() -> Bool { + match route_parse_at(text: "if c then a else b", production: ^dag_production_if_expr) { + Rejected { diagnostics: _ } => false + Accepted { value: tree, diagnostics: _ } => + (length(xs: route_shells(n: tree, emitted: ^dag_surface_if_expr, acc: [])) == 1) + && (length(xs: route_shells(n: tree, emitted: ^dag_surface_fn_body, acc: [])) == 0) + } +} diff --git a/dag/test/claim/parse_test_newline_dual_role_operator_mutation_test.dag b/dag/test/claim/parse_test_newline_dual_role_operator_mutation_test.dag index 6f4c34a4c3d..800bcaf5225 100644 --- a/dag/test/claim/parse_test_newline_dual_role_operator_mutation_test.dag +++ b/dag/test/claim/parse_test_newline_dual_role_operator_mutation_test.dag @@ -2,8 +2,9 @@ module test.claim.parse_test_newline_dual_role_operator_mutation import v2.compiler.parse { PreparedGrammar, parse_module_prepared, prepare_grammar } import v2.std.diagnostic { Outcome } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_grammar, dag_lex } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { dag_grammar } import v2.std.algebra { fold_list } import v2.std.grammar { AfterLineBreak, @@ -13,6 +14,7 @@ import v2.std.grammar { GrammarExprFold, GrammarProduction, GrammarRoot, + InfixTable, LiteralTerminal, ModeledGrammar, Nonterminal, @@ -35,6 +37,7 @@ import v2.std.parse_refusal_reason { KeywordAsValueName, LayoutUnknownWithoutSource, NewlineBeforeDualRoleOperator, + NonAssociativeOperatorChain, ParseRefusalReason } import v2.std.integer { Int } @@ -51,9 +54,9 @@ import v2.test.parse.newline_dual_role_operator_parse { // replaced by a terminal no token carries -- an alternative that can never match -- and asserts that // such arms exist. Only that reason is stripped, so the mutant differs from the live grammar in that // refusal and nothing else by construction, whatever other refusal rows the grammar carries (the -// declared-path keyword refusal, v2.extdeps.languages.dag dag_grammar_declared_path_expr, is one). There is more than one arm because the additive row -// is a helper inlined into each operand position of the comparison and equality rows; it is one -// authored row. Under the mutant the +// declared-path keyword refusal, v2.extdeps.languages.dag dag_grammar_declared_path_expr, is one). The +// arm is the continuation guard of the binary operator table (v2.extdeps.languages.dag +// dag_grammar_binary_expr_expr), one authored row. Under the mutant the // next-line sources parse again (the silent merge into one subtraction is back); under the live // grammar they refuse (the controls in v2.test.parse.newline_dual_role_operator_parse). The // same-line form parses under both. @@ -83,7 +86,8 @@ fn strip_refusals(expr: GrammarExpr) -> GrammarExpr { } }, after_line_break: fn(e) { AfterLineBreak { element: e } }, - not_predicate: fn(e) { NotPredicate { element: e } } + not_predicate: fn(e) { NotPredicate { element: e } }, + infix_table: fn(operand, guard, operators) { InfixTable { operand: operand, continuation_guard: guard, operators: operators } } } ) } @@ -99,6 +103,7 @@ fn is_newline_refusal(reason: ParseRefusalReason) -> Bool { DeclarationMarkerNotTest => false KeywordAsValueName => false TokenStartsNotIncreasing => false + NonAssociativeOperatorChain => false } } @@ -116,7 +121,8 @@ fn newline_refusal_arm_count(expr: GrammarExpr) -> Int { expect: fn(e, _reason) { e }, refuse_on_match: fn(e, reason) { if is_newline_refusal(reason: reason) { e + 1 } else { e } }, after_line_break: fn(e) { e }, - not_predicate: fn(e) { e } + not_predicate: fn(e) { e }, + infix_table: fn(operand, guard, _operators) { operand + guard } } ) } @@ -164,11 +170,11 @@ fn nlm_mutant_prepared() -> Outcome { fn accepts_prepared(text: String, file: Symbol, prepared: Outcome) -> Bool { match prepared { Rejected { diagnostics: _ } => false - Accepted { value: p, diagnostics: residue } => + Accepted { value: p, diagnostics: _ } => match tokenize(text: text, file: file, rules: dag_lex()) { Rejected { diagnostics: _ } => false Accepted { value: stream, diagnostics: _ } => - match parse_module_prepared(tokens: stream, prepared: p, validation_residue: residue) { + match parse_module_prepared(tokens: stream, prepared: p) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } diff --git a/dag/test/claim/pcb_stackup_witness_test.dag b/dag/test/claim/pcb_stackup_witness_test.dag index 8c0e0920c3b..b407a7d93cb 100644 --- a/dag/test/claim/pcb_stackup_witness_test.dag +++ b/dag/test/claim/pcb_stackup_witness_test.dag @@ -153,7 +153,11 @@ test fn w_red_a_multilayer_board_without_a_reference_plane_is_refused() -> Bool match admit_stackup(s: all_signal, authority: stackup_authority) { StackupAdmitted { admitted: _ } => false StackupRefused { causes: c } => - count(c) == 1 && c.first() == RoutedBoardHasNoReferencePlane + count(c) == 1 + && match c.first() { + Present { value: cause } => cause == RoutedBoardHasNoReferencePlane + Absent => false + } } } @@ -192,7 +196,12 @@ test fn w_red_a_zero_thickness_dielectric_alone_is_refused() -> Bool { } match admit_stackup(s: shorted, authority: stackup_authority) { StackupAdmitted { admitted: _ } => false - StackupRefused { causes: c } => count(c) == 1 && c.first() == DielectricLayerHasNoThickness + StackupRefused { causes: c } => + count(c) == 1 + && match c.first() { + Present { value: cause } => cause == DielectricLayerHasNoThickness + Absent => false + } } } diff --git a/dag/test/claim/primitive_projection_authority_witness_test.dag b/dag/test/claim/primitive_projection_authority_witness_test.dag index 2f4723f6c6b..dbd910a9deb 100644 --- a/dag/test/claim/primitive_projection_authority_witness_test.dag +++ b/dag/test/claim/primitive_projection_authority_witness_test.dag @@ -3,6 +3,7 @@ module test.claim.primitive_projection_authority_witness_test import std.decl_ref { DeclarationRef, decl_ref } import std.primitive_projection { DeclarationPrimitiveUndisposed, + PrimitiveProjection, DeclarationProjectsNoPrimitive, DeclarationProjectsPrimitive, DivergentProjection, @@ -20,6 +21,7 @@ import std.primitive_projection { } import std.primitive_identity { BuiltinRegistry, + primitive_projection_roster, DerivedFromAuthority, EmitHandler, InterpreterDispatch, @@ -50,7 +52,7 @@ import std.types { Bool, List, String } // Executes the question a candidate collector asks of this carrier: given a DECLARATION, is it the // modeled surface of a PRIMITIVE, and are the two one authority or two. The discriminating control -// is the DivergentProjection row -- v2.std.collection map_get -- which must answer NOT one +// is the DivergentProjection row -- v2.std.collection map_get_checked -- which must answer NOT one // authority while the seam and modeled rows answer that they are. A carrier that collapsed the // three fidelities into a boolean suppress flag reds w_divergent_projection_is_not_one_authority, // which is the whole reason the third variant exists. @@ -91,7 +93,7 @@ test fn w_modeled_projection_answers_with_its_fidelity() -> Bool { test fn w_divergent_projection_answers_with_its_fidelity() -> Bool { match primitive_projection_for_declaration( - declaration: projection_declaration(module_path: "v2.std.collection", decl_name: "map_get") + declaration: projection_declaration(module_path: "v2.std.collection", decl_name: "map_get_checked") ) { DeclarationProjectsNoPrimitive => false DeclarationPrimitiveUndisposed { authored_symbol: _ } => false @@ -158,7 +160,7 @@ test fn w_undisposed_is_not_one_authority_and_so_never_suppresses() -> Bool { test fn w_divergent_projection_is_not_one_authority() -> Bool { !declaration_and_primitive_are_one_authority( - declaration: projection_declaration(module_path: "v2.std.collection", decl_name: "map_get"), + declaration: projection_declaration(module_path: "v2.std.collection", decl_name: "map_get_checked"), primitive: primitive_map_get, ) } @@ -337,3 +339,41 @@ test fn w_live_census_answers_runtime_coverage_for_a_named_symbol() -> Bool { RuntimeRowAbsentFromEnumeratedSurfaces => false } } + +// ---- the forked-spelling census (RFM one_spelling_names_a_primitive_*) ---------------------- +// A roster row whose declared function bears its primitive's OWN runtime name while carrying a +// DIVERGENT contract is one spelling naming two meanings. The declaration must stop squatting on +// the primitive's name: the census refuses the class. This measures the roster DATA; the +// compiler-side census check over v1.compiler.infer_method's declaration set is the next rung and +// is reported as a follow-up, not built here. +fn projection_row_shares_spelling_with_divergent_contract(row: PrimitiveProjection) -> Bool { + match row.fidelity { + DivergentProjection { divergence: _ } => + (row.declaration.decl_name as String) == primitive_identity_runtime_name(identity: row.primitive) + _ => false + } +} + +test fn w_projection_census_declares_no_divergent_shared_spelling() -> Bool { + primitive_projection_roster() |> fold(init: true, f: fn(acc, row) { + acc && !projection_row_shares_spelling_with_divergent_contract(row: row) + }) +} + +// Positive control: the projection FACT is kept — the declared Outcome-wrapped lookup still +// projects the primitive, under its own non-primitive spelling, with the divergence named. +fn projection_row_is_the_outcome_lookup_projection(row: PrimitiveProjection) -> Bool { + match row.fidelity { + DivergentProjection { divergence: _ } => + (row.declaration.module_path as String) == "v2.std.collection" + && (row.declaration.decl_name as String) == "map_get_checked" + && (row.primitive.slug as String) == "primitive.map_get" + _ => false + } +} + +test fn w_projection_census_keeps_the_outcome_lookup_projection_under_its_own_name() -> Bool { + primitive_projection_roster() |> fold(init: false, f: fn(acc, row) { + acc || projection_row_is_the_outcome_lookup_projection(row: row) + }) +} diff --git a/dag/test/claim/provisioning/seed_artifact_fetch_forged_probe_witness_test.dag b/dag/test/claim/provisioning/seed_artifact_fetch_forged_probe_witness_test.dag new file mode 100644 index 00000000000..d710142603d --- /dev/null +++ b/dag/test/claim/provisioning/seed_artifact_fetch_forged_probe_witness_test.dag @@ -0,0 +1,58 @@ +module test.claim.seed_artifact_fetch_forged_probe_witness + +import std.types { String, Bool, Int, List } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE REBOUND-FETCH ROUTES, ENROLLED AS EXECUTED REDS (DESIGN §4b: a state unrepresentable in the +// accepted corpus is still representable as source handed to the compiler). An AdmittedSeedArtifact +// is admitted against one input's seed population and fetched only into that input's seed +// directory under the remaster's work directory. test.probe.seed_artifact_fetch_forged_probe calls +// both fetch helpers from outside the remaster with a seed directory of its own choosing, and mints +// an admitted artifact without the admission; each must refuse at its own subject. A control source +// that only NAMES the modules refuses at none of them, so the refusals are the forgeries'. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn probe() -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: "dag/test/probe/seed_artifact_fetch_forged_probe.dag").content) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_seed_artifact_fetch_harness_control\nimport std.types { Bool, String }\nimport extdeps.provisioning.ubuntu_seeded_install_media { AdmittedSeedArtifact }\nimport extdeps.provisioning.ubuntu_seeded_install_media_remaster { install_media_remaster_write_nocloud_seed }\nfn named(a: AdmittedSeedArtifact) -> String { a.basename.value as String }\n" + +data sealed_helpers: List = ["install_media_remaster_fetch_artifact", "install_media_remaster_fetch_artifacts"] + +data sealed_records: List = ["SeedArtifactBasename", "AdmittedSeedArtifact"] + +test fn a_source_that_only_names_the_remaster_rebinds_nothing() -> Bool { + let c = census_of(source: control_source) + (sealed_helpers |> all(h => blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: h) == 0)) + && (sealed_records |> all(r => blocking(c: c, wanted: "SoleConstructorViolation", subject: r) == 0)) +} + +test fn each_fetch_helper_refuses_a_caller_outside_the_checked_route() -> Bool { + let c = probe() + sealed_helpers |> all(h => blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: h) >= 1) +} + +test fn an_admitted_artifact_cannot_be_minted_without_the_admission() -> Bool { + let c = probe() + sealed_records |> all(r => blocking(c: c, wanted: "SoleConstructorViolation", subject: r) >= 1) +} diff --git a/dag/test/claim/public_workload_census_witness_test.dag b/dag/test/claim/public_workload_census_witness_test.dag index f54a2b9beb4..4fe3d2b016b 100644 --- a/dag/test/claim/public_workload_census_witness_test.dag +++ b/dag/test/claim/public_workload_census_witness_test.dag @@ -74,6 +74,8 @@ data candidate_biome_blob_mismatch_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -123,6 +125,8 @@ data candidate_biome_matrix_selector_mismatch_control: WorkloadCandidate = Workl status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -178,6 +182,8 @@ data candidate_pdns_axis_name_mismatch_control: WorkloadCandidate = WorkloadCand status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -238,6 +244,8 @@ data candidate_pdns_axis_name_swap_control: WorkloadCandidate = WorkloadCandidat status: Completed, conclusion: Present { value: Success }, labels: ["ubuntu-24.04-arm"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T09:57:28Z" }, completed_at: Present { value: "2026-09-04T10:07:09Z" }, @@ -291,6 +299,8 @@ data candidate_biome_job_key_mismatch_control: WorkloadCandidate = WorkloadCandi status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -339,6 +349,8 @@ data candidate_biome_job_key_titlecase_uninspected_control: WorkloadCandidate = status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -387,6 +399,8 @@ data candidate_biome_multi_label_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["self-hosted", "linux", "arm64"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -435,6 +449,8 @@ data candidate_empty_labels_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -483,6 +499,8 @@ data candidate_absent_conclusion_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Absent, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -531,6 +549,8 @@ data candidate_run_attempt_zero_control: WorkloadCandidate = WorkloadCandidate { status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, @@ -579,6 +599,8 @@ data candidate_empty_matrix_axes_control: WorkloadCandidate = WorkloadCandidate status: Completed, conclusion: Present { value: Success }, labels: ["depot-ubuntu-24.04-arm-16"], + runner_id: none, + runner_name: none, created_at: Absent, started_at: Present { value: "2026-09-04T16:03:32Z" }, completed_at: Present { value: "2026-09-04T16:08:13Z" }, diff --git a/dag/test/claim/repository_convergence_wet_witness_test.dag b/dag/test/claim/repository_convergence_wet_witness_test.dag index d412f654bc1..6f061b10e45 100644 --- a/dag/test/claim/repository_convergence_wet_witness_test.dag +++ b/dag/test/claim/repository_convergence_wet_witness_test.dag @@ -18,7 +18,8 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // dag/test/claim/repository_convergence_wet_witness_test.dag --functions : // detached_primary_transition_executes_and_preserves_packed_loose_refs_and_linked_worktree, // already_at_candidate_repairs_a_stale_index, cas_race_is_observed_after_fetch, -// reset_refusal_after_successful_cas_is_nonzero +// reset_refusal_after_successful_cas_is_nonzero, +// a_legacy_derived_roster_file_is_retired_and_read_back_absent // Scratch-repository WET matrix over the real git transport. // Record/replay receipt recipe (the setup invocation is stable; its recorded stdout carries the @@ -189,3 +190,25 @@ test fn wrong_primary_standing_refuses_from_gits_roster() -> Bool { } } } + +// A LEGACY DERIVED ROSTER LEFT BY AN OLDER gunbc IS RETIRED BY CONVERGENCE AND READ BACK ABSENT. +// The fixture plants the file the old binary wrote; convergence must remove it (the loader never +// does) and only then report success. +test fn a_legacy_derived_roster_file_is_retired_and_read_back_absent() -> Bool { + match scratch_repository(index_lock: false, already_candidate: false, racing_fetch: false) { + Absent => false + Present { value: s } => { + let roster = join([s.repo, "/dag/gunbc/rung_drop/roster.dag"], "") + let planted = mutate_scratch(s: s, body: join([ + "set -eu", + join(["mkdir -p ", s.repo, "/dag/gunbc/rung_drop"], ""), + join(["printf legacy > ", roster], ""), + ], "\n")) + let outcome = run_scratch(s: s) + let absent = mutate_scratch(s: s, body: join(["test ! -e ", roster], "")) + let cleaned = remove_scratch(s: s) + let converged = match outcome { ExitSuccess => true _ => false } + planted && converged && absent && cleaned + } + } +} diff --git a/dag/test/claim/repository_convergence_witness_test.dag b/dag/test/claim/repository_convergence_witness_test.dag index 2658d20a22b..8c2a74aba52 100644 --- a/dag/test/claim/repository_convergence_witness_test.dag +++ b/dag/test/claim/repository_convergence_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.repository_convergence_witness_test import std.types { String, Bool, List } +import v2.std.algebra { any } import extdeps.git.enumeration { GitRefSetObservation, GitRefSetReadRefused, git_ref_set_observation_of, } @@ -8,6 +9,8 @@ import extdeps.git.worktree_enumeration { GitWorktreeSetObservation, git_worktree_set_observation_of, } import gunbc.live_deploy.repository_convergence { + superseded_workspace_file_paths, + ConvergenceSupersededFileNotRetired, RepositoryState, GitTextObserved, RepositoryCoherenceObservation, RepositoryCoherenceObserved, @@ -687,3 +690,20 @@ test fn a_symref_targeting_another_ref_that_moved_still_refuses() -> Bool { } } +// THE RETIREMENT POPULATION IS THE FILE-GRAIN SupersededAuthority ROWS OF gunbc.repo_workspace AND +// NOTHING ELSE: both legacy roster files are in it, and the superseded DIRECTORY rows +// (`/gunbc-planning/`, `/scripts/fixtures/`) are not -- a directory pattern is never retired here. +test fn superseded_file_retirement_population_is_the_two_legacy_roster_files() -> Bool { + let paths = superseded_workspace_file_paths() + length(xs: paths) == 2 + && any(paths, p => p == "dag/gunbc/recurring_failure_mode/roster.dag") + && any(paths, p => p == "dag/gunbc/rung_drop/roster.dag") + && !any(paths, p => string_contains(s: p, pattern: "gunbc-planning")) +} + +test fn an_unretired_superseded_file_is_not_converged() -> Bool { + !repository_convergence_converged(outcome: ConvergenceSupersededFileNotRetired { + path: "/opt/gunbc/gunbc/dag/gunbc/rung_drop/roster.dag", + detail: "delete refused: permission denied", + }) +} diff --git a/dag/test/claim/resolved_call_emission_identity_witness_test.dag b/dag/test/claim/resolved_call_emission_identity_witness_test.dag index a795d682574..d2a133d618d 100644 --- a/dag/test/claim/resolved_call_emission_identity_witness_test.dag +++ b/dag/test/claim/resolved_call_emission_identity_witness_test.dag @@ -63,11 +63,11 @@ test fn w_imported_generic_contains_keeps_declared_identity() -> Bool { // "lower to the bridge" would go red here: the row is DivergentProjection -- the declaration // returns Outcome> and the primitive returns Optional -- and the declaration is // what the author called. -test fn w_divergent_map_get_keeps_declared_outcome_identity() -> Bool { +test fn w_divergent_map_get_checked_keeps_declared_outcome_identity() -> Bool { compile_dag_rust_emit_check( - "module test.claim.declared_map_get_probe\nimport std.types { Int, String, Map }\nimport std.occurrence_identity { OccurrenceId }\nimport v2.std.collection { map_get }\nimport v2.std.diagnostic { Outcome }\nimport v2.std.optional { Optional }\nfn probe(m: Map) -> Outcome> { map_get(m: m, key: \"key\") }\n", + "module test.claim.declared_map_get_probe\nimport std.types { Int, String, Map }\nimport std.occurrence_identity { OccurrenceId }\nimport v2.std.collection { map_get_checked }\nimport v2.std.diagnostic { Outcome }\nimport std.optional { Optional }\nfn probe(m: Map) -> Outcome> { map_get_checked(m: m, key: \"key\") }\n", "src/test_claim_declared_map_get_probe.rs", - ["crate::v2_std_collection::map_get("], + ["crate::v2_std_collection::map_get_checked("], ["v1_rt::map_get("] ) } diff --git a/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag b/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag index 70241f2e6f0..368e2fa2376 100644 --- a/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag +++ b/dag/test/claim/roadmap/dispatch_worker_confinement_witness_test.dag @@ -1,6 +1,6 @@ module test.claim.roadmap.dispatch_worker_confinement_witness_test -import std.types { Bool, String, List } +import std.types { Bool, String, List, NonEmptyStr } import gunbc.roadmap_dispatch_actuator { dispatch_worker_unit_properties, attempt_state_prepare_argv_for_instance, dispatch_attempt_verification_dir_for_instance, attempt_unit_substate_has_exited, dispatch_review_unit_properties } import extdeps.systemd { SystemdUnitProperty, systemd_unit_property_wire } import extdeps.systemd.systemd_run { SystemdRunProperty } @@ -21,7 +21,7 @@ fn has_property(props: List, needle: String) -> Bool { test fn the_worker_unit_confines_writes_to_the_instance_root() -> Bool { let inst = srv2_deploy_dashboard_instance() - let props = dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1048576), process_bounds: session_process_bounds) + let props = dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1048576), process_bounds: session_process_bounds, exit_hook: "mkdir -- /i/attempt-exit-spool/inbox/n+k+x" as NonEmptyStr) has_property(props: props, needle: "ProtectSystem=strict") && has_property(props: props, needle: "ProtectHome=read-only") && has_property(props: props, needle: "PrivateTmp=yes") @@ -53,27 +53,39 @@ test fn the_prepare_step_creates_the_verification_directory_with_the_attempt_dir && string_contains(s: verification_dir, pattern: "/verification") } -// DISCRIMINATING: a successful worker's unit must stay observable after exit, and the observer must -// read the exit from SubState rather than ActiveState (which stays active under RemainAfterExit). -// Before 2026-09-05 a unit that exited 0 was collected and read as never having run. -test fn the_worker_unit_outlives_its_process_and_substate_decides_exit() -> Bool { +// DISCRIMINATING: the worker unit no longer outlives its process (reversal of 2026-09-05, bold-bee-114 +// ruling 2026-10-06): it carries no RemainAfterExit, so it stops when the worker exits and its +// ExecStopPost exit record is written at that moment; the record, not unit-loaded state, is the exit +// evidence once the unit is collected. SubState still decides exit for a unit that is loaded (a failed +// worker is retained as failed). +test fn the_worker_unit_stops_with_its_process_and_writes_its_exit_record() -> Bool { let inst = srv2_deploy_dashboard_instance() - let props = dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1048576), process_bounds: session_process_bounds) - has_property(props: props, needle: "RemainAfterExit=yes") + let props = dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1048576), process_bounds: session_process_bounds, exit_hook: "mkdir -- /i/attempt-exit-spool/inbox/n+k+x" as NonEmptyStr) + !has_property(props: props, needle: "RemainAfterExit") + && has_property(props: props, needle: "ExecStopPost=mkdir -- /i/attempt-exit-spool/inbox/n+k+x") && attempt_unit_substate_has_exited(substate: "exited") && attempt_unit_substate_has_exited(substate: "failed") && !attempt_unit_substate_has_exited(substate: "running") - && !attempt_unit_substate_has_exited(substate: "start-pre") + && !attempt_unit_substate_has_exited(substate: "stop-post") } // DISCRIMINATING: the reviewer may write only its review directory, and it runs from the repo root -// (the worktree as cwd made `gunbc run` refuse its own source roots, exit 101). +// (the worktree as cwd made `gunbc run` refuse its own source roots, exit 101). With an exit hook it +// stops with its process and records its exit (bold-bee-114 ruling 2026-10-06: the reviewer's +// RemainAfterExit is removed); the hook is given, the inbox is NOT among its writable paths (the "+" +// prefix lets the hook write there while the reviewer cannot). The supervisor passes no hook and +// keeps RemainAfterExit. test fn the_reviewer_unit_writes_only_its_review_directory_and_runs_from_the_repo_root() -> Bool { let inst = srv2_deploy_dashboard_instance() - let props = dispatch_review_unit_properties(instance: inst, working_directory: inst.repo_root as String, log_path: "/r/log", review_dir: "/state/a/review") + let props = dispatch_review_unit_properties(instance: inst, working_directory: inst.repo_root as String, log_path: "/r/log", review_dir: "/state/a/review", exit_hook: Present { value: "+mkdir -- /i/attempt-exit-spool/inbox/x" as NonEmptyStr }) + let supervisor = dispatch_review_unit_properties(instance: inst, working_directory: inst.repo_root as String, log_path: "/r/log", review_dir: "/state/a/supervisor", exit_hook: none) has_property(props: props, needle: "ReadWritePaths=/state/a/review") && !has_property(props: props, needle: "/.git/worktrees") + && !has_property(props: props, needle: "ReadWritePaths=/i/attempt-exit-spool") && has_property(props: props, needle: join(["WorkingDirectory=", inst.repo_root as String], "")) && has_property(props: props, needle: "ProtectHome=read-only") - && has_property(props: props, needle: "RemainAfterExit=yes") + && has_property(props: props, needle: "ExecStopPost=+mkdir -- /i/attempt-exit-spool/inbox/x") + && !has_property(props: props, needle: "RemainAfterExit") + && has_property(props: supervisor, needle: "RemainAfterExit=yes") + && !has_property(props: supervisor, needle: "ExecStopPost") } diff --git a/dag/test/claim/roadmap/roadmap_attempt_exit_spool_witness_test.dag b/dag/test/claim/roadmap/roadmap_attempt_exit_spool_witness_test.dag new file mode 100644 index 00000000000..6c19df8f597 --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_attempt_exit_spool_witness_test.dag @@ -0,0 +1,117 @@ +module test.claim.roadmap.roadmap_attempt_exit_spool_witness_test + +import extdeps.posix.signal { linux_standard_signal_number } +import std.types { String, List, Bool } +import gunbc.roadmap.roadmap_attempt_exit_spool { + attempt_exit_hook, AttemptExitHookRendered, AttemptExitHookRefused, AttemptWorkerRole, AttemptReviewerRole, + attempt_exit_entry_decode, AttemptExitEntryDecoded, AttemptExitEntryMalformed, + AttemptProcessExitedWith, AttemptProcessSignaled, + AttemptWorkerRole, AttemptReviewerRole, AttemptAuditorRole, + attempt_exit_record_from_listings, AttemptExitStageNames, AttemptExitStageUnreadable, + AttemptExitRecordFound, AttemptExitRecordNone, AttemptExitRecordUnreadable, +} +import extdeps.systemd.service_exec { ServiceResultSuccess, ServiceResultSignal } + +// The hook is one typed mkdir argv whose single operand is the record's name, with the four stop +// variables spelled as systemd expansions and no shell anywhere. +test fn exit_hook_is_a_create_only_mkdir_of_the_record_name() -> Bool { + match attempt_exit_hook(instance_root: "/opt/gunbc-srv2", node_id: "shell-dag-live", attempt_key: "01feb0b65dee1377", role: AttemptWorkerRole) { + AttemptExitHookRendered { exec_line } => + (exec_line as String) == "+mkdir -- /opt/gunbc-srv2/attempt-exit-spool/inbox/shell-dag-live+01feb0b65dee1377+worker+$\{INVOCATION_ID}+$\{SERVICE_RESULT}+$\{EXIT_CODE}+$\{EXIT_STATUS}" + AttemptExitHookRefused { reason: _ } => false + } +} + +test fn exit_hook_refuses_an_instance_root_systemd_would_reinterpret() -> Bool { + match attempt_exit_hook(instance_root: "/opt/gunbc %n", node_id: "a", attempt_key: "b", role: AttemptWorkerRole) { + AttemptExitHookRefused { reason: _ } => true + AttemptExitHookRendered { exec_line: _ } => false + } +} + +test fn an_exit_record_decodes_to_its_status_and_a_kill_to_its_signal() -> Bool { + let exited = match attempt_exit_entry_decode(entry_name: "n-1+k1+worker+0123456789abcdef0123456789abcdef+success+exited+0") { + AttemptExitEntryDecoded { record } => + record.node_id == "n-1" && record.attempt_key == "k1" + && (match record.result { ServiceResultSuccess => true _ => false }) + && (match record.end { AttemptProcessExitedWith { status } => status == 0 AttemptProcessSignaled { signal: _ } => false }) + AttemptExitEntryMalformed { entry_name: _, reason: _ } => false + } + let killed = match attempt_exit_entry_decode(entry_name: "n-1+k1+worker+0123456789abcdef0123456789abcdef+signal+killed+TERM") { + AttemptExitEntryDecoded { record } => + (match record.result { ServiceResultSignal => true _ => false }) + && (match record.end { AttemptProcessSignaled { signal } => linux_standard_signal_number(signal: signal) == 15 AttemptProcessExitedWith { status: _ } => false }) + AttemptExitEntryMalformed { entry_name: _, reason: _ } => false + } + exited && killed +} + +test fn a_name_that_is_not_seven_parts_or_whose_status_contradicts_its_code_is_malformed() -> Bool { + let short = match attempt_exit_entry_decode(entry_name: "n+k+worker+inv+success+exited") { AttemptExitEntryMalformed { entry_name: _, reason: _ } => true AttemptExitEntryDecoded { record: _ } => false } + let contradiction = match attempt_exit_entry_decode(entry_name: "n+k+worker+inv+success+exited+TERM") { AttemptExitEntryMalformed { entry_name: _, reason: _ } => true AttemptExitEntryDecoded { record: _ } => false } + let unlisted_code = match attempt_exit_entry_decode(entry_name: "n+k+worker+inv+success+vanished+0") { AttemptExitEntryMalformed { entry_name: _, reason: _ } => true AttemptExitEntryDecoded { record: _ } => false } + short && contradiction && unlisted_code +} + +// The observer's three arms over supplied listings: a record in any stage is found; every stage read +// and none for this attempt is None (never "never ran"); a stage that could not be read is +// Unreadable, never None. +test fn record_found_in_any_stage_none_when_all_read_unreadable_when_one_is_not() -> Bool { + let other = "other+k9+worker+inv+success+exited+0" + let mine = "n-1+k1+worker+inv+exit-code+exited+3" + let found = match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [other] }, AttemptExitStageNames { names: [] }, AttemptExitStageNames { names: [mine] }]) { + AttemptExitRecordFound { record } => match record.end { AttemptProcessExitedWith { status } => status == 3 AttemptProcessSignaled { signal: _ } => false } + AttemptExitRecordNone => false + AttemptExitRecordUnreadable { reason: _ } => false + } + let none_found = match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [other] }, AttemptExitStageNames { names: [] }, AttemptExitStageNames { names: [] }]) { + AttemptExitRecordNone => true + AttemptExitRecordFound { record: _ } => false + AttemptExitRecordUnreadable { reason: _ } => false + } + let unreadable = match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [mine] }, AttemptExitStageUnreadable { reason: "EACCES" }, AttemptExitStageNames { names: [] }]) { + AttemptExitRecordUnreadable { reason: _ } => true + AttemptExitRecordFound { record: _ } => false + AttemptExitRecordNone => false + } + found && none_found && unreadable +} + +// The role is part of the subject: a reviewer's record for the same attempt never answers for its +// worker, and the reviewer's hook names its criterion and head. +test fn a_records_role_is_part_of_its_subject() -> Bool { + let only_reviewer = match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: ["n-1+k1+review.conformance-leasing.abc123+inv+success+exited+0"] }]) { + AttemptExitRecordNone => true + AttemptExitRecordFound { record: _ } => false + AttemptExitRecordUnreadable { reason: _ } => false + } + let reviewer_found = match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptReviewerRole { criterion_key: "conformance-leasing", head_sha: "abc123" }, listings: [AttemptExitStageNames { names: ["n-1+k1+worker+inv+success+exited+0", "n-1+k1+review.conformance-leasing.abc123+inv+success+exited+0"] }]) { + AttemptExitRecordFound { record } => record.role == "review.conformance-leasing.abc123" + AttemptExitRecordNone => false + AttemptExitRecordUnreadable { reason: _ } => false + } + only_reviewer && reviewer_found +} + +// A SIGNAL NAME OUTSIDE signal(7)'s standard set has no constructor, so the record that names one is +// refused at decode rather than carried as text to the point of use. +test fn an_unlisted_signal_name_is_refused_at_decode() -> Bool { + match attempt_exit_entry_decode(entry_name: "n-1+k1+worker+0123456789abcdef0123456789abcdef+signal+killed+RTMIN") { + AttemptExitEntryMalformed { entry_name: _, reason: _ } => true + AttemptExitEntryDecoded { record: _ } => false + } +} + +// Review 78301: an entry that names this attempt but cannot be decoded (non-numeric status) is its end +// UNOBSERVED, never absent -- settlement must not free on it. Another attempt's malformed entry is not this one's. +test fn an_undecodable_entry_for_this_attempt_is_unreadable_and_another_attempts_is_not() -> Bool { + let mine = "n-1+k1+worker+inv+exit-code+exited+BOGUS" + let other = "n-2+k9+worker+inv+exit-code+exited+BOGUS" + (match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [mine] }]) { + AttemptExitRecordUnreadable { reason: _ } => true + _ => false + }) && (match attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [other] }]) { + AttemptExitRecordNone => true + _ => false + }) +} diff --git a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag index d11c8408f5d..725a79909c5 100644 --- a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag @@ -110,7 +110,10 @@ import gunbc.roadmap_belt_actuate { belt_commit_outcome_receipt, belt_commit_outcome_receipt_json, belt_commit_outcome_is_failure, belt_commit_outcome_is_recorded, belt_commit_pass_outcome_from, belt_commit_outcome_json_value, - belt_tick_commit_poisoned_count, belt_tick_result_json_value, + belt_tick_result_json_value, + BeltDiscovered, BeltDiscoveryRefused, + BeltAttemptObligationReceiptPresent, BeltAttemptObligationReceiptAbsent, BeltAttemptObligationReceiptUnreadable, + belt_attempt_obligation_receipt_decode, belt_pass_outcome_json, BeltAttemptObligationReceiptRead, belt_attempt_obligation_run_json, BeltAttemptObligationsRan, BeltAttemptObligationsNotCurrent, BeltAttemptObligationStep, BeltExecOutcome, ExecOk, ExecFailed, ExecRefused, belt_exec, belt_exec_all, @@ -1392,176 +1395,184 @@ test fn the_known_spawn_workdir_resolves_to_its_own_instance() -> Bool { } } -// RED control (operator P1, 2026-08-02): a tmux-list refusal must NOT zero the verify pass. The -// defect class was belt_tick returning empty verify_outcomes on ObserveRefused — an absorbing -// fallback that made the whole tick look like verification never ran. The witness constructs the -// post-fix shape synthetically: spawn_pass Refused while verify_pass is still Recorded when verify -// outcomes exist. - -test fn observe_refused_spawn_does_not_zero_verify_pass() -> Bool { +// RED control (operator P1, 2026-08-02), carried onto the demoted tick: a tmux-list refusal must NOT +// zero the tick's other pass. The defect class was an absorbing fallback that made the whole tick look +// like nothing ran; verification has since left the tick (dogfood intervention #6), and the pass that +// stands beside spawn now is discovery. A spawn refused on session observation leaves discovery +// recorded on its own evidence, and a discovery whose ledger could not be read refuses ITSELF without +// touching spawn. +test fn observe_refused_spawn_does_not_zero_the_discovery_pass() -> Bool { let result = BeltTickResult { wholesale: TickPassesRun, host_refusal: Present { value: LaunchSessionsUnobservable { reason: "belt_observe: tmux ls failed; session containers are unknowable" } }, reconcile: belt_tick_empty_reconcile(state: ManualReady), spawn_outcomes: [], teardown_outcomes: [], - commit_outcomes: [], - verify_outcomes: [ - VerifyRecorded { - node_id: "v2-emitter-producer-provenance", - verdict_key: "refused", - detail: "validation refused", - }, - ], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], + discovery: BeltDiscovered { owed: 1, enqueues: [BeltObligationStarted { unit: "gunbc-obligation-attempt-n-k.service" as NonEmptyStr }] }, } let receipt = belt_tick_receipt_from_result( result: result, observed_at: "2026-08-02T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, ) - match receipt.spawn_pass { - BeltPassRefused { reason: _ } => true - _ => false - } - && match receipt.verify_pass { - BeltPassRecorded => true - _ => false - } + (match receipt.spawn_pass { BeltPassRefused { reason: _ } => true _ => false }) + && (match receipt.discovery_pass { BeltPassRecorded => true _ => false }) } -test fn observe_refused_with_empty_verify_is_deferred_not_recorded() -> Bool { - let result = BeltTickResult { - wholesale: TickPassesRun, - host_refusal: Present { value: LaunchSessionsUnobservable { reason: "tmux refused" } }, - reconcile: belt_tick_empty_reconcile(state: ManualReady), - spawn_outcomes: [], - teardown_outcomes: [], - commit_outcomes: [], - verify_outcomes: [ - VerifyDeferred { - node_id: "node-a", - reason: "agent obligation is not satisfied", - }, - ], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], - } - match belt_tick_receipt_from_result( - result: result, +test fn an_unreadable_ledger_refuses_discovery_and_a_refused_handoff_fails_it() -> Bool { + let refused = belt_tick_receipt_from_result( + result: BeltTickResult { + wholesale: TickPassesRun, + host_refusal: Absent, + reconcile: belt_tick_empty_reconcile(state: ManualReady), + spawn_outcomes: [], + teardown_outcomes: [], + discovery: BeltDiscoveryRefused { reason: "the attempts ledger is unobservable" }, + }, observed_at: "2026-08-02T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, - ).verify_pass { - BeltPassDeferred { reason: _ } => true - BeltPassRecorded => false - BeltPassRefused { reason: _ } => false - BeltPassFailed { reason: _ } => false - } + ) + let failed = belt_tick_receipt_from_result( + result: BeltTickResult { + wholesale: TickPassesRun, + host_refusal: Absent, + reconcile: belt_tick_empty_reconcile(state: ManualReady), + spawn_outcomes: [], + teardown_outcomes: [], + discovery: BeltDiscovered { owed: 2, enqueues: [ + BeltObligationHeld { unit: "gunbc-obligation-attempt-a-1.service" as NonEmptyStr }, + BeltObligationEnqueueRefused { unit: "gunbc-obligation-attempt-b-2.service" as NonEmptyStr, reason: "systemd-run exited 1" }, + ] }, + }, + observed_at: "2026-08-02T02:00:00Z", + execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, + ) + (match refused.discovery_pass { BeltPassRefused { reason } => string_contains(s: reason, pattern: "unobservable") _ => false }) + && (match failed.discovery_pass { BeltPassFailed { reason } => string_contains(s: reason, pattern: "attempt-b-2") && !string_contains(s: reason, pattern: "attempt-a-1") _ => false }) } // review 47210 spec-without-execution: decoupling was witnessed at the receipt layer but the // presentation join had no executing consumer. These rows execute -// belt_verification_presentation_detail and the wire join against synthetic tick reads so the -// headline strings ('Verification queued - next belt tick', 'Verification blocked · …', -// observe-refusal vs verify-recorded at the UI layer) cannot drift without going red. +// belt_verification_presentation_detail and the wire join against synthetic reads so the headline +// strings cannot drift without going red. Since dogfood intervention #6 a pending verification reads +// its OWN attempt's obligation receipt first and the tick's discovery pass only when the attempt has no +// verify step on record. -test fn verification_presentation_absent_receipt_is_queued() -> Bool { +test fn verification_presentation_absent_receipts_are_queued() -> Bool { belt_verification_presentation_detail( progress: completed_attempt_progress(node_id: "pres-absent-node"), tick_read: BeltTickReceiptAbsent, - ) == "Verification queued - next belt tick" + obligation_read: BeltAttemptObligationReceiptAbsent, + ) == "Verification queued - awaiting its obligation" } -test fn verification_presentation_unreadable_receipt_is_blocked() -> Bool { - let detail = belt_verification_presentation_detail( +test fn verification_presentation_unreadable_receipts_are_blocked() -> Bool { + let tick = belt_verification_presentation_detail( progress: completed_attempt_progress(node_id: "pres-unreadable-node"), tick_read: BeltTickReceiptUnreadable { reason: "belt tick receipt schema is unknown" }, + obligation_read: BeltAttemptObligationReceiptAbsent, ) - string_contains(s: detail, pattern: "Verification blocked · belt tick receipt unreadable: ") - && string_contains(s: detail, pattern: "schema is unknown") -} - -test fn verification_presentation_decouples_verify_from_spawn_observe_refusal() -> Bool { - let receipt = belt_tick_receipt_from_result( - result: BeltTickResult { - wholesale: TickPassesRun, - host_refusal: Present { value: LaunchSessionsUnobservable { reason: "belt_observe: tmux ls failed; session containers are unknowable" } }, - reconcile: belt_tick_empty_reconcile(state: ManualReady), - spawn_outcomes: [], - teardown_outcomes: [], - commit_outcomes: [], - verify_outcomes: [ - VerifyRecorded { - node_id: "v2-emitter-producer-provenance", - verdict_key: "refused", - detail: "validation refused", - }, - ], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], - }, - observed_at: "2026-08-02T02:00:00Z", - execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, - ) - let detail = belt_verification_presentation_detail( - progress: completed_attempt_progress(node_id: "pres-decouple-node"), - tick_read: BeltTickReceiptPresent { receipt: receipt }, + let own = belt_verification_presentation_detail( + progress: completed_attempt_progress(node_id: "pres-unreadable-node"), + tick_read: BeltTickReceiptAbsent, + obligation_read: BeltAttemptObligationReceiptUnreadable { reason: "attempt obligation receipt schema is unknown" }, ) - !string_contains(s: detail, pattern: "tmux ls failed") - && string_contains(s: detail, pattern: "Verification queued") + string_contains(s: tick, pattern: "Verification blocked · belt tick receipt unreadable: ") + && string_contains(s: own, pattern: "Verification blocked · attempt obligation receipt unreadable: ") } -test fn verification_presentation_verify_refused_is_blocked() -> Bool { - let receipt = BeltTickReceipt { +// THE ATTEMPT'S OWN VERIFY STEP DECIDES, AND A REFUSED DISCOVERY ELSEWHERE DOES NOT OVERRIDE IT. +test fn verification_presentation_reads_the_attempts_own_verify_step() -> Bool { + let refused_tick = BeltTickReceipt { observed_at: "2026-08-02T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, launches: [], spawn_pass: BeltPassRecorded, teardown_pass: BeltPassRecorded, - commit_pass: BeltPassRecorded, - verify_pass: BeltPassRefused { reason: "validation receipt could not be read" }, - publish_pass: BeltPassDeferred { reason: "publish waits on verify" }, - metering_pass: BeltPassRecorded, - } - let detail = belt_verification_presentation_detail( - progress: completed_attempt_progress(node_id: "pres-verify-refused-node"), - tick_read: BeltTickReceiptPresent { receipt: receipt }, + discovery_pass: BeltPassRefused { reason: "the attempts ledger is unobservable" }, + } + let blocked = belt_verification_presentation_detail( + progress: completed_attempt_progress(node_id: "pres-own-node"), + tick_read: BeltTickReceiptPresent { receipt: refused_tick }, + obligation_read: BeltAttemptObligationReceiptPresent { verify: Present { value: BeltPassRefused { reason: "validation receipt could not be read" } } }, + ) + let failed = belt_verification_presentation_detail( + progress: completed_attempt_progress(node_id: "pres-own-node"), + tick_read: BeltTickReceiptPresent { receipt: refused_tick }, + obligation_read: BeltAttemptObligationReceiptPresent { verify: Present { value: BeltPassFailed { reason: "validation verdict failed" } } }, ) - detail == "Verification blocked · validation receipt could not be read" + let no_step_falls_to_tick = belt_verification_presentation_detail( + progress: completed_attempt_progress(node_id: "pres-own-node"), + tick_read: BeltTickReceiptPresent { receipt: refused_tick }, + obligation_read: BeltAttemptObligationReceiptPresent { verify: none }, + ) + blocked == "Verification blocked · validation receipt could not be read" + && failed == "Verification failed · validation verdict failed" + && no_step_falls_to_tick == "Verification blocked · the attempts ledger is unobservable" +} + +// The receipt's verify step decodes from the bytes the run writes: a ran receipt whose verify step +// failed reads back that pass, one with no verify step reads back none, and another schema refuses. +test fn the_attempt_obligation_receipt_round_trips_its_verify_step() -> Bool { + let run = BeltAttemptObligationsRan { node_id: "n", attempt_key: "k", steps: [ + BeltAttemptObligationStep { kind: CaptureObligation, pass: BeltPassRecorded, detail: json_string(s: "captured") }, + BeltAttemptObligationStep { kind: VerifyObligation, pass: BeltPassFailed { reason: "validation verdict failed" }, detail: json_string(s: "failed") }, + ] } + let with_verify = match decode_receipt_for_n_k(raw: serialize_json(v: belt_attempt_obligation_run_json(run: run, observed_at: "2026-10-06T00:00:00Z", claimed: ["e1"]))) { + BeltAttemptObligationReceiptPresent { verify: Present { value: BeltPassFailed { reason } } } => reason == "validation verdict failed" + _ => false + } + let without = match decode_receipt_for_n_k(raw: serialize_json(v: belt_attempt_obligation_run_json(run: BeltAttemptObligationsNotCurrent { node_id: "n", attempt_key: "k", reason: "superseded" }, observed_at: "2026-10-06T00:00:00Z", claimed: ["e1"]))) { + BeltAttemptObligationReceiptPresent { verify: Absent } => true + _ => false + } + with_verify && without && receipt_unreadable(raw: "{\"schema\":\"roadmap-belt-attempt-obligations/v0\"}") } -test fn verification_presentation_verify_failed_is_failed() -> Bool { - let receipt = BeltTickReceipt { - observed_at: "2026-08-02T02:00:00Z", - execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, - launches: [], - spawn_pass: BeltPassRecorded, - teardown_pass: BeltPassRecorded, - commit_pass: BeltPassRecorded, - verify_pass: BeltPassFailed { reason: "validation verdict failed" }, - publish_pass: BeltPassDeferred { reason: "publish waits on verify" }, - metering_pass: BeltPassRecorded, - } - let detail = belt_verification_presentation_detail( - progress: completed_attempt_progress(node_id: "pres-verify-failed-node"), - tick_read: BeltTickReceiptPresent { receipt: receipt }, - ) - detail == "Verification failed · validation verdict failed" +fn decode_receipt_for_n_k(raw: String) -> BeltAttemptObligationReceiptRead { + belt_attempt_obligation_receipt_decode(raw: raw, node_id: "n", attempt_key: "k") +} + +fn receipt_unreadable(raw: String) -> Bool { + match decode_receipt_for_n_k(raw: raw) { + BeltAttemptObligationReceiptUnreadable { reason: _ } => true + _ => false + } +} + +fn receipt_with_steps(node: String, attempt: String, standing: String, steps: String) -> String { + join(["{\"schema\":\"roadmap-belt-attempt-obligations/v1\",\"observed_at\":\"2026-10-06T00:00:00Z\",\"claimed_exits\":[],\"node\":\"", node, "\",\"attempt\":\"", attempt, "\",\"standing\":\"", standing, "\",\"steps\":", steps, "}"], "") +} + +// THE DECODER IS TOTAL AND SUBJECT-BOUND (bold-bee-114, 2026-10-09): a foreign node or attempt, a duplicated +// verify, a malformed or unknown kind, a malformed non-verify step and an unknown standing are each +// unreadable, and the well-formed receipt for the subject is the positive control. +test fn the_attempt_obligation_receipt_decoder_is_total_and_subject_bound() -> Bool { + let good_pass = belt_pass_outcome_json(o: BeltPassRecorded) + let verify = join(["{\"kind\":\"verify\",\"pass\":", serialize_json(v: good_pass), "}"], "") + let capture = join(["{\"kind\":\"capture\",\"pass\":", serialize_json(v: good_pass), "}"], "") + let positive = match decode_receipt_for_n_k(raw: receipt_with_steps(node: "n", attempt: "k", standing: "ran", steps: join(["[", capture, ",", verify, "]"], ""))) { + BeltAttemptObligationReceiptPresent { verify: Present { value: BeltPassRecorded } } => true + _ => false + } + positive + && receipt_unreadable(raw: receipt_with_steps(node: "other", attempt: "k", standing: "ran", steps: join(["[", verify, "]"], ""))) + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "other", standing: "ran", steps: join(["[", verify, "]"], ""))) + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "k", standing: "ran", steps: join(["[", verify, ",", verify, "]"], ""))) + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "k", standing: "ran", steps: "[{\"kind\":7,\"pass\":{}}]")) + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "k", standing: "ran", steps: join(["[{\"kind\":\"bogus\",\"pass\":", serialize_json(v: good_pass), "}]"], ""))) + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "k", standing: "ran", steps: join(["[{\"kind\":\"capture\",\"pass\":{\"outcome\":\"???\"}},", verify, "]"], ""))) + && receipt_unreadable(raw: "{\"schema\":\"roadmap-belt-attempt-obligations/v1\",\"observed_at\":\"2026-10-06T00:00:00Z\",\"node\":\"n\",\"attempt\":\"k\",\"standing\":\"not-current\",\"reason\":\"r\"}") + && receipt_unreadable(raw: receipt_with_steps(node: "n", attempt: "k", standing: "weird", steps: join(["[", verify, "]"], ""))) } test fn verification_presentation_wire_carries_detail() -> Bool { let j = serialize_json(v: belt_workflow_attempt_presentation_json_with_tick_read( progress: completed_attempt_progress(node_id: "pres-wire-node"), tick_read: BeltTickReceiptAbsent, + obligation_read: BeltAttemptObligationReceiptAbsent, )) - string_contains(s: j, pattern: "\"verification_presentation\": \"Verification queued - next belt tick\"") + string_contains(s: j, pattern: "\"verification_presentation\": \"Verification queued - awaiting its obligation\"") } test fn malformed_pass_reason_makes_receipt_unreadable() -> Bool { @@ -1570,9 +1581,7 @@ test fn malformed_pass_reason_makes_receipt_unreadable() -> Bool { "\"observed_at\":\"2026-08-02T02:00:00Z\",", "\"spawn_pass\":{\"outcome\":\"recorded\"},", "\"teardown_pass\":{\"outcome\":\"recorded\"},", - "\"commit_pass\":{\"outcome\":\"recorded\"},", - "\"verify_pass\":{\"outcome\":\"refused\"},", - "\"publish_pass\":{\"outcome\":\"recorded\"}}", + "\"discovery_pass\":{\"outcome\":\"refused\"}}", ], "") match belt_tick_receipt_decode(raw: raw) { BeltTickReceiptUnreadable { reason: r } => @@ -2346,14 +2355,13 @@ fn pass_refused_with(o: BeltPassOutcome, pattern: String) -> Bool { } } -fn all_four_refused_with(receipt: BeltTickReceipt, pattern: String) -> Bool { +fn all_passes_refused_with(receipt: BeltTickReceipt, pattern: String) -> Bool { pass_refused_with(o: receipt.spawn_pass, pattern: pattern) && pass_refused_with(o: receipt.teardown_pass, pattern: pattern) - && pass_refused_with(o: receipt.verify_pass, pattern: pattern) - && pass_refused_with(o: receipt.publish_pass, pattern: pattern) + && pass_refused_with(o: receipt.discovery_pass, pattern: pattern) } -test fn transition_halted_tick_refuses_all_four_passes() -> Bool { +test fn transition_halted_tick_refuses_every_pass() -> Bool { let refusal = LaunchTransitionInhibited { reason: "deployment transition in force on this host" } let receipt = belt_tick_receipt_from_result( result: BeltTickResult { @@ -2362,20 +2370,15 @@ test fn transition_halted_tick_refuses_all_four_passes() -> Bool { reconcile: belt_tick_empty_reconcile(state: ManualReady), spawn_outcomes: [], teardown_outcomes: [], - commit_outcomes: [], - verify_outcomes: [], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], + discovery: BeltDiscoveryRefused { reason: launch_refusal_reason(r: refusal) }, }, observed_at: "2026-08-30T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, ) - all_four_refused_with(receipt: receipt, pattern: "deployment transition in force") + all_passes_refused_with(receipt: receipt, pattern: "deployment transition in force") } -test fn revision_refusal_keeps_verify_independently_recorded() -> Bool { +test fn revision_refusal_keeps_discovery_independently_recorded() -> Bool { let receipt = belt_tick_receipt_from_result( result: BeltTickResult { wholesale: TickPassesRun, @@ -2383,20 +2386,14 @@ test fn revision_refusal_keeps_verify_independently_recorded() -> Bool { reconcile: belt_tick_empty_reconcile(state: ManualReady), spawn_outcomes: [], teardown_outcomes: [], - commit_outcomes: [], - verify_outcomes: [ VerifyRecorded { node_id: "n", verdict_key: "pass", detail: "ok" } ], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], + discovery: BeltDiscovered { owed: 0, enqueues: [] }, }, observed_at: "2026-08-30T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, ) (match receipt.spawn_pass { BeltPassRefused { reason } => string_contains(s: reason, pattern: "deployed revision could not be observed") _ => false }) && (match receipt.teardown_pass { BeltPassRefused { reason: _ } => true _ => false }) - && (match receipt.verify_pass { BeltPassRecorded => true _ => false }) - && (match receipt.publish_pass { BeltPassRecorded => true _ => false }) + && (match receipt.discovery_pass { BeltPassRecorded => true _ => false }) } fn launch_fixture_canary_graph() -> LaunchGraphStanding { @@ -2609,34 +2606,6 @@ test fn a_real_commit_failure_still_fails_the_pass_beside_poisoned_ones() -> Boo failed && deferred_honestly && recorded_over_poison } -// A POISONED ATTEMPT IS COUNTED, NOT SILENTLY DROPPED: the number is how many attempts this tick -// are held for repair, counted in the tick result beside the siblings' verdicts. A zero here -// would make the poison frequency vanish by construction, the deficit this witness exists to -// keep visible. -test fn the_poisoned_attempt_count_counts_only_capture_poisoned_rows() -> Bool { - let result = BeltTickResult { - wholesale: TickPassesRun, - host_refusal: Absent, - reconcile: belt_tick_empty_reconcile(state: ManualReady), - spawn_outcomes: [], - teardown_outcomes: [], - commit_outcomes: [ - CommitCapturePoisoned { node_id: "n1", detail: "the recorded base names zero files" }, - CommitSkipped { node_id: "n2", reason: "worktree clean" }, - CommitCapturePoisoned { node_id: "n3", detail: "two or more arcs depend on this node" }, - ], - verify_outcomes: [], - publish_outcomes: [], - review_outcomes: [], - audit_outcomes: [], - metering_outcomes: [], - } - belt_tick_commit_poisoned_count(t: result) == 2 - && string_contains( - s: serialize_json(v: belt_tick_result_json_value(t: result)), - pattern: "\"commit_poisoned\": 2", - ) -} // THE POISON IS NAMED AT BOTH DURABLE RECORDS, the tick result's outcome row and the per-attempt // receipt that survives the pass receipt's collapse -- the operator repairs against the attempt's diff --git a/dag/test/claim/roadmap/roadmap_belt_base_advance_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_base_advance_witness_test.dag index b815aaa0a74..fdbb9af6086 100644 --- a/dag/test/claim/roadmap/roadmap_belt_base_advance_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_base_advance_witness_test.dag @@ -87,7 +87,7 @@ fn advanced_population(changes: List, texts: Map Present { value: tip } => match find_corpus_manifest_record(store: repository.store, identity: tip.root.locator) { CorpusManifestFound(record) => { - let folded = belt_scm_advance_fold(path: "/nonexistent/envelope.json", store: repository.store, tip_record: record, changes: changes, texts: texts) + let folded = belt_scm_advance_fold(store: repository.store, tip_record: record, changes: changes, texts: texts) match folded.refused { Present { value: _ } => none Absent => Present { value: population(entries: path_unique_entry_list(unique: folded.unique)) } diff --git a/dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag new file mode 100644 index 00000000000..04407803988 --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag @@ -0,0 +1,277 @@ +module test.claim.roadmap.roadmap_belt_exit_drain_wet_witness + +import extdeps.shell +import std.types { Bool, String, List, NonEmptyStr } +import gunbc.host_layout { attempt_exit_inbox_dir, attempt_exit_queued_dir, attempt_exit_running_dir, attempt_exit_done_dir, attempt_exit_refused_dir } +import gunbc.roadmap.roadmap_attempt_exit_spool { + attempt_exit_hook, AttemptExitHookRendered, AttemptExitHookRefused, AttemptObligationRole, attempt_exit_record_observe, AttemptExitRecordFound, AttemptExitRecordNone, AttemptExitRecordUnreadable, AttemptWorkerRole, + attempt_exit_stage_list, AttemptExitInbox, AttemptExitQueued, AttemptExitRunning, AttemptExitDone, AttemptExitRefused, AttemptExitSpoolStage, + AttemptExitStageNames, AttemptExitStageUnreadable, +} +import gunbc.roadmap.roadmap_belt_obligation { BeltObligationEnqueue, BeltObligationStarted, BeltObligationHeld, belt_attempt_obligation_unit_name, belt_instance_obligation_unit_name, LaunchObligation } +import gunbc.roadmap_belt_actuate { + belt_exit_drain_at, BeltExitDrain, BeltExitDrained, BeltExitDrainRefused, BeltExitEntryHanded, BeltExitEntryRefused, + belt_exit_entries_settle_at, belt_exit_entries_claim_at, BeltExitClaimed, BeltExitClaimRefused, belt_exec, ExecOk, ExecFailed, ExecRefused, +} +import gunbc.roadmap_dispatch_actuator { + host_exec_argv, AttemptUnitAbsent, AttemptUnitActive, AttemptUnitObservation, AttemptUnitStartRecorded, attempt_worker_unit_observation_of, worker_process_from_attempt_unit, +} +import gunbc.worker_lifecycle { WorkerProcessExited } + +// THE EXIT DRAIN AND THE COLLECTED-WORKER READ, RUN OVER A REAL SPOOL. Each control makes a spool under +// a fresh temp directory with the deployment's stage layout (gunbc.host_layout), writes exit records +// the way the stop hook does -- one `mkdir` whose name is the record -- and runs the production drain +// (belt_exit_drain_at) and the production record read (attempt_exit_record_observe). The listings, the +// decodes and every move are the real ones; only starting a unit in a user manager is supplied, by a +// hand-off that answers Started and names the unit it would have started. Enrolled on the required +// floor's local-repo wet lane with the temp-directory write named in gunbc.ci.ci_layer_roots. + +fn fresh_spool(tag: String) -> String { + let root = shell.Mktemp.DirWithTemplate(template: join(["/tmp/gunbc_exit_spool_", tag, ".XXXXXX"], "")).path + let made = belt_exec(cmd: host_exec_argv(program: "mkdir", args: ["-p", attempt_exit_inbox_dir(instance_root: root), attempt_exit_queued_dir(instance_root: root), attempt_exit_running_dir(instance_root: root), attempt_exit_done_dir(instance_root: root), attempt_exit_refused_dir(instance_root: root)]), workdir: "/") + root +} + +fn record(root: String, name: String) -> Bool { + match belt_exec(cmd: host_exec_argv(program: "mkdir", args: ["--", join([attempt_exit_inbox_dir(instance_root: root), "/", name], "")]), workdir: "/") { + ExecOk { stdout: _ } => true + ExecFailed { program: _, exit_code: _, stderr: _ } => false + ExecRefused { program: _, reason: _ } => false + } +} + +fn names_in(root: String, stage_is_queued: Bool) -> List { + match attempt_exit_stage_list(instance_root: root, stage: if stage_is_queued { AttemptExitQueued } else { AttemptExitInbox }) { + AttemptExitStageNames { names } => names + AttemptExitStageUnreadable { reason: _ } => [""] + } +} + +fn names_at(root: String, stage: AttemptExitSpoolStage) -> List { + match attempt_exit_stage_list(instance_root: root, stage: stage) { + AttemptExitStageNames { names } => names + AttemptExitStageUnreadable { reason: _ } => [""] + } +} + +fn claimed_of(root: String) -> List { + match belt_exit_entries_claim_at(root: root, node_id: "wet-node", attempt_key: "feedfacefeedface") { + BeltExitClaimed { entries } => entries + BeltExitClaimRefused { reason: _ } => [""] + } +} + +fn held(node_id: String, attempt_key: String) -> BeltObligationEnqueue { + BeltObligationHeld { unit: belt_attempt_obligation_unit_name(node_id: node_id, attempt_key: attempt_key) } +} + +fn sweep_started_for_attempt(drained: BeltExitDrain) -> Bool { + match drained { + BeltExitDrained { entries, launch: _ } => + any(entries, e => match e { + BeltExitEntryHanded { entry: _, enqueue: BeltObligationStarted { unit } } => (unit as String) == (belt_attempt_obligation_unit_name(node_id: "wet-node", attempt_key: "feedfacefeedface") as String) + _ => false + }) + BeltExitDrainRefused { reason: _ } => false + } +} + +fn gone() -> AttemptUnitObservation { + AttemptUnitAbsent +} + +fn live() -> AttemptUnitObservation { + AttemptUnitActive +} + +fn handed(node_id: String, attempt_key: String) -> BeltObligationEnqueue { + BeltObligationStarted { unit: belt_attempt_obligation_unit_name(node_id: node_id, attempt_key: attempt_key) } +} + +fn launched() -> BeltObligationEnqueue { + BeltObligationStarted { unit: belt_instance_obligation_unit_name(instance_id: "witness", kind: LaunchObligation) } +} + +data worker_exit: String = "wet-node+feedfacefeedface+worker+0123456789abcdef0123456789abcdef+success+exited+0" +data reviewer_exit: String = "wet-node+feedfacefeedface+review.conformance-leasing.abc123+0123456789abcdef0123456789abcdef+success+exited+0" + +// RED FIRST, DOGFOOD #4 (the hand-off half): a recorded worker exit is handed to ITS attempt's unit, +// leaves the inbox for queued, and queues the launch; nothing else has to run for that to happen. +test fn a_recorded_worker_exit_is_handed_to_its_attempt_and_leaves_the_inbox() -> Bool { + let root = fresh_spool(tag: "worker") + let wrote = record(root: root, name: worker_exit) + let drained = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let handed_to_attempt = match drained { + BeltExitDrained { entries, launch } => + count(entries) == 1 + && any(entries, e => match e { + BeltExitEntryHanded { entry, enqueue: BeltObligationStarted { unit } } => entry == worker_exit && (unit as String) == (belt_attempt_obligation_unit_name(node_id: "wet-node", attempt_key: "feedfacefeedface") as String) + _ => false + }) + && (match launch { Present { value: BeltObligationStarted { unit: _ } } => true _ => false }) + BeltExitDrainRefused { reason: _ } => false + } + wrote && handed_to_attempt && count(names_in(root: root, stage_is_queued: false)) == 0 && names_in(root: root, stage_is_queued: true) == [worker_exit] +} + +// A reviewer's exit is handed to its attempt the same way but frees no worker capacity, so it does +// not queue the launch; a name that is not a record leaves the inbox for refused, not stays to re-fire +// the path unit. +test fn a_reviewer_exit_queues_no_launch_and_a_malformed_entry_is_refused() -> Bool { + let root = fresh_spool(tag: "reviewer") + let wrote = record(root: root, name: reviewer_exit) && record(root: root, name: "not-a-record") + let drained = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let shape = match drained { + BeltExitDrained { entries, launch } => + (match launch { Absent => true _ => false }) + && any(entries, e => match e { BeltExitEntryRefused { entry, reason: _ } => entry == "not-a-record" _ => false }) + && any(entries, e => match e { BeltExitEntryHanded { entry, enqueue: _ } => entry == reviewer_exit _ => false }) + BeltExitDrainRefused { reason: _ } => false + } + wrote && shape && count(names_in(root: root, stage_is_queued: false)) == 0 +} + +// bold-bee-114 CONDITION 3: THE UNIT IS GONE AND THE SPOOL ENTRY ALONE PRODUCES THE EXIT VERDICT. After +// the drain has moved the record to queued and the attempt's run has settled it to done, the worker's +// unit is supplied as collected (the manager answers not-found), and the real record read over the +// real spool yields the worker's exit -- not "never ran", not unobserved. +test fn a_collected_workers_exit_is_read_from_the_spool_alone() -> Bool { + let root = fresh_spool(tag: "collected") + let wrote = record(root: root, name: worker_exit) + let drained = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let unsettled = belt_exit_entries_settle_at(root: root, claimed: claimed_of(root: root)) + let unit = "gunbc-attempt-wet-node-feedfacefeedface.service" as NonEmptyStr + let verdict = worker_process_from_attempt_unit( + observation: attempt_worker_unit_observation_of( + unit: unit, + loaded: AttemptUnitAbsent, + record: attempt_exit_record_observe(instance_root: root, node_id: "wet-node", attempt_key: "feedfacefeedface", role: AttemptWorkerRole), + start: AttemptUnitStartRecorded, + ), + unit: unit, + ) + wrote && count(unsettled) == 0 && count(names_in(root: root, stage_is_queued: true)) == 0 + && (match verdict { WorkerProcessExited { exit_code, last_command: _ } => exit_code == 0 _ => false }) +} + +// bold-bee-114 NO-LAND (2026-10-09), CUT A -- an exit that arrives AFTER THE RUN'S FINAL OBSERVATION BUT +// BEFORE SETTLEMENT is not acknowledged by a receipt that never observed it. The first exit is claimed +// (queued -> running) and the second arrives while the unit is held; settling the claim moves only the +// first to done, the second stays queued, and a later run's claim is what carries it to done. +test fn an_exit_after_the_final_observation_stays_queued_until_a_later_runs_claim_settles_it() -> Bool { + let root = fresh_spool(tag: "cut-a") + let first = record(root: root, name: worker_exit) + let d1 = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let claim = claimed_of(root: root) + let second = record(root: root, name: reviewer_exit) + let d2 = belt_exit_drain_at(root: root, enqueue: (n, k) => held(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => live()) + let unsettled = belt_exit_entries_settle_at(root: root, claimed: claim) + let after_first_run = claim == [worker_exit] + && names_at(root: root, stage: AttemptExitDone) == [worker_exit] + && names_at(root: root, stage: AttemptExitQueued) == [reviewer_exit] + && count(names_at(root: root, stage: AttemptExitRunning)) == 0 + let next_claim = claimed_of(root: root) + let unsettled_next = belt_exit_entries_settle_at(root: root, claimed: next_claim) + first && second && count(unsettled) == 0 && after_first_run && next_claim == [reviewer_exit] && count(unsettled_next) == 0 + && count(names_at(root: root, stage: AttemptExitQueued)) == 0 && count(names_at(root: root, stage: AttemptExitDone)) == 2 +} + +// CUT B -- an exit that arrives AFTER SETTLEMENT BUT BEFORE THE UNIT EXITS is handed to a unit that is +// still held (so the hand-off is a no-op and nothing ran for it). The entry stays queued: the queued +// level starts the drain again, and once the unit is gone the drain's sweep starts a run for the +// attempt, which claims the entry and settles it to done under its own claim. +test fn an_exit_after_settlement_is_swept_to_a_new_run_once_the_unit_is_gone() -> Bool { + let root = fresh_spool(tag: "cut-b") + let first = record(root: root, name: worker_exit) + let d1 = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let unsettled = belt_exit_entries_settle_at(root: root, claimed: claimed_of(root: root)) + let late = record(root: root, name: reviewer_exit) + let while_held = belt_exit_drain_at(root: root, enqueue: (n, k) => held(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let still_queued = names_at(root: root, stage: AttemptExitQueued) == [reviewer_exit] && !(names_at(root: root, stage: AttemptExitDone) == [worker_exit, reviewer_exit]) + let unit_gone = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let new_run_claim = claimed_of(root: root) + let settled = belt_exit_entries_settle_at(root: root, claimed: new_run_claim) + first && late && count(unsettled) == 0 && still_queued && sweep_started_for_attempt(drained: unit_gone) + && new_run_claim == [reviewer_exit] && count(settled) == 0 && count(names_at(root: root, stage: AttemptExitQueued)) == 0 + && count(names_at(root: root, stage: AttemptExitDone)) == 2 +} + +// CUT C -- THE UNIT OR THE HOST IS GONE WHILE AN ENTRY IS QUEUED. The drain runs with an EMPTY inbox, the +// sweep alone finds the queued entry and starts its attempt's run, and that run's claim is the only thing +// that moves it to done. +test fn a_queued_exit_needs_no_new_event_to_reach_done_under_a_runs_claim() -> Bool { + let root = fresh_spool(tag: "cut-c") + let first = record(root: root, name: worker_exit) + let handed_once = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let sweep_only = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let adopted = claimed_of(root: root) + let settled = belt_exit_entries_settle_at(root: root, claimed: adopted) + first && sweep_started_for_attempt(drained: sweep_only) && adopted == [worker_exit] + && count(settled) == 0 && count(names_at(root: root, stage: AttemptExitQueued)) == 0 && names_at(root: root, stage: AttemptExitDone) == [worker_exit] +} + +// RED (a) -- A LONE ORPHANED CLAIM, NO OTHER EVENT (bold-bee-114, 2026-10-09). The entry was queued, a run +// claimed it into `running`, and the run died before its receipt (host or user-manager restart: no hook ran). +// Inbox and queued are EMPTY and the unit is absent; the drain -- all a boot establishes -- must return the +// claim to queued, start the attempt, and the new run must adopt it and settle it to done under its claim. +test fn a_lone_orphaned_claim_is_recovered_by_the_drain_alone() -> Bool { + let root = fresh_spool(tag: "orphan-a") + let first = record(root: root, name: worker_exit) + let handed_once = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let dead_run_claim = claimed_of(root: root) + let stranded = names_at(root: root, stage: AttemptExitRunning) == [worker_exit] && count(names_at(root: root, stage: AttemptExitInbox)) == 0 && count(names_at(root: root, stage: AttemptExitQueued)) == 0 + let boot = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let adopted = claimed_of(root: root) + let settled = belt_exit_entries_settle_at(root: root, claimed: adopted) + first && dead_run_claim == [worker_exit] && stranded && sweep_started_for_attempt(drained: boot) && adopted == [worker_exit] + && count(settled) == 0 && count(names_at(root: root, stage: AttemptExitRunning)) == 0 && names_at(root: root, stage: AttemptExitDone) == [worker_exit] +} + +// A claim whose unit is LIVE is not an orphan: the drain leaves it in running and starts nothing. +test fn a_claim_held_by_a_live_unit_is_not_reclaimed() -> Bool { + let root = fresh_spool(tag: "orphan-live") + let first = record(root: root, name: worker_exit) + let handed_once = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let claimed = claimed_of(root: root) + let again = belt_exit_drain_at(root: root, enqueue: (n, k) => held(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => live()) + first && claimed == [worker_exit] && names_at(root: root, stage: AttemptExitRunning) == [worker_exit] && count(names_at(root: root, stage: AttemptExitQueued)) == 0 +} + +data obligation_exit: String = "wet-node+feedfacefeedface+obligation+0123456789abcdef0123456789abcdef+timeout+killed+TERM" + +// RED (b) -- THE CLAIMANT IS KILLED BY ITS OWN RuntimeMaxSec. The unit's stop hook (the production +// attempt_exit_hook for AttemptObligationRole) records an `obligation` entry while the unit is still +// stopping. A drain that finds the unit live must NOT consume it; the next drain, the unit gone, returns +// the orphaned claim to queued, consumes the entry, starts the attempt, and the run adopts and settles. +test fn a_claimant_killed_at_its_run_bound_is_rescued_by_its_stop_hook_entry() -> Bool { + let root = fresh_spool(tag: "orphan-b") + let hook_renders = match attempt_exit_hook(instance_root: root, node_id: "wet-node", attempt_key: "feedfacefeedface", role: AttemptObligationRole) { + AttemptExitHookRendered { exec_line } => string_contains(s: exec_line as String, pattern: "+obligation+") && string_contains(s: exec_line as String, pattern: attempt_exit_inbox_dir(instance_root: root)) + AttemptExitHookRefused { reason: _ } => false + } + let first = record(root: root, name: worker_exit) + let handed_once = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let dead_run_claim = claimed_of(root: root) + let hook = record(root: root, name: obligation_exit) + let while_stopping = belt_exit_drain_at(root: root, enqueue: (n, k) => held(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => live()) + let kept = names_at(root: root, stage: AttemptExitInbox) == [obligation_exit] && names_at(root: root, stage: AttemptExitRunning) == [worker_exit] + let after_exit = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let adopted = claimed_of(root: root) + let settled = belt_exit_entries_settle_at(root: root, claimed: adopted) + hook_renders && first && hook && dead_run_claim == [worker_exit] && kept && sweep_started_for_attempt(drained: after_exit) && adopted == [worker_exit] + && count(settled) == 0 && count(names_at(root: root, stage: AttemptExitInbox)) == 0 && count(names_at(root: root, stage: AttemptExitRunning)) == 0 + && any(names_at(root: root, stage: AttemptExitDone), e => e == worker_exit) && any(names_at(root: root, stage: AttemptExitDone), e => e == obligation_exit) +} + +// A SETTLED RUN'S HOOK ENTRY STARTS NOTHING (the loop guard): running is empty, so consuming the entry +// enqueues no unit and the exits stop there. +test fn a_settled_runs_hook_entry_is_consumed_and_starts_nothing() -> Bool { + let root = fresh_spool(tag: "orphan-settled") + let hook = record(root: root, name: obligation_exit) + let drained = belt_exit_drain_at(root: root, enqueue: (n, k) => handed(node_id: n, attempt_key: k), launch: fn() { launched() }, observe: (n, k) => gone()) + let quiet = match drained { + BeltExitDrained { entries, launch } => count(entries) == 0 && launch == none + BeltExitDrainRefused { reason: _ } => false + } + hook && quiet && count(names_at(root: root, stage: AttemptExitInbox)) == 0 && names_at(root: root, stage: AttemptExitDone) == [obligation_exit] +} diff --git a/dag/test/claim/roadmap/roadmap_belt_obligation_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_obligation_witness_test.dag new file mode 100644 index 00000000000..59f669896f9 --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_belt_obligation_witness_test.dag @@ -0,0 +1,117 @@ +module test.claim.roadmap.roadmap_belt_obligation_witness_test + +import std.types { Bool, String, List, NonEmptyStr } +import gunbc.roadmap_workflow_stage { + WorkflowSegment, WorkflowSegmentKind, WorkflowSegmentState, + SubmissionWorkflowSegment, VerificationWorkflowSegment, PublicationWorkflowSegment, ReviewWorkflowSegment, GoalAuditWorkflowSegment, + WorkflowSegmentPending, WorkflowSegmentActive, WorkflowSegmentComplete, WorkflowSegmentFailed, +} +import gunbc.roadmap_provider_events { ProviderExecutionState, ProviderRunning, ProviderCompleted, ProviderFailed } +import gunbc.roadmap.roadmap_belt_obligation { + BeltAttemptObligationKind, CaptureObligation, VerifyObligation, ReviewObligation, AuditObligation, PublishObligation, + belt_attempt_outstanding, belt_attempt_next_after, belt_attempt_obligation_kind_key, + belt_attempt_obligation_unit_name, belt_instance_obligation_unit_name, + ServedObservationObligation, LaunchObligation, belt_record_moved_obligation_kinds, belt_instance_obligation_kind_key, +} +import gunbc.roadmap_belt_actuate { + belt_obligation_hold_decision, BeltObligationMayStart, BeltObligationIsHeld, BeltObligationHoldUnread, +} +import gunbc.roadmap_dispatch_actuator { AttemptUnitActive, AttemptUnitAbsent, AttemptUnitFinished, AttemptUnitUnreadable } +import gunbc.live_deploy.emit { live_deploy_belt_exit_path_unit_file, live_deploy_belt_exit_drain_service_unit_file } +import gunbc.live_deploy.spec { deployment_belt_exit_drain_service_unit_name, deployment_owned_steps, DeploymentArtifactStep } +import gunbc.live_deploy.desired { deployment_spec_srv1 } +import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance } +import gunbc.host_layout { attempt_exit_inbox_dir } +import extdeps.systemd.unit_file { systemd_unit_file_lines } + +// THE OBLIGATION MODEL'S DECISIONS over supplied evidence (DESIGN section 3: a witness discriminates at +// one interface, so the attempt's segments are supplied, not produced by running a worker). The +// production route that consumes them -- gunbc.roadmap_belt_actuate belt_attempt_obligations_run and +// belt_discover_for_instance -- reads the same derivation; the wet witness beside this file runs the +// exit drain over a real spool. + +fn seg(kind: WorkflowSegmentKind, state: WorkflowSegmentState) -> WorkflowSegment { + WorkflowSegment { kind: kind, state: state, detail: "" } +} + +fn keys(kinds: List) -> String { + join(kinds |> map(k => belt_attempt_obligation_kind_key(kind: k)), ",") +} + +fn segments(submission: WorkflowSegmentState, verification: WorkflowSegmentState, review: WorkflowSegmentState, audit: WorkflowSegmentState, publication: WorkflowSegmentState) -> List { + [ + seg(kind: SubmissionWorkflowSegment, state: submission), + seg(kind: VerificationWorkflowSegment, state: verification), + seg(kind: ReviewWorkflowSegment, state: review), + seg(kind: GoalAuditWorkflowSegment, state: audit), + seg(kind: PublicationWorkflowSegment, state: publication), + ] +} + +// Each kind is owed exactly when its predecessor's evidence is complete and its own is unsettled. +test fn each_obligation_is_owed_only_on_its_predecessors_evidence() -> Bool { + let running = keys(kinds: belt_attempt_outstanding(provider: ProviderRunning { activity: "turn 3" }, segments: segments(submission: WorkflowSegmentPending, verification: WorkflowSegmentPending, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + let exited = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentPending, verification: WorkflowSegmentPending, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + let captured = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentPending, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + let verified = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentComplete, review: WorkflowSegmentActive, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + let reviewed = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentComplete, review: WorkflowSegmentComplete, audit: WorkflowSegmentComplete, publication: WorkflowSegmentPending))) + let settled = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentComplete, review: WorkflowSegmentComplete, audit: WorkflowSegmentComplete, publication: WorkflowSegmentComplete))) + running == "" && exited == "capture" && captured == "verify" && verified == "review,audit" && reviewed == "publish" && settled == "" +} + +// A FAILED STAGE STOPS THE LINE FOR ITS ATTEMPT: it is not owed again, so nothing retries it without +// the operator (2026-10-03 manual-resume ruling). A failed worker still owes its capture. +test fn a_failed_stage_is_not_owed_and_a_failed_worker_still_owes_capture() -> Bool { + let failed_verify = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentFailed, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + let failed_worker = keys(kinds: belt_attempt_outstanding(provider: ProviderFailed { activity: "turn 2", detail: "exit 1" }, segments: segments(submission: WorkflowSegmentPending, verification: WorkflowSegmentPending, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) + failed_verify == "" && failed_worker == "capture" +} + +// THE CHAIN NEVER LOOPS: after a kind runs, only what lies strictly downstream is next, so a capture +// that left its submission unsettled is not run again by its own chain. +test fn after_a_kind_runs_only_strictly_downstream_kinds_are_next() -> Bool { + let all = [CaptureObligation, VerifyObligation, ReviewObligation, AuditObligation, PublishObligation] + keys(kinds: belt_attempt_next_after(ran: CaptureObligation, outstanding: all)) == "verify,review,audit,publish" + && keys(kinds: belt_attempt_next_after(ran: ReviewObligation, outstanding: all)) == "publish" + && keys(kinds: belt_attempt_next_after(ran: AuditObligation, outstanding: [ReviewObligation, PublishObligation])) == "publish" + && keys(kinds: belt_attempt_next_after(ran: PublishObligation, outstanding: all)) == "" +} + +// RED FIRST, DOGFOOD #6: A VERIFY THAT RUNS LONG DOES NOT DELAY PUBLISH OF AN UNRELATED ATTEMPT, NOR +// THE SERVED OBSERVATION. Attempt A is mid-verify -- its unit is loaded and running for as long as the +// verify takes. Attempt B is reviewed and owes its publish. The only thing the two could share is a +// unit, so the claim is that they do not: B's unit and the observation's unit are different names from +// A's, and the hold decision -- the one gate between an owed obligation and its unit starting -- holds +// A and lets B and the observation start, reading nothing of A when it decides B. Before this change +// the claim was false by construction: B's publish and the observation ran inside the same process as +// A's verify, after it. +test fn a_long_verify_does_not_delay_another_attempts_publish_or_the_observation() -> Bool { + let a_unit = belt_attempt_obligation_unit_name(node_id: "node-a", attempt_key: "aaaa") + let b_unit = belt_attempt_obligation_unit_name(node_id: "node-b", attempt_key: "bbbb") + let observe_unit = belt_instance_obligation_unit_name(instance_id: "srv2-deploy", kind: ServedObservationObligation) + let b_owes_publish = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentComplete, review: WorkflowSegmentComplete, audit: WorkflowSegmentComplete, publication: WorkflowSegmentPending))) == "publish" + let a_mid_verify = keys(kinds: belt_attempt_outstanding(provider: ProviderCompleted { activity: "done" }, segments: segments(submission: WorkflowSegmentComplete, verification: WorkflowSegmentActive, review: WorkflowSegmentPending, audit: WorkflowSegmentPending, publication: WorkflowSegmentPending))) == "verify" + let a_held = match belt_obligation_hold_decision(observation: AttemptUnitActive) { BeltObligationIsHeld => true _ => false } + let b_starts = match belt_obligation_hold_decision(observation: AttemptUnitAbsent) { BeltObligationMayStart => true _ => false } + let observe_starts = match belt_obligation_hold_decision(observation: AttemptUnitFinished { exit_status: "0" }) { BeltObligationMayStart => true _ => false } + let unread_refuses = match belt_obligation_hold_decision(observation: AttemptUnitUnreadable { reason: "bus" }) { BeltObligationHoldUnread { reason: _ } => true _ => false } + (a_unit as String) != (b_unit as String) && (a_unit as String) != (observe_unit as String) && (b_unit as String) != (observe_unit as String) + && b_owes_publish && a_mid_verify && a_held && b_starts && observe_starts && unread_refuses + && any(belt_record_moved_obligation_kinds, k => belt_instance_obligation_kind_key(kind: k) == "observe") +} + +// RED FIRST, DOGFOOD #4 (the deployment half): WITH NO TIMER INSTALLED, a worker exit reaches its +// capture. The deployment installs no belt timer -- none of its owned steps is a timer member -- and +// what it installs instead watches the instance's exit inbox and starts the drain, which hands the exit +// to its attempt (the wet witness runs that hand-off over a real spool; the confinement witness shows +// the worker unit writes the record). +test fn the_deployment_wakes_the_drain_on_an_exit_and_installs_no_belt_timer() -> Bool { + let spec = deployment_spec_srv1() + let lines = systemd_unit_file_lines(file: live_deploy_belt_exit_path_unit_file(spec: spec)) + let inbox = attempt_exit_inbox_dir(instance_root: spec.service.instance_root as String) + let steps = deployment_owned_steps(instance: srv1_live_dashboard_instance(), names: spec.names) + any(lines, l => l == join(["DirectoryNotEmpty=", inbox], "")) + && any(lines, l => l == join(["Unit=", deployment_belt_exit_drain_service_unit_name(names: spec.names) as String], "")) + && !any(steps, st => string_contains(s: st.path as String, pattern: "-belt.timer")) + && any(systemd_unit_file_lines(file: live_deploy_belt_exit_drain_service_unit_file(spec: spec)), l => string_contains(s: l, pattern: "--function belt_exit_drain_cli")) +} diff --git a/dag/test/claim/roadmap/roadmap_dashboard_instance_apply_witness_test.dag b/dag/test/claim/roadmap/roadmap_dashboard_instance_apply_witness_test.dag index 3d169d4d871..239a646619d 100644 --- a/dag/test/claim/roadmap/roadmap_dashboard_instance_apply_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dashboard_instance_apply_witness_test.dag @@ -308,8 +308,8 @@ test fn dashboard_health_requires_intended_instance_identity() -> Bool { test fn dashboard_plan_is_end_to_end_and_production_disjoint() -> Bool { let lab = srv1_lab_dashboard_instance() let rows = dashboard_instance_provision_plan_rows(instance: lab) - count(rows) == 18 - && any(rows, row => row.kind == "ensure-provider-state-root") + count(rows) == 9 + && !any(rows, row => row.kind == "ensure-provider-state-root" || row.kind == "ensure-directory") && any(rows, row => row.kind == "ensure-serve-binary") && any(rows, row => row.kind == "hydrate-controller-history") && any(rows, row => row.kind == "prepare-git-bundle") diff --git a/dag/test/claim/roadmap/roadmap_dashboard_instance_witness_test.dag b/dag/test/claim/roadmap/roadmap_dashboard_instance_witness_test.dag index d2b368b2630..f494b8c7c2b 100644 --- a/dag/test/claim/roadmap/roadmap_dashboard_instance_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dashboard_instance_witness_test.dag @@ -1,6 +1,13 @@ module test.claim.roadmap_dashboard_instance_witness import std.types { Bool, String, NonEmptyStr, FilePath, Port } +import gunbc.roadmap.dashboard_instance_directories { dashboard_instance_directory_demands } +import gunbc.owned_directory { owned_directory_path } +import gunbc.auth.session_store { session_store_directory } +import gunbc.auth.login_transaction_store { login_transaction_store_directory } +import gunbc.auth.oidc_id_token_verification { jwks_cache_directory } +import gunbc.auth.profile_projection { profile_store_directory } +import gunbc.roadmap_view_preference_store { issue_view_store_directory } import extdeps.tmux { shape_tmux_ls_argv_for, DefaultTmuxServer } import gunbc.provider_account { CodexDefaultAccount, @@ -22,7 +29,6 @@ import gunbc.host_layout { srv1_codex_bundled_executable, } import gunbc.deployment_risk { deployment_id } -import gunbc.roadmap.roadmap_event_carrier { roadmap_event_carrier_layout_for_instance } import gunbc.roadmap_dashboard_instance { fleet_admitted_revision_source, HostDashboardInstance, @@ -32,10 +38,6 @@ import gunbc.roadmap_dashboard_instance { DashboardHydrateControllerHistory, DashboardPrepareGitBundle, DashboardStageGitBundle, - DashboardEnsureDirectory, - dashboard_auth_stores, - dashboard_instance_auth_store_root, - DashboardEnsureProviderStateRoot, DashboardEnsureServeBinary, DashboardEnsureOwnershipMarker, DashboardEnsureLocalOrigin, @@ -225,8 +227,8 @@ fn plan_binds_source_ref_translation( test fn dashboard_provision_plan_consumes_one_instance() -> Bool { let lab = srv1_lab_dashboard_instance() let plan = dashboard_instance_provision_plan(instance: lab) - count(plan) == 18 - && plan_ensures_every_auth_store_root(plan: plan, instance: lab) + count(plan) == 9 + && demands_cover_every_auth_store(instance: lab) && plan_binds_instance_paths(plan: plan, instance: lab) && plan_binds_source_ref_translation(plan: plan, instance: lab) } @@ -254,8 +256,8 @@ fn lab_observed( } else { "source revision drift: desired=abc observed=def" }, - attempts_present: true, - attempt_state_present: true, + directories_converged: true, + directory_refusals: [], provider_runtime_ready: true, provider_runtime_detail: "codex-cli test", provider_authenticated: true, @@ -375,8 +377,8 @@ test fn dashboard_bundle_is_required_for_convergence() -> Bool { source_revision_observed: true, source_revision_current: true, source_revision_detail: "desired=abc observed=abc", - attempts_present: true, - attempt_state_present: true, + directories_converged: true, + directory_refusals: [], provider_runtime_ready: true, provider_runtime_detail: "codex-cli test", provider_authenticated: true, @@ -412,7 +414,7 @@ test fn dashboard_source_capture_has_no_manual_await_step() -> Bool { anchor_ref, } => controller_repo == srv1_dashboard_controller_repo - && anchor_ref == "refs/heads/session/codex-provider-feedback-v0" + && anchor_ref == "refs/remotes/origin/main" _ => false }) && any(plan, op => match op { @@ -437,8 +439,8 @@ test fn dashboard_reconcile_refuses_unmarked_existing_source() -> Bool { source_revision_observed: false, source_revision_current: false, source_revision_detail: "source repository unavailable", - attempts_present: false, - attempt_state_present: false, + directories_converged: false, + directory_refusals: [], provider_runtime_ready: false, provider_runtime_detail: "provider runtime unavailable", provider_authenticated: false, @@ -537,8 +539,8 @@ fn witness_synthetic_instance() -> HostDashboardInstance { // page all read correctly — no refusal, just unintended content rendered plausibly. Unlike the host // facts, which produced identical values on both hosts and therefore no wrong output at all, this // one produces a page that looks right and is not.\n\nThe claim asserts the anchors are -// per-instance and DIFFER, so collapsing them back to one shared literal fails whichever way it -// collapses. +// per-instance: the synthetic instance's own anchor fails if the constructor collapses back to one +// shared literal. Two labs may lawfully follow the same ref (srv1 and srv2 labs both follow main). test fn source_anchor_is_per_instance_not_one_shared_literal() -> Bool { let synthetic = instance_anchor_ref(i: witness_synthetic_instance()) @@ -546,8 +548,7 @@ test fn source_anchor_is_per_instance_not_one_shared_literal() -> Bool { let srv1_lab = instance_anchor_ref(i: srv1_lab_dashboard_instance()) synthetic == "refs/heads/witness-anchor" && srv2_lab == "refs/remotes/origin/main" - && srv1_lab == "refs/heads/session/codex-provider-feedback-v0" - && !(srv2_lab == srv1_lab) + && srv1_lab == "refs/remotes/origin/main" } // serve_binary MOVED OFF THE LAYOUT AND THIS CLAIM CAUGHT IT, which is the whole reason it exists. @@ -623,36 +624,29 @@ test fn srv2_lab_owns_paths_disjoint_from_preview_and_production() -> Bool { test fn srv2_lab_has_a_provision_plan_bound_to_itself() -> Bool { let lab = srv2_lab_dashboard_instance() let plan = dashboard_instance_provision_plan(instance: lab) - count(plan) == 18 - && plan_ensures_every_auth_store_root(plan: plan, instance: lab) + count(plan) == 9 + && demands_cover_every_auth_store(instance: lab) && plan_binds_instance_paths(plan: plan, instance: lab) && plan_binds_source_ref_translation(plan: plan, instance: lab) } -// TWO PATHS THE PLAN VERIFIED WITHOUT EVER CREATING, both found by running the thing rather than reading it. The provider state root: DashboardVerifyProviderAuth reads it, nothing made it, so the only command that can populate it failed with 'that path does not exist' before any OAuth began — an operator hitting a CLI error instead of a typed refusal. The serve binary: instances used to BORROW production's, which is why no install step existed; making each instance own its own closed a retract hazard and left the install with no owner.\n\nBoth claims are stated as containment in the plan rather than as a count, because a count says the plan has sixteen things and nothing about whether the right two are among them. Each asserts the op targets the INSTANCE's own path — a plan that ensured some other instance's binary would satisfy a count and fail here. -// THE AUTH STORES ARE PROVISIONED, stated as containment: every store the auth modules write -// into has an ensure step for THIS instance's root. The profile store once had none, so every -// login's profile publication was refused and the assignee surfaces rendered the raw principal. -fn plan_ensures_every_auth_store_root( - plan: List, - instance: HostDashboardInstance, -) -> Bool { - all(dashboard_auth_stores, store => any(plan, op => match op { - DashboardEnsureDirectory { path } => - path == dashboard_instance_auth_store_root(instance: instance, store: store) - _ => false - })) +// TWO PATHS THE APPLY VERIFIED WITHOUT EVER CREATING, both found by running the thing rather than reading it. The provider state root: DashboardVerifyProviderAuth reads it, nothing made it, so the only command that can populate it failed with 'that path does not exist' before any OAuth began. The serve binary: instances used to BORROW production's, which is why no install step existed.\n\nThe directories are now owned-directory demands (gunbc.roadmap.dashboard_instance_directories) ensured before the plan runs, and the binary is a plan op; each claim is containment of THIS instance's own path. +// THE AUTH STORES ARE DEMANDED, stated as containment: every store the auth modules write into has +// a demand for THIS instance's root, read off the store's own demand function. The profile store once +// had no creator, so every login's profile publication was refused. +fn demands_contain_path(instance: HostDashboardInstance, path: FilePath) -> Bool { + any(dashboard_instance_directory_demands(instance: instance), t => owned_directory_path(t: t) == path) } -fn plan_ensures_provider_state_root( - plan: List, - instance: HostDashboardInstance, -) -> Bool { - any(plan, op => match op { - DashboardEnsureProviderStateRoot { path } => - path == dashboard_instance_provider_state_root(instance: instance) - _ => false - }) +fn demands_cover_every_auth_store(instance: HostDashboardInstance) -> Bool { + all([ + session_store_directory(instance: instance), + login_transaction_store_directory(instance: instance), + jwks_cache_directory(instance: instance), + profile_store_directory(instance: instance), + issue_view_store_directory(instance: instance), + ], t => demands_contain_path(instance: instance, path: owned_directory_path(t: t)) + && dashboard_path_is_under(path: owned_directory_path(t: t), root: instance.instance_root)) } fn plan_ensures_serve_binary( @@ -666,45 +660,16 @@ fn plan_ensures_serve_binary( }) } -test fn the_plan_creates_every_path_it_later_verifies() -> Bool { +test fn the_apply_creates_every_path_it_later_verifies() -> Bool { let lab = srv1_lab_dashboard_instance() let srv2 = srv2_lab_dashboard_instance() - let lab_plan = dashboard_instance_provision_plan(instance: lab) - let srv2_plan = dashboard_instance_provision_plan(instance: srv2) - plan_ensures_provider_state_root(plan: lab_plan, instance: lab) - && plan_ensures_serve_binary(plan: lab_plan, instance: lab) - && plan_ensures_provider_state_root(plan: srv2_plan, instance: srv2) - && plan_ensures_serve_binary(plan: srv2_plan, instance: srv2) - && any(lab_plan, op => match op { - DashboardEnsureDirectory { path } => - path == dashboard_instance_dispatch_worktree_root(instance: lab) - _ => false - }) - && any(lab_plan, op => match op { - DashboardEnsureDirectory { path } => - path == dashboard_instance_dispatch_attempt_state_root(instance: lab) - _ => false - }) -} - -// THE EVENT WORKTREE IS PROVISIONED, stated against the CARRIER'S OWN layout rather than a -// literal: the plan ensures exactly the directory gunbc.roadmap.roadmap_event_carrier checks the -// event branch out into. srv1's instance root is root-owned, so with no ensure step the carrier's -// `git worktree add` could not create it and every issue page answered 503. Both srv1 instances -// are checked because the live one is the one that failed. -fn plan_ensures_roadmap_events_worktree(instance: HostDashboardInstance) -> Bool { - any(dashboard_instance_provision_plan(instance: instance), op => match op { - DashboardEnsureDirectory { path } => - path == roadmap_event_carrier_layout_for_instance(instance: instance).worktree - _ => false - }) + demands_contain_path(instance: lab, path: dashboard_instance_provider_state_root(instance: lab)) + && plan_ensures_serve_binary(plan: dashboard_instance_provision_plan(instance: lab), instance: lab) + && demands_contain_path(instance: srv2, path: dashboard_instance_provider_state_root(instance: srv2)) + && plan_ensures_serve_binary(plan: dashboard_instance_provision_plan(instance: srv2), instance: srv2) + && demands_contain_path(instance: lab, path: dashboard_instance_dispatch_worktree_root(instance: lab)) + && demands_contain_path(instance: lab, path: dashboard_instance_dispatch_attempt_state_root(instance: lab)) } - -test fn the_plan_provisions_the_roadmap_event_worktree() -> Bool { - plan_ensures_roadmap_events_worktree(instance: srv1_live_dashboard_instance()) - && plan_ensures_roadmap_events_worktree(instance: srv1_lab_dashboard_instance()) -} - test fn each_instance_ensures_its_own_binary_not_a_shared_one() -> Bool { let lab = srv1_lab_dashboard_instance() let srv2 = srv2_lab_dashboard_instance() @@ -736,8 +701,8 @@ fn observed_all_absent(refusals: List) -> DashboardInstanceObserved { source_revision_observed: false, source_revision_current: false, source_revision_detail: "", - attempts_present: false, - attempt_state_present: false, + directories_converged: false, + directory_refusals: [], provider_runtime_ready: false, provider_runtime_detail: "", provider_authenticated: false, @@ -806,6 +771,7 @@ fn witness_production_slot( admitted_revision: fleet_admitted_revision_source, toolchain: srv1_linux_toolchain, dispatch_worker_memory_max: dispatch_worker_memory_ceiling, + directory_owner: srv1_live_dashboard_instance().directory_owner, } } @@ -1159,6 +1125,7 @@ fn outside_instance_claiming_live_id_on_live_footprint() -> HostDashboardInstanc admitted_revision: live.admitted_revision, toolchain: live.toolchain, dispatch_worker_memory_max: live.dispatch_worker_memory_max, + directory_owner: live.directory_owner, } } diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index dcccbf697a5..32867dd4f52 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -20,12 +20,13 @@ import gunbc.roadmap_model { PrsMerged, WorkItemDispatch, NotDispatchable, Dispatchable, SectionElement, Group, SectionGroup, + NodeParent, AdmittedRoot, RoadmapNodeIdentity, } -import gunbc.roadmap_status { DerivableLine, Unsigned } +import gunbc.roadmap_status { DerivableLine, Unsigned, TicketLine, TicketFields } +import gunbc.roadmap.roadmap_lineage { lineage_walk, lineage_is_rooted } import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } import gunbc.roadmap_document { RoadmapDocument, RoadmapSection } import gunbc.roadmap_spawner { next_spawnable, dispatch_brief_template, node_title } -import gunbc.roadmap_authority { declared_roadmap_nodes } import std.upsert_decision { Absent, Apply, @@ -76,11 +77,14 @@ import gunbc.roadmap_dispatch_actuator { dispatch_provider_inner_argv, DispatchProviderInnerArgv, DispatchProviderInnerArgvReady, - CursorProviderSpawnUnwired, + CursorProviderOffCustodyHost, + CursorProviderCredentialUnbound, + CodexProviderOffCustodyHost, CodexProviderExecutableAbsent, GunbcHarnessSpawnNeedsInstance, ClaudeProviderCredentialUnbound, ClaudeCodeProvider, + dispatch_credential_snapshot_path_for_instance, CodexDispatchProvider, dispatch_tmux_session_name, dispatch_tmux_session_prefix, @@ -175,6 +179,7 @@ import gunbc.roadmap_dashboard_instance { HostDashboardInstance, srv1_lab_dashboard_instance, srv1_live_dashboard_instance, + srv2_lab_dashboard_instance, dashboard_instance_provider_executable, } import gunbc.roadmap.roadmap_attempt_continuation { @@ -182,6 +187,7 @@ import gunbc.roadmap.roadmap_attempt_continuation { continuation_alignment_l0_pointer, continuation_alignment_ask, } import gunbc.roadmap.roadmap_alignment { + alignment_chain, AlignmentResolution, AlignmentReady, AlignmentUnresolved, AlignmentChained, AlignmentChainLink, AlignmentRowUndeclared, AlignmentWalkUnrooted, AlignmentNestingCyclic, @@ -456,9 +462,9 @@ fn codex_tmux_arm_shape_holds(spawn: HostExecArgv, provider_start: HostExecArgv, test fn witness_selected_provider_shapes_codex_tmux_command() -> Bool { let node = sized_derivable(id: "codex-selected", prs: [1], title: "Codex selected") match dispatch_spawn_commands_for_inventory( - instance: srv1_live_dashboard_instance(), + instance: srv2_lab_dashboard_instance(), inventory: fixture_inventory_codex_only_available( - instance: srv1_live_dashboard_instance(), + instance: srv2_lab_dashboard_instance(), ), node: node, session_uuid: "uuid-unused-by-codex", @@ -477,7 +483,7 @@ test fn witness_selected_provider_shapes_codex_tmux_command() -> Bool { codex_tmux_command_shape_holds( cmds: cmds, codex_executable: match dashboard_instance_provider_executable( - instance: srv1_live_dashboard_instance(), + instance: srv2_lab_dashboard_instance(), ) { Present { value: executable } => executable as String Absent => "" @@ -495,10 +501,6 @@ fn lab_instance_command_shape_holds(commands: DispatchSpawnCommands, lab: HostDa } fn lab_instance_tmux_shape_holds(commands: DispatchSpawnCommands, lab: HostDashboardInstance, spawn: HostExecArgv, event_pipe: HostExecArgv, provider_start: HostExecArgv) -> Bool { - match dashboard_instance_provider_executable(instance: lab) { - Absent => false - Present { value: codex_path } => { - let codex_executable = codex_path as String let git_wire = join(commands.worktree_add.args, separator: "\0") let state_wire = join(commands.attempt_state_prepare.args, separator: "\0") let pointer_wire = join(commands.attempt_current_pointer.args, separator: "\0") @@ -534,37 +536,52 @@ fn lab_instance_tmux_shape_holds(commands: DispatchSpawnCommands, lab: HostDashb ) && string_contains( s: provider_wire, - pattern: concat("\0CODEX_HOME=/home/briansrls/.codex@gunbc-roadmap\0", codex_executable), + pattern: dispatch_credential_snapshot_path_for_instance( + instance: lab, + node_id: "ts-dispatch-provider-events" as NonEmptyStr as RoadmapNodeId, + attempt_key: "feedfacefeedface", + provider: ClaudeCodeProvider, + ) as String, ) - } - } } -test fn witness_lab_instance_moves_repo_tmux_and_codex_state_together() -> Bool { +// THE LAB INSTANCE MOVES ITS REPO, TMUX SERVER AND ATTEMPT STATE TOGETHER, shown with the Claude +// spawn (its credential is held on srv1). It no longer moves a codex state root with it: codex worker +// turns run on the one credential held on its custody host (gunbc.codex_harness_credential, operator +// ruling 2026-10-05), so the same lab refuses a codex spawn at the custody-host step -- the control +// that a codex turn cannot fall back to the lab's ambient codex login. +test fn witness_lab_instance_moves_repo_tmux_and_attempt_state_together() -> Bool { let lab = srv1_lab_dashboard_instance() - match dispatch_spawn_commands_for_inventory( + let node = sized_derivable(id: "ts-dispatch-provider-events", prs: [], title: "Provider execution facts") + let claude_moves = match dispatch_spawn_commands_for_resolved_instance( instance: lab, - inventory: fixture_inventory_codex_only_available(instance: lab), - node: sized_derivable( - id: "ts-dispatch-provider-events", - prs: [], - title: "Provider execution facts", - ), + node: node, session_uuid: "00000000-0000-4000-8000-000000000001", attempt_key: "feedfacefeedface", - request: SelectCodex { - profile: ProfileAutomatic, - account: codex_roadmap_account_selection(), - model: dispatch_codex_default_model, - effort: ReasoningMedium, - operator_asserted_native_limit_bucket: Absent, - }, + provider: ClaudeCodeProvider, + effort: ReasoningMedium, + model: ProviderAccountDefaultModel, + origin: FreshFromBase, resolution: fixture_ready_resolution(node_id: "ts-dispatch-provider-events", project_ids: []), ) { DispatchSpawnRefused { step: _, detail: _ } => false - DispatchSpawnReady { commands } => - lab_instance_command_shape_holds(commands: commands, lab: lab) + DispatchSpawnReady { commands } => lab_instance_command_shape_holds(commands: commands, lab: lab) } + let codex_refused = match dispatch_spawn_commands_for_resolved_instance( + instance: lab, + node: node, + session_uuid: "00000000-0000-4000-8000-000000000001", + attempt_key: "feedfacefeedface", + provider: CodexDispatchProvider, + effort: ReasoningMedium, + model: dispatch_codex_default_model, + origin: FreshFromBase, + resolution: fixture_ready_resolution(node_id: "ts-dispatch-provider-events", project_ids: []), + ) { + DispatchSpawnRefused { step, detail: _ } => step as String == "codex-credential-custody-host" + DispatchSpawnReady { commands: _ } => false + } + claude_moves && codex_refused } test fn witness_tmux_attempt_panes_parse_alive_and_exit() -> Bool { @@ -1110,7 +1127,9 @@ test fn witness_codex_account_default_passes_no_model_flag() -> Bool { fn witness_inner_argv_wire(inner: DispatchProviderInnerArgv) -> String { match inner { DispatchProviderInnerArgvReady { argv } => join(argv, separator: "\0") - CursorProviderSpawnUnwired { reason: r } => r as String + CursorProviderOffCustodyHost { reason: r } => r as String + CursorProviderCredentialUnbound { reason: r } => r as String + CodexProviderOffCustodyHost { reason: r } => r as String CodexProviderExecutableAbsent { reason: r } => r as String GunbcHarnessSpawnNeedsInstance { reason: r } => r as String ClaudeProviderCredentialUnbound { reason: r } => r as String @@ -1189,6 +1208,7 @@ test fn witness_review_unit_argv_mints_per_criterion_attempt_identity() -> Bool brief: "brief", head_sha: "abc123", criterion_key: "conformance-external-facts", + exit_hook: "+mkdir -- /i/attempt-exit-spool/inbox/x" as NonEmptyStr, ) { SystemdRunCommandRefused { cause: _ } => false SystemdRunCommandReady { command: command } => { @@ -1217,9 +1237,9 @@ test fn witness_continuation_origin_branches_the_worktree_from_the_prior_attempt node: sized_derivable(id: "ts-continuation-origin", prs: [], title: "Continuation origin wiring"), session_uuid: "00000000-0000-4000-8000-000000000002", attempt_key: "feedfacefeedface", - provider: CodexDispatchProvider, + provider: ClaudeCodeProvider, effort: ReasoningMedium, - model: dispatch_codex_default_model, + model: ProviderAccountDefaultModel, origin: ContinueFromAttempt { parent_attempt_key: "deadbeefdeadbeef", branch: "roadmap/ts-continuation-origin/deadbeefdeadbeef", @@ -1237,37 +1257,89 @@ test fn witness_continuation_origin_branches_the_worktree_from_the_prior_attempt } } -// THE AUTHORITY ROW, READ BY THE TEST ITSELF. The drift controls below require the preamble to -// carry the arc's title AS THE AUTHORITY DECLARES IT, so the expectation is projected here rather -// than transcribed: editing the authority row moves this expectation with it, and a hand-copied -// preamble goes red. -fn authority_node_opt(id: String) -> RoadmapNode? { - (declared_roadmap_nodes() |> filter(n => (n.node as String) == id)).first() +// THE SUPPLIED UNROOTED SPECIMEN (§3: a witness discriminates at one interface). The claims below +// judge the refusal an unrooted chain produces, so the unrooted node is built HERE -- a row with no +// parent -- rather than borrowed from the live roadmap authority, whose parents change for +// unrelated reasons (#13052 gave the former live specimen one and turned both claims red). +fn unrooted_specimen_id() -> String { "fixture-unrooted-specimen" } + +fn specimen_row(parent: NodeParent?) -> RoadmapNode { + RoadmapNode { + identity: "rnfx_R2PE6DBK7RVVR2NHNQN0" as RoadmapNodeIdentity, + node: roadmap_dispatch_actuator_nid(s: unrooted_specimen_id()), + line: TicketLine { + done: false, + ticket: TicketFields { + headline: "Specimen headline", + brief: "Specimen brief", + displaced_cost: "none", + current_state: "fixture", + first_slice: "fixture", + red_control: "fixture", + out_of_scope: "fixture", + handback: "fixture", + updates: [], + authored_on: "", + last_verified_on: "", + }, + }, + carriers: [], + dispatch: Dispatchable { repo: "gunbc", acceptance: PrsMerged }, + execution: ExecutionContractUnspecified, + owner: "", + signoff: Unsigned, + parent: parent, + centering: none, + } +} + +fn unrooted_specimen() -> RoadmapNode { specimen_row(parent: none) } + +fn rooted_twin_specimen() -> RoadmapNode { specimen_row(parent: Present { value: AdmittedRoot }) } + +fn unrooted_specimen_resolution() -> AlignmentResolution { + alignment_resolve_chain( + chain: alignment_chain(node_id: unrooted_specimen_id(), nodes: [unrooted_specimen()]), + turns_elapsed: 1, + state: alignment_base_cadence_state(), + ) +} + +// THE DISCRIMINATOR AND THE INHABITANCE CHECK: the real lineage rule and the real chain derivation +// refuse the parentless row and accept the same row once it declares AdmittedRoot, so the refusal +// below is the rule's, not a fixture artifact. Not tied to any live node id. +test fn the_real_lineage_rule_refuses_an_unrooted_row_and_roots_its_twin() -> Bool { + !lineage_is_rooted(w: lineage_walk(node_id: unrooted_specimen_id(), nodes: [unrooted_specimen()])) + && lineage_is_rooted(w: lineage_walk(node_id: unrooted_specimen_id(), nodes: [rooted_twin_specimen()])) + && (match alignment_resolve_chain( + chain: alignment_chain(node_id: unrooted_specimen_id(), nodes: [rooted_twin_specimen()]), + turns_elapsed: 1, + state: alignment_base_cadence_state(), + ) { + AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => true + AlignmentUnresolved { cause: _ } => false + }) } // THE ONE-AUTHORITY PREAMBLE (review finding 2026-09-26): the preamble IS the resolution's // rendered_prompt — gunbc.roadmap.roadmap_alignment owns the alignment answer and this lane only -// concatenates. For the REAL node 2-scm-git-upstream-model -- an accepted history row that declares -// no parent -- the derived chain refuses UNROOTED, so the honest preamble is the refusal marker +// concatenates. For a SUPPLIED row that declares +// no parent the derived chain refuses UNROOTED, so the honest preamble is the refusal marker // naming where the walk halted (review 5354823380), never a confident projection rendered while // execution proceeded. test fn the_preamble_names_the_derived_refusal_for_an_unrooted_node() -> Bool { - let resolution = alignment_resolve( - node_id: "2-scm-git-upstream-model", - turns_elapsed: 2, - state: alignment_base_cadence_state(), - ) + let resolution = unrooted_specimen_resolution() let preamble = dispatch_continuation_alignment_preamble(resolution: resolution) match resolution { AlignmentUnresolved { cause } => preamble.contains("Alignment UNRESOLVED") && (match cause { AlignmentWalkUnrooted { at, walked } => - at == "2-scm-git-upstream-model" && count(walked) == 0 + at == unrooted_specimen_id() && count(walked) == 0 AlignmentRowUndeclared { node_id: _ } => false AlignmentNestingCyclic { node_id: _ } => false }) - && string_contains(s: preamble, pattern: "2-scm-git-upstream-model") + && string_contains(s: preamble, pattern: unrooted_specimen_id()) AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false } } @@ -1332,38 +1404,30 @@ test fn the_alignment_preamble_is_continuation_only_and_follows_the_verdict() -> }) } -// NO SPAWN ON AN UNRESOLVED CHAIN — THE REAL PRODUCER, THE REAL AUTHORITY (the review finding's -// discriminating control). The production node 2-scm-git-upstream-model's chain refuses UNROOTED -// on the live authority (an accepted history row that declares no parent, review 5354823380); the -// same resolution the belt derives at spawn time is handed to the real -// spawn fold, and the fold's answer is DispatchSpawnRefused at the alignment gate — the Refused -// arm carries no DispatchSpawnCommands, so zero spawn commands exist: no worktree add, no state -// prepare, no session container. Execution cannot proceed on a chain the authority cannot read. -// -// The subject moved on 2026-09-30, when the former subject (extended-admission-cardinality) got its -// parent: an accepted history row with no parent stays unrooted by construction of the lineage -// rule, so it keeps this control discriminating without a row kept unrooted for the purpose. +// NO SPAWN ON AN UNRESOLVED CHAIN — THE REAL PRODUCER, A SUPPLIED UNROOTED ROW (the review +// finding's discriminating control). The supplied parentless row's chain refuses UNROOTED through +// the real derivation (review 5354823380); the same resolution the belt derives at spawn time is +// handed to the real spawn fold, and the fold's answer is DispatchSpawnRefused at the alignment +// gate — the Refused arm carries no DispatchSpawnCommands, so zero spawn commands exist: no +// worktree add, no state prepare, no session container. The rooted twin (see the lineage claim) +// resolves Ready, so removing the unrooted refusal turns this control red. test fn an_unresolved_alignment_refuses_the_real_spawn_fold_with_zero_commands() -> Bool { - match authority_node_opt(id: "2-scm-git-upstream-model") { - Absent => false - Present { value: node } => - match dispatch_spawn_commands_for_resolved_instance( - instance: srv1_lab_dashboard_instance(), - node: node, - session_uuid: "00000000-0000-4000-8000-000000000004", - attempt_key: "feedfacefeedface", - provider: CodexDispatchProvider, - effort: ReasoningMedium, - model: dispatch_codex_default_model, - origin: FreshFromBase, - resolution: alignment_resolve(node_id: "2-scm-git-upstream-model", turns_elapsed: 1, state: alignment_base_cadence_state()), - ) { - DispatchSpawnRefused { step, detail } => - step as String == "alignment" - && string_contains(s: detail, pattern: "2-scm-git-upstream-model") - && string_contains(s: detail, pattern: "admitted root") - DispatchSpawnReady { commands: _ } => false - } + match dispatch_spawn_commands_for_resolved_instance( + instance: srv1_lab_dashboard_instance(), + node: unrooted_specimen(), + session_uuid: "00000000-0000-4000-8000-000000000004", + attempt_key: "feedfacefeedface", + provider: CodexDispatchProvider, + effort: ReasoningMedium, + model: dispatch_codex_default_model, + origin: FreshFromBase, + resolution: unrooted_specimen_resolution(), + ) { + DispatchSpawnRefused { step, detail } => + step as String == "alignment" + && string_contains(s: detail, pattern: unrooted_specimen_id()) + && string_contains(s: detail, pattern: "admitted root") + DispatchSpawnReady { commands: _ } => false } } @@ -1419,6 +1483,7 @@ test fn witness_auditor_launch_writes_the_events_path_metering_reads() -> Bool { worktree_path: "/tmp/fci-witness-worktree", brief: "witness brief", head_sha: "fci1witnesssha", + exit_hook: "+mkdir -- /i/attempt-exit-spool/inbox/x" as NonEmptyStr, ) { SystemdRunCommandRefused { cause: _ } => false SystemdRunCommandReady { command: command } => diff --git a/dag/test/claim/roadmap/roadmap_dogfood_route_wet_witness_test.dag b/dag/test/claim/roadmap/roadmap_dogfood_route_wet_witness_test.dag index 2ee1bc90002..f548678d81e 100644 --- a/dag/test/claim/roadmap/roadmap_dogfood_route_wet_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dogfood_route_wet_witness_test.dag @@ -45,7 +45,7 @@ import gunbc.roadmap_belt_actuate { WorkflowAttemptsObserved } // WET CONTROLS FOR THE DOGFOOD START RECEIPT'S STORE (side-chat ruling on #13077 at e7e9ca9b). Each // claim builds a FRESH instance layout under a temporary directory and runs the production recorder // against it. The store root is the instance's attempt-state root -- the path the deployment's -// AttemptStateRoot member converges (gunbc.live_deploy.spec deployment_owned_steps derives it from +// attempt-state demand converges (gunbc.roadmap_dashboard_instance dashboard_instance_dispatch_attempt_state_directory, whose path is // the same dashboard_instance_dispatch_attempt_state_root) -- so a claim either leaves it missing, // as an unconverged host would, or creates it as the deploy does. On the required floor's // local-repo wet lane (v2.workflow.local_repo_wet_terminal local_repo_wet_schedule), with the diff --git a/dag/test/claim/roadmap/roadmap_forecast_witness_test.dag b/dag/test/claim/roadmap/roadmap_forecast_witness_test.dag index 7419857db2a..f627029e713 100644 --- a/dag/test/claim/roadmap/roadmap_forecast_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_forecast_witness_test.dag @@ -431,7 +431,10 @@ test fn witness_empty_calibration_cell_refuses() -> Bool { ) { ScheduleForecastKnown { forecast: _ } => false ScheduleForecastRefused { refusals } => any(refusals, refusal => match refusal { - NoCalibrationSample { gaps, population_exclusions: _, assumptions: _, scenario_adjustment: _ } => count(gaps) == 1 && first(gaps).nodes == [n.node] + NoCalibrationSample { gaps, population_exclusions: _, assumptions: _, scenario_adjustment: _ } => count(gaps) == 1 && match first(gaps) { + Present { value: g } => g.nodes == [n.node] + Absent => false + } ForecastGraphEmpty => false DependencyCycle { nodes: _ } => false DependencyEndpointUnknown { nodes: _ } => false diff --git a/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag b/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag index f61c2f13a8b..338202d0c61 100644 --- a/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag @@ -793,10 +793,7 @@ test fn witness_tick_receipt_round_trips_launch_identities() -> Bool { launches: [id], spawn_pass: BeltPassRecorded, teardown_pass: BeltPassRecorded, - commit_pass: BeltPassRecorded, - verify_pass: BeltPassRecorded, - publish_pass: BeltPassRecorded, - metering_pass: BeltPassRecorded, + discovery_pass: BeltPassRecorded, } match belt_tick_receipt_decode(raw: serialize_json(v: belt_tick_receipt_json_value(receipt: receipt))) { BeltTickReceiptPresent { receipt: back } => diff --git a/dag/test/claim/roadmap/roadmap_launch_deployment_observe_witness_test.dag b/dag/test/claim/roadmap/roadmap_launch_deployment_observe_witness_test.dag index 7a9bafb39d7..b7363e7d0e9 100644 --- a/dag/test/claim/roadmap/roadmap_launch_deployment_observe_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_launch_deployment_observe_witness_test.dag @@ -11,10 +11,7 @@ fn fixture_v4_tick() -> BeltTickReceipt { launches: [], spawn_pass: BeltPassRecorded, teardown_pass: BeltPassRecorded, - commit_pass: BeltPassRecorded, - verify_pass: BeltPassRecorded, - publish_pass: BeltPassRecorded, - metering_pass: BeltPassRecorded, + discovery_pass: BeltPassRecorded, } } @@ -33,14 +30,29 @@ test fn witness_iso8601_utc_epoch_seconds_matches_known_instants() -> Bool { && (match iso8601_utc_epoch_seconds(text: iso8601_utc_text(unix: 4102444799)) { Present { value: s } => second_count(s) == 4102444799 Absent => false }) } -// (2) Freshness: fresh within the cadence bound, stale beyond it, refused before the deploy, and -// refused on an unparseable instant -- four typed arms, none of which is a default. -test fn witness_tick_freshness_arms() -> Bool { - let deploy = "2026-08-30T12:00:00Z" - (match belt_tick_freshness(observed_at: "2026-08-30T12:01:00Z", deploy_completed_at: deploy, now: "2026-08-30T12:02:00Z") { TickFresh { age_seconds: a } => second_count(a) == 60 _ => false }) - && (match belt_tick_freshness(observed_at: "2026-08-30T12:01:00Z", deploy_completed_at: deploy, now: "2026-08-30T12:10:00Z") { TickStale { age_seconds: a, bound_seconds: b } => second_count(a) == 540 && second_count(b) == 180 _ => false }) - && (match belt_tick_freshness(observed_at: "2026-08-30T11:59:00Z", deploy_completed_at: deploy, now: "2026-08-30T12:00:30Z") { TickBeforeDeploy { observed_at: _, deploy_completed_at: _ } => true _ => false }) - && (match belt_tick_freshness(observed_at: "clock-unreadable", deploy_completed_at: deploy, now: "2026-08-30T12:00:30Z") { TickInstantUnparseable { which: w, text: _ } => string_contains(s: w, pattern: "observed_at") _ => false }) +// (2) Event backlog, the liveness row that replaced tick freshness when the belt timer was removed: +// entries within their stage's bound are clear and counted; an inbox entry past the drain bound and a +// queued entry past the obligation bound each refuse with StaleBacklog naming the OLDEST stale entry; +// a fresh queued entry older than the drain bound is not stale (the bounds are per stage). +fn queued_bound_count() -> Nat { + match event_backlog_queued_bound() { Present { value: b } => second_count(s: b) Absent => 0 } +} + +test fn witness_event_backlog_arms() -> Bool { + let clear = match event_backlog_judge(entries: [ + EventBacklogEntry { stage: AttemptExitInbox, entry: "a", age: second(count: 10) }, + EventBacklogEntry { stage: AttemptExitQueued, entry: "b", age: second(count: 1200) }, + ]) { EventBacklogClear { inbox, queued } => inbox == 1 && queued == 1 _ => false } + let stale_inbox = match event_backlog_judge(entries: [ + EventBacklogEntry { stage: AttemptExitInbox, entry: "young", age: second(count: 30) }, + EventBacklogEntry { stage: AttemptExitInbox, entry: "old", age: second(count: second_count(s: event_backlog_drain_bound) + 1) }, + ]) { StaleBacklog { stage, entry, age: _, bound } => stage == AttemptExitInbox && entry == "old" && second_count(s: bound) == second_count(s: event_backlog_drain_bound) _ => false } + let oldest_named = match event_backlog_judge(entries: [ + EventBacklogEntry { stage: AttemptExitInbox, entry: "stale-inbox", age: second(count: second_count(s: event_backlog_drain_bound) + 5) }, + EventBacklogEntry { stage: AttemptExitQueued, entry: "stale-queued", age: second(count: queued_bound_count() + 100) }, + ]) { StaleBacklog { stage, entry, age: _, bound: _ } => stage == AttemptExitQueued && entry == "stale-queued" _ => false } + let empty = match event_backlog_judge(entries: []) { EventBacklogClear { inbox, queued } => inbox == 0 && queued == 0 _ => false } + clear && stale_inbox && oldest_named && empty } // (3) /instance.json decodes the members the serve route writes, and refuses another schema. @@ -159,3 +171,9 @@ test fn witness_root_tree_standing_refuses_a_mismatched_tree() -> Bool { } } } + +// THE QUEUED BOUND IS DERIVED, NOT COPIED: it is the obligation unit's RuntimeMaxSec plus the drain bound, +// so changing belt_tick_run_bound moves it (plus the drain bound). +test fn witness_queued_bound_is_the_run_bound_plus_the_drain_bound() -> Bool { + queued_bound_count() == second_count(s: belt_tick_run_bound) + second_count(s: event_backlog_drain_bound) +} diff --git a/dag/test/claim/roadmap/roadmap_launch_deployment_receipt_witness_test.dag b/dag/test/claim/roadmap/roadmap_launch_deployment_receipt_witness_test.dag index 77055c1f69e..620223dc834 100644 --- a/dag/test/claim/roadmap/roadmap_launch_deployment_receipt_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_launch_deployment_receipt_witness_test.dag @@ -49,9 +49,9 @@ import gunbc.live_deploy.release_binding { import gunbc.live_deploy.unit_standing { BeltOneshotConverged, BeltOneshotStanding, - BeltTimerConverged, - BeltTimerRefused, - BeltTimerStanding, + BeltEventPathConverged, + BeltEventPathRefused, + BeltEventPathStanding, DashboardServiceConverged, LaunchUnitStanding, } @@ -76,8 +76,10 @@ import gunbc.roadmap_launch_deployment_cli { rlm1_merge_commit, rlm_workflow_file_path, } -import gunbc.roadmap_launch_deployment_observe { TickStale, belt_tick_freshness } +import gunbc.roadmap_launch_deployment_observe { EventBacklogFact, EventBacklogClear, StaleBacklog, EventBacklogUnobserved, event_backlog_drain_bound } import gunbc.roadmap_launch_deployment_receipt { + unit_standing_blockers, + event_backlog_blockers, ApplyReceiptFact, ApplyReceiptRead, DashboardReceiptFact, @@ -250,21 +252,19 @@ fn rlm_tick(instance_id: String, revision: String, mode: SpawnMode, observed_at: launches: [], spawn_pass: BeltPassRecorded, teardown_pass: BeltPassRecorded, - commit_pass: BeltPassRecorded, - verify_pass: BeltPassRecorded, - publish_pass: BeltPassRecorded, - metering_pass: BeltPassRecorded, + discovery_pass: BeltPassRecorded, } } fn rlm_tick_fact(t: BeltTickReceipt) -> TickFact { - TickObserved { receipt: t, freshness: belt_tick_freshness(observed_at: t.observed_at, deploy_completed_at: rlm_fixture_deploy_completed_at, now: rlm_fixture_now) } + TickObserved { receipt: t } } -fn rlm_units(timer: BeltTimerStanding, oneshot: BeltOneshotStanding) -> LaunchUnitStanding { +fn rlm_units(exit_path: BeltEventPathStanding, oneshot: BeltOneshotStanding) -> LaunchUnitStanding { LaunchUnitStanding { dashboard: DashboardServiceConverged { unit: "gunbc-roadmap.service" }, - timer: timer, + exit_path: exit_path, + answer_path: BeltEventPathConverged { unit: "gunbc-roadmap-belt-discovery.path" }, oneshot: oneshot, } } @@ -319,8 +319,9 @@ fn rlm_complete_facts() -> RoadmapLaunchDeploymentFacts { release_binding: rlm_release_binding(revision_hex: rlm_fixture_r), readiness: ReadinessServed { revision: rlm_fixture_r, surface: rlm_fixture_surface }, transition: ActuationPermitted, - unit_standing: rlm_units(timer: BeltTimerConverged { unit: "gunbc-belt.timer" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "inactive" }), + unit_standing: rlm_units(exit_path: BeltEventPathConverged { unit: "gunbc-roadmap-belt-exit.path" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "inactive" }), tick_standing: rlm_tick_fact(t: tick), + event_backlog: EventBacklogClear { inbox: 0, queued: 0 }, footprint: FootprintAdmitted, provider_preflight: ProviderPreflightOk, dispatch_preflight: DispatchPreflightAdmittedFact { instance_id: "srv1-live", revision_hex: rlm_fixture_r }, @@ -343,54 +344,54 @@ fn incomplete_at(facts: RoadmapLaunchDeploymentFacts, member: String) -> Bool { } fn with_plan(f: RoadmapLaunchDeploymentFacts, plan: PlanReceiptFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: plan, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: plan, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_apply(f: RoadmapLaunchDeploymentFacts, apply: ApplyReceiptFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: apply, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: apply, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_dashboard(f: RoadmapLaunchDeploymentFacts, d: DashboardReceiptFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: d, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: d, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_tree(f: RoadmapLaunchDeploymentFacts, t: DeployedRootTreeStanding) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: t, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: t, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_transition(f: RoadmapLaunchDeploymentFacts, t: ActuationStanding) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: t, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: t, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_units(f: RoadmapLaunchDeploymentFacts, u: LaunchUnitStanding) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: u, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: u, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_tick(f: RoadmapLaunchDeploymentFacts, t: TickFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: t, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: t, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_footprint(f: RoadmapLaunchDeploymentFacts, a: BeltFootprintAdmission) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: a, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: a, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_provider(f: RoadmapLaunchDeploymentFacts, p: BeltProviderPreflight) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: p, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: p, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_workflow(f: RoadmapLaunchDeploymentFacts, w: WorkflowEndpointFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: w, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: w, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_principal(f: RoadmapLaunchDeploymentFacts, p: PrincipalFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: p, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: p, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_host(f: RoadmapLaunchDeploymentFacts, h: HostFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: h, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: h, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_release_binding(f: RoadmapLaunchDeploymentFacts, b: DeployedReleaseBinding) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: b, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: b, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } fn with_window(f: RoadmapLaunchDeploymentFacts, w: ObservationWindowFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: w, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: w, run_provenance: f.run_provenance } } fn with_run_provenance(f: RoadmapLaunchDeploymentFacts, rp: RunProvenanceFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: rp } + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: rp } } fn with_desired(f: RoadmapLaunchDeploymentFacts, d: DesiredRevisionFact) -> RoadmapLaunchDeploymentFacts { - RoadmapLaunchDeploymentFacts { desired_revision: d, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } + RoadmapLaunchDeploymentFacts { desired_revision: d, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: f.event_backlog, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } // ---------------------------------------------------------------- POSITIVE CONTROL @@ -455,11 +456,6 @@ test fn witness_deployed_tree_mismatch_is_incomplete_at_deployed_tree() -> Bool incomplete_at(facts: with_tree(f: rlm_complete_facts(), t: DeployedRootTreeMismatch { candidate_hex: rlm_fixture_tree, deployed_hex: rlm_fixture_other_tree }), member: "deployed_tree") } -// v3, stale, wrong-instance, wrong-revision, or non-ManualReady tick receipt -test fn witness_stale_tick_is_incomplete_at_tick_standing() -> Bool { - incomplete_at(facts: with_tick(f: rlm_complete_facts(), t: rlm_tick_fact(t: rlm_tick(instance_id: "srv1-live", revision: rlm_fixture_r, mode: ManualReady, observed_at: "2026-08-30T11:00:00Z"))), member: "tick_standing") -} - test fn witness_wrong_instance_tick_is_incomplete_at_tick_standing() -> Bool { incomplete_at(facts: with_tick(f: rlm_complete_facts(), t: rlm_tick_fact(t: rlm_tick(instance_id: "srv1-lab", revision: rlm_fixture_r, mode: ManualReady, observed_at: rlm_fixture_tick_observed_at))), member: "tick_standing") } @@ -484,10 +480,10 @@ test fn witness_v3_tick_on_disk_is_incomplete_at_tick_standing() -> Bool { && incomplete_at(facts: with_tick(f: rlm_complete_facts(), t: fact), member: "tick_standing") } -// disabled timer beside an otherwise fresh, manually fabricated tick -test fn witness_disabled_timer_beside_fresh_tick_is_incomplete_at_unit_standing() -> Bool { +// a disabled exit path unit beside an otherwise complete population: the watcher is not armed +test fn witness_disabled_exit_path_is_incomplete_at_unit_standing() -> Bool { incomplete_at( - facts: with_units(f: rlm_complete_facts(), u: rlm_units(timer: BeltTimerRefused { unit: "gunbc-belt.timer", property: "UnitFileState", expected: "enabled", observed: "disabled" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "inactive" })), + facts: with_units(f: rlm_complete_facts(), u: rlm_units(exit_path: BeltEventPathRefused { unit: "gunbc-roadmap-belt-exit.path", property: "UnitFileState", expected: "enabled", observed: "disabled" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "inactive" })), member: "unit_standing", ) } @@ -496,10 +492,7 @@ test fn witness_disabled_timer_beside_fresh_tick_is_incomplete_at_unit_standing( // exactly that (active_state "inactive") and is Complete; an ACTIVE oneshot mid-tick is equally // accepted. test fn witness_active_oneshot_mid_tick_is_still_complete() -> Bool { - match roadmap_launch_deployment_verdict(subject: rlm_subject(), facts: with_units(f: rlm_complete_facts(), u: rlm_units(timer: BeltTimerConverged { unit: "gunbc-belt.timer" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "active" }))) { - DeploymentComplete { subject: _, facts: _, proof: _ } => true - DeploymentIncomplete { subject: _, facts: _, first_blocker: _, further_blockers: _ } => false - } + unit_standing_blockers(f: rlm_units(exit_path: BeltEventPathConverged { unit: "gunbc-roadmap-belt-exit.path" }, oneshot: BeltOneshotConverged { unit: "gunbc-belt.service", active_state: "active" })).length() == 0 } // wrong effective principal @@ -745,48 +738,34 @@ test fn witness_route_change_during_window_is_incomplete_at_observation_window() incomplete_at(facts: with_window(f: rlm_complete_facts(), w: WindowChanged { opened: rlm_snapshot(), closed: closed, changed: ["running_release_hex"] }), member: "observation_window") } -// Review 5062738052 B3: tick freshness is a typed-arm object in the durable document -- the -// observed age and the cadence-derived bound are recovered by JSON navigation, never from prose. -// Two witnesses (fresh doc, stale doc) so each stays inside the floor's 500ms CPU line: one -// document build + parse each; the single two-document form was BUDGET-REFUSED on run 33358233304. -fn rlm_doc_freshness(doc: String) -> JsonValue? { +// Review 5062738052 B3, carried onto the row that replaced tick freshness: the event backlog is a +// typed-arm object in the durable document, recovered by JSON navigation, never from prose. A stale +// backlog names its stage, its oldest entry and both numbers, and is a blocker at event_backlog -- +// the liveness half an armed .path unit cannot prove (bold-bee-114 ruling, 2026-10-06). +fn rlm_doc_backlog(doc: String) -> JsonValue? { match parse_json_document(s: doc) { JsonDocumentUnreadable { gap: _ } => none JsonDocumentParsed { value: root } => match rlm_doc_member(doc: root, key: "facts") { Absent => none - Present { value: facts } => - match rlm_doc_member(doc: facts, key: "tick_standing") { - Absent => none - Present { value: tick } => rlm_doc_member(doc: tick, key: "freshness") - } + Present { value: facts } => rlm_doc_member(doc: facts, key: "event_backlog") } } } -test fn witness_receipt_document_fresh_age_is_machine_readable() -> Bool { - let doc = roadmap_launch_deployment_receipt_document(v: roadmap_launch_deployment_verdict(subject: rlm_subject(), facts: rlm_complete_facts())) - match rlm_doc_freshness(doc: doc) { - Absent => false - Present { value: fr } => rlm_doc_string(doc: fr, key: "arm") == "fresh" && rlm_doc_string(doc: fr, key: "age_seconds") == "30" - } +fn with_backlog(f: RoadmapLaunchDeploymentFacts, b: EventBacklogFact) -> RoadmapLaunchDeploymentFacts { + RoadmapLaunchDeploymentFacts { desired_revision: f.desired_revision, plan_receipt: f.plan_receipt, apply_receipt: f.apply_receipt, dashboard_receipt: f.dashboard_receipt, deployed_tree: f.deployed_tree, release_binding: f.release_binding, readiness: f.readiness, transition: f.transition, unit_standing: f.unit_standing, tick_standing: f.tick_standing, event_backlog: b, footprint: f.footprint, provider_preflight: f.provider_preflight, dispatch_preflight: f.dispatch_preflight, workflow_endpoint: f.workflow_endpoint, instance_endpoint: f.instance_endpoint, principal: f.principal, observed_host: f.observed_host, observation_window: f.observation_window, run_provenance: f.run_provenance } } -test fn witness_receipt_document_stale_age_and_bound_are_machine_readable() -> Bool { - let stale_tick = rlm_tick(instance_id: "srv1-live", revision: rlm_fixture_r, mode: ManualReady, observed_at: rlm_fixture_tick_observed_at) - let doc = roadmap_launch_deployment_receipt_document(v: roadmap_launch_deployment_verdict( - subject: rlm_subject(), - facts: with_tick(f: rlm_complete_facts(), t: TickObserved { receipt: stale_tick, freshness: TickStale { age_seconds: second(count: 540), bound_seconds: second(count: 180) } }), - )) - match rlm_doc_freshness(doc: doc) { - Absent => false - Present { value: fr } => - rlm_doc_string(doc: fr, key: "arm") == "stale" - && rlm_doc_string(doc: fr, key: "age_seconds") == "540" - && rlm_doc_string(doc: fr, key: "bound_seconds") == "180" - } +test fn witness_stale_backlog_is_incomplete_and_names_its_oldest_entry() -> Bool { + let stale = StaleBacklog { stage: AttemptExitInbox, entry: "n+k+worker+inv+success+exited+0", age: second(count: second_count(s: event_backlog_drain_bound) + 60), bound: event_backlog_drain_bound } + incomplete_at(facts: with_backlog(f: rlm_complete_facts(), b: stale), member: "event_backlog") } +test fn witness_clear_and_unobserved_backlog_decide_without_the_verdict() -> Bool { + event_backlog_blockers(f: EventBacklogClear { inbox: 0, queued: 0 }).length() == 0 + && event_backlog_blockers(f: EventBacklogUnobserved { detail: "the exit inbox could not be listed" }).length() == 1 +} // --------------------------------------------------------------------------- // THE TRANSACTION REQUIRES THE LAUNCH-ENVIRONMENT SCOPE, AND THE SCOPE IS FIXED RATHER THAN GIVEN. diff --git a/dag/test/claim/roadmap/roadmap_publish_witness_test.dag b/dag/test/claim/roadmap/roadmap_publish_witness_test.dag index 5bd29bac0a4..8c192d08736 100644 --- a/dag/test/claim/roadmap/roadmap_publish_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_publish_witness_test.dag @@ -718,7 +718,10 @@ test fn an_ambiguous_binding_round_trips_its_pull_request_numbers() -> Bool { match receipt.outcome { ReceiptBindingAmbiguous { pr_numbers } => count(pr_numbers) == 2 - && pr_numbers.first() == 11 + && match pr_numbers.first() { + Present { value: n } => n == 11 + Absent => false + } ReceiptBound { pr_number: _, url: _, draft: _ } => false ReceiptHeadDiverged { pr_number: _, pr_head: _, url: _ } => false ReceiptRemoteBranchAtHeadWithoutPullRequest { head_sha: _ } => false diff --git a/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag index c27b53a2bb7..f880f0fd436 100644 --- a/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag @@ -51,7 +51,7 @@ import gunbc.roadmap.roadmap_result_returned { ResultReturnedObservation, ResultReturnedObserved, ResultNotReturned, ResultNotOwed, ResultReturnedUnobserved, result_returned_observation_of, } -import gunbc.roadmap_belt_tick_cli { belt_tick_result_tail_exit, belt_tick_exit_with_snapshot_closes } +import gunbc.roadmap_belt_tick_cli { belt_tick_exit_with_snapshot_closes } import gunbc.roadmap.roadmap_event_carrier { SnapshotNotOpened, SnapshotClosed, SnapshotCloseFailed } import test.claim.roadmap_belt_actuate_witness { completed_attempt_progress } @@ -698,20 +698,7 @@ test fn an_undelivered_result_fails_the_tick_and_is_named_beside_an_observation_ ResultReturnNoRequest { node_id: "b", attempt_key: "k2", reason: "launched unclaimed" }, ] } let clean = all([clean_pass, ResultReturnPassRan { outcomes: [] }, ResultReturnPassWithheld { reason: "observe-only" }], p => match result_return_pass_exit(pass: p) { ExitSuccess => true ExitFailure { code: _, reason: _ } => false }) - let both = match belt_tick_result_tail_exit(returned: undelivered_pass, tail: ExitFailure { code: 1, reason: "served observation write failed" }) { - ExitFailure { code, reason } => code == 1 && string_contains(s: reason, pattern: "served observation write failed") && string_contains(s: reason, pattern: "b attempt k2 (publication-refused)") - ExitSuccess => false - } - let observation_only = match belt_tick_result_tail_exit(returned: clean_pass, tail: ExitFailure { code: 1, reason: "served observation write failed" }) { - ExitFailure { code: _, reason } => reason == "served observation write failed" - ExitSuccess => false - } - let result_only = match belt_tick_result_tail_exit(returned: undelivered_pass, tail: ExitSuccess) { - ExitFailure { code: _, reason } => string_contains(s: reason, pattern: "b attempt k2") - ExitSuccess => false - } - let neither = match belt_tick_result_tail_exit(returned: clean_pass, tail: ExitSuccess) { ExitSuccess => true ExitFailure { code: _, reason: _ } => false } - undelivered && unreadable && clean && both && observation_only && result_only && neither + undelivered && unreadable && clean } // A SNAPSHOT THE RESULT RETURN LEFT BEHIND FAILS THE TICK AND IS NAMED, AND REPLACES NOTHING. A diff --git a/dag/test/claim/roadmap/roadmap_verification_receipt_witness_test.dag b/dag/test/claim/roadmap/roadmap_verification_receipt_witness_test.dag index 9a4090f2127..b3280c0e5ac 100644 --- a/dag/test/claim/roadmap/roadmap_verification_receipt_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_verification_receipt_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.roadmap_verification_receipt_witness_test import std.types { Bool, String, NonEmptyStr, FilePath, CommitSha } +import std.optional { Present } import std.content_hash { Fnv1a64Structural, content_hash_of_value, @@ -106,7 +107,7 @@ test fn complete_exact_head_receipt_constructs_and_matches() -> Bool { ) { VerificationEvidenceForSubject { receipt: found } => count(found.executions) == 1 - && found.executions.first().command.args.first() == "run" + && match found.executions.first() { Present { value: e } => (match e.command.args.first() { Present { value: a } => (a as String) == "run" _ => false }) _ => false } && verification_evidence_moves_lamp( evidence: VerificationEvidenceForSubject { receipt: found }, ) diff --git a/dag/test/claim/roadmap/roadmap_worker_exit_observation_witness_test.dag b/dag/test/claim/roadmap/roadmap_worker_exit_observation_witness_test.dag new file mode 100644 index 00000000000..d6553903218 --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_worker_exit_observation_witness_test.dag @@ -0,0 +1,87 @@ +module test.claim.roadmap.roadmap_worker_exit_observation_witness_test + +import std.types { Bool, String, List, NonEmptyStr } +import gunbc.roadmap_dispatch_actuator { + AttemptUnitObservation, AttemptUnitAbsent, AttemptUnitActive, AttemptUnitFinished, AttemptUnitUnreadable, + AttemptUnitStartRecorded, AttemptUnitNeverStarted, AttemptUnitStartUnread, + attempt_worker_unit_observation_of, worker_process_from_attempt_unit, +} +import gunbc.worker_lifecycle { WorkerProcessEvidence, WorkerProcessUnobserved, WorkerProcessAbsent, WorkerProcessRunning, WorkerProcessExited } +import gunbc.roadmap.roadmap_attempt_exit_spool { + AttemptWorkerRole, AttemptReviewerRole, AttemptAuditorRole, + attempt_exit_record_from_listings, AttemptExitStageNames, AttemptExitStageUnreadable, +} +import gunbc.compute.attempt_lifecycle { UnitLifecycle, UnitTerminatedSuccess, UnitTerminatedFailure, UnitTerminatedCauseUnknown, UnitStillRunning, UnitLifecycleUnknown } +import gunbc.roadmap_session_placement { session_worker_unit_lifecycle } + +// The worker unit's observation now that the unit is collected when its process ends (the +// 2026-09-05 RemainAfterExit is reversed). Each claim supplies the manager's answer and the spool's +// listings and runs the production decision and the production WorkerProcessEvidence projection; the +// wet witness beside this file runs the real spool read. + +data unit: NonEmptyStr = "gunbc-attempt-n-1-k1.service" as NonEmptyStr + +fn evidence(loaded: AttemptUnitObservation, names: List, start_recorded: Bool) -> WorkerProcessEvidence { + worker_process_from_attempt_unit( + observation: attempt_worker_unit_observation_of( + unit: unit, + loaded: loaded, + record: attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: names }, AttemptExitStageNames { names: [] }, AttemptExitStageNames { names: [] }]), + start: if start_recorded { AttemptUnitStartRecorded } else { AttemptUnitNeverStarted }, + ), + unit: unit, + ) +} + +fn exited_with(e: WorkerProcessEvidence, code: Int) -> Bool { + match e { WorkerProcessExited { exit_code, last_command: _ } => exit_code == code _ => false } +} + +fn unobserved(e: WorkerProcessEvidence) -> Bool { + match e { WorkerProcessUnobserved { reason: _ } => true _ => false } +} + +fn absent(e: WorkerProcessEvidence) -> Bool { + match e { WorkerProcessAbsent => true _ => false } +} + +// The collected worker is read from its record: a success exits 0, a failure carries its status, and +// a SIGTERM reads 15 -- the number ExecMainStatus reported before, so no consumer's decision moves. +test fn a_collected_worker_is_read_from_its_exit_record() -> Bool { + exited_with(e: evidence(loaded: AttemptUnitAbsent, names: ["n-1+k1+worker+inv+success+exited+0"], start_recorded: true), code: 0) + && exited_with(e: evidence(loaded: AttemptUnitAbsent, names: ["n-1+k1+worker+inv+exit-code+exited+3"], start_recorded: true), code: 3) + && exited_with(e: evidence(loaded: AttemptUnitAbsent, names: ["n-1+k1+worker+inv+signal+killed+TERM"], start_recorded: true), code: 15) +} + +// Condition 1: a started worker whose unit is gone with NO record is indeterminate, never "never ran"; +// only an attempt with no start receipt (no placement record) answers absent. +test fn a_gone_unit_without_a_record_is_unobserved_unless_it_never_started() -> Bool { + unobserved(e: evidence(loaded: AttemptUnitAbsent, names: ["other+k9+worker+inv+success+exited+0"], start_recorded: true)) + && absent(e: evidence(loaded: AttemptUnitAbsent, names: [], start_recorded: false)) +} + +// A loaded unit is the manager's answer and the record is not consulted: a retained failed unit reads +// its own ExecMainStatus, and a running one is running even if a stale record names it. +test fn a_loaded_unit_is_read_from_the_manager() -> Bool { + exited_with(e: evidence(loaded: AttemptUnitFinished { exit_status: "1" }, names: ["n-1+k1+worker+inv+success+exited+0"], start_recorded: true), code: 1) + && (match evidence(loaded: AttemptUnitActive, names: ["n-1+k1+worker+inv+success+exited+0"], start_recorded: true) { WorkerProcessRunning { command: _ } => true _ => false }) +} + +// The settlement's read: a collected worker that exited 0 settles as SUCCESS (it read cause-unknown +// before the record existed); a gone unit with no record keeps cause-unknown, the arm that still frees +// only after the launch deadline; an unreadable spool frees nothing. +test fn settlement_reads_the_record_and_keeps_cause_unknown_without_one() -> Bool { + let success = match session_worker_unit_lifecycle(unit: unit, loaded: AttemptUnitAbsent, record: attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: ["n-1+k1+worker+inv+success+exited+0"] }])) { + UnitTerminatedSuccess { detail: _ } => true + _ => false + } + let unknown = match session_worker_unit_lifecycle(unit: unit, loaded: AttemptUnitAbsent, record: attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageNames { names: [] }])) { + UnitTerminatedCauseUnknown { detail: _ } => true + _ => false + } + let unreadable = match session_worker_unit_lifecycle(unit: unit, loaded: AttemptUnitAbsent, record: attempt_exit_record_from_listings(node_id: "n-1", attempt_key: "k1", role: AttemptWorkerRole, listings: [AttemptExitStageUnreadable { reason: "EACCES" }])) { + UnitLifecycleUnknown { detail: _ } => true + _ => false + } + success && unknown && unreadable +} diff --git a/dag/test/claim/roadmap/session_placement_witness_test.dag b/dag/test/claim/roadmap/session_placement_witness_test.dag index 633db54e91a..0826814f81d 100644 --- a/dag/test/claim/roadmap/session_placement_witness_test.dag +++ b/dag/test/claim/roadmap/session_placement_witness_test.dag @@ -223,7 +223,7 @@ test fn settlement_releases_only_a_closed_window_or_an_ended_unit() -> Bool { // same fold, carries no process bounds. test fn the_worker_unit_is_capped_at_its_grant_by_the_compute_fold() -> Bool { let inst = srv2_deploy_dashboard_instance() - let props = property_words(props: dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1024), process_bounds: session_process_bounds)) + let props = property_words(props: dispatch_worker_unit_properties(instance: inst, working_directory: "/wt", worker_log_path: "/wt/log", granted: kibibyte(count: 1024), process_bounds: session_process_bounds, exit_hook: "mkdir -- /i/attempt-exit-spool/inbox/n+k+x" as NonEmptyStr)) let compute = property_words(props: compute_unit_properties(layout: compute_layout(instance: inst, identity_hex: "abc"), granted: kibibyte(count: 1024))) contains(props, "MemoryMax=1048576") && contains(props, "CPUQuota=800%") diff --git a/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag new file mode 100644 index 00000000000..f61a111ca19 --- /dev/null +++ b/dag/test/claim/runner/qualification_job_log_receipt_forged_probe_witness_test.dag @@ -0,0 +1,67 @@ +module test.claim.qualification_job_log_receipt_forged_probe_witness + +import std.types { String, Bool, Int, List } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE FORGED JOB-LOG RECEIPT ROUTES, ENROLLED AS EXECUTED REDS (DESIGN 4b: a state unrepresentable +// in the accepted corpus is still representable as source handed to the compiler, and the refusal +// is the enrollable evidence). test.probe.qualification_job_log_receipt_forged_probe writes the +// sealed receipt from invented readings and calls its one minter from outside the collect stage; +// each must refuse at its own subject. Counts are scoped to class AND subject, and a control source +// that only NAMES the module and calls its open decoder must refuse at neither, so the refusals are +// attributable to the forgeries rather than to importing the module at all. The pattern is +// test.claim.jit_deregistration_forged_probe_witness's. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn probe() -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: "dag/test/probe/qualification_job_log_receipt_forged_probe.dag").content) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_qualification_receipt_harness_control\nimport std.types { String }\nimport gunbc.runner.runner_qualification_dispatch { JobLogInstrumentDecode, decode_job_log_instruments }\nfn decoded(t: String) -> JobLogInstrumentDecode { decode_job_log_instruments(log: t, slot_unit: \"x.service\") }\n" + +test fn a_source_that_only_names_the_module_forges_nothing() -> Bool { + let c = census_of(source: control_source) + blocking(c: c, wanted: "SoleConstructorViolation", subject: "QualificationJobLogReceipt") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "collect_qualification_instruments_under") == 0 + && blocking(c: c, wanted: "ConstructorCallAdmissionRefused", subject: "read_claim_cost_artifact") == 0 +} + +test fn a_forged_job_log_receipt_refuses_at_its_own_name() -> Bool { + blocking(c: probe(), wanted: "SoleConstructorViolation", subject: "QualificationJobLogReceipt") >= 1 +} + +test fn the_receipt_minter_refuses_a_caller_other_than_the_collect_stage() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "job_log_standing") >= 1 +} + +// THE COLLECTOR'S READS ARE REACHABLE ONLY THROUGH ITS READ PLAN. collect_qualification_instruments_under +// takes no job or run id -- they are the sealed collection's -- and admits only the checked entry, so a +// caller holding a genuine collection cannot read it under another installation's token. +test fn the_reads_refuse_a_caller_other_than_the_checked_entry() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "collect_qualification_instruments_under") >= 1 +} + +// AND THE CLAIM-COST READ CANNOT BE AIMED AT ANOTHER RUN: it admits only the confined reads above. +test fn the_claim_cost_read_refuses_a_free_token_and_run_id() -> Bool { + blocking(c: probe(), wanted: "ConstructorCallAdmissionRefused", subject: "read_claim_cost_artifact") >= 1 +} diff --git a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag index 962d432e01d..20c0fd288c1 100644 --- a/dag/test/claim/runner/runner_attempt_launch_witness_test.dag +++ b/dag/test/claim/runner/runner_attempt_launch_witness_test.dag @@ -1,5 +1,11 @@ module test.claim.runner_attempt_launch_witness +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner_jit_mint { MicroVmSlotBinding } +import gunbc.runner_microvm_attempt { bind_owned_attempt } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, restricted_group_1 } +import extdeps.github.actions_jit_runner { jit_config_mint_outcome } import gunbc.runner_attempt_launch { GitHubLaunchPayload, launch_payload_device } import std.types { String, Bool, Int, List, NonEmptyStr } @@ -135,18 +141,40 @@ fn minted_credential(encoded: String) -> List { // something that is not a whole credential, handed on as if it were. data short_blob: String = "ZmFrZS1qaXQtY29uZmln" -fn received_for(attempt: MicroVmAttempt, encoded: String) -> JitMintPerformance { - match minted_credential(encoded: encoded) { - Cons { head: c, tail: _ } => +// THE DELIVERY IS MINTED, NOT HAND-BUILT, AND IT NEVER LEAVES THIS FUNCTION. A successful delivery is +// a sealed BoundJitCredential that only receive_jit_mint constructs, carrying the authorized dispatch +// that produced it; this fixture takes the real route for the credential's attempt -- an owned cell +// binding, dispatch_jit_mint and receive_jit_mint over upstream's own 201 outcome fold, both of which +// admit this function by name -- and hands back only the PLAN the planner built from it. A fixture +// that returned the delivery would be a public proxy for the mint (an admitted call edge does not +// confine a value the caller hands back), so a plan, which carries no delivery, is the only egress. +// minted_for differs from attempt in the claim about a credential bound to a sibling attempt. +fn planned_with( + pair: IssuedPair, + account: MoneyAccount, + attempt: MicroVmAttempt, + minted_for: MicroVmAttempt, + encoded: String, + ground: AttemptGround, +) -> AttemptLaunchPlan { + let mint = match dispatch_jit_mint( + binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: minted_for, owned: true) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: restricted_group_1(), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => JitMintReceived { - delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: attempt }, credential: c, runner_id: registration_fixture_id }, + delivery: receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: d }, + outcome: jit_config_mint_outcome(status: 201, encoded: encoded, runner_id: registration_fixture_id, error_body: ""), + ), } - Empty {} => JitMintAppJwtUnsigned { detail: "fixture: credential fixture did not mint" } + JitMintDispatchRefused { refusal: _ } => JitMintAppJwtUnsigned { detail: "fixture: the attempt's dispatch was refused" } } -} - -fn live_grant(attempt: MicroVmAttempt) -> JitMintPerformance { - received_for(attempt: attempt, encoded: doubled(s: "A", k: 10)) + plan_for(pair: pair, account: account, attempt: attempt, mint: mint, ground: ground) } fn plan_for( @@ -209,7 +237,7 @@ test fn a_below_floor_credential_stages_no_device_and_starts_no_vmm() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 1, id: "cred0") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: short_blob), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: short_blob, ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialBelowFloor { bytes: n }, tail: _ } => byte_size_count(b: n) == 20 @@ -233,7 +261,7 @@ test fn a_credential_at_the_ceiling_in_code_points_but_over_it_in_bytes_is_refus match accepted_attempt(i: 7, id: "multibyte") { Cons { head: a, tail: _ } => { let wide = doubled(s: "\u{00e9}", k: 16) - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: wide), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: wide, ground: GroundClear) string_length(wide) == 65536 && byte_size_count(b: jit_drive_utf8_size(text: wide)) == 131072 && no_device_no_vmm(plan: plan) @@ -253,7 +281,7 @@ test fn an_above_ceiling_credential_stages_no_device_and_starts_no_vmm() -> Bool Cons { head: pair, tail: _ } => match accepted_attempt(i: 1, id: "credbig") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: received_for(attempt: a, encoded: doubled(s: "A", k: 17)), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 17), ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialAboveCeiling { bytes: _ }, tail: _ } => true @@ -275,7 +303,7 @@ test fn a_credential_bound_to_another_attempt_stages_no_device() -> Bool { Cons { head: a, tail: _ } => match accepted_attempt(i: 6, id: "theirs") { Cons { head: b, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: b), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: b, encoded: doubled(s: "A", k: 10), ground: GroundClear) no_device_no_vmm(plan: plan) && match refusal_of(plan: plan) { Cons { head: JitCredentialBoundToDifferentAttempt { bound_to: other }, tail: _ } => other == MicroVmJitSlot { attempt: b } @@ -336,9 +364,9 @@ test fn an_unauthorized_grant_yields_zero_jailer_launches() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "unauth") { Cons { head: a, tail: _ } => - match plan_for(pair: pair, account: exhausted_account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) { + match planned_with(pair: pair, account: exhausted_account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) { LaunchRefusedUnauthorized { authorization: _ } => - no_device_no_vmm(plan: plan_for(pair: pair, account: exhausted_account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear)) + no_device_no_vmm(plan: planned_with(pair: pair, account: exhausted_account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear)) _ => false } Empty {} => false @@ -352,9 +380,9 @@ test fn prior_resources_refuse_rather_than_reuse() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 3, id: "prior") { Cons { head: a, tail: _ } => - match plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundOccupied) { + match planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundOccupied) { LaunchRefusedPriorResources => - no_device_no_vmm(plan: plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundOccupied)) + no_device_no_vmm(plan: planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundOccupied)) _ => false } Empty {} => false @@ -378,7 +406,7 @@ test fn a_live_grant_stages_exactly_one_device_and_records_the_registration() -> Cons { head: pair, tail: _ } => match accepted_attempt(i: 4, id: "ok1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) planned_jailer_launches(plan: plan) == 1 && match plan { LaunchAuthorized { @@ -550,7 +578,7 @@ fn staging_with(jit_of: Bool, obs: WorkspaceStagingObservation, ground: AttemptG Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: ground) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: ground) staging_label(v: attempt_staging_verdict( plan: plan, jail: staged_jail_material(), @@ -600,7 +628,7 @@ fn jail_label(m: JailMaterialStaging) -> String { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) staging_label(v: attempt_staging_verdict( plan: plan, jail: m, @@ -652,7 +680,7 @@ fn admitted_receipt_names_this_attempts_paths() -> Bool { Cons { head: pair, tail: _ } => match accepted_attempt(i: 2, id: "ws1") { Cons { head: a, tail: _ } => { - let plan = plan_for(pair: pair, account: pair.account, attempt: a, mint: live_grant(attempt: a), ground: GroundClear) + let plan = planned_with(pair: pair, account: pair.account, attempt: a, minted_for: a, encoded: doubled(s: "A", k: 10), ground: GroundClear) match attempt_staging_verdict(plan: plan, jail: staged_jail_material(), jit: staged_device_of(plan: plan), workspace: good_workspace()) { StagingAdmitsJailer { receipt: r } => r.bootstrap_device_path == concat(attempt_jail_chroot_root(attempt: a, firecracker_binary: "/bin/firecracker"), jail_jit_path as String) diff --git a/dag/test/claim/runner/runner_host_file_converge_witness_test.dag b/dag/test/claim/runner/runner_host_file_converge_witness_test.dag index a1ccb5a5a75..138433059cc 100644 --- a/dag/test/claim/runner/runner_host_file_converge_witness_test.dag +++ b/dag/test/claim/runner/runner_host_file_converge_witness_test.dag @@ -10,7 +10,7 @@ import extdeps.needrestart { needrestart_unit_name_prefix_regex, } import product.host_identity { HostIdentity } -import gunbc.runner_slot_population_census { desired_unit_names_range } +import gunbc.runner_slot_population_census { desired_unit_names_range, committed_unit_names_at } import gunbc.runner_slot_allocation { gunbc_runner_committed_width, RunnerWidthDerived, RunnerWidthUnresolved } import gunbc.runner_width_transition { transition_population_agreement, TransitionPopulationAgrees, TransitionPopulationDisagrees, TransitionPopulationUndecidable } import gunbc.fleet_host_identity { operator_host_srv1 } @@ -728,22 +728,29 @@ fn srv1_width() -> Int? { } } +// THE LIVE SET IS THE COMMITTED IDENTITIES, NOT 1..width. Since #12339 srv1's committed population +// is srv1-01..(width - 1) plus the shakedown slot srv1-13 (gunbc.runner_slot_allocation +// host_committed_identities_at), so cutting the listing at the width named a slot the width no longer +// charges as live and masked srv1-13. The live set comes from the census authority's one derivation; +// the retiring set is the rest of the listing. fn srv1_live_names() -> List { - match srv1_width() { Absent => [] Present { value: w } => srv1_unit_names(slot: 1, limit: w) } + match srv1_width() { Absent => [] Present { value: w } => committed_unit_names_at(host: "srv1" as HostIdentity, committed: w) } } fn srv1_retiring_names(limit: Int) -> List { - match srv1_width() { Absent => [] Present { value: w } => srv1_unit_names(slot: w + 1, limit: limit) } + let live = srv1_live_names() + match srv1_width() { Absent => [] Present { value: _ } => srv1_unit_names(slot: 1, limit: limit) |> filter(u => !any(live, l => l == u)) } } // The last declared-live slot: the one the REDs below mask or lose to show that retirement never // excuses a live slot. fn srv1_last_live_names() -> List { - match srv1_width() { Absent => [] Present { value: w } => srv1_unit_names(slot: w, limit: w) } + fold(srv1_live_names(), init: [], f: (acc, u) => [u]) } fn srv1_all_but_last_live_names() -> List { - match srv1_width() { Absent => [] Present { value: w } => srv1_unit_names(slot: 1, limit: w - 1) } + let last = srv1_last_live_names() + srv1_live_names() |> filter(u => !any(last, l => l == u)) } fn every_unit_holds(pop: RunnerSlotPopulation, units: List, wire: String) -> Bool { diff --git a/dag/test/claim/runner/runner_host_medium_witness_test.dag b/dag/test/claim/runner/runner_host_medium_witness_test.dag new file mode 100644 index 00000000000..c3fa18b2689 --- /dev/null +++ b/dag/test/claim/runner/runner_host_medium_witness_test.dag @@ -0,0 +1,358 @@ +module test.claim.runner_host_medium_witness + +import std.types { Bool, List, NonEmptyStr, String } +import std.measure { kibibyte_count, hardware_thread_count_value } +import extdeps.toolchain.types { Aarch64, X86_64 } +import extdeps.crypto.hash { sha256_digest, sha512_digest } +import std.process { ExitSuccess } +import extdeps.linux.proc_uptime { UptimeRefused } +import gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle { SmproNotTaken } +import gunbc.host_boot_attempt_admission { attempt_configuration_not_recorded } +import std.optional { Present } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.github.actions_runner { actions_runner_release_2_337_0 } +import extdeps.provisioning.ubuntu_seeded_install_media_remaster { seed_artifact_readback_holds } +import extdeps.provisioning.ubuntu_seeded_install_media { + NoCloudSeedArtifact, UbuntuSeededInstallMediaBuildInput, seeded_install_media_build_key, + seeded_install_media_seed_artifact_admission, SeedArtifactsAdmitted, SeedArtifactsRefused, + SeedArtifactNameNotABasename, SeedArtifactNameReserved, SeedArtifactNameCollidesWithSeedFile, SeedArtifactNameDuplicated, +} +import gunbc.boot_medium { BootMedium, BootMediumRunnerHostUp, BootMediumVolumeLabel, BootMediumNoTerminal } +import gunbc.fleet_host_identity { operator_host_mtcollins1 } +import gunbc.managed_host_boot { AdmittedBootSubject, BootSubjectAdmitted, BootSubjectRefused, boot_subject_admission_for } +import gunbc.machine_intake_mtcollins1_census_image { mtcollins1_census_machine, mtcollins1_census_console_device, mtcollins1_census_console_arg } +import gunbc.machine_intake_mtcollins1_boot_run { + MtCollins1BootTerminalVerdict, TerminalAccepted, TerminalRunnerHostAccepted, TerminalRefused, TerminalPending, + WatchRunnerHostAccepted, watch_tick, SolChannelLive, + TerminalWatch, actuation_watch_of, actuation_after_watch, actuation_with_outcome, actuation_not_powered_on, mtcollins1_boot_attempt_of, BmcAccessNotReached, + mtcollins1_boot_terminal_verdict, mtcollins1_boot_terminal_cause_text, mtcollins1_boot_medium_refusal, +} +import gunbc.runner.runner_host_medium { + RunnerHostMediumHost, RunnerHostMediumImage, RunnerHostMediumImageDerived, RunnerHostMediumRunnerUnpublished, RunnerHostMediumUserDataRefused, + RunnerHostUp, RunnerHostUpReadback, RunnerHostUpPending, RunnerHostUpRefused, RunnerHostUpObserved, + RunnerHostSectionFailed, RunnerHostTarballDigestDiffers, RunnerHostMachineNotAarch64, RunnerHostMachineUnrecognized, RunnerHostSectionAbsent, RunnerHostLabelAbsent, + runner_host_medium_image, runner_host_boot_medium, runner_host_up_readback, runner_host_medium_volume_id, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data mtcollins1_runner_host: RunnerHostMediumHost = RunnerHostMediumHost { + machine: mtcollins1_census_machine, + console_device: mtcollins1_census_console_device, + console_kernel_arg: mtcollins1_census_console_arg, +} + +data supplied_digest: NonEmptyStr = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + +fn runner_subject() -> AdmittedBootSubject? { + subject_with(medium: runner_host_boot_medium(output_digest: supplied_digest)) +} + +fn subject_with(medium: BootMedium) -> AdmittedBootSubject? { + match boot_subject_admission_for(host: operator_host_mtcollins1, medium: medium, execution_revision: "0123456789abcdef0123456789abcdef01234567" as NonEmptyStr) { + BootSubjectAdmitted { subject: s } => Present { value: s } + BootSubjectRefused { host: _, cause: _ } => none + } +} + +fn section(name: String, argv: String, body: String, exit_code: String) -> String { + concat("----SECTION ", name, "----\n----ARGV ", name, ": ", argv, "\n", body, "----EXIT ", name, ": ", exit_code, "\n") +} + +data begin_line: String = "=====GUNBC-HOST-CAPTURE mtcollins1 boot-id=aaaaaaaa-0000-4000-8000-000000000001 begin 2026-10-04T00:00:00Z=====\n" +data end_line: String = "=====GUNBC-HOST-CAPTURE mtcollins1 boot-id=aaaaaaaa-0000-4000-8000-000000000001 end 2026-10-04T00:01:00Z=====\n" + +data meminfo_body: String = "MemTotal: 134217728 kB\nMemFree: 130000000 kB\nMemAvailable: 131000000 kB\nBuffers: 1024 kB\nCached: 2000000 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\nHugePages_Total: 0\n" + +fn sections(install_exit: String, machine: String) -> String { + sections_with_digest(install_exit: install_exit, machine: machine, tarball_digest: actions_runner_release_2_337_0.digest_aarch64.hex as String) +} + +fn sections_with_digest(install_exit: String, machine: String, tarball_digest: String) -> String { + concat( + section(name: "boot-media", argv: "ls -1 /dev/disk/by-label", body: "GUNBC_RUNNER_HOST_2404_3\nEFI\n", exit_code: "0"), + section(name: "runner-tarball-digest", argv: "sha256sum -- (the tarball on the medium)", body: concat(tarball_digest, " /cdrom/gunbc-runner/actions-runner-linux-arm64-2.337.0.tar.gz\n"), exit_code: "0"), + section(name: "runner-clear", argv: "rm -rf /opt/actions-runner", body: "", exit_code: "0"), + section(name: "runner-dir", argv: "mkdir -p /opt/actions-runner", body: "", exit_code: "0"), + section(name: "runner-extract", argv: "(the rendered extract)", body: "", exit_code: install_exit), + section(name: "runner-listener", argv: "test -x /opt/actions-runner/bin/Runner.Listener", body: "", exit_code: "0"), + section(name: "systemd-manager", argv: "test -d /run/systemd/system", body: "", exit_code: "0"), + section(name: "meminfo", argv: "cat /proc/meminfo", body: meminfo_body, exit_code: "0"), + section(name: "nproc", argv: "nproc", body: "80\n", exit_code: "0"), + section(name: "uname-machine", argv: "uname -m", body: concat(machine, "\n"), exit_code: "0"), + ) +} + +fn closed(install_exit: String, machine: String) -> String { + concat(begin_line, sections(install_exit: install_exit, machine: machine), end_line) +} + +fn readback(capture: String) -> RunnerHostUpReadback? { + match runner_subject() { + Absent => none + Present { value: s } => Present { value: runner_host_up_readback(capture: capture, subject: s) } + } +} + +// THE DERIVATION INHABITS THE SEEDED-MEDIA BUILD INPUT WITH THE RUNNER PAYLOAD: the arm64 artifact of +// the host-unit slot's release as a seed ARTIFACT under its cited arm64 digest (the boot fetches +// nothing), a seed program that verifies it off the medium and RETURNS +// (no power-off, nothing written to a disk), and the medium's own volume id. +test fn the_runner_image_derives_with_the_slot_runner_and_a_program_that_never_halts() -> Bool { + match runner_host_medium_image(host: mtcollins1_runner_host) { + RunnerHostMediumImageDerived { input: input, runner: runner } => { + let program = fold(input.seed_files, init: "", f: (acc, f) => concat(acc, f.content as String)) + (runner.binary_release.version as String) == (actions_runner_release_2_337_0.version as String) + && (input.volume_id as String) == (runner_host_medium_volume_id as String) + && string_contains(s: program, pattern: "sha256sum") + && !string_contains(s: program, pattern: "set -e") + && !string_contains(s: program, pattern: "curl") + && count(input.seed_artifacts) == 1 + && all(input.seed_artifacts, a => (a.digest.hex as String) == (actions_runner_release_2_337_0.digest_aarch64.hex as String)) + && string_contains(s: program, pattern: "Runner.Listener") + && !string_contains(s: program, pattern: "poweroff") + && string_contains(s: input.grub_kernel_cmdline as String, pattern: mtcollins1_census_console_arg as String) + } + RunnerHostMediumRunnerUnpublished { binary_release: _ } => false + RunnerHostMediumUserDataRefused { reason: _ } => false + } +} + +// THE MEDIUM IS ADMITTED BY THE REAL BOOT SUBJECT and carries the runner-host-up terminal, so the +// boot run's pre-controller medium gate does not refuse it as a terminal-less medium. +test fn the_runner_medium_is_admitted_and_names_the_runner_host_up_terminal() -> Bool { + let medium = runner_host_boot_medium(output_digest: supplied_digest) + (match medium.terminal { + BootMediumRunnerHostUp { label: l } => (l as String) == (runner_host_medium_volume_id as String) + BootMediumVolumeLabel { label: _ } => false + BootMediumNoTerminal { reason: _ } => false + }) + && (match mtcollins1_boot_medium_refusal(medium: medium) { Absent => true Present { value: _ } => false }) + && (match runner_subject() { Present { value: _ } => true Absent => false }) +} + +// A CLOSED ENVELOPE WITH EVERY SECTION HELD IS THE RUNNER HOST UP, carrying the idle readings. +test fn a_closed_envelope_with_every_section_held_reads_the_runner_host_up() -> Bool { + match readback(capture: closed(install_exit: "0", machine: "aarch64")) { + Present { value: RunnerHostUpObserved { up: up } } => + kibibyte_count(k: up.mem_total) == 134217728 && kibibyte_count(k: up.mem_available) == 131000000 && hardware_thread_count_value(t: up.threads) == 80 && (match up.machine { Aarch64 => true _ => false }) + _ => false + } +} + +// THE REDS: a runner install whose digest check failed, and a host that is not aarch64, each refuse +// and BOTH are named on one boot; an open envelope whose install already failed refuses before END; +// a closed envelope missing a section refuses rather than accepting on what it has. +test fn a_failed_install_and_a_foreign_machine_both_refuse() -> Bool { + match readback(capture: closed(install_exit: "1", machine: "x86_64")) { + Present { value: RunnerHostUpRefused { refusals: rs } } => + any(rs, r => match r { RunnerHostSectionFailed { name: n, exit_code: c } => (n as String) == "runner-extract" && c == "1" _ => false }) + && any(rs, r => match r { RunnerHostMachineNotAarch64 { observed: o } => (match o { X86_64 => true _ => false }) _ => false }) + _ => false + } +} + +test fn an_open_envelope_refuses_early_on_a_failed_install_and_otherwise_waits() -> Bool { + let failed_open = concat(begin_line, section(name: "runner-extract", argv: "(the rendered extract)", body: "tar: Unexpected EOF in archive\n", exit_code: "2")) + let held_open = concat(begin_line, section(name: "runner-extract", argv: "(the rendered extract)", body: "", exit_code: "0")) + (match readback(capture: failed_open) { Present { value: RunnerHostUpRefused { refusals: _ } } => true _ => false }) + && (match readback(capture: held_open) { Present { value: RunnerHostUpPending { reason: _ } } => true _ => false }) +} + +test fn a_closed_envelope_missing_the_meminfo_section_refuses() -> Bool { + let missing = concat(begin_line, section(name: "nproc", argv: "nproc", body: "80\n", exit_code: "0"), end_line) + match readback(capture: missing) { + Present { value: RunnerHostUpRefused { refusals: rs } } => + any(rs, r => match r { RunnerHostSectionAbsent { name: n } => (n as String) == "meminfo" _ => false }) + _ => false + } +} + +// THE ROUTE: the boot run's terminal verdict, given a subject admitted with the runner medium, reaches +// runner_host_up_readback -- it accepts the held envelope and refuses the foreign machine with the +// runner-host cause, which the census predicate (it reads no uname-machine section) could not produce. +// And the accepted reading survives the delegation and the watch tick WHOLE: the consumer reads the +// exact idle MemTotal, MemAvailable, thread count and machine from the one accepted boot reading. +test fn the_boot_run_terminal_verdict_delegates_and_carries_the_whole_reading() -> Bool { + match runner_subject() { + Absent => false + Present { value: s } => { + let verdict = mtcollins1_boot_terminal_verdict(capture: closed(install_exit: "0", machine: "aarch64"), subject: s) + (match verdict { + TerminalRunnerHostAccepted { up: up } => kibibyte_count(k: up.mem_total) == 134217728 && kibibyte_count(k: up.mem_available) == 131000000 && hardware_thread_count_value(t: up.threads) == 80 && (match up.machine { Aarch64 => true _ => false }) + _ => false + }) + && (match watch_tick(verdict: verdict, channel: SolChannelLive) { + WatchRunnerHostAccepted { up: up } => kibibyte_count(k: up.mem_total) == 134217728 && kibibyte_count(k: up.mem_available) == 131000000 && hardware_thread_count_value(t: up.threads) == 80 && (match up.machine { Aarch64 => true _ => false }) + _ => false + }) + && (match mtcollins1_boot_terminal_verdict(capture: closed(install_exit: "0", machine: "x86_64"), subject: s) { + TerminalRefused { causes: cs } => any(cs, c => string_contains(s: mtcollins1_boot_terminal_cause_text(cause: c), pattern: "uname -m read back X86_64")) + _ => false + }) + } + } +} + +// THE LABEL IS THE ADMITTED SUBJECT'S, NOT A MODULE CONSTANT: a subject admitted with the runner +// terminal but label OTHER is refused by a capture of this image (whose host lists +// GUNBC_RUNNER_HOST_2404_3), while the exact-label subject accepts the same capture. +test fn a_subject_admitted_with_another_label_is_refused_by_this_images_capture() -> Bool { + let base = runner_host_boot_medium(output_digest: supplied_digest) + let other = BootMedium { image_name: base.image_name, digest: base.digest, export: base.export, terminal: BootMediumRunnerHostUp { label: "OTHER" } } + let capture = closed(install_exit: "0", machine: "aarch64") + (match subject_with(medium: other) { + Present { value: s } => match runner_host_up_readback(capture: capture, subject: s) { + RunnerHostUpRefused { refusals: rs } => any(rs, r => match r { RunnerHostLabelAbsent { expected: e, observed: _ } => (e as String) == "OTHER" _ => false }) + _ => false + } + Absent => false + }) + && (match readback(capture: capture) { Present { value: RunnerHostUpObserved { up: _ } } => true _ => false }) +} + +fn with_artifacts(input: UbuntuSeededInstallMediaBuildInput, artifacts: List) -> UbuntuSeededInstallMediaBuildInput { + UbuntuSeededInstallMediaBuildInput { + stock_artifact: input.stock_artifact, + nocloud_dir_on_iso: input.nocloud_dir_on_iso, + user_data_body: input.user_data_body, + seed_files: input.seed_files, + seed_artifacts: artifacts, + grub_kernel_cmdline: input.grub_kernel_cmdline, + volume_id: input.volume_id, + pinned_dates: input.pinned_dates, + image_stem: input.image_stem, + builder_revision: input.builder_revision, + } +} + +// THE ARTIFACT'S DIGEST IS IN THE BUILD KEY AND ITS URL IS NOT: a different tarball is a different +// image, the same bytes from another URL are the same image. (An input with no artifact keeps its +// pre-artifact key: the census build key claim in test.claim.mtcollins1_census_medium_readback_witness +// pins c001ac127d718e72.) +test fn the_seed_artifact_digest_keys_the_build_and_its_url_does_not() -> Bool { + match runner_host_medium_image(host: mtcollins1_runner_host) { + RunnerHostMediumImageDerived { input: input, runner: _ } => { + let moved = map(input.seed_artifacts, a => NoCloudSeedArtifact { name: a.name, source_url: "https://mirror.invalid/runner.tar.gz", digest: a.digest }) + let other = map(input.seed_artifacts, a => NoCloudSeedArtifact { name: a.name, source_url: a.source_url, digest: actions_runner_release_2_337_0.digest_x86_64 }) + let key = seeded_install_media_build_key(input: input) as String + key == (seeded_install_media_build_key(input: with_artifacts(input: input, artifacts: moved)) as String) + && key != (seeded_install_media_build_key(input: with_artifacts(input: input, artifacts: other)) as String) + && key != (seeded_install_media_build_key(input: with_artifacts(input: input, artifacts: [])) as String) + } + RunnerHostMediumRunnerUnpublished { binary_release: _ } => false + RunnerHostMediumUserDataRefused { reason: _ } => false + } +} + +// THE ARTIFACT NAME IS ADMITTED BEFORE ANY FETCH (the remaster's fetch takes only an +// AdmittedSeedArtifact; its real-execution control is test.claim.srv3_seeded_install_media_real_execution). +// The runner image's own tarball is admitted; an escaping path, the image's own program name, a +// NoCloud datasource name and a duplicate each refuse with their own cause. +fn admission_refuses_with(input: UbuntuSeededInstallMediaBuildInput, extra: NoCloudSeedArtifact, cause: String) -> Bool { + match seeded_install_media_seed_artifact_admission(input: with_artifacts(input: input, artifacts: append(input.seed_artifacts, items: [extra]))) { + SeedArtifactsAdmitted { artifacts: _ } => false + SeedArtifactsRefused { refusals: rs } => any(rs, r => match r { + SeedArtifactNameNotABasename { name: _ } => cause == "basename" + SeedArtifactNameReserved { name: _ } => cause == "reserved" + SeedArtifactNameCollidesWithSeedFile { name: _ } => cause == "collides" + SeedArtifactNameDuplicated { name: _ } => cause == "duplicated" + }) + } +} + +fn artifact_named(n: String) -> NoCloudSeedArtifact { + NoCloudSeedArtifact { name: n as NonEmptyStr, source_url: "https://example.invalid/x", digest: sha256_digest(hex: supplied_digest) } +} + +test fn seed_artifact_names_are_admitted_only_as_fresh_basenames() -> Bool { + match runner_host_medium_image(host: mtcollins1_runner_host) { + RunnerHostMediumImageDerived { input: input, runner: _ } => { + let own = map(input.seed_artifacts, a => a.name as String) + (match seeded_install_media_seed_artifact_admission(input: input) { + SeedArtifactsAdmitted { artifacts: admitted } => map(admitted, a => a.basename.value as String) == own + SeedArtifactsRefused { refusals: _ } => false + }) + && admission_refuses_with(input: input, extra: artifact_named(n: "../../boot/grub/grub.cfg"), cause: "basename") + && admission_refuses_with(input: input, extra: artifact_named(n: ".."), cause: "basename") + && admission_refuses_with(input: input, extra: artifact_named(n: "runner-host-up.sh"), cause: "collides") + && admission_refuses_with(input: input, extra: artifact_named(n: "user-data"), cause: "reserved") + && admission_refuses_with(input: input, extra: artifact_named(n: "meta-data"), cause: "reserved") + && admission_refuses_with(input: input, extra: artifact_named(n: fold(own, init: "", f: (acc, n) => n)), cause: "duplicated") + } + RunnerHostMediumRunnerUnpublished { binary_release: _ } => false + RunnerHostMediumUserDataRefused { reason: _ } => false + } +} + +fn reading_is_the_capture(up: RunnerHostUp) -> Bool { + kibibyte_count(k: up.mem_total) == 134217728 && kibibyte_count(k: up.mem_available) == 131000000 && hardware_thread_count_value(t: up.threads) == 80 && (match up.machine { Aarch64 => true _ => false }) +} + +fn through(subject: AdmittedBootSubject, reading: RunnerHostUp?) -> RunnerHostUp? { + let watch = TerminalWatch { outcome: ExitSuccess, census_ended: false, smpro_census: SmproNotTaken { reason: "supplied" }, screen_reported: [], runner_host: reading } + let aw = actuation_watch_of(watch: watch, selection: subject.selection, at_power_on: SmproNotTaken { reason: "supplied" }, smpro_read: UptimeRefused { detail: "supplied" }, terminal_read: UptimeRefused { detail: "supplied" }) + let base = actuation_not_powered_on(outcome: aw.outcome, reason: "supplied") + let act = actuation_after_watch(observed: aw, media: base.media, handoff_media: base.handoff_media, end_media: base.end_media, override_before: base.override_before, override_after: base.override_after, power_after: base.power_after, outcome: aw.outcome, screen: base.screen, timing: base.timing) + let held = actuation_with_outcome(act: act, outcome: ExitSuccess) + mtcollins1_boot_attempt_of(act: held, bmc: BmcAccessNotReached { reason: "supplied" }, configuration: attempt_configuration_not_recorded(host: "mtcollins1", attempt: "supplied", reason: "supplied")).runner_host +} + +// THE READING REACHES THE COMPLETED ATTEMPT, through every real post-watch projection the production +// path calls -- actuation_watch_of, actuation_after_watch (mtcollins1_boot_actuate), actuation_with_outcome +// (mtcollins1_boot_actuate_held) and mtcollins1_boot_attempt_of -- so dropping the reading in any of them +// turns this red, and +// GuestIdleMeminfoAndNproc reads all four values off MtCollins1BootAttempt. A watch that accepted no +// runner host carries none all the way through. +test fn the_accepted_reading_reaches_the_boot_attempt_result() -> Bool { + match runner_subject() { + Absent => false + Present { value: s } => + match watch_tick(verdict: mtcollins1_boot_terminal_verdict(capture: closed(install_exit: "0", machine: "aarch64"), subject: s), channel: SolChannelLive) { + WatchRunnerHostAccepted { up: up } => { + (match through(subject: s, reading: Present { value: up }) { Present { value: r } => reading_is_the_capture(up: r) Absent => false }) + && (match through(subject: s, reading: none) { Absent => true Present { value: _ } => false }) + } + _ => false + } + } +} + +// THE TARBALL'S DIGEST IS EVIDENCE THE PREDICATE JUDGES, NOT A SHELL GATE: a capture whose +// runner-tarball-digest section reads another sha256 (here the x86_64 tarball's) refuses with the typed +// cause naming both digests, even though every install section exited 0. +test fn a_tarball_that_reads_back_another_digest_refuses_the_terminal() -> Bool { + let wrong = actions_runner_release_2_337_0.digest_x86_64.hex as String + match readback(capture: concat(begin_line, sections_with_digest(install_exit: "0", machine: "aarch64", tarball_digest: wrong), end_line)) { + Present { value: RunnerHostUpRefused { refusals: rs } } => + any(rs, r => match r { RunnerHostTarballDigestDiffers { expected: e, observed: o } => o == wrong && (e as String) == (actions_runner_release_2_337_0.digest_aarch64.hex as String) _ => false }) + _ => false + } +} + +// THE MACHINE IS PARSED AT THE uname BOUNDARY: a spelling the Linux projection does not cite refuses +// as unrecognized rather than being compared as a string. +test fn an_unrecognized_machine_spelling_refuses_as_unrecognized() -> Bool { + match readback(capture: closed(install_exit: "0", machine: "arm64")) { + Present { value: RunnerHostUpRefused { refusals: rs } } => + any(rs, r => match r { RunnerHostMachineUnrecognized { observed: o } => o == "arm64" _ => false }) + _ => false + } +} + +// THE PIN CARRIES ITS ALGORITHM: the same hex under Sha512 is another artifact (another build key) and +// can never satisfy the sha256sum readback, while a Sha256 pin holds only on its own hex. +test fn a_seed_artifact_pin_is_judged_with_its_algorithm_not_by_hex_alone() -> Bool { + match runner_host_medium_image(host: mtcollins1_runner_host) { + RunnerHostMediumImageDerived { input: input, runner: _ } => { + let hex = actions_runner_release_2_337_0.digest_aarch64.hex + let as_512 = map(input.seed_artifacts, a => NoCloudSeedArtifact { name: a.name, source_url: a.source_url, digest: sha512_digest(hex: hex) }) + (seeded_install_media_build_key(input: input) as String) != (seeded_install_media_build_key(input: with_artifacts(input: input, artifacts: as_512)) as String) + && seed_artifact_readback_holds(read_back_sha256_hex: hex as String, pin: sha256_digest(hex: hex)) + && !seed_artifact_readback_holds(read_back_sha256_hex: hex as String, pin: sha512_digest(hex: hex)) + && !seed_artifact_readback_holds(read_back_sha256_hex: supplied_digest as String, pin: sha256_digest(hex: hex)) + } + RunnerHostMediumRunnerUnpublished { binary_release: _ } => false + RunnerHostMediumUserDataRefused { reason: _ } => false + } +} diff --git a/dag/test/claim/runner/runner_microvm_shakedown_workflow_witness_test.dag b/dag/test/claim/runner/runner_microvm_shakedown_workflow_witness_test.dag index 2901a28ab83..a2837d3bea7 100644 --- a/dag/test/claim/runner/runner_microvm_shakedown_workflow_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_shakedown_workflow_witness_test.dag @@ -20,8 +20,14 @@ test fn the_shakedown_job_asks_for_the_provenance_label_and_the_attempt_alone() match microvm_shakedown_job(script: "").runner { SelfHosted { labels: ls } => count(ls) == 2 - && ls[0] == self_hosted_provenance_label - && ls[1] == microvm_shakedown_attempt_expression() + && match ls[0] { + Present { value: l } => l == self_hosted_provenance_label + Absent => false + } + && match ls[1] { + Present { value: l } => l == microvm_shakedown_attempt_expression() + Absent => false + } _ => false } } diff --git a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag index 58a8cd86699..46cc57310e9 100644 --- a/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_slot_controller_witness_test.dag @@ -1,5 +1,11 @@ module test.claim.runner.runner_microvm_slot_controller_witness_test +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner_jit_mint { MicroVmSlotBinding } +import gunbc.runner_microvm_attempt { bind_owned_attempt } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import test.claim.github_app_registry { app_credential, observed_grant, runner_grant, gunbai_org_ref, restricted_group_1 } +import extdeps.github.actions_jit_runner { jit_config_mint_outcome } import v2.std.algebra { any } import std.algebra { list_append } import std.types { String, NonEmptyStr, Bool, Int, List } @@ -297,8 +303,22 @@ fn delivered_mint() -> Bool { Absent => false Present { value: a } => match jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: "") { - JitConfigMinted { config: c, runner_id: id } => - minted_registration(mint: JitMintReceived { delivery: JitCredentialBoundToAttempt { slot: MicroVmJitSlot { attempt: a }, credential: c, runner_id: id } }) == Present { value: 4242 } + JitConfigMinted { config: _, runner_id: _ } => + match dispatch_jit_mint( + binding: MicroVmSlotBinding { binding: bind_owned_attempt(attempt: a, owned: true) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: restricted_group_1(), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => + minted_registration(mint: JitMintReceived { delivery: receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: d }, + outcome: jit_config_mint_outcome(status: 201, encoded: "ZmFrZS1qaXQtY29uZmlnLXRoYXQtaXMtbG9uZy1lbm91Z2gtdG8tcGFzcy10aGUtZmxvb3ItY2hlY2stZm9yLXRoZS13aXRuZXNz", runner_id: 4242, error_body: ""), + ) }) == Present { value: 4242 } + JitMintDispatchRefused { refusal: _ } => false + } _ => false } } diff --git a/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag new file mode 100644 index 00000000000..a6994244df9 --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_delivery_rewrap_witness_test.dag @@ -0,0 +1,77 @@ +module test.claim.runner_qualification_delivery_rewrap_witness + +import std.types { String, Bool, Int } +import extdeps.filesystem.filesystem_io +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, CompileDiagnosticCensusRow, CensusObserved, CensusNotRunnable, +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE REWRAP ROUTE, ENROLLED AS AN EXECUTED RED (DESIGN 4b: a state unrepresentable in the accepted +// corpus is still representable as source handed to the compiler). test.probe.qualification_delivery_ +// rewrap_probe relabels delivery B's credential and runner id as dispatch A's; it must refuse at +// BoundJitCredential, the sealed carrier receive_jit_mint alone constructs, so no +// QualificationDispatchSubject can ever be built over a rewrapped delivery. A control source that only +// NAMES the same types must refuse at none of them, so the refusal is attributable to the forgery. + +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +fn blocking(c: CompileDiagnosticCensus, wanted: String, subject: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.subject_name == subject && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +data control_source: String = "module probe_qualification_delivery_control\nimport std.types { Bool }\nimport gunbc.runner.runner_jit_perform { BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, jit_credential_delivered, receive_jit_mint }\nimport gunbc.github_effect_perform { jit_mint_performance_of_generate }\nimport gunbc.runner.runner_qualification_dispatch { CollectedQualificationRun }\nfn delivered(d: JitCredentialDelivery) -> Bool { jit_credential_delivered(delivery: d) }\n" + +fn blocking_class(c: CompileDiagnosticCensus, wanted: String) -> Int { + match c { + CensusNotRunnable { cause: _ } => -1 + CensusObserved { rows: rows } => + rows + |> filter(r => (r.diagnostic_class as String) == wanted && r.blocking) + |> fold(init: 0, f: (acc, r) => acc + r.count) + } +} + +fn probe(name: String) -> CompileDiagnosticCensus { + census_of(source: filesystem_read(path: concat(concat("dag/test/probe/", name), ".dag")).content) +} + +// ONE CONTROL FOR EVERY RED BELOW: a source that IMPORTS each sealed name -- the carriers and the two +// admitted mints -- and calls none of them refuses at none of the walls, so each red is the call or +// the literal, never the import. +test fn a_source_that_only_names_the_carrier_forges_nothing() -> Bool { + let c = census_of(source: control_source) + blocking(c: c, wanted: "SoleConstructorViolation", subject: "BoundJitCredential") == 0 + && blocking_class(c: c, wanted: "SoleConstructorViolation") == 0 + && blocking_class(c: c, wanted: "ConstructorCallAdmissionRefused") == 0 +} + +// THE REWRAP WITH NO FORGED LITERAL (review 5408711178 item 1): delivery B's credential and runner id +// handed to receive_jit_mint under dispatch A. Each probe holds ONE route, so its refusal is that +// route's. +test fn a_rewrap_through_receive_jit_mint_refuses_at_its_admission() -> Bool { + blocking_class(c: probe(name: "qualification_receive_jit_mint_rewrap_probe"), wanted: "ConstructorCallAdmissionRefused") >= 1 +} + +test fn a_supplied_generate_answer_refuses_outside_the_perform_function() -> Bool { + blocking_class(c: probe(name: "qualification_generate_answer_rewrap_probe"), wanted: "ConstructorCallAdmissionRefused") >= 1 +} + +// item 2: collecting outside the token join is minting a collection the join never admitted. +test fn a_collection_minted_outside_the_token_join_refuses_at_the_sealed_carrier() -> Bool { + blocking(c: probe(name: "qualification_collection_relabel_probe"), wanted: "SoleConstructorViolation", subject: "CollectedQualificationRun") >= 1 +} + +test fn a_rewrapped_delivery_refuses_at_the_sealed_carrier() -> Bool { + blocking(c: census_of(source: filesystem_read(path: "dag/test/probe/qualification_delivery_rewrap_probe.dag").content), wanted: "SoleConstructorViolation", subject: "BoundJitCredential") >= 1 +} diff --git a/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag new file mode 100644 index 00000000000..517532787b0 --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_dispatch_witness_test.dag @@ -0,0 +1,892 @@ +module test.claim.runner_qualification_dispatch_witness + +import std.types { Bool, Bytes, CommitSha, Int, List, NonEmptyStr, String } +import std.bytes { utf8_encode_bytes } +import std.algebra { Cons, Empty } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import product.placement_supply { HostIdentity } +import extdeps.transports.rest { + RestResult, RestAnswered, RestRefused, RestRefusal, RestTransportRefused, RestStatusRefused, RestBodyUndecodable, + RestExchangeStatusRefused, RestExchangeUnreached, RestExchangeUndecodable, +} +import extdeps.github.workflows { WorkflowDispatchReceipt } +import extdeps.github.git_database { GitRefWire, GitRefObjectWire } +import extdeps.github.actions { Workflow, Job, WorkflowDispatch, RunsOnExpression, SelfHosted, RunnerSpec, Step, RunStep, UsesStep } +import extdeps.github.effect { DispatchWorkflow, GitHubOrganizationRef } +import extdeps.github.workflow_runs { + WorkflowRun, WorkflowJobRun, WorkflowJobRunList, Completed, InProgress, Success, Failure, +} +import gunbc.managed_host { managed_hosts, managed_host_binding } +import gunbc.runner.runner_host_unit_slot { host_unit_slot_for } +import gunbc.runner_jit_mint { HostUnitSlotBinding } +import gunbc.runner.runner_jit_admission { jit_admission_axis_standing, AttemptBindingAxis } +import gunbc.runner.runner_jit_perform { JitMintDispatchAuthorized, JitMintDispatchRefused, dispatch_jit_mint, receive_jit_mint } +import gunbc.runner_throughput_qualification_route { ephemeral_slot_labels, ephemeral_slot_unit, qualification_instruments, FloorPhaseRows, FloorCgroupRows } +import test.claim.github_app_registry { + app_credential, other_app_credential, other_installation_credential, a_pull_request_job_credential, + bot_installation_id, other_installation_id, observed_grant, runner_grant, gunbai_org_ref, group_restricted_to, minted_outcome, +} +import gunbc.auth.github_apps { gunbai_bot_declared, gunbai_ci_declared } +import gunbc.runner_group_restriction_observation { MicrovmRunnerGroupStanding, observe_runner_group_restriction } +import gunbc.fleet.org_actions_inspection { runner_groups_of_rest_read } +import extdeps.github.org_actions { OrgRunnerGroupWire, OrgRunnerGroupListWire } +import gunbc.runner.runner_qualification_ref_pin { + qualification_selected_workflow, qualification_workflow_path, qualification_branch_name, QualificationBranchNamed, QualificationBranchNameRefused, + qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, +} +import gunbc.runner.runner_qualification_dispatch { + qualification_attempt_dispatch_input, qualification_revision_dispatch_input, qualification_dispatch_inputs, qualification_dispatch_effect, + qualification_floor_revision_guard_step, SubjectFloorRevisionGuardNotFirst, SubjectFloorStepRunsPastTheGuard, + QualificationSubjectAdmitted, QualificationSubjectRefused, + SubjectSlotForAnotherWorkflow, SubjectAttemptInputUndeclared, SubjectFloorJobAbsent, SubjectFloorRunsOnNotTheAttemptInput, + SubjectFloorNameNotUnique, SubjectDeliveryForAnotherDispatch, + dispatch_authority_refusal, DispatchAuthorityForAnotherInstallation, DispatchAuthorityNotAnInstallation, + qualification_dispatch_subject, qualification_dispatch_subject_over, + qualification_slot_refusal, qualification_floor_admission, QualificationFloorAdmitted, QualificationFloorRefused, + qualification_run_expectation, QualificationRunExpectation, + qualification_dispatch_standing, QualificationDispatchStanding, + DispatchStandingSucceeded, DispatchStandingRefused, DispatchStandingCommitAmbiguous, + hold_covers_slot_host, token_names_registration, + plan_qualification_collection, planned_collection_reads, CollectionRefusedBeforeAnyRead, + ReadQualificationRun, ReadQualificationAttemptJobs, RunCollectedUnderAnotherInstallation, + decide_qualification_run, QualificationRunVerdict, RunVerdictCollected, RunVerdictRefused, + RunReadUnreadable, RunSubjectMismatch, RunAdvancedPastDispatchedAttempt, RunRevisionNotPinned, RunOnAnotherBranch, RunNotCompleted, + RunCompletedWithoutConclusion, RunJobsUnreadable, RunJobsTruncated, RunFloorJobNotUnique, + RunNeverReachedAttemptRunner, RunRunnerNameDisagrees, RunRegisteredRunnerRanAnotherJob, RunFloorJobNotStarted, + AttemptJobLog, AttemptJobLogRead, AttemptJobLogUnreadable, + DesignatedJobLog, DesignatedJobLogText, DesignatedJobLogRefused, designated_job_log, + JobLogReadRefusal, JobLogOfAnotherJob, JobLogUnreadable, JobLogEmpty, + JobLogInstrumentDecode, JobLogInstrumentsDecoded, JobLogInstrumentsUndecodable, decode_job_log_instruments, + QualificationLogReadings, InstrumentReadRefusal, FloorPhaseRowGarbledInLog, FloorCgroupRowGarbledInLog, + FloorPhaseRowsAbsent, SlotCgroupLevelAbsent, + qualification_instrument_read_from_job_log, qualification_instrument_extraction_frontier_rows, + ClaimCostArtifactRead, ClaimCostArtifactAbsent, ClaimCostArtifactExpired, ClaimCostArtifactListTruncated, + ClaimCostArchiveUnreadable, ClaimCostArchiveNotInflatable, + conclude_claim_cost_listing, conclude_claim_cost_download, conclude_claim_cost_attempt, + collect_read_plan, collect_read_plan_reads, CollectReadsRefused, CollectReadsPlanned, + ClaimCostArtifactAmbiguous, ClaimCostRunAdvancedPastAttempt, ClaimCostRunUnreadable, +} +import gunbc.required_ci_phase_roster { FloorPhaseTimed, FloorPhaseUntimed } +import extdeps.linux.cgroup_v2_memory { CgroupMemoryLimited } +import std.measure { byte_size_count, millisecond_count } +import extdeps.github.actions_artifacts { ActionsArtifact, ActionsArtifactList } +import test.fixture.recorded_required_floor_log_excerpt { + recorded_required_floor_log_excerpt, recorded_floor_log_slot_unit, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data attempt: NonEmptyStr = "q-1" +data pin_branch: NonEmptyStr = "qualification/q-1" +data revision: CommitSha = "1111111111111111111111111111111111111111" +data floor_job_id: NonEmptyStr = "qualification-floor" +data registered_runner: NonEmptyStr = "gunbc-qualification-fixture-q-1.service" +data registered_runner_id: Int = 4242 + +// DESIGN section 3, A WITNESS DISCRIMINATES AT ONE INTERFACE. Every claim but one supplies its +// inputs as values at the interface it judges: the pure scope checks (qualification_slot_refusal, +// qualification_floor_admission), the dispatch standing, the hold comparison, and the run decision +// over a supplied QualificationRunExpectation. The pairing obligation is discharged by ONE claim that +// runs the real path: the route's JIT mint over the real managed-host roster, the sealed subject +// mint over a supplied floor workflow, and the expectation derived from that subject. + +// A group standing for the dedicated name under another id, through the same sealed observer. +fn group_with_id(id: Int, workflow: NonEmptyStr) -> MicrovmRunnerGroupStanding { + observe_runner_group_restriction( + name: "runner-qualification", + selected_workflow: workflow, + listed: runner_groups_of_rest_read(outcome: RestAnswered { answer: OrgRunnerGroupListWire { + total_count: 1, + runner_groups: [OrgRunnerGroupWire { + id: id, + name: "runner-qualification", + visibility: "selected", + allows_public_repositories: false, + restricted_to_workflows: true, + selected_workflows: [workflow as String], + workflow_restrictions_read_only: false, + }], + } }), + ) +} + +fn floor_job(runs_on: RunnerSpec) -> Job { + floor_job_with_steps(runs_on: runs_on, steps: [qualification_floor_revision_guard_step(), floor_work_step(if_condition: none)]) +} + +fn floor_work_step(if_condition: String?) -> Step { + RunStep { + name: Present { value: "run the floor" }, id: none, run: "claim_executor --required-ci", + shell: none, env: none, working_directory: none, if_condition: if_condition, continue_on_error: none, timeout_minutes: none, + } +} + +fn floor_job_with_steps(runs_on: RunnerSpec, steps: List) -> Job { + Job { + id: floor_job_id as String, + name: Present { value: "qualification floor" }, + runner: runs_on, + environment: none, + steps: steps, + needs: [], + env: none, + outputs: none, + if_condition: none, + timeout_minutes: none, + continue_on_error: none, + concurrency: none, + permissions: none, + } +} + +data attempt_runs_on: RunnerSpec = RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}" } + +fn workflow_with(declares_input: Bool, runs_on: RunnerSpec) -> Workflow { + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: if declares_input { [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] } else { [qualification_revision_dispatch_input] } }], + concurrency: none, + jobs: [floor_job(runs_on: runs_on)], + env: none, + permissions: none, + } +} + +// ── THE ONE REAL-PATH CLAIM ───────────────────────────────────────────────────────────────────── +// The route's own JIT mint over the first real managed host, delivered through upstream's 201 fold; +// the sealed subject over a floor workflow carries the slot's attempt and host, the registration's +// minted name (ephemeral_slot_unit), and the floor job's name; the dispatch sends exactly the declared +// input with a label the slot registered; the effect names fleet-converge.yml in gunbc_repository at +// the pinned revision; the expectation the collection judges against is derived from that subject. +test fn the_real_mint_admits_the_attempt_through_the_sealed_subject() -> Bool { + match first(managed_hosts()) { + Present { value: h } => + match dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestAnswered { answer: GitRefWire { ref: "refs/heads/qualification/q-1", object: GitRefObjectWire { type: "commit", sha: revision as String } } }, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + { + let delivered = receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome) + (match qualification_dispatch_subject_over( + dispatch: d, delivery: delivered, + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, + ) { + QualificationSubjectAdmitted { subject: s } => { + let expected = qualification_run_expectation(subject: s) + s.attempt == attempt + && s.runner_id == registered_runner_id && expected.registered_runner_id == registered_runner_id + && token_names_registration(token_app: s.registration.app, token_installation: s.registration.installation, subject: s) + && !token_names_registration(token_app: gunbai_ci_declared, token_installation: s.registration.installation, subject: s) + && !token_names_registration(token_app: s.registration.app, token_installation: other_installation_id, subject: s) + && count(collect_read_plan_reads(plan: collect_read_plan(subject: s, token_app: gunbai_ci_declared, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77))) == 0 + && count(collect_read_plan_reads(plan: collect_read_plan(subject: s, token_app: s.registration.app, token_installation: other_installation_id, floor_job_id: 501, run_id: 77))) == 0 + && (match collect_read_plan(subject: s, token_app: gunbai_ci_declared, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77) { + CollectReadsRefused { token_app: _, token_installation: _ } => true + CollectReadsPlanned { job_id: _, run_id: _ } => false + }) + && (match collect_read_plan(subject: s, token_app: s.registration.app, token_installation: s.registration.installation, floor_job_id: 501, run_id: 77) { + CollectReadsPlanned { job_id: 501, run_id: 77 } => true + _ => false + }) + && count(planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: gunbai_ci_declared, token_installation: s.registration.installation))) == 0 + && count(planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: s.registration.app, token_installation: other_installation_id))) == 0 + && (match plan_qualification_collection(subject: s, run_id: 77, token_app: gunbai_ci_declared, token_installation: s.registration.installation) { + CollectionRefusedBeforeAnyRead { refusal: RunCollectedUnderAnotherInstallation { token_app: a, token_installation: _ } } => a == gunbai_ci_declared + _ => false + }) + && (match planned_collection_reads(plan: plan_qualification_collection(subject: s, run_id: 77, token_app: s.registration.app, token_installation: s.registration.installation)) { + Cons { head: ReadQualificationRun { run_id: 77 }, tail: Cons { head: ReadQualificationAttemptJobs { run_id: 77, attempt: 1 }, tail: Empty {} } } => true + _ => false + }) + && (s.host as String) == (h.host as String) + && (expected.registered_runner as String) == (ephemeral_slot_unit(host: h.host, attempt: attempt) as String) + && (expected.floor_job_name as String) == "qualification floor" + && (expected.revision as String) == (revision as String) + && count(map_keys(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision))) == 2 + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision), qualification_revision_dispatch_input.name) { + Present { value: v } => v == (revision as String) + Absent => false + }) + && (match map_get(qualification_dispatch_inputs(attempt: s.attempt, revision: s.pin.revision), qualification_attempt_dispatch_input.name) { + Present { value: v } => v == (attempt as String) && (ephemeral_slot_labels(host: h.host, attempt: attempt) |> any(l => l == v)) + Absent => false + }) + && (match qualification_dispatch_effect(subject: s) { + DispatchWorkflow { repository: r, workflow: w, subject: sha } => + r.full_name == "gunb-ai/gunbc" && (w.file_name as String) == "fleet-converge.yml" && (sha as String) == (revision as String) + _ => false + }) + } + QualificationSubjectRefused { refusal: _ } => false + }) + } + } + } + JitMintDispatchRefused { refusal: _ } => false + } + Absent => false + } +} + +// THE PRODUCTION MINT OVER TODAY'S GENERATED WORKFLOW REFUSES, AND THE CLAIM ASSERTS THE ROUTE: the +// real JIT mint, then qualification_dispatch_subject, which reads the real fleet-converge inputs +// declaration first and refuses SubjectAttemptInputUndeclared naming runner_attempt_label. When the +// boot leg lands the qualification mode, this turns red and is replaced by the admitted case. +test fn the_production_mint_over_todays_fleet_converge_workflow_refuses_at_the_attempt_input() -> Bool { + match first(managed_hosts()) { + Present { value: h } => + match dispatch_jit_mint( + binding: HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) }, + authority: app_credential, + control_plane: observed_grant(perms: runner_grant), + organization: gunbai_org_ref, + runner_group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis), + ) { + JitMintDispatchAuthorized { dispatch: d } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestAnswered { answer: GitRefWire { ref: "refs/heads/qualification/q-1", object: GitRefObjectWire { type: "commit", sha: revision as String } } }, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + + match qualification_dispatch_subject( + dispatch: d, + delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: d }, outcome: minted_outcome), + floor_job_id: floor_job_id, + pin: p, + ) { + QualificationSubjectRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "runner_attempt_label" + _ => false + } + } + } + JitMintDispatchRefused { refusal: _ } => false + } + Absent => false + } +} + +// ── THE RULING'S SCOPE, AT ITS PURE INTERFACES ────────────────────────────────────────────────── +// The subject mint above composes exactly these two checks; these claims supply their inputs. + +fn slot_refused_as_another_workflow(slot_workflow: NonEmptyStr) -> Bool { + match qualification_slot_refusal(slot_workflow: slot_workflow, branch: pin_branch) { + Present { value: SubjectSlotForAnotherWorkflow { slot_workflow: sw, qualification_workflow: _ } } => (sw as String) == (slot_workflow as String) + _ => false + } +} + +// RED: a slot for another workflow file, or for the qualification path on another branch, cannot +// become a subject; the qualification slot at this branch passes (the positive control). +test fn a_slot_for_another_workflow_or_branch_cannot_become_a_subject() -> Bool { + slot_refused_as_another_workflow(slot_workflow: "gunb-ai/gunbc/.github/workflows/witnesses.yml@refs/heads/main") + && slot_refused_as_another_workflow(slot_workflow: qualification_selected_workflow(branch: "feature")) + && (match qualification_slot_refusal(slot_workflow: qualification_selected_workflow(branch: pin_branch), branch: pin_branch) { Absent => true Present { value: _ } => false }) +} + +fn runs_on_refused(runs_on: RunnerSpec) -> Bool { + match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: runs_on), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorRunsOnNotTheAttemptInput { job_id: _, runs_on: _ } } => true + _ => false + } +} + +// RED: a floor runs-on that is a literal label, another input, the attempt input composed with +// anything, or a bare string refuses; so does an undeclared input or a missing floor job. Positive +// control: exactly the attempt input admits the floor job by its name. +test fn a_floor_runs_on_other_than_exactly_the_attempt_input_refuses() -> Bool { + runs_on_refused(runs_on: SelfHosted { labels: [attempt as String] }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "${{ inputs.runner }}" }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "${{ inputs.runner_attempt_label }}-x" }) + && runs_on_refused(runs_on: RunsOnExpression { expression: "gunbc-qualification" }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: false, runs_on: attempt_runs_on), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: _ } } => true + _ => false + }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: "another-job") { + QualificationFloorRefused { refusal: SubjectFloorJobAbsent { job_id: _ } } => true + _ => false + }) + && (match qualification_floor_admission(workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id) { + QualificationFloorAdmitted { floor_job_name: n } => (n as String) == "qualification floor" + QualificationFloorRefused { refusal: _ } => false + }) + && (match qualification_floor_admission(workflow: workflow_sharing_the_floor_name(), floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorNameNotUnique { name: _, jobs_with_name: k } } => k == 2 + _ => false + }) +} + +// A HELPER JOB WITH THE FLOOR'S DISPLAY NAME. The readback identifies the floor by the name GitHub +// reports, so two authored jobs sharing it would let the helper stand in for the floor; the subject +// mint refuses such a workflow before any dispatch exists. +fn workflow_sharing_the_floor_name() -> Workflow { + let helper = Job { + id: "helper", + name: Present { value: "qualification floor" }, + runner: SelfHosted { labels: ["self-hosted"] }, + environment: none, + steps: [], + needs: [], + env: none, + outputs: none, + if_condition: none, + timeout_minutes: none, + continue_on_error: none, + concurrency: none, + permissions: none, + } + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] }], + concurrency: none, + jobs: [floor_job(runs_on: attempt_runs_on), helper], + env: none, + permissions: none, + } +} + +// ── THE INSTALLATION THAT PERFORMS THE REQUEST IS THE ONE WHOSE PERMISSION IS ASKED ───────────── +// The registration and token are App gunbai-bot installation 999 (app_credential). RED: an authority +// for another App, or for another installation of the same App, refuses before any network; an +// Actions job credential names no installation and refuses. Positive control: the matching authority. +test fn the_dispatch_authority_must_name_the_tokens_installation() -> Bool { + (match dispatch_authority_refusal(authority: app_credential, app: gunbai_bot_declared, installation: bot_installation_id) { Absent => true Present { value: _ } => false }) + && (match dispatch_authority_refusal(authority: other_app_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityForAnotherInstallation { authority_app: _, authority_installation: _ } } => true + _ => false + }) + && (match dispatch_authority_refusal(authority: other_installation_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityForAnotherInstallation { authority_app: _, authority_installation: _ } } => true + _ => false + }) + && (match dispatch_authority_refusal(authority: a_pull_request_job_credential, app: gunbai_bot_declared, installation: bot_installation_id) { + Present { value: DispatchAuthorityNotAnInstallation } => true + _ => false + }) +} + +// A DELIVERY CARRIES THE AUTHORIZED DISPATCH THAT PRODUCED IT, SEALED, AND THE SUBJECT COMPARES THE +// COMPLETE REQUEST. Two authorized mints for the SAME host-unit slot -- under another organization, and +// under the same slot, App, installation, organization and runner name but another runner group -- +// produce deliveries that agree on slot, credential and runner id; the subject over dispatch A refuses +// each rather than claiming A's registration for a credential another mint produced. (Rewrapping one +// delivery's credential with A's dispatch is unconstructable: +// test.claim.runner_qualification_delivery_rewrap_witness.) +test fn a_delivery_from_another_dispatch_on_the_same_slot_refuses() -> Bool { + match first(managed_hosts()) { + Present { value: h } => { + let binding = HostUnitSlotBinding { standing: host_unit_slot_for(host: managed_host_binding(host: h), attempt: attempt, workflow: qualification_selected_workflow(branch: pin_branch)) } + let group = group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)) + match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: a } => match qualification_branch_name(attempt: attempt) { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: nm } => + match qualification_ref_pin_of( + name: nm, revision: revision, readback: RestAnswered { answer: GitRefWire { ref: "refs/heads/qualification/q-1", object: GitRefObjectWire { type: "commit", sha: revision as String } } }, + group: group_restricted_to(workflow: qualification_selected_workflow(branch: pin_branch)), + ) { + QualificationRefPinRefused { attempt: _, refusal: _ } => false + QualificationRefPinned { pin: p } => + + match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: GitHubOrganizationRef { login: "another-org" }, runner_group: group, attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: b } => + a.slot == b.slot + && (match qualification_dispatch_subject_over( + dispatch: a, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: b }, outcome: minted_outcome), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, + ) { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true + _ => false + }) + && (match dispatch_jit_mint(binding: binding, authority: app_credential, control_plane: observed_grant(perms: runner_grant), organization: gunbai_org_ref, runner_group: group_with_id(id: 4, workflow: qualification_selected_workflow(branch: pin_branch)), attempt_binding: jit_admission_axis_standing(axis: AttemptBindingAxis)) { + JitMintDispatchAuthorized { dispatch: c } => + a.slot == c.slot && a.app == c.app && a.installation == c.installation && a.organization == c.organization && a.request.name == c.request.name + && a.request.runner_group_id != c.request.runner_group_id + && (match qualification_dispatch_subject_over( + dispatch: a, delivery: receive_jit_mint(dispatch: JitMintDispatchAuthorized { dispatch: c }, outcome: minted_outcome), + workflow: workflow_with(declares_input: true, runs_on: attempt_runs_on), floor_job_id: floor_job_id, pin: p, + ) { + QualificationSubjectRefused { refusal: SubjectDeliveryForAnotherDispatch } => true + _ => false + }) + JitMintDispatchRefused { refusal: _ } => false + }) + JitMintDispatchRefused { refusal: _ } => false + } + } + } + JitMintDispatchRefused { refusal: _ } => false + } + } + Absent => false + } +} + +// ── THE DISPATCH STANDING AND THE INTERLOCK ───────────────────────────────────────────────────── + +fn mutation_standing(refusal: RestRefusal) -> QualificationDispatchStanding { + qualification_dispatch_standing(dispatch: RestRefused { refusal: refusal }) +} + +data dispatch_receipt: WorkflowDispatchReceipt = WorkflowDispatchReceipt { + workflow_run_id: 77, + run_url: "https://api.github.com/repos/gunb-ai/gunbc/actions/runs/77" as NonEmptyStr, + html_url: "https://github.com/gunb-ai/gunbc/actions/runs/77" as NonEmptyStr, +} + +fn ambiguous(s: QualificationDispatchStanding) -> Bool { + match s { DispatchStandingCommitAmbiguous { detail: _ } => true _ => false } +} + +// THREE STANDINGS, FROM THE MUTATION CLASSIFIER: a decided 422 is refused; no response, a 503, and an +// undecodable 200 are commit-ambiguous; only a succeeded exchange dispatches. The interlock: a hold +// covers the slot's own host and no other. +test fn the_dispatch_has_three_standings_and_an_interlock() -> Bool { + (match mutation_standing(refusal: RestStatusRefused { status: 422, body: "Unexpected inputs provided" }) { DispatchStandingRefused { status: st, body: _ } => st == 422 _ => false }) + && ambiguous(s: mutation_standing(refusal: RestTransportRefused { cause: "fixture: connection reset after send" })) + && ambiguous(s: mutation_standing(refusal: RestStatusRefused { status: 503, body: "fixture" })) + && ambiguous(s: mutation_standing(refusal: RestBodyUndecodable { status: 200, cause: "fixture: truncated body" })) + && (match qualification_dispatch_standing(dispatch: RestAnswered { answer: dispatch_receipt }) { DispatchStandingSucceeded { receipt: r } => r.workflow_run_id == 77 _ => false }) + && hold_covers_slot_host(held: "mtcollins1" as HostIdentity, slot_host: "mtcollins1" as HostIdentity) + && !hold_covers_slot_host(held: "srv1" as HostIdentity, slot_host: "mtcollins1" as HostIdentity) +} + +// ── THE COLLECTION, OVER A SUPPLIED EXPECTATION ───────────────────────────────────────────────── + +data expect: QualificationRunExpectation = QualificationRunExpectation { + registered_runner_id: registered_runner_id, + registered_runner: registered_runner, + floor_job_name: "qualification floor", + revision: revision, + branch: pin_branch, +} + +fn run(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool) -> WorkflowRun { + run_on(id: id, attempt_n: attempt_n, event: event, path: path, sha: sha, completed: completed, concluded: concluded, on_branch: pin_branch as String) +} + +fn run_on(id: Int, attempt_n: Int, event: String, path: String, sha: CommitSha, completed: Bool, concluded: Bool, on_branch: String) -> WorkflowRun { + WorkflowRun { + id: id, + name: none, + path: Present { value: path }, + head_sha: sha, + head_branch: Present { value: on_branch }, + event: event, + status: if completed { Completed } else { InProgress }, + conclusion: if concluded { Present { value: Failure } } else { none }, + workflow_id: 9, + run_number: 1, + run_attempt: attempt_n, + actor: none, + triggering_actor: none, + html_url: "https://github.com/gunb-ai/gunbc/actions/runs/77", + created_at: "2026-10-04T00:00:00Z", + updated_at: "2026-10-04T01:00:00Z", + pull_requests: [], + } +} + +fn dispatched_run(attempt_n: Int) -> WorkflowRun { + run(id: 77, attempt_n: attempt_n, event: "workflow_dispatch", path: qualification_workflow_path() as String, sha: revision, completed: true, concluded: true) +} + +fn job(id: Int, name: String, runner_id: Int, runner: String, started: Bool, attempt_n: Int) -> WorkflowJobRun { + WorkflowJobRun { + id: id, + run_id: 77, + run_attempt: attempt_n, + name: name, + status: Completed, + conclusion: Present { value: Success }, + labels: [attempt as String], + runner_id: Present { value: runner_id }, + runner_name: Present { value: runner }, + created_at: Present { value: "2026-10-04T00:00:10Z" }, + started_at: if started { Present { value: "2026-10-04T00:01:00Z" } } else { none }, + completed_at: Present { value: "2026-10-04T00:50:00Z" }, + } +} + +fn floor_on(runner_id: Int) -> WorkflowJobRun { + job(id: 1, name: "qualification floor", runner_id: runner_id, runner: registered_runner as String, started: true, attempt_n: 1) +} + +fn jobs(js: List) -> WorkflowJobRunList { + WorkflowJobRunList { total_count: count(js), jobs: js } +} + +fn decide_run(r: WorkflowRun, js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_qualification_run(subject: expect, run_id: 77, run_read: RestAnswered { answer: r }, jobs_read: RestAnswered { answer: js }) +} + +fn decide_jobs(js: WorkflowJobRunList) -> QualificationRunVerdict { + decide_run(r: dispatched_run(attempt_n: 1), js: js) +} + +fn mismatch(v: QualificationRunVerdict) -> Bool { + match v { + RunVerdictRefused { refusal: RunSubjectMismatch { observed_run_id: _, observed_event: _, observed_path: _, foreign_jobs: _ } } => true + _ => false + } +} + +// THE RUNNER IS JOINED BY ITS MINTED ID, WITH THE NAME AS CORROBORATION. Positive control: the named +// floor job on the registered runner id is collected with the run's own conclusion (a Failure is +// still this host's run). REDs: the same runner name and labels under another runner id; the minted +// id reported under another name; a helper job on the registered runner while the floor ran +// elsewhere; a floor on the registered runner that never started; two jobs on the registered runner. +test fn the_collection_joins_the_floor_job_to_the_registered_runner() -> Bool { + let mine = registered_runner as String + (match decide_jobs(js: jobs(js: [floor_on(runner_id: registered_runner_id)])) { + RunVerdictCollected { run: r, floor_job: f, conclusion: c } => r.id == 77 && f.id == 1 && (match c { Failure => true _ => false }) + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: 99, runner: mine, started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunNeverReachedAttemptRunner { registered_runner_id: rid, runner_ids_seen: seen } } => rid == registered_runner_id && count(seen) == 1 + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: "another-name", started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunRunnerNameDisagrees { registered_runner_id: _, registered_runner: _, observed_runner_name: _ } } => true + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 2, name: "helper", runner_id: registered_runner_id, runner: mine, started: true, attempt_n: 1), floor_on(runner_id: 99)])) { + RunVerdictRefused { refusal: RunRegisteredRunnerRanAnotherJob { registered_runner: _, job_name: n } } => n == "helper" + _ => false + }) + && (match decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: mine, started: false, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotStarted { job_id: id } } => id == 1 + _ => false + }) + && (match decide_jobs(js: jobs(js: [floor_on(runner_id: registered_runner_id), job(id: 2, name: "qualification floor", runner_id: registered_runner_id, runner: mine, started: true, attempt_n: 1)])) { + RunVerdictRefused { refusal: RunFloorJobNotUnique { floor_job_name: _, matching: m } } => m == 2 + _ => false + }) +} + +// THE SUBJECT IS THE DISPATCHED RUN'S FIRST ATTEMPT ON THE PIN BRANCH. REDs: the run re-run to +// attempt 2 (whose floor job carries the same labels and runner); a job of attempt 2 in the listing; +// another run id; another event; another workflow path; a run on another branch (the default branch +// after a move, which the pinned group would also refuse to hand our runner). +test fn a_rerun_or_another_run_cannot_be_collected_as_the_dispatch() -> Bool { + let floor = jobs(js: [floor_on(runner_id: registered_runner_id)]) + let path = qualification_workflow_path() as String + (match decide_run(r: dispatched_run(attempt_n: 2), js: floor) { + RunVerdictRefused { refusal: RunAdvancedPastDispatchedAttempt { observed_attempt: a } } => a == 2 + _ => false + }) + && mismatch(v: decide_jobs(js: jobs(js: [job(id: 1, name: "qualification floor", runner_id: registered_runner_id, runner: registered_runner as String, started: true, attempt_n: 2)]))) + && mismatch(v: decide_run(r: run(id: 78, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "push", path: path, sha: revision, completed: true, concluded: true), js: floor)) + && mismatch(v: decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: ".github/workflows/witnesses.yml", sha: revision, completed: true, concluded: true), js: floor)) + && (match decide_run(r: run_on(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: true, on_branch: "main"), js: floor) { + RunVerdictRefused { refusal: RunOnAnotherBranch { pinned: _, observed: _ } } => true + _ => false + }) +} + +test fn an_unfinished_unpinned_or_unreadable_run_refuses() -> Bool { + let floor = jobs(js: [floor_on(runner_id: registered_runner_id)]) + let path = qualification_workflow_path() as String + (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: false, concluded: false), js: floor) { + RunVerdictRefused { refusal: RunNotCompleted { status: InProgress } } => true + _ => false + }) + && (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: revision, completed: true, concluded: false), js: floor) { + RunVerdictRefused { refusal: RunCompletedWithoutConclusion } => true + _ => false + }) + && (match decide_run(r: run(id: 77, attempt_n: 1, event: "workflow_dispatch", path: path, sha: "2222222222222222222222222222222222222222", completed: true, concluded: true), js: floor) { + RunVerdictRefused { refusal: RunRevisionNotPinned { pinned: _, observed: _ } } => true + _ => false + }) + && (match decide_run(r: dispatched_run(attempt_n: 1), js: WorkflowJobRunList { total_count: 2, jobs: floor.jobs }) { + RunVerdictRefused { refusal: RunJobsTruncated { returned: rt, total: t } } => rt == 1 && t == 2 + _ => false + }) + && (match decide_qualification_run(subject: expect, run_id: 77, run_read: RestRefused { refusal: RestTransportRefused { cause: "fixture" } }, jobs_read: RestAnswered { answer: floor }) { + RunVerdictRefused { refusal: RunReadUnreadable { refusal: _ } } => true + _ => false + }) + && (match decide_qualification_run(subject: expect, run_id: 77, run_read: RestAnswered { answer: dispatched_run(attempt_n: 1) }, jobs_read: RestRefused { refusal: RestStatusRefused { status: 404, body: "fixture" } }) { + RunVerdictRefused { refusal: RunJobsUnreadable { refusal: _ } } => true + _ => false + }) +} + +// THE FLOOR CONTRACT CHECKS OUT THE PINNED REVISION FIRST AND NOTHING RUNS PAST A REFUSAL +// (decision: eager-gull-22, 2026-10-04). The guard is the floor job's FIRST step; on a mismatch it exits +// nonzero, and GitHub skips every later step of a failed job unless the step opts back in -- so the +// contract also refuses any step with an if: condition or continue-on-error, and a job with +// continue-on-error. REDs: the revision input undeclared; no guard; the guard after another step; a +// later step with if: always(); a later step with continue-on-error. The guard itself compares HEAD with +// the revision input and exits 1 on a mismatch. The post-run head_sha refusal is +// RunRevisionNotPinned (an_unfinished_unpinned_or_unreadable_run_refuses). +fn floor_refused(workflow: Workflow) -> Bool { + match qualification_floor_admission(workflow: workflow, floor_job_id: floor_job_id) { + QualificationFloorRefused { refusal: SubjectFloorRevisionGuardNotFirst { job_id: _ } } => true + QualificationFloorRefused { refusal: SubjectFloorStepRunsPastTheGuard { job_id: _ } } => true + QualificationFloorRefused { refusal: SubjectAttemptInputUndeclared { input: i } } => i == "qualification_revision" + _ => false + } +} + +fn workflow_with_floor(job: Job, inputs_declare_revision: Bool) -> Workflow { + Workflow { + name: "fleet-converge", + run_name: none, + on: [WorkflowDispatch { inputs: if inputs_declare_revision { [qualification_attempt_dispatch_input, qualification_revision_dispatch_input] } else { [qualification_attempt_dispatch_input] } }], + concurrency: none, + jobs: [job], + env: none, + permissions: none, + } +} + +test fn the_floor_checks_out_the_pinned_revision_first_and_nothing_runs_past_a_refusal() -> Bool { + let guard = qualification_floor_revision_guard_step() + let work = floor_work_step(if_condition: none) + let tolerant = RunStep { + name: Present { value: "run the floor anyway" }, id: none, run: "claim_executor --required-ci", + shell: none, env: none, working_directory: none, if_condition: none, continue_on_error: Present { value: true }, timeout_minutes: none, + } + (match qualification_floor_admission(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, work]), inputs_declare_revision: true), floor_job_id: floor_job_id) { + QualificationFloorAdmitted { floor_job_name: _ } => true + QualificationFloorRefused { refusal: _ } => false + }) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, work]), inputs_declare_revision: false)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [work]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [work, guard]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, floor_work_step(if_condition: Present { value: "always()" })]), inputs_declare_revision: true)) + && floor_refused(workflow: workflow_with_floor(job: floor_job_with_steps(runs_on: attempt_runs_on, steps: [guard, tolerant]), inputs_declare_revision: true)) + && (match guard { + RunStep { name: _, id: _, run: r, shell: _, env: e, working_directory: _, if_condition: c, continue_on_error: ce, timeout_minutes: _ } => + string_contains(s: r, pattern: "git rev-parse HEAD") + && string_contains(s: r, pattern: "!= \"$QUALIFICATION_REVISION\"") + && string_contains(s: r, pattern: "exit 1") + && (match c { Absent => true Present { value: _ } => false }) + && (match ce { Absent => true Present { value: _ } => false }) + && (match e { Present { value: kvs } => kvs |> any(x => x.key == "QUALIFICATION_REVISION") Absent => false }) + UsesStep { name: _, id: _, uses: _, with: _, env: _, if_condition: _, continue_on_error: _, timeout_minutes: _ } => false + }) +} + +// ── THE DESIGNATED JOB'S LOG ────────────────────────────────────────────────────────────────── + +// RED: a log read for any job other than the designated one is refused, whatever it says. +test fn a_log_of_another_job_is_not_the_floor_jobs_log() -> Bool { + (match designated_job_log(designated_job_id: 502, log: AttemptJobLogRead { job_id: 501, log: "[floor-phase] phase=x wall_ms=1" }) { + DesignatedJobLogRefused { refusal: JobLogOfAnotherJob { designated_job_id: 502, read_job_id: 501 } } => true + _ => false + }) + && (match designated_job_log(designated_job_id: 502, log: AttemptJobLogUnreadable { job_id: 502, refusal: RestExchangeStatusRefused { status: 410, body: "Gone" } }) { + DesignatedJobLogRefused { refusal: JobLogUnreadable { job_id: 502, refusal: _ } } => true + _ => false + }) + && (match designated_job_log(designated_job_id: 502, log: AttemptJobLogRead { job_id: 502, log: "" }) { + DesignatedJobLogRefused { refusal: JobLogEmpty { job_id: 502 } } => true + _ => false + }) +} + +// ── THE PURE DECODER, OVER SUPPLIED TEXT ────────────────────────────────────────────────────── + +data slot: NonEmptyStr = "gunbc-qualification-mtcollins1-q-20261004-1.service" + +data phase_line: String = "2026-10-04T00:10:00.0000000Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0" +data started_line: String = "2026-10-04T00:09:00.0000000Z [floor-phase] phase=strict-preparation state=started" +data slot_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/gunbc-qualification-mtcollins1-q-20261004-1.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0]" +data slice_level_line: String = "2026-10-04T00:09:00.0000000Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=1 peak=364675248128" + +fn decode(lines: List) -> JobLogInstrumentDecode { + decode_job_log_instruments(log: join(lines, "\n"), slot_unit: slot) +} + +fn undecodable_with(d: JobLogInstrumentDecode, check: fn(InstrumentReadRefusal) -> Bool) -> Bool { + match d { + JobLogInstrumentsUndecodable { refusal: r } => check(r) + JobLogInstrumentsDecoded { readings: _ } => false + } +} + +test fn the_job_log_instruments_are_the_two_the_extraction_reads() -> Bool { + qualification_instrument_read_from_job_log(i: FloorPhaseRows) + && qualification_instrument_read_from_job_log(i: FloorCgroupRows) + && count(qualification_instruments |> filter(i => qualification_instrument_read_from_job_log(i: i))) == 2 + && count(qualification_instrument_extraction_frontier_rows) == 4 +} + +// THE POSITIVE CONTROL: timed and untimed phase rows are both carried, and only the slot's own +// level is taken -- the slice above it has a larger peak and must not be read as the slot's. +test fn a_log_with_phase_rows_and_the_slot_level_is_decoded() -> Bool { + match decode(lines: ["noise before the floor", started_line, slot_level_line, slice_level_line, phase_line]) { + JobLogInstrumentsDecoded { readings: rd } => + count(rd.floor_phase_rows) == 2 + && count(rd.slot_cgroup_rows) == 1 + && (rd.slot_cgroup_rows |> all(l => byte_size_count(b: l.peak) == 10444185600)) + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: _, wall: w } => millisecond_count(m: w) == 22194 _ => false })) + JobLogInstrumentsUndecodable { refusal: _ } => false + } +} + +test fn a_garbled_row_refuses_rather_than_being_skipped() -> Bool { + undecodable_with(d: decode(lines: [slot_level_line, phase_line, "[floor-phase] phase=gate-closure wall_ms=twelve"]), check: r => match r { + FloorPhaseRowGarbledInLog { line: _, cause: _ } => true + _ => false + }) + && undecodable_with(d: decode(lines: [phase_line, "[floor-cgroup] when=beat-10 level=/sys/fs/cgroup/x.service high=lots peak=1"]), check: r => match r { + FloorCgroupRowGarbledInLog { line: _, cause: _ } => true + _ => false + }) +} + +// RED, THE SPLIT-ACROSS-JOBS CASE: the phase rows of one job and the slot row of another never meet, +// because only the designated job's log is decoded -- read alone, the job carrying only phase rows +// refuses for want of the slot level, and the one carrying only the slot level refuses for want of a +// timed phase row. +test fn rows_split_across_two_jobs_are_not_one_measurement() -> Bool { + undecodable_with(d: decode(lines: [started_line, phase_line]), check: r => match r { + SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } => u == slot && count(seen) == 0 + _ => false + }) + && undecodable_with(d: decode(lines: [slot_level_line, slice_level_line]), check: r => match r { + FloorPhaseRowsAbsent => true + _ => false + }) +} + +test fn a_log_without_the_slot_level_names_the_levels_it_saw() -> Bool { + undecodable_with(d: decode(lines: [phase_line, slice_level_line]), check: r => match r { + SlotCgroupLevelAbsent { slot_unit: u, levels_seen: seen } => u == slot && count(seen) == 1 + _ => false + }) +} + +// ── INHABITANCE: THE REAL PRODUCER'S BYTES ───────────────────────────────────────────────────── +// +// The supplied lines above are designed against the row shapes; this claim is what keeps them +// readings. It hands decode_job_log_instruments the verbatim [floor-phase] and [floor-cgroup] lines +// claim_executor printed on a recorded required-floor run (test.fixture +// recorded_required_floor_log_excerpt), under that run's real slot unit, and asserts the ROUTE as well +// as the answer: every one of the 79 phase rows is read (none garbled, none dropped), the timed rows +// carry the gate-closure wall the log printed, and of the 12 level rows only the 4 at the slot's own +// level -- floor entry and three heartbeats -- are taken, with the slot's memory.high read as a byte +// bound and floor entry's memory.peak as printed. If the host's row encoding moves, this goes red +// while the designed fixtures above stay green. +test fn the_decoder_reads_a_recorded_required_floor_log() -> Bool { + match decode_job_log_instruments(log: recorded_required_floor_log_excerpt, slot_unit: recorded_floor_log_slot_unit as NonEmptyStr) { + JobLogInstrumentsDecoded { readings: rd } => + count(rd.floor_phase_rows) == 79 + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseTimed { phase: ph, wall: w } => (ph as String) == "gate-closure" && millisecond_count(m: w) == 22194 _ => false })) + && (rd.floor_phase_rows |> any(p => match p { FloorPhaseUntimed { phase: ph } => (ph as String) == "strict-preparation" _ => false })) + && count(rd.slot_cgroup_rows) == 4 + && (rd.slot_cgroup_rows |> all(l => match l.high { CgroupMemoryLimited { bytes: b } => byte_size_count(b: b) == 16106127360 _ => false })) + && (rd.slot_cgroup_rows |> any(l => (l.when as String) == "floor-entry" && byte_size_count(b: l.peak) == 10444185600)) + JobLogInstrumentsUndecodable { refusal: _ } => false + } +} + +// SUPPLIED RestResult VALUES FOR THE CLAIM-COST DECISIONS, built once so the claims below stay readable. +fn answered_listing(l: ActionsArtifactList) -> RestResult { + RestAnswered { answer: l } +} + +fn answered_run(r: WorkflowRun) -> RestResult { + RestAnswered { answer: r } +} + +data unreached_run: RestResult = RestRefused { refusal: RestTransportRefused { cause: "unreached" } } + +fn archive_refused(r: RestRefusal) -> RestResult { + RestRefused { refusal: r } +} + +data archive_arrived: RestResult = RestAnswered { answer: utf8_encode_bytes(s: "PK") } + +// ── THE CLAIM-COST READ'S REFUSALS ───────────────────────────────────────────────────────────── +// +// Every way the artifact read can end today is a typed arm, and none of them is a reading: the +// listing truncated, the artifact absent (with the names that were there), expired, the archive +// unreadable (the RestBodyUndecodable a zip body produces), or an archive that arrived with no +// inflate reader to open it. +fn artifact(name: String, expired: Bool) -> ActionsArtifact { + ActionsArtifact { id: 9001, name: name, expired: expired } +} + +test fn every_claim_cost_read_ends_in_a_typed_refusal() -> Bool { + let present = ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-floor-claim-cost", expired: false)] } + let gone = archive_refused(r: RestStatusRefused { status: 410, body: "Gone" }) + let undecodable = archive_refused(r: RestBodyUndecodable { status: 200, cause: "stream did not contain valid UTF-8" }) + (match conclude_claim_cost_listing(listed: answered_listing(l: ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-ci-measurement-receipt", expired: false)] })) { + Present { value: ClaimCostArtifactAbsent { names_seen: ns } } => count(ns) == 1 + _ => false + }) + && (match conclude_claim_cost_listing(listed: answered_listing(l: ActionsArtifactList { total_count: 1, artifacts: [artifact(name: "required-floor-claim-cost", expired: true)] })) { + Present { value: ClaimCostArtifactExpired { artifact_id: 9001 } } => true + _ => false + }) + && (match conclude_claim_cost_listing(listed: answered_listing(l: ActionsArtifactList { total_count: 3, artifacts: [] })) { + Present { value: ClaimCostArtifactListTruncated { returned: 0, total: 3 } } => true + _ => false + }) + && (match conclude_claim_cost_listing(listed: answered_listing(l: present)) { Absent => true _ => false }) + && (match conclude_claim_cost_download(artifact_id: 9001, archive: undecodable) { + ClaimCostArchiveUnreadable { artifact_id: 9001, refusal: _ } => true + _ => false + }) + && (match conclude_claim_cost_download(artifact_id: 9001, archive: gone) { + ClaimCostArtifactExpired { artifact_id: 9001 } => true + _ => false + }) + && (match conclude_claim_cost_download(artifact_id: 9001, archive: archive_arrived) { + ClaimCostArchiveNotInflatable { artifact_id: 9001 } => true + _ => false + }) +} + +// TWO SAME-NAME ARTIFACTS ARE A REFUSAL, NEVER FIRST-ONE-WINS. +fn claim_cost_artifact_with(id: Int) -> ActionsArtifact { + ActionsArtifact { id: id, name: "required-floor-claim-cost", expired: false } +} + +test fn two_same_name_claim_cost_artifacts_refuse_as_ambiguous() -> Bool { + match conclude_claim_cost_listing(listed: answered_listing(l: ActionsArtifactList { total_count: 2, artifacts: [claim_cost_artifact_with(id: 1), claim_cost_artifact_with(id: 2)] })) { + Present { value: ClaimCostArtifactAmbiguous { artifact_ids: ids } } => count(ids) == 2 + _ => false + } +} + +// THE LISTING IS BOUND TO THE COLLECTED ATTEMPT: a run re-read after the listing at attempt 2 means the +// listing may hold attempt 2's artifact, so no claim-cost read proceeds for attempt 1; at attempt 1 it +// proceeds; an unreadable re-read refuses rather than assuming attempt 1. +test fn a_rerun_seen_after_the_listing_refuses_the_claim_cost_read() -> Bool { + (match conclude_claim_cost_attempt(run: answered_run(r: dispatched_run(attempt_n: 2))) { + Present { value: ClaimCostRunAdvancedPastAttempt { observed_attempt: 2 } } => true + _ => false + }) + && (match conclude_claim_cost_attempt(run: answered_run(r: dispatched_run(attempt_n: 1))) { Absent => true _ => false }) + && (match conclude_claim_cost_attempt(run: unreached_run) { + Present { value: ClaimCostRunUnreadable { refusal: _ } } => true + _ => false + }) +} diff --git a/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag b/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag new file mode 100644 index 00000000000..3f0fe4237ef --- /dev/null +++ b/dag/test/claim/runner/runner_qualification_ref_pin_witness_test.dag @@ -0,0 +1,172 @@ +module test.claim.runner_qualification_ref_pin_witness + +import std.types { Bool, CommitSha, Int, List, NonEmptyStr, String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.transports.rest { RestAnswered, RestRefused, RestStatusRefused, RestTransportRefused } +import extdeps.github.git_database { GitRefWire, GitRefObjectWire } +import extdeps.github.actions { + WorkflowTrigger, Push, PullRequest, Schedule, WorkflowDispatch, WorkflowCall, WorkflowRunCompleted, MergeGroup, +} +import gunbc.fleet.org_actions_inspection { RunnerGroupListDecoded, RunnerGroupIdentity } +import gunbc.runner_group_restriction_observation { RunnerGroupRestrictionRefused, RunnerGroupAbsent } +import gunbc.fleet_desired_admission_workflow { fleet_desired_admission_workflow } +import test.claim.github_app_registry { group_restricted_to } +import gunbc.runner.runner_qualification_ref_pin { + qualification_branch_name, QualificationBranchNamed, QualificationBranchNameRefused, branch_full_ref, + qualification_selected_workflow, qualification_runner_group_name, + qualification_group_target, QualificationGroupTargeted, QualificationGroupNotTargetable, + branch_create_refusal, PinBranchPreexisting, PinBranchCreateRefused, PinBranchCreateAmbiguous, + qualification_ref_pin_of, QualificationRefPinned, QualificationRefPinRefused, PinBranchReadbackMismatch, PinGroupReadbackRefused, + classify_branch_removal, QualificationBranchRemoved, QualificationBranchRemovalRefused, BranchStillPresent, BranchReadUnreadable, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A REF AS GitHub's GET /git/ref RETURNS IT, at a supplied commit: the readback this module judges. +fn ref_at(sha: String) -> GitRefWire { + GitRefWire { ref: "refs/heads/qualification/q-1", object: GitRefObjectWire { type: "commit", sha: sha } } +} + +data revision: CommitSha = "1111111111111111111111111111111111111111" + +fn named_ok(attempt: NonEmptyStr) -> Bool { + match qualification_branch_name(attempt: attempt) { + QualificationBranchNamed { name: n } => (n.branch as String) == concat("qualification/", attempt as String) && (branch_full_ref(name: n) as String) == concat("refs/heads/qualification/", attempt as String) + QualificationBranchNameRefused { attempt: _, cause: _ } => false + } +} + +fn named_refused(attempt: NonEmptyStr) -> Bool { + match qualification_branch_name(attempt: attempt) { + QualificationBranchNamed { name: _ } => false + QualificationBranchNameRefused { attempt: _, cause: _ } => true + } +} + +// RED: OUT OF PREFIX. A label carrying a slash, a parent step, a ref metacharacter, a leading dot or +// a .lock suffix cannot name refs/heads/qualification/ and refuses rather than escaping. +// Positive control: an ordinary attempt label names exactly refs/heads/qualification/. +test fn a_ref_outside_refs_heads_qualification_cannot_be_named() -> Bool { + named_ok(attempt: "q-1") + && named_refused(attempt: "../main") + && named_refused(attempt: "q/1") + && named_refused(attempt: "main~1") + && named_refused(attempt: ".hidden") + && named_refused(attempt: "q-1.lock") + && named_refused(attempt: "q 1") +} + +// RED: CREATE-ONCE. GitHub's 422 on CreateRef (the ref exists) is the pre-existing refusal, never an +// adoption; a lost or undecodable answer is ambiguous (removal still owed); only a created ref +// proceeds to readback. +test fn a_preexisting_branch_refuses_and_a_lost_create_is_ambiguous() -> Bool { + (match branch_create_refusal(created: RestRefused { refusal: RestStatusRefused { status: 422, body: "Reference already exists" } }) { + Present { value: PinBranchPreexisting { observed_sha: _ } } => true + _ => false + }) + && (match branch_create_refusal(created: RestRefused { refusal: RestStatusRefused { status: 403, body: "fixture" } }) { + Present { value: PinBranchCreateRefused { status: s, body: _ } } => s == 403 + _ => false + }) + && (match branch_create_refusal(created: RestRefused { refusal: RestStatusRefused { status: 502, body: "fixture: 502 after the write" } }) { + Present { value: PinBranchCreateAmbiguous { detail: _ } } => true + _ => false + }) + && (match branch_create_refusal(created: RestRefused { refusal: RestTransportRefused { cause: "fixture: connection reset after send" } }) { + Present { value: PinBranchCreateAmbiguous { detail: _ } } => true + _ => false + }) + && (match branch_create_refusal(created: RestAnswered { answer: ref_at(sha: revision as String) }) { Absent => true Present { value: _ } => false }) +} + +fn group_row(id: Int, name: NonEmptyStr) -> RunnerGroupIdentity { + RunnerGroupIdentity { id: id, name: name, allows_public_repositories: false, restricted_to_workflows: true, selected_workflows: [] } +} + +// RED: A WRITE TO ANY OTHER GROUP. Only the dedicated qualification group can become a write target: +// the shakedown group refuses even when it is present, as do an absent or duplicated dedicated group. +// Positive control: the dedicated group found once. +test fn a_restriction_write_to_any_other_group_refuses() -> Bool { + let listed = RunnerGroupListDecoded { groups: [group_row(id: 1, name: "microvm-shakedown"), group_row(id: 3, name: qualification_runner_group_name)] } + (match qualification_group_target(requested: "microvm-shakedown", listed: listed) { QualificationGroupNotTargetable { cause: _ } => true _ => false }) + && (match qualification_group_target(requested: "Default", listed: listed) { QualificationGroupNotTargetable { cause: _ } => true _ => false }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: RunnerGroupListDecoded { groups: [group_row(id: 1, name: "microvm-shakedown")] }) { + QualificationGroupNotTargetable { cause: _ } => true + _ => false + }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: RunnerGroupListDecoded { groups: [group_row(id: 3, name: qualification_runner_group_name), group_row(id: 4, name: qualification_runner_group_name)] }) { + QualificationGroupNotTargetable { cause: _ } => true + _ => false + }) + && (match qualification_group_target(requested: qualification_runner_group_name, listed: listed) { + QualificationGroupTargeted { target: t } => t.group_id == 3 + QualificationGroupNotTargetable { cause: _ } => false + }) +} + +// THE PIN IS MINTED ONLY FROM A READBACK AT THE EXACT REVISION AND A GROUP OBSERVED PINNED TO THE +// BRANCH. REDs: the branch read back at another commit; the group not observed restricted to the +// qualification workflow at the branch. Positive control: both hold. +test fn the_pin_is_minted_only_from_the_exact_revision_and_the_pinned_group() -> Bool { + match qualification_branch_name(attempt: "q-1") { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: n } => { + let pinned_group = group_restricted_to(workflow: qualification_selected_workflow(branch: n.branch)) + (match qualification_ref_pin_of(name: n, revision: revision, readback: RestAnswered { answer: ref_at(sha: revision as String) }, group: pinned_group) { + QualificationRefPinned { pin: p } => (p.name.branch as String) == "qualification/q-1" && (p.revision as String) == (revision as String) && p.group.group.value == 3 + QualificationRefPinRefused { attempt: _, refusal: _ } => false + }) + && (match qualification_ref_pin_of(name: n, revision: revision, readback: RestAnswered { answer: ref_at(sha: "2222222222222222222222222222222222222222") }, group: pinned_group) { + QualificationRefPinRefused { attempt: _, refusal: PinBranchReadbackMismatch { pinned: _, observed: _ } } => true + _ => false + }) + && (match qualification_ref_pin_of(name: n, revision: revision, readback: RestAnswered { answer: ref_at(sha: revision as String) }, group: RunnerGroupRestrictionRefused { name: qualification_runner_group_name, cause: RunnerGroupAbsent }) { + QualificationRefPinRefused { attempt: _, refusal: PinGroupReadbackRefused { standing: _ } } => true + _ => false + }) + } + } +} + +// REMOVAL IS A READBACK. A read finding the branch absent (404) is removed; a branch still present +// after the delete refuses BranchStillPresent; an unreadable read refuses rather than reporting it gone. +test fn the_branch_is_removed_only_when_a_read_finds_it_absent() -> Bool { + match qualification_branch_name(attempt: "q-1") { + QualificationBranchNameRefused { attempt: _, cause: _ } => false + QualificationBranchNamed { name: n } => + (match classify_branch_removal(name: n, readback: RestRefused { refusal: RestStatusRefused { status: 404, body: "Not Found" } }, deleted: true, delete_refusal: none) { + QualificationBranchRemoved { name: _, deleted: d } => d + _ => false + }) + && (match classify_branch_removal(name: n, readback: RestAnswered { answer: ref_at(sha: revision as String) }, deleted: true, delete_refusal: none) { + QualificationBranchRemovalRefused { name: _, cause: BranchStillPresent { observed_sha: o, delete_refusal: _ } } => o == (revision as String) + _ => false + }) + && (match classify_branch_removal(name: n, readback: RestRefused { refusal: RestStatusRefused { status: 500, body: "fixture" } }, deleted: true, delete_refusal: none) { + QualificationBranchRemovalRefused { name: _, cause: BranchReadUnreadable { refusal: _ } } => true + _ => false + }) + } +} + +// CREATING A QUALIFICATION BRANCH FIRES NO WORKFLOW. WorkflowTrigger has no `create` variant, so no +// generated workflow can trigger on branch creation by construction; the one generated workflow with a +// push trigger is gunbc.fleet_desired_admission_workflow, whose branch filter must not admit +// qualification/. A filter containing any glob is read as possibly matching (refused here +// rather than evaluated), so a widened filter turns this red. +fn push_trigger_could_fire_on(trigger: WorkflowTrigger, branch: String) -> Bool { + match trigger { + Push { branches: bs, paths: _ } => count(bs) == 0 || (bs |> any(b => b == branch || string_contains(s: b, pattern: "*"))) + PullRequest { branches: _, types: _ } => false + Schedule { cron: _ } => false + WorkflowDispatch { inputs: _ } => false + WorkflowCall { inputs: _ } => false + WorkflowRunCompleted { workflows: _, branches: _ } => false + MergeGroup => false + } +} + +test fn creating_a_qualification_branch_fires_no_generated_push_workflow() -> Bool { + !(fleet_desired_admission_workflow.on |> any(t => push_trigger_could_fire_on(trigger: t, branch: "qualification/q-1"))) + && push_trigger_could_fire_on(trigger: Push { branches: ["qualification/**"], paths: [] }, branch: "qualification/q-1") +} diff --git a/dag/test/claim/runner/runner_slot_retirement_witness_test.dag b/dag/test/claim/runner/runner_slot_retirement_witness_test.dag index 4562224d2e5..b0f7262f386 100644 --- a/dag/test/claim/runner/runner_slot_retirement_witness_test.dag +++ b/dag/test/claim/runner/runner_slot_retirement_witness_test.dag @@ -1085,6 +1085,27 @@ test fn a_derived_target_width_yields_the_prior_minus_target_obligation() -> Boo } } +// THE OBLIGATION IS PRIOR MINUS THE COMMITTED IDENTITIES, NOT `target + 1 .. prior`. On srv1 the +// committed population at width 11 is srv1-01..10 plus the shakedown slot srv1-13 (#12339), so the +// obligation must name srv1-11 and srv1-12 -- the fleet slots the width stopped charging for -- and +// must NOT name srv1-13. The index-range derivation got both wrong with the same count (39), which +// is why this asserts membership rather than length alone. +test fn the_obligation_excludes_a_committed_member_above_the_fleet_range() -> Bool { + match transition_row_for_host(host: operator_host_srv1).first() { + Absent => false + Present { value: t } => + match transition_required_retirees_at(t: t, target_width: RunnerWidthDerived { slots: 11 }) { + RetireesKnown { units: units } => + list_length(items: units) == 39 + && any(units, u => (u as String) == "actions-runner@srv1-11.service") + && any(units, u => (u as String) == "actions-runner@srv1-12.service") + && !any(units, u => (u as String) == "actions-runner@srv1-13.service") + && !any(units, u => (u as String) == "actions-runner@srv1-10.service") + RetireesUnresolved { host: _, cause: _ } => false + } + } +} + // AND THE WRAPPER REACHES THE DECISION ON THE REAL PATH, which is what keeps the two supplied-input // rows above from being claims about a function nothing calls: srv4's width resolves today, so the // production read yields the same derived obligation the decision produces for that width. diff --git a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag index 21a65bf2872..4c7da3a0c46 100644 --- a/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag +++ b/dag/test/claim/runner/runner_throughput_qualification_witness_test.dag @@ -741,33 +741,33 @@ test fn the_route_classifies_every_mutation_at_its_grain() -> Bool { match stage_effect(stage: s) { GitHubControlPlaneWrite { authority: _ } => true _ => false } DeregisterRunnerSlot { unit: _, deregistration: _ } => match stage_effect(stage: s) { GitHubControlPlaneWrite { authority: _ } => true _ => false } - CollectInstruments { instruments: _ } => - match stage_effect(stage: s) { ObservationOnly => true _ => false } + CollectInstruments { instruments: _, collection: _ } => + match stage_effect(stage: s) { ObservationOnly { collection: _ } => true _ => false } BootHostUnderApprovalGate { host: _, boot: _, image: _ } => match stage_effect(stage: s) { BmcWrite { gate: _, boot: _, image: _ } => true _ => false } } )) } -// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the boot and dispatch legs are still -// declared frontiers. This flips to the regression control that the bindings stay when they land. +// THE ROUTE IS NOT EXECUTABLE TODAY, AND THE CLAIM SAYS SO: the image, boot, registration, dispatch and +// deregistration authorities are implemented but not wired. This flips to the regression control that the bindings stay when they land. test fn the_route_is_not_executable_while_any_effectful_leg_is_unbound() -> Bool { !route_is_executable(route: route()) && !leg_is_wired(leg: qualification_run_producer_frontier) } -// THE IMAGE LEG HAS AN IMPLEMENTED AUTHORITY AND IS NOT WIRED; THE BOOT LEG HAS NO AUTHORITY. The -// claim tells the two apart: wiring the image leg before a production caller builds the runner -// image, pointing it at another declaration, or binding the boot leg to the census-only boot run, -// turns this red. -test fn the_image_leg_is_implemented_not_wired_and_the_boot_leg_awaits_a_runner_medium() -> Bool { - leg_names(leg: mtcollins1_route_legs.image, module_path: "extdeps.provisioning.ubuntu_seeded_install_media", decl_name: "UbuntuSeededInstallMediaBuildInput") +// THE IMAGE AND BOOT LEGS HAVE IMPLEMENTED RUNNER-OWNED AUTHORITIES AND ARE NOT WIRED. Wiring either +// before a production caller boots the runner image, or pointing either at another declaration +// (the census-only boot run among them), turns this red. +test fn the_image_and_boot_legs_are_implemented_by_the_runner_medium_and_not_wired() -> Bool { + leg_names(leg: mtcollins1_route_legs.image, module_path: "gunbc.runner.runner_host_medium", decl_name: "runner_host_medium_image") && !leg_is_wired(leg: mtcollins1_route_legs.image) - && match mtcollins1_route_legs.boot { LegAwaitingAuthority { lands_with: _, sufficient_for: _ } => true _ => false } + && leg_names(leg: mtcollins1_route_legs.boot, module_path: "gunbc.runner.runner_host_medium", decl_name: "runner_host_up_readback") + && !leg_is_wired(leg: mtcollins1_route_legs.boot) } -// REGISTRATION AND DEREGISTRATION HAVE IMPLEMENTED AUTHORITIES AND ARE NOT WIRED -- by declaration -// identity, so pointing either leg at some other declaration, or promoting either to LegWired while -// nothing on the route calls it, turns this red -- and the route stays unexecutable. +// REGISTRATION, DISPATCH AND DEREGISTRATION HAVE IMPLEMENTED AUTHORITIES AND ARE NOT WIRED -- by +// declaration identity, so pointing any leg at some other declaration, or promoting one to LegWired +// while nothing on the route calls it, turns this red -- and the route stays unexecutable. fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> Bool { match leg { LegAuthorityImplemented { authority: a, wiring_owed: _ } => (a.module_path as String) == module_path && (a.decl_name as String) == decl_name @@ -775,13 +775,16 @@ fn leg_names(leg: RouteLegStanding, module_path: String, decl_name: String) -> B } } -test fn the_registration_and_deregistration_legs_are_implemented_not_wired_and_the_route_is_not_executable() -> Bool { +test fn the_registration_dispatch_and_deregistration_legs_are_implemented_not_wired_and_the_route_is_not_executable() -> Bool { leg_names(leg: mtcollins1_route_legs.registration, module_path: "gunbc.runner.runner_jit_perform", decl_name: "dispatch_jit_mint") + && leg_names(leg: mtcollins1_route_legs.dispatch, module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "dispatch_qualification_floor") && leg_names(leg: mtcollins1_route_legs.deregistration, module_path: "gunbc.runner.runner_jit_deregistration", decl_name: "ensure_jit_runner_deregistered") && !leg_is_wired(leg: mtcollins1_route_legs.registration) && !leg_is_wired(leg: mtcollins1_route_legs.deregistration) && !leg_is_wired(leg: mtcollins1_route_legs.boot) && !leg_is_wired(leg: mtcollins1_route_legs.dispatch) + && leg_names(leg: mtcollins1_route_legs.collect, module_path: "gunbc.runner.runner_qualification_dispatch", decl_name: "collect_qualification_instruments") + && !leg_is_wired(leg: mtcollins1_route_legs.collect) && !route_is_executable(route: route()) } @@ -797,7 +800,7 @@ test fn only_the_runner_stages_open_the_network_and_only_default_drop() -> Bool Absent => match s { BootHostUnderApprovalGate { host: _, boot: _, image: _ } => true - CollectInstruments { instruments: _ } => true + CollectInstruments { instruments: _, collection: _ } => true _ => false } Present { value: policy } => @@ -819,7 +822,7 @@ test fn the_collect_stage_owes_a_sealed_concurrent_cohort() -> Bool { !leg_is_wired(leg: qualification_run_producer_frontier) && (route() |> any(s => match s { - CollectInstruments { instruments: is } => + CollectInstruments { instruments: is, collection: _ } => is |> any(i => match i { ConcurrentCohortSeatWindows => { @@ -837,7 +840,7 @@ test fn the_collect_stage_owes_a_sealed_concurrent_cohort() -> Bool { test fn the_collect_stage_owes_the_rate_specimen_and_the_cgroup_rows() -> Bool { route() |> any(s => match s { - CollectInstruments { instruments: is } => + CollectInstruments { instruments: is, collection: _ } => (is |> any(i => match i { CalibrationSpecimenWitnessLine => { diff --git a/dag/test/claim/scm/scm_affected_set_capture_witness_test.dag b/dag/test/claim/scm/scm_affected_set_capture_witness_test.dag index b463435ac8b..dfee5f8e135 100644 --- a/dag/test/claim/scm/scm_affected_set_capture_witness_test.dag +++ b/dag/test/claim/scm/scm_affected_set_capture_witness_test.dag @@ -51,6 +51,8 @@ import gunbc.scm.repository_load { rows_by_digest, shard_objects_agree_with_roster, load_repository_indexed, RepositoryLoaded, RepositoryLoadRefused, RepositoryDocumentRefused, } +import gunbc.roadmap_belt_actuate { belt_scm_rebound_identities } +import gunbc.scm.worktree_inventory { WorktreeChange, WorktreeBind } import gunbc.scm.write_spine { commit_staged, StagedCommitMinted, StagedCommitCandidateRefused, StagedCommitNothingToFreeze, StagedCommitAllocatorInvalid, StagedCommitSourceUnrecoverable, @@ -599,3 +601,32 @@ fn envelope_over(indexed: RepositoryEnvelope, store: ObjectStore) -> RepositoryE staged: indexed.staged, } } + +fn rebound_changes(from: Int, to: Int) -> List { + map(planted_indices(n: to, i: from, acc: []), i => WorktreeBind { path: join(["src/planted/file_", to_string(i), ".dag"], "") as NonEmptyStr }) +} + +fn rebound_texts(from: Int, to: Int) -> Map { + fold(planted_indices(n: to, i: from, acc: []), init: empty_map(), f: fn(m, i) { map_insert(m, join(["src/planted/file_", to_string(i), ".dag"], ""), planted_text(i: i)) }) +} + +// THE REBIND'S SHARD DEMAND IS DERIVED FROM THE WHOLE CHANGE POPULATION, so a shard holding k of its +// re-bound members is planned once. The srv2 recapture demanded each re-bound identity separately +// and read the one shard k times (30x for the largest). The control is the receipt shape: the +// population demanded together names exactly one shard, while the same identities demanded one at a +// time name k -- the count the per-path route paid. +test fn a_rebound_population_plans_each_shard_once() -> Bool { + match planted_base(n: planted_files) { + Absent => false + Present { value: whole } => + match indexed_of(whole: whole) { + Absent => false + Present { value: indexed } => { + let ids = belt_scm_rebound_identities(store: indexed.store, changes: rebound_changes(from: 1, to: 5), texts: rebound_texts(from: 1, to: 5)) + count(ids) == 4 + && demand_shard_count(repo: indexed, identities: ids) == 1 + && fold(ids, init: 0, f: fn(n, id) { n + demand_shard_count(repo: indexed, identities: [id]) }) == 4 + } + } + } +} diff --git a/dag/test/claim/scm/scm_object_table_json_witness_test.dag b/dag/test/claim/scm/scm_object_table_json_witness_test.dag index 74a21f7bb41..fc8ed0cd576 100644 --- a/dag/test/claim/scm/scm_object_table_json_witness_test.dag +++ b/dag/test/claim/scm/scm_object_table_json_witness_test.dag @@ -20,7 +20,7 @@ module test.claim.scm_object_table_json_witness // closure document's concern; what is asserted is that the TABLE preserves what it was given. import std.types { Bool, Int, String, List, list_length, NonEmptyStr } -import v2.std.node { Node, Edge, Symbol, Authored, Conj, Atom, TypeNode, node_synthetic } +import v2.std.node { Node, Edge, Symbol, Authored, Conj, Atom, TypeNode, node_synthetic, AdmitCallersEdge, StructuralLabel, CoreMarker } import extdeps.languages.json.emit { json_object, json_kv, json_array, json_string, json_null, JsonValue } import gunbc.scm.object_store { ObjectId, @@ -62,6 +62,8 @@ import gunbc.scm.object_store { CorpusManifestIsAuthoredSource, StoreAdmission, store_admission_store, + stored_edge_label_of, + ScmObject, } import gunbc.scm.object_table_json { ObjectTableDecodeRefusal, @@ -669,3 +671,79 @@ test fn ot_a_contained_manifest_source_at_a_manifest_stands_as_a_wrong_kind_requ } } } + +// THE WALL'S EDGE IS A CLOSED-LIST MARKER (side chat review 5439746794). The caller-admission +// carrier AdmitCallersEdge rides core_edge_labels(), the closed list this codec decodes markers +// through -- the same list object_table_json's decode_object_table folds when it reads a +// "structural_core" edge's marker lexeme. A marker the list does not name is a LOUD, typed +// refusal: the decoder answers NodeEntryUnknownLabelTag and the whole entry fails to decode -- +// a round-trip/compatibility refusal, not silent edge loss, and exactly what the wall's edge +// would hit if AdmitCallersEdge were dropped from core_edge_labels(). So the control is here, in +// the codec's own witness: an entry carrying the wall's marker must decode, and the marker must +// come home as itself -- edge_is_core names it -- not merely fail to refuse. +fn ot_admit_edge_entry(marker: String) -> JsonValue { + json_object( + members: [ + json_kv( + key: "kind", + value: json_object( + members: [ + json_kv(key: "kind", value: json_string(s: "type")), + json_kv(key: "connective", value: json_string(s: "conj")), + ] + ), + ), + json_kv( + key: "children", + value: json_array( + elements: [ + json_object( + members: [ + json_kv(key: "label", value: json_string(s: "structural_core")), + json_kv(key: "marker", value: json_string(s: marker)), + json_kv( + key: "target", + value: json_object( + members: [ + json_kv(key: "uncontained", value: json_string(s: "aabbccdd00112233")) + ] + ), + ), + ] + ), + ] + ), + ), + ] + ) +} + +// THE POSITIVE: the marker is a member of the closed list, so the entry decodes and the decoded +// edge's label is AdmitCallersEdge -- the codec hands back the wall's edge, not a stripped node. +test fn ot_the_admit_callers_edge_marker_decodes_to_itself() -> Bool { + match ot_decoded(entries: [ot_admit_edge_entry(marker: "core_admit_callers_edge")]) { + DecodeAcc { refusal: Absent, admission } => + match ot_admitted_store(admission: admission).objects { + Empty => false + Cons { head: first, tail: _ } => + match first { + SemanticNodeObject(record) => + match record.children { + Empty => false + Cons { head: edge, tail: _ } => + stored_edge_label_of(e: edge) + == StructuralLabel { label: CoreMarker { marker: AdmitCallersEdge } } + } + _ => false + } + } + DecodeAcc { refusal: Present { value: _ }, admission: _ } => false + } +} + +// THE NEGATIVE: a marker the closed list does not name refuses as an unknown label. This is the +// discriminating arm -- if AdmitCallersEdge left core_edge_labels(), the positive claim above +// would land here too. +test fn ot_a_marker_the_closed_list_does_not_name_refuses() -> Bool { + ot_tag_of(entries: [ot_admit_edge_entry(marker: "core_nonsuch_edge")]) == "label" +} diff --git a/dag/test/claim/secret_rotation_witness_test.dag b/dag/test/claim/secret_rotation_witness_test.dag index 4bc8e9b906b..23c49bdd304 100644 --- a/dag/test/claim/secret_rotation_witness_test.dag +++ b/dag/test/claim/secret_rotation_witness_test.dag @@ -552,7 +552,10 @@ test fn retirement_targets_every_enabled_version_except_the_added_one() -> Bool ]), ) { RetirementAuthorized { targets: t, destroy_permitted: d } => - (t |> count) == 2 && !d && t.first().etag == concat("etag-", v1_name) + (t |> count) == 2 && !d && match t.first() { + Present { value: x } => x.etag == concat("etag-", v1_name) + Absent => false + } RetirementRefused { cause: _ } => false } } @@ -637,7 +640,10 @@ test fn retirement_advances_when_the_prior_version_carries_an_etag() -> Bool { ]), ) { RetirementAuthorized { targets: t, destroy_permitted: _ } => - (t |> count) == 1 && t.first().etag == concat("etag-", v2_name) + (t |> count) == 1 && match t.first() { + Present { value: x } => x.etag == concat("etag-", v2_name) + Absent => false + } RetirementRefused { cause: _ } => false } } diff --git a/dag/test/claim/seeded_filler_real_execution_witness_test.dag b/dag/test/claim/seeded_filler_real_execution_witness_test.dag new file mode 100644 index 00000000000..2e646e53c23 --- /dev/null +++ b/dag/test/claim/seeded_filler_real_execution_witness_test.dag @@ -0,0 +1,94 @@ +module test.claim.seeded_filler_real_execution + +import std.types { String, NonEmptyStr, Int, Bool, FilePath } +import std.measure { ByteSize, byte_size, measure_count } +import std.optional { Present, Absent } +import extdeps.shell +import extdeps.tools.coreutils_stat +import extdeps.tools.coreutils_truncate +import extdeps.tools.gzip +import extdeps.tools.sha256sum +import extdeps.crypto.seeded_filler { seeded_filler_write, SeededFillerWritten, SeededFillerRefused } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE SEEDED FILLER BY REAL EXECUTION: truncate and openssl enc run on the host, inside a directory +// the witness mints and removes. Three facts the workspace pack relies on: the size is exact, one +// seed reproduces its bytes, and two seeds give different bytes. The red control is the reason the +// filler exists at all: a zero file of the same size -- what truncate alone would leave -- gzips to +// almost nothing, while the filler does not shrink, so a zero or patterned stand-in would understate +// what kopia and the chunk store pay to carry the real content. + +fn with_temp_dir(scenario: fn(String) -> Bool) -> Bool { + let dir = shell.Mktemp.Dir() + if dir.success == false { + false + } else { + let observed = scenario(dir.path as String) + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + observed && cleaned.success + } +} + +fn written_at(seed: String, size: Int, path: String) -> Bool { + match seeded_filler_write(seed: seed as NonEmptyStr, size: byte_size(count: size), path: path as NonEmptyStr) { + SeededFillerWritten { path: p, size: s } => (p as String) == path && measure_count(m: s) == size + SeededFillerRefused { path: _, step: _, detail: _ } => false + } +} + +fn stat_size(path: String) -> Int { + let st = coreutils.Stat.PathSize(path: path as NonEmptyStr) + match parse_int(s: replace(st.size_text, "\n", "")) { + Present { value: n } => if st.success { n } else { -1 } + Absent => -1 + } +} + +fn digest_of(path: String) -> String { + let d = sha256sum.Sha256.DigestFile(path: path as NonEmptyStr) + if d.success { substring(s: d.line, start: 0, end: 64) } else { join(["unread:", path], "") } +} + +fn gzipped_size(path: String) -> Int { + if gzip.Compress.BesideKeepBest(path: path as NonEmptyStr).success { stat_size(path: concat(path, ".gz")) } else { -1 } +} + +test fn a_seeded_filler_is_exactly_its_size_and_reproducible_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let a1 = join([dir, "/a1"], "") + let a2 = join([dir, "/a2"], "") + let one = join([dir, "/one"], "") + let empty = join([dir, "/empty"], "") + written_at(seed: "home/u/.claude/projects/p/s.jsonl", size: 70001, path: a1) + && written_at(seed: "home/u/.claude/projects/p/s.jsonl", size: 70001, path: a2) + && written_at(seed: "x", size: 1, path: one) + && written_at(seed: "x", size: 0, path: empty) + && stat_size(path: a1) == 70001 && stat_size(path: one) == 1 && stat_size(path: empty) == 0 + && digest_of(path: a1) == digest_of(path: a2) + }) +} + +test fn two_seeds_give_different_fillers_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let a = join([dir, "/a"], "") + let b = join([dir, "/b"], "") + written_at(seed: "home/u/.claude/history.jsonl", size: 4096, path: a) + && written_at(seed: "home/u/.codex/history.jsonl", size: 4096, path: b) + && digest_of(path: a) != digest_of(path: b) + }) +} + +// THE RED CONTROL: zeros of the same size compress to a sliver; the filler does not compress at all. +test fn a_zero_stand_in_compresses_and_the_filler_does_not_red_control_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let filler = join([dir, "/filler"], "") + let zeros = join([dir, "/zeros"], "") + let size = 262144 + written_at(seed: "home/u/.claude/projects/p/s.jsonl", size: size, path: filler) + && coreutils.Truncate.ToSize(size_bytes: to_string(size) as NonEmptyStr, path: zeros as NonEmptyStr).success + && gzipped_size(path: zeros) > 0 && gzipped_size(path: zeros) < size / 100 + && gzipped_size(path: filler) >= size + }) +} diff --git a/dag/test/claim/self_host_emitted_call_target_realization_witness_test.dag b/dag/test/claim/self_host_emitted_call_target_realization_witness_test.dag index 882a0809198..06b093815b1 100644 --- a/dag/test/claim/self_host_emitted_call_target_realization_witness_test.dag +++ b/dag/test/claim/self_host_emitted_call_target_realization_witness_test.dag @@ -57,7 +57,7 @@ test fn w_map_type_position_still_uses_its_declared_host_realization() -> Bool { // seed fallback, instead of silently constructing the structural algebra carrier under that type. test fn w_host_map_record_shape_refuses_instead_of_constructing_a_second_carrier() -> Bool { compile_dag_rust_emit_check( - "module test.claim.map_record_shape_probe\nimport std.types { Int, String, Map }\nimport v2.std.optional { Present }\nfn probe(m: Map, key: String, value: Int) -> Map { Map { lookup: fn(candidate) { if candidate == key { Present { value: value } } else { m.lookup(candidate) } } } }\n", + "module test.claim.map_record_shape_probe\nimport std.types { Int, String, Map }\nimport std.optional { Present }\nfn probe(m: Map, key: String, value: Int) -> Map { Map { lookup: fn(candidate) { if candidate == key { Present { value: value } } else { m.lookup(candidate) } } } }\n", "src/test_claim_map_record_shape_probe.rs", ["HashMap", "record-shaped carrier has no realization"], ["FinitelySupportedFunction", "Map {\n"] @@ -278,10 +278,10 @@ test fn w_clone_bound_forwards_transitively_over_two_call_hops() -> Bool { // row proves the qualified path reaches the DECLARATION and not the bridge. test fn w_qualified_call_emits_only_the_declaration_last_segment() -> Bool { compile_dag_rust_emit_check( - "module test.claim.qualified_call_probe\nimport std.types { Int, String, Map }\nimport std.occurrence_identity { OccurrenceId }\nimport v2.std.collection { map_get }\nimport v2.std.diagnostic { Outcome }\nimport v2.std.optional { Optional }\nfn probe(m: Map) -> Outcome> { v2.std.collection.map_get(m: m, key: \"key\") }\n", + "module test.claim.qualified_call_probe\nimport std.types { Int, String, Map }\nimport std.occurrence_identity { OccurrenceId }\nimport v2.std.collection { map_get_checked }\nimport v2.std.diagnostic { Outcome }\nimport std.optional { Optional }\nfn probe(m: Map) -> Outcome> { v2.std.collection.map_get_checked(m: m, key: \"key\") }\n", "src/test_claim_qualified_call_probe.rs", - ["crate::v2_std_collection::map_get("], - ["::v2.std.collection", "v2.std.collection.map_get("] + ["crate::v2_std_collection::map_get_checked("], + ["::v2.std.collection", "v2.std.collection.map_get_checked("] ) } @@ -696,7 +696,7 @@ test fn w_alias_of_a_keyed_collection_renders_the_host_map_template() -> Bool { // call (E0277, three sites in v2_std_collection). The bound is read from the signature. test fn w_generic_map_key_parameter_carries_the_host_key_bounds() -> Bool { compile_dag_rust_emit_check( - "module test.claim.map_key_bound_probe\nimport v2.std.collection { Map, map_lookup }\nimport v2.std.optional { Optional }\nfn probe(m: Map, key: K) -> Optional {\n map_lookup(m: m, key: key)\n}\n", + "module test.claim.map_key_bound_probe\nimport v2.std.collection { Map, map_lookup }\nimport std.optional { Optional }\nfn probe(m: Map, key: K) -> Optional {\n map_lookup(m: m, key: key)\n}\n", "src/test_claim_map_key_bound_probe.rs", ["pub fn probe Bool { compile_dag_rust_emit_check( - "module test.claim.optional_param_probe\nimport std.types { Int }\nimport v2.std.optional { Optional }\nfn inspect(opt: Optional) -> Optional { opt }\nfn caller(x: Optional) -> Optional { inspect(opt: x) }\n", + "module test.claim.optional_param_probe\nimport std.types { Int }\nimport std.optional { Optional }\nfn inspect(opt: Optional) -> Optional { opt }\nfn caller(x: Optional) -> Optional { inspect(opt: x) }\n", "src/test_claim_optional_param_probe.rs", ["inspect(x.clone())"], [".expect(\"fail-closed: an optional value flowed into non-optional parameter"] diff --git a/dag/test/claim/served_surface_browser_observation_witness_test.dag b/dag/test/claim/served_surface_browser_observation_witness_test.dag index b3ae6a72a4d..6e643645789 100644 --- a/dag/test/claim/served_surface_browser_observation_witness_test.dag +++ b/dag/test/claim/served_surface_browser_observation_witness_test.dag @@ -125,7 +125,10 @@ test fn witness_bodied_502_retains_network_document() -> Bool { test fn witness_reload_trial_claims_sufficiency_not_necessity() -> Bool { let observation = playwright_chromium_151_linux_arm64_headless_navigation_run.specimens.connection_refused - let trial = observation.recovery_trials.first() - trial.action == ExplicitReload - && trial.outcome == HealthyResponseDocumentObserved + match observation.recovery_trials.first() { + Present { value: trial } => + trial.action == ExplicitReload + && trial.outcome == HealthyResponseDocumentObserved + Absent => false + } } diff --git a/dag/test/claim/serving_routing_witness_test.dag b/dag/test/claim/serving_routing_witness_test.dag index d9928bb3401..473e44cd312 100644 --- a/dag/test/claim/serving_routing_witness_test.dag +++ b/dag/test/claim/serving_routing_witness_test.dag @@ -13,7 +13,7 @@ import gunbc.serving.admitted_model { serving_admits_id, serving_admit_advertised, ServingModelAdmitted, ServingModelNotAdmitted, } import gunbc.harness.harness_reasoning_wire { - DeepseekV4FlashOnVllm, Glm53FlashOnVllm, harness_unit_model_id, + DeepseekV4FlashOnVllm, Glm53FlashOnVllm, NemotronUltraFreeOnOpenRouter, harness_unit_model_id, harness_unit_declared_context_ceiling, } import gunbc.harness.harness_wire { harness_context_budget_for } @@ -126,7 +126,7 @@ fn w_unit_is_glm(id: String) -> Bool { match serving_admits_id(id: id) { ServingModelNotAdmitted { advertised: _ } => false ServingModelAdmitted { id: _, unit: u } => - match u { Glm53FlashOnVllm => true DeepseekV4FlashOnVllm => false } + match u { Glm53FlashOnVllm => true DeepseekV4FlashOnVllm => false NemotronUltraFreeOnOpenRouter => false } } } @@ -174,7 +174,7 @@ test fn the_admitted_id_is_the_advertised_one_and_not_the_units_canonical_id() - ServingModelAdmitted { id: i, unit: u } => (i as String) == "glm-5.3-flash" && (i as String) != (harness_unit_model_id(unit: Glm53FlashOnVllm) as String) - && (match u { Glm53FlashOnVllm => true DeepseekV4FlashOnVllm => false }) + && (match u { Glm53FlashOnVllm => true DeepseekV4FlashOnVllm => false NemotronUltraFreeOnOpenRouter => false }) ServingModelNotAdmitted { advertised: _ } => false }) && (harness_unit_model_id(unit: Glm53FlashOnVllm) as String) != "glm-5.3-flash" diff --git a/dag/test/claim/shell/shell_dag_codex_app_server_trip_script_witness_test.dag b/dag/test/claim/shell/shell_dag_codex_app_server_trip_script_witness_test.dag index 1153d4b20d1..5552275c336 100644 --- a/dag/test/claim/shell/shell_dag_codex_app_server_trip_script_witness_test.dag +++ b/dag/test/claim/shell/shell_dag_codex_app_server_trip_script_witness_test.dag @@ -50,6 +50,7 @@ fn trip_surface(codex_home: String) -> String { let invocation = codex_press_account_trip_invocation( executable: "/usr/bin/codex" as FilePath, codex_home: codex_home as FilePath, + lock: none, ) let argument_words: List = list_flat_map( xs: cli_surface_arguments(surface: process_argv_expansion_surface(expansion: invocation)), diff --git a/dag/test/claim/spark/fabric_blob_peer_witness_test.dag b/dag/test/claim/spark/fabric_blob_peer_witness_test.dag index 9574c6a52b0..cb7b2e35370 100644 --- a/dag/test/claim/spark/fabric_blob_peer_witness_test.dag +++ b/dag/test/claim/spark/fabric_blob_peer_witness_test.dag @@ -39,7 +39,10 @@ test fn the_daemon_exports_the_blob_root_read_only() -> Bool { // answers and the unit that started it owns its lifetime. test fn the_daemon_binds_the_rail_address_in_the_foreground() -> Bool { let argv = rsync_daemon_foreground_argv(address: "192.168.110.12" as NonEmptyStr, port: 873, config_path: fabric_blob_rsyncd_conf_path) - argv.first() == "rsync" + match argv.first() { + Present { value: a } => a == "rsync" + Absent => false + } && any(argv, w => w == "--daemon") && any(argv, w => w == "--no-detach") && any(argv, w => w == "--address=192.168.110.12") @@ -59,7 +62,10 @@ test fn the_peer_source_pulls_the_digest_from_the_rail_into_staging() -> Bool { match src.locator { SourceUrl { url: _ } => false SourceArgv { argv: a } => - a.first() == "rsync" + match a.first() { + Present { value: x } => x == "rsync" + Absent => false + } && any(a, w => w == "--partial") && any(a, w => w == join(["rsync://192.168.110.12:873/blobs/", fixture_hex as String], "")) && any(a, w => w == (fabric_blob_staging_path(hex: fixture_hex) as String)) diff --git a/dag/test/claim/spark/glm53_runtime_image_build_witness_test.dag b/dag/test/claim/spark/glm53_runtime_image_build_witness_test.dag index f6df3733692..df435b8ba55 100644 --- a/dag/test/claim/spark/glm53_runtime_image_build_witness_test.dag +++ b/dag/test/claim/spark/glm53_runtime_image_build_witness_test.dag @@ -83,8 +83,14 @@ test fn the_prepare_plan_lands_files_then_runs_sh_path() -> Bool { Present { value: steps } => remote_operation_count(operations: map(steps, s => s.operation)) == 4 && remote_operation_file_write_count(operations: map(steps, s => s.operation)) == 2 - && steps.first().identity == "derived-context-dir" as NonEmptyStr - && steps.skip(n: 3).first().identity == "derived-prepare-run" as NonEmptyStr + && match steps.first() { + Present { value: st } => st.identity == "derived-context-dir" as NonEmptyStr + Absent => false + } + && match steps.skip(n: 3).first() { + Present { value: st } => st.identity == "derived-prepare-run" as NonEmptyStr + Absent => false + } && match steps.skip(n: 3).first() { Absent => false Present { value: run_step } => @@ -92,8 +98,14 @@ test fn the_prepare_plan_lands_files_then_runs_sh_path() -> Bool { Absent => false Present { value: argv } => argv.length() == 2 - && argv.first() == "sh" - && argv.skip(n: 1).first() == (glm53_derived_prepare_script_path() as String) + && match argv.first() { + Present { value: w } => w == "sh" + Absent => false + } + && match argv.skip(n: 1).first() { + Present { value: w } => w == (glm53_derived_prepare_script_path() as String) + Absent => false + } } } && string_contains(s: glm53_derived_dockerfile_path() as String, pattern: glm53_derived_context_dir() as String) @@ -129,13 +141,31 @@ test fn the_save_and_load_argv_are_dockers_own_verbs() -> Bool { let save = docker_image_save_command(image: "img:tag" as NonEmptyStr, output_path: "/tmp/x.tar" as NonEmptyStr) let load = docker_image_load_command(input_path: "/tmp/x.tar" as NonEmptyStr) save.arguments.length() == 4 - && save.arguments.skip(n: 1).first() == "-o" - && save.arguments.skip(n: 2).first() == "/tmp/x.tar" - && save.arguments.skip(n: 3).first() == "img:tag" + && match save.arguments.skip(n: 1).first() { + Present { value: w } => w == "-o" + Absent => false + } + && match save.arguments.skip(n: 2).first() { + Present { value: w } => w == "/tmp/x.tar" + Absent => false + } + && match save.arguments.skip(n: 3).first() { + Present { value: w } => w == "img:tag" + Absent => false + } && load.arguments.length() == 3 - && load.arguments.first() == "load" - && load.arguments.skip(n: 1).first() == "-i" - && load.arguments.skip(n: 2).first() == "/tmp/x.tar" + && match load.arguments.first() { + Present { value: w } => w == "load" + Absent => false + } + && match load.arguments.skip(n: 1).first() { + Present { value: w } => w == "-i" + Absent => false + } + && match load.arguments.skip(n: 2).first() { + Present { value: w } => w == "/tmp/x.tar" + Absent => false + } } // THE scp OPERANDS KEEP THEIR DIRECTION: download carries the remote operand first, upload @@ -146,12 +176,27 @@ test fn the_scp_operand_direction_is_fixed() -> Bool { let down = scp_download_command(trust_words: trust, principal: "exec" as NonEmptyStr, endpoint: "h1" as NonEmptyStr, remote_path: "/tmp/r.tar" as NonEmptyStr, local_path: "/tmp/l.tar" as NonEmptyStr) let up = scp_upload_command(trust_words: trust, principal: "exec" as NonEmptyStr, endpoint: "h1" as NonEmptyStr, local_path: "/tmp/l.tar" as NonEmptyStr, remote_path: "/tmp/r.tar" as NonEmptyStr) down.arguments.length() == 4 - && down.arguments.first() == "-o" - && down.arguments.skip(n: 2).first() == "exec@h1:/tmp/r.tar" - && down.arguments.skip(n: 3).first() == "/tmp/l.tar" + && match down.arguments.first() { + Present { value: w } => w == "-o" + Absent => false + } + && match down.arguments.skip(n: 2).first() { + Present { value: w } => w == "exec@h1:/tmp/r.tar" + Absent => false + } + && match down.arguments.skip(n: 3).first() { + Present { value: w } => w == "/tmp/l.tar" + Absent => false + } && up.arguments.length() == 4 - && up.arguments.skip(n: 2).first() == "/tmp/l.tar" - && up.arguments.skip(n: 3).first() == "exec@h1:/tmp/r.tar" + && match up.arguments.skip(n: 2).first() { + Present { value: w } => w == "/tmp/l.tar" + Absent => false + } + && match up.arguments.skip(n: 3).first() { + Present { value: w } => w == "exec@h1:/tmp/r.tar" + Absent => false + } } // THE DISTRIBUTION ROSTER IS THE ARM MINUS THE SOURCE: delivering to the host that built the diff --git a/dag/test/claim/spark/rendezvous_participation_witness_test.dag b/dag/test/claim/spark/rendezvous_participation_witness_test.dag index 1f8eb73bc8a..b16a6e40ee2 100644 --- a/dag/test/claim/spark/rendezvous_participation_witness_test.dag +++ b/dag/test/claim/spark/rendezvous_participation_witness_test.dag @@ -155,11 +155,17 @@ test fn the_adrift_line_names_the_epoch_and_every_detached_host() -> Bool { test fn the_participation_query_asks_for_established_connections_to_the_exact_peer() -> Bool { let argv = ss_established_to_peer_argv(peer: t_peer) argv.length() == 8 - && argv.first() == "/usr/bin/ss" + && (match argv.first() { + Present { value: v } => v == "/usr/bin/ss" + Absent => false + }) && any(argv, a => a == "-H") && any(argv, a => a == "state") && any(argv, a => a == "established") - && argv.skip(n: 7).first() == (t_peer as String) + && (match argv.skip(n: 7).first() { + Present { value: v } => v == (t_peer as String) + Absent => false + }) } // AN EMPTY COMPLETED ENUMERATION IS ZERO, NOT A REFUSAL. This is the boundary the head/rank pair diff --git a/dag/test/claim/spark/serving_incarnation_observe_witness_test.dag b/dag/test/claim/spark/serving_incarnation_observe_witness_test.dag index 011d47b4e4e..09b35b7f872 100644 --- a/dag/test/claim/spark/serving_incarnation_observe_witness_test.dag +++ b/dag/test/claim/spark/serving_incarnation_observe_witness_test.dag @@ -265,7 +265,10 @@ test fn a_non_sha256_image_id_refuses() -> Bool { test fn the_pgrep_listing_reads_pids() -> Bool { match pgrep_pid_listing(stdout: "1809052\n1809081\n") { - PgrepPidsRead { pids: ps } => count(ps) == 2 && ps.first() == 1809052 + PgrepPidsRead { pids: ps } => count(ps) == 2 && (match ps.first() { + Present { value: v } => v == 1809052 + Absent => false + }) PgrepPidLineUnparseable { line: _ } => false } } diff --git a/dag/test/claim/spark/spark_training_ready_witness_test.dag b/dag/test/claim/spark/spark_training_ready_witness_test.dag index 32900799157..8e4da5c1f2a 100644 --- a/dag/test/claim/spark/spark_training_ready_witness_test.dag +++ b/dag/test/claim/spark/spark_training_ready_witness_test.dag @@ -3,6 +3,7 @@ module test.claim.spark.spark_training_ready_witness_test import std.types { String, Bool, List, NonEmptyStr, Int } import product.host_identity { HostIdentity } import gunbc.fleet_host_identity { operator_host_srv5, operator_host_srv6 } +import gunbc.spark.cell_role { SparkCellRoleAssignment, SparkServingCell, SparkTrainingCell } import gunbc.spark.training_ready { SparkServingProbeCapture, ProbeNeverRan, @@ -18,6 +19,7 @@ import gunbc.spark.training_ready { SparkTrainingReady, SparkTrainingNotReady, spark_training_readiness, + spark_training_readiness_in, spark_serving_absence_from_probe, spark_serving_absence_refusal, SparkServingAbsenceEvidence, @@ -133,6 +135,16 @@ fn w_pool_or_refused( } } +// THE ROSTER IS SUPPLIED, NOT READ FROM PRODUCTION. The 2026-09-02 operator decision (#10009) made +// srv6 a serving cell, so against the production roster the positive control below had no subject +// and every refusal claim over srv6 passed on role alone rather than on the axis it names. srv6 +// trains and srv5 serves in this authored roster; the production wrapper is exercised by +// w_the_production_roster_holds_no_training_cell. +data w_roster: List = [ + SparkCellRoleAssignment { host: operator_host_srv5, role: SparkServingCell }, + SparkCellRoleAssignment { host: operator_host_srv6, role: SparkTrainingCell }, +] + fn w_readiness( retirement: SparkServingRetirementObservation, meminfo_stdout: String, @@ -140,7 +152,7 @@ fn w_readiness( match w_pool_or_refused(meminfo_stdout: meminfo_stdout, after: retirement) { Absent => false Present { value: pool } => - match spark_training_readiness(pool: pool) { + match spark_training_readiness_in(pool: pool, assignments: w_roster) { SparkTrainingReady { proof: _ } => true SparkTrainingNotReady { cause: _ } => false } @@ -151,6 +163,19 @@ test fn w_a_fully_retired_training_cell_is_ready() -> Bool { w_readiness(retirement: w_retired_srv6(), meminfo_stdout: "97656250") } +// THE PRODUCTION WRAPPER READS THE PRODUCTION ROSTER, where srv6 serves since #10009: the same +// observation the positive control admits is refused there, on role. +test fn w_the_production_roster_holds_no_training_cell() -> Bool { + match w_pool_or_refused(meminfo_stdout: "97656250", after: w_retired_srv6()) { + Absent => false + Present { value: pool } => + match spark_training_readiness(pool: pool) { + SparkTrainingReady { proof: _ } => false + SparkTrainingNotReady { cause: _ } => true + } + } +} + // THE POSITIVE CONTROL'S MIRROR: THE SERVING CELL IS NEVER READY. // // srv5's retirement observation is deliberately WELL-FORMED and fully absent here. If role were not diff --git a/dag/test/claim/srv3/srv3_install_media_fetch_witness_test.dag b/dag/test/claim/srv3/srv3_install_media_fetch_witness_test.dag index bd6e7074dda..db9dcdc7294 100644 --- a/dag/test/claim/srv3/srv3_install_media_fetch_witness_test.dag +++ b/dag/test/claim/srv3/srv3_install_media_fetch_witness_test.dag @@ -51,8 +51,14 @@ test fn install_media_fetch_absent_plans_mirror_order() -> Bool { FetchRequired { install_path: p, mirror_order: mirrors } => p == srv3_ubuntu_install_media_install_path && mirrors == ubuntu_install_media_mirror_rows - && mirrors.first().host == ReleasesUbuntuCom - && mirrors.skip(n: 1).first().host == CdimageUbuntuCom + && (match mirrors.first() { + Present { value: v } => v.host == ReleasesUbuntuCom + Absent => false + }) + && (match mirrors.skip(n: 1).first() { + Present { value: v } => v.host == CdimageUbuntuCom + Absent => false + }) _ => false } } diff --git a/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag b/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag index c04a8b68a90..ee0b9b91ef0 100644 --- a/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag +++ b/dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag @@ -1,8 +1,12 @@ module test.claim.srv3_seeded_install_media_real_execution import std.logic { Bool } -import std.types { String, NonEmptyStr } -import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_grub_kernel_cmdline } +import std.types { List, String, NonEmptyStr } +import extdeps.crypto.hash { sha256_digest } +import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_grub_kernel_cmdline, srv3_seeded_install_media_input_for } +import std.optional { Present } +import extdeps.provisioning.ubuntu_seeded_install_media { NoCloudSeedArtifact, UbuntuSeededInstallMediaBuildInput } +import extdeps.provisioning.ubuntu_seeded_install_media_remaster { install_media_remaster_write_nocloud_seed, install_media_remaster_digest } import extdeps.provisioning.ubuntu_seeded_install_media_remaster { install_media_remaster_ensure_grub_cmdline, install_media_remaster_grub_cfg_with_cmdline, GrubCmdlineApplied, GrubCmdlineRefused } import extdeps.provisioning.ubuntu_install_media { noble_numbat_2404_3_live_server_arm64_grub_cfg } import gunbc.host_command_binding { LocalCommandHost } @@ -36,3 +40,49 @@ test fn install_media_remaster_ensure_grub_cmdline_inserts_by_real_execution() - && wrote_other.success && refused_other && other_after.success && !string_contains(s: other_after.content, pattern: cmdline as String) && cleaned.success } + +fn with_seed_artifacts(input: UbuntuSeededInstallMediaBuildInput, artifacts: List) -> UbuntuSeededInstallMediaBuildInput { + UbuntuSeededInstallMediaBuildInput { + stock_artifact: input.stock_artifact, + nocloud_dir_on_iso: input.nocloud_dir_on_iso, + user_data_body: input.user_data_body, + seed_files: input.seed_files, + seed_artifacts: artifacts, + grub_kernel_cmdline: input.grub_kernel_cmdline, + volume_id: input.volume_id, + pinned_dates: input.pinned_dates, + image_stem: input.image_stem, + builder_revision: input.builder_revision, + } +} + +// A BINARY SEED MEMBER, BY REAL EXECUTION: a safe basename is fetched (file:// through the same curl +// the build uses), read back under the seed directory and admitted; the same bytes under a wrong +// digest refuse; and an escaping name refuses BEFORE the seed directory exists, so no fetch ran and +// nothing landed outside the seed. +test fn a_seed_artifact_is_fetched_read_back_and_an_escaping_name_moves_no_byte_by_real_execution() -> Bool { + let dir = shell.Mktemp.Dir() + let root = dir.path as String + let source = concat(root, "/runner.tar.gz") + let wrote = Filesystem.Write(path: source, content: "runner tarball bytes") + let base = srv3_seeded_install_media_input_for(user_data_body: "#cloud-config\n" as NonEmptyStr) + let digest = match install_media_remaster_digest(host: LocalCommandHost, path: source as NonEmptyStr) { Present { value: d } => d Absent => "unread" } + let url = concat("file://", source) as NonEmptyStr + let safe_work = concat(root, "/safe") as NonEmptyStr + let safe = install_media_remaster_write_nocloud_seed(host: LocalCommandHost, work_dir: safe_work, input: with_seed_artifacts(input: base, artifacts: [NoCloudSeedArtifact { name: "runner.tar.gz", source_url: url, digest: sha256_digest(hex: digest as NonEmptyStr) }])) + let landed = Filesystem.Read(path: concat(safe_work as String, "/", base.nocloud_dir_on_iso as String, "/runner.tar.gz")) + let wrong_work = concat(root, "/wrong") as NonEmptyStr + let wrong = install_media_remaster_write_nocloud_seed(host: LocalCommandHost, work_dir: wrong_work, input: with_seed_artifacts(input: base, artifacts: [NoCloudSeedArtifact { name: "runner.tar.gz", source_url: url, digest: sha256_digest(hex: "0000000000000000000000000000000000000000000000000000000000000000") }])) + let escape_work = concat(root, "/escape") as NonEmptyStr + let escape = install_media_remaster_write_nocloud_seed(host: LocalCommandHost, work_dir: escape_work, input: with_seed_artifacts(input: base, artifacts: [NoCloudSeedArtifact { name: "../../boot/grub/grub.cfg", source_url: url, digest: sha256_digest(hex: digest as NonEmptyStr) }])) + let escape_seed = Filesystem.Read(path: concat(escape_work as String, "/", base.nocloud_dir_on_iso as String, "/user-data")) + let escaped = Filesystem.Read(path: concat(escape_work as String, "/boot/grub/grub.cfg")) + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + dir.success && wrote.success && digest != "unread" + && (match safe { Absent => true Present { value: _ } => false }) + && landed.success && landed.content == "runner tarball bytes" + && (match wrong { Present { value: r } => string_contains(s: r, pattern: "read back sha256") Absent => false }) + && (match escape { Present { value: r } => string_contains(s: r, pattern: "not a single path component") Absent => false }) + && !escape_seed.success && !escaped.success + && cleaned.success +} diff --git a/dag/test/claim/superseded_run_starvation_census_witness_test.dag b/dag/test/claim/superseded_run_starvation_census_witness_test.dag index e5da6157f55..96091a15fd6 100644 --- a/dag/test/claim/superseded_run_starvation_census_witness_test.dag +++ b/dag/test/claim/superseded_run_starvation_census_witness_test.dag @@ -213,6 +213,8 @@ data job_cancelled: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Cancelled }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:01:00Z" }, started_at: Present { value: "2026-09-01T00:10:00Z" }, completed_at: Present { value: "2026-09-01T00:20:00Z" }, @@ -226,6 +228,8 @@ data job_failed: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Failure }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:00:00Z" }, started_at: Present { value: "2026-09-01T00:00:00Z" }, completed_at: Present { value: "2026-09-01T00:15:00Z" }, @@ -239,6 +243,8 @@ data job_success: WorkflowJobRun = WorkflowJobRun { status: Completed, conclusion: Present { value: Success }, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:02:00Z" }, started_at: Present { value: "2026-09-01T00:05:00Z" }, completed_at: Present { value: "2026-09-01T00:10:00Z" }, @@ -252,6 +258,8 @@ data job_queued: WorkflowJobRun = WorkflowJobRun { status: Queued, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:03:00Z" }, started_at: none, completed_at: none, @@ -265,6 +273,8 @@ data job_in_progress: WorkflowJobRun = WorkflowJobRun { status: InProgress, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: Present { value: "2026-09-01T00:04:00Z" }, started_at: Present { value: "2026-09-01T00:30:00Z" }, completed_at: none, @@ -339,6 +349,8 @@ test fn earliest_created_at_returns_none_when_no_created_at() -> Bool { status: InProgress, conclusion: none, labels: [], + runner_id: none, + runner_name: none, created_at: none, started_at: none, completed_at: none, diff --git a/dag/test/claim/supplier/oracle_oci_always_free_ensure_witness_test.dag b/dag/test/claim/supplier/oracle_oci_always_free_ensure_witness_test.dag new file mode 100644 index 00000000000..b7a7ae82c18 --- /dev/null +++ b/dag/test/claim/supplier/oracle_oci_always_free_ensure_witness_test.dag @@ -0,0 +1,332 @@ +module test.claim.supplier.oracle_oci_always_free_ensure_witness + +import std.measure { gigabyte_count, second, second_count } +import std.types { Bool, Int, List, NonEmptyStr, String } +import extdeps.languages.json.emit { JsonValue, JsonArray, JsonObject, JsonNull, JsonBool, JsonNumber, JsonString } +import extdeps.languages.json.parse { parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable } +import gunbc.oracle_oci.api { OciListRead, OciListed, OciListRefused, OciListInaccessible, OciAnswered, classify_oci_list } +import extdeps.network.ipv4 { ipv4_same_network, render_ipv4_prefix } +import std.optional { Absent, Present } +import gunbc.oracle_oci.tenancy_usage { + OciUsagePart, UsagePart, UsagePartRefused, oci_instance_usage, oci_volume_usage, oci_usage_part_add, +} +import gunbc.oracle_oci.network_ensure { + OciStep, OciStepReady, OciStepStopped, oci_admit_wired, + oci_vcn_wiring, oci_igw_wiring, oci_access_disagreement, + oci_free_vcn_cidr, oci_free_subnet_cidr, + OciNamedDecision, NamedReady, NamedCreate, NamedStop, + oci_named_decision, oci_named_resource_of, oci_route_table_wiring, oci_subnet_wiring, +} +import gunbc.oracle_oci.instance_ensure { + OciInstanceDecision, InstanceHeld, InstanceLaunch, InstanceStop, + oci_instance_decision, oci_observed_instance_of, oci_free_plan, oci_launch_body, + oci_query_value, OciQueryEncoded, OciQueryRefused, oci_out_of_host_capacity, oci_free_authorized_keys, + oci_select_offering, oci_domain_answer, DomainOffers, DomainLacks, DomainReadRefused, + oci_attachments_disagreement, oci_answer_retires_record, oci_parse_launch_record, oci_launch_record_text, + oci_replay_window_open, OciLaunchRecorded, OciLaunchRecordMalformed, + CandidateDomains, CandidateDomainsRefused, +} +import product.supplier.oracle_oci { FitsAlwaysFree, ExceedsAlwaysFree, oci_always_free_fit } + +// The subject is each decision point of the Always Free converge at its list boundary, over supplied +// list answers shaped as Oracle's SDK models them. The signed exchange runs only live, on srv1. Its +// receipt is the inhabitance evidence for these boundaries, and these rows are what it must agree with. + +fn json_items(text: String) -> List { + match parse_json_document(s: text) { + JsonDocumentUnreadable { gap } => [] + JsonDocumentParsed { value } => match value { + JsonArray { elements } => elements + JsonObject { members } => [] + JsonNull => [] + JsonBool { value: b } => [] + JsonNumber { lexeme } => [] + JsonString { value: s } => [] + } + } +} + +fn one(text: String) -> JsonValue { + match json_items(text: join(["[", text, "]"], "")).first() { + Present { value: v } => v + Absent => JsonNull + } +} + +fn named(display: String, text: String) -> String { + match oci_named_decision(kind: "vcn", display_name: display, read: OciListed { items: json_items(text: text) }) { + NamedReady { resource } => concat("ready:", resource.id) + NamedCreate => "create" + NamedStop { line } => "stop" + } +} + +// ── THE OPERATOR'S PLAN ─────────────────────────────────────────────────────────────────────── + +test fn the_operators_plan_fits_the_always_free_allowance() -> Bool { + match oci_always_free_fit(plan: oci_free_plan) { + FitsAlwaysFree => true + ExceedsAlwaysFree { breaches } => false + } +} + +// ── LISTS ───────────────────────────────────────────────────────────────────────────────────── + +// Absence needs a complete list: an answer carrying opc-next-page refuses, even when its page is empty. +test fn a_continued_list_refuses_and_a_finished_one_lists() -> Bool { + let continued = match classify_oci_list(operation: "List", exchange: OciAnswered { status: 200, body: "[]", next_page: "t" }) { + OciListRefused { cause } => true + OciListed { items } => false + OciListInaccessible { status, body } => false + } + let finished = match classify_oci_list(operation: "List", exchange: OciAnswered { status: 200, body: "[{\"id\":\"x\"}]", next_page: "" }) { + OciListed { items } => items.length() == 1 + OciListRefused { cause } => false + OciListInaccessible { status, body } => false + } + continued && finished +} + +// A network row missing a required field refuses the read. It does not read as "none live", which +// would create a duplicate. +test fn a_malformed_network_row_stops_rather_than_creating() -> Bool { + named(display: "gunbc-always-free", text: "[{\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"AVAILABLE\"}]") == "stop" + && named(display: "gunbc-always-free", text: "[{}]") == "stop" +} + +// ── NETWORK ─────────────────────────────────────────────────────────────────────────────────── + +test fn a_network_resource_is_created_only_when_none_live_has_the_name() -> Bool { + named(display: "gunbc-always-free", text: "[]") == "create" + && named(display: "gunbc-always-free", text: "[{\"id\":\"v1\",\"displayName\":\"other\",\"lifecycleState\":\"AVAILABLE\"}]") == "create" + && named(display: "gunbc-always-free", text: "[{\"id\":\"v1\",\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"TERMINATED\"}]") == "create" +} + +test fn an_available_namesake_is_ready_and_a_provisioning_one_stops() -> Bool { + named(display: "gunbc-always-free", text: "[{\"id\":\"v1\",\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"AVAILABLE\"}]") == "ready:v1" + && named(display: "gunbc-always-free", text: "[{\"id\":\"v1\",\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"PROVISIONING\"}]") == "stop" + && named(display: "gunbc-always-free", text: "[{\"id\":\"v1\",\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"AVAILABLE\"},{\"id\":\"v2\",\"displayName\":\"gunbc-always-free\",\"lifecycleState\":\"AVAILABLE\"}]") == "stop" +} + +test fn a_route_table_is_admitted_only_if_it_reaches_this_gateway() -> Bool { + let good = oci_named_resource_of(v: one(text: "{\"id\":\"rt\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"routeRules\":[{\"destination\":\"0.0.0.0/0\",\"destinationType\":\"CIDR_BLOCK\",\"networkEntityId\":\"igw1\"}]}")) + let other = oci_named_resource_of(v: one(text: "{\"id\":\"rt\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"routeRules\":[{\"destination\":\"0.0.0.0/0\",\"destinationType\":\"CIDR_BLOCK\",\"networkEntityId\":\"igw2\"}]}")) + let empty = oci_named_resource_of(v: one(text: "{\"id\":\"rt\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"routeRules\":[]}")) + oci_route_table_wiring(r: good, igw: "igw1") == "" + && oci_route_table_wiring(r: other, igw: "igw1") != "" + && oci_route_table_wiring(r: empty, igw: "igw1") != "" +} + +test fn a_subnet_is_admitted_only_on_this_route_table() -> Bool { + let s = oci_named_resource_of(v: one(text: "{\"id\":\"sn\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"vcnId\":\"v1\",\"routeTableId\":\"rt1\",\"cidrBlock\":\"10.0.0.0/24\",\"prohibitPublicIpOnVnic\":false}")) + oci_subnet_wiring(r: s, vcn: "v1", route_table: "rt1") == "" && oci_subnet_wiring(r: s, vcn: "v1", route_table: "rt2") != "" +} + +// ── INSTANCES ───────────────────────────────────────────────────────────────────────────────── + +fn decide_a1(text: String) -> String { + match oci_free_plan.skip(n: 2).first() { + Absent => "no-plan" + Present { value: p } => match oci_instance_decision(planned: p, observed: map(json_items(text: text), i => oci_observed_instance_of(v: i))) { + InstanceHeld { line } => "held" + InstanceLaunch => "launch" + InstanceStop { line } => "stop" + } + } +} + +test fn the_a1_instance_launches_when_absent_and_holds_when_running() -> Bool { + decide_a1(text: "[]") == "launch" + && decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"TERMINATED\",\"shape\":\"VM.Standard.A1.Flex\"}]") == "launch" + && decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\",\"shapeConfig\":{\"ocpus\":2.0,\"memoryInGBs\":12.0}}]") == "held" +} + +// A stopped instance was stopped by someone, and a wrong-shaped namesake is not this instance. Neither +// is started, reshaped or replaced. +test fn a_stopped_or_wrong_shaped_namesake_stops_rather_than_launching() -> Bool { + decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"STOPPED\",\"shape\":\"VM.Standard.A1.Flex\",\"shapeConfig\":{\"ocpus\":2.0,\"memoryInGBs\":12.0}}]") == "stop" + && decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.E2.1.Micro\"}]") == "stop" +} + +// The query values go through extdeps.uri's encoder: an availability domain's colon and the image +// query's space come out percent-encoded, and an empty value refuses before any read is sent. +test fn query_values_are_encoded_by_the_shared_uri_encoder() -> Bool { + let colon = match oci_query_value(value: "Uocm:US-ASHBURN-AD-1") { OciQueryEncoded { wire } => wire == "Uocm%3AUS-ASHBURN-AD-1" OciQueryRefused { line } => false } + let space = match oci_query_value(value: "Oracle Linux") { OciQueryEncoded { wire } => wire == "Oracle%20Linux" OciQueryRefused { line } => false } + let empty = match oci_query_value(value: "") { OciQueryRefused { line } => true OciQueryEncoded { wire } => false } + colon && space && empty +} + +test fn out_of_host_capacity_is_read_from_oracles_500_text_only() -> Bool { + oci_out_of_host_capacity(status: 500, body: "{\"code\":\"InternalError\",\"message\":\"Out of host capacity.\"}") + && !oci_out_of_host_capacity(status: 500, body: "{\"code\":\"InternalError\",\"message\":\"something else\"}") + && !oci_out_of_host_capacity(status: 400, body: "Out of host capacity.") +} + +// The A1 launch carries its shapeConfig and both keys. The micro launch carries no shapeConfig, +// because its size is fixed. +test fn launch_bodies_carry_shape_config_only_for_the_flex_shape() -> Bool { + let micro = match oci_free_plan.first() { Present { value: p } => oci_launch_body(planned: p, compartment: "c", subnet: "s", domain: "AD-1", image: "img") Absent => "" } + let a1 = match oci_free_plan.skip(n: 2).first() { Present { value: p } => oci_launch_body(planned: p, compartment: "c", subnet: "s", domain: "AD-1", image: "img") Absent => "" } + !string_contains(s: micro, pattern: "shapeConfig") + && string_contains(s: a1, pattern: "\"shapeConfig\": {\"ocpus\": 2, \"memoryInGBs\": 12}") + && string_contains(s: a1, pattern: "\"bootVolumeSizeInGBs\": 50") + && string_contains(s: oci_free_authorized_keys(), pattern: "fleet-automation@gunbc") + && string_contains(s: oci_free_authorized_keys(), pattern: "macbook-m4") +} + +// A refused ListShapes read is not "this domain lacks the shape" (review 76366). One refused domain +// refuses the selection with its cause, even beside a domain that answered and offers the shape. +// With every read answered, the offering domain is selected. +test fn a_refused_shape_read_refuses_the_selection_rather_than_reading_as_lacking() -> Bool { + let refused = match oci_select_offering(answers: [oci_domain_answer(domain: "AD-1", offer: DomainReadRefused { line: "ListShapes for AD-1 refused: 500" }), oci_domain_answer(domain: "AD-2", offer: DomainOffers)]) { + CandidateDomainsRefused { line } => string_contains(s: line, pattern: "AD-1") + CandidateDomains { domains } => false + } + let selected = match oci_select_offering(answers: [oci_domain_answer(domain: "AD-1", offer: DomainLacks), oci_domain_answer(domain: "AD-2", offer: DomainOffers)]) { + CandidateDomains { domains } => domains == ["AD-2"] + CandidateDomainsRefused { line } => false + } + refused && selected +} + +// The subnet is inside the VCN: its network shares the VCN's network at the VCN's prefix, and its +// prefix is at least as long. The rendered CIDR text is what the create bodies send. +test fn the_subnet_sits_inside_the_vcn() -> Bool { + let contained = match ipv4_same_network(a: oci_free_subnet_cidr.network, b: oci_free_vcn_cidr.network, prefix: oci_free_vcn_cidr.length) { + Present { value } => value + Absent => false + } + contained + && oci_free_subnet_cidr.length >= oci_free_vcn_cidr.length + && render_ipv4_prefix(p: oci_free_vcn_cidr) == "10.0.0.0/16" + && render_ipv4_prefix(p: oci_free_subnet_cidr) == "10.0.0.0/24" +} + +// ── TENANCY USAGE ───────────────────────────────────────────────────────────────────────────── + +fn usage_text(u: OciUsagePart) -> String { + match u { + UsagePart { a1_ocpus, a1_memory, micros, block } => join([to_string(a1_ocpus), to_string(gigabyte_count(g: a1_memory)), to_string(micros), to_string(gigabyte_count(g: block))], "/") + UsagePartRefused { line } => "refused" + } +} + +// What each live instance holds: an A1 its shapeConfig OCPUs and memory, read exactly from Oracle's +// float numerals; a micro one micro. A terminated instance holds nothing. A stopped one still counts. +test fn instances_contribute_their_allocation_and_terminated_ones_nothing() -> Bool { + usage_text(u: oci_instance_usage(row: one(text: "{\"id\":\"i\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\",\"shapeConfig\":{\"ocpus\":2.0,\"memoryInGBs\":12.0}}"))) == "2/12/0/0" + && usage_text(u: oci_instance_usage(row: one(text: "{\"id\":\"i\",\"lifecycleState\":\"STOPPED\",\"shape\":\"VM.Standard.E2.1.Micro\"}"))) == "0/0/1/0" + && usage_text(u: oci_instance_usage(row: one(text: "{\"id\":\"i\",\"lifecycleState\":\"TERMINATED\",\"shape\":\"VM.Standard.A1.Flex\"}"))) == "0/0/0/0" +} + +// A fractional or missing allocation refuses rather than being rounded or read as zero, and a refusal +// absorbs every later sum, so a usage total can never silently omit a row. +test fn an_unreadable_allocation_refuses_and_stays_refused_through_the_sum() -> Bool { + let fractional = oci_instance_usage(row: one(text: "{\"id\":\"i\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\",\"shapeConfig\":{\"ocpus\":1.5,\"memoryInGBs\":6}}")) + let missing = oci_instance_usage(row: one(text: "{\"id\":\"i\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\"}")) + usage_text(u: fractional) == "refused" + && usage_text(u: missing) == "refused" + && usage_text(u: oci_usage_part_add(a: oci_volume_usage(row: one(text: "{\"id\":\"v\",\"lifecycleState\":\"AVAILABLE\",\"sizeInGBs\":50}")), b: fractional)) == "refused" +} + +// Volumes hold storage in every state but TERMINATED, including a boot volume a terminated instance +// left behind. +test fn volumes_hold_storage_until_terminated() -> Bool { + usage_text(u: oci_volume_usage(row: one(text: "{\"id\":\"v\",\"lifecycleState\":\"AVAILABLE\",\"sizeInGBs\":50}"))) == "0/0/0/50" + && usage_text(u: oci_volume_usage(row: one(text: "{\"id\":\"v\",\"lifecycleState\":\"TERMINATED\",\"sizeInGBs\":50}"))) == "0/0/0/0" +} + +// A running A1 namesake is held only at the planned size. A different size, or a size that cannot be +// read, stops rather than being taken as converged (side-chat review on gunbc#13351). +test fn an_a1_namesake_is_held_only_at_the_planned_size() -> Bool { + decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\",\"shapeConfig\":{\"ocpus\":1.0,\"memoryInGBs\":6.0}}]") == "stop" + && decide_a1(text: "[{\"id\":\"i\",\"displayName\":\"gunbc-free-a1\",\"lifecycleState\":\"RUNNING\",\"shape\":\"VM.Standard.A1.Flex\"}]") == "stop" +} + +// One admission for found and read-back resources: wiring that disagrees stops, whichever path +// reached it (side-chat review on gunbc#13351). +test fn wiring_admission_stops_on_any_disagreement() -> Bool { + let r = oci_named_resource_of(v: one(text: "{\"id\":\"rt\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\"}")) + let ok = match oci_admit_wired(kind: "route table", display_name: "p", resource: r, disagreement: "", how: "created and AVAILABLE on read-back") { OciStepReady { id, line } => true OciStepStopped { line } => false } + let bad = match oci_admit_wired(kind: "route table", display_name: "p", resource: r, disagreement: "route table has no rule sending 0.0.0.0/0 to igw1", how: "created and AVAILABLE on read-back") { OciStepStopped { line } => true OciStepReady { id, line } => false } + ok && bad +} + +// The network is admitted on what it claims to provide: a disabled gateway, a private subnet, a subnet +// with another range, and a VCN without the planned range each stop, where only the route table was +// checked before (side-chat review and review 78205 on gunbc#13351). +test fn the_network_is_admitted_on_enabled_public_planned_properties() -> Bool { + let igw_on = oci_named_resource_of(v: one(text: "{\"id\":\"g\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"vcnId\":\"v1\",\"isEnabled\":true}")) + let igw_off = oci_named_resource_of(v: one(text: "{\"id\":\"g\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"vcnId\":\"v1\",\"isEnabled\":false}")) + let private = oci_named_resource_of(v: one(text: "{\"id\":\"sn\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"vcnId\":\"v1\",\"routeTableId\":\"rt1\",\"cidrBlock\":\"10.0.0.0/24\",\"prohibitPublicIpOnVnic\":true}")) + let other_range = oci_named_resource_of(v: one(text: "{\"id\":\"sn\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"vcnId\":\"v1\",\"routeTableId\":\"rt1\",\"cidrBlock\":\"10.0.9.0/24\",\"prohibitPublicIpOnVnic\":false}")) + let vcn_ok = oci_named_resource_of(v: one(text: "{\"id\":\"v1\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"cidrBlocks\":[\"10.0.0.0/16\"]}")) + let vcn_other = oci_named_resource_of(v: one(text: "{\"id\":\"v1\",\"displayName\":\"p\",\"lifecycleState\":\"AVAILABLE\",\"cidrBlocks\":[\"192.168.0.0/16\"]}")) + oci_igw_wiring(r: igw_on, vcn: "v1") == "" && oci_igw_wiring(r: igw_off, vcn: "v1") != "" + && oci_subnet_wiring(r: private, vcn: "v1", route_table: "rt1") != "" + && oci_subnet_wiring(r: other_range, vcn: "v1", route_table: "rt1") != "" + && oci_vcn_wiring(r: vcn_ok) == "" && oci_vcn_wiring(r: vcn_other) != "" +} + +// Access rules are read, not assumed: SSH ingress from anywhere plus all egress admits; a list without +// the SSH rule, or a named list that was not read, stops. +test fn subnet_access_rules_are_admitted_only_when_read_and_sufficient() -> Bool { + let good = one(text: "{\"id\":\"sl1\",\"ingressSecurityRules\":[{\"protocol\":\"6\",\"source\":\"0.0.0.0/0\",\"tcpOptions\":{\"destinationPortRange\":{\"min\":22,\"max\":22}}}],\"egressSecurityRules\":[{\"protocol\":\"all\",\"destination\":\"0.0.0.0/0\"}]}") + let no_ssh = one(text: "{\"id\":\"sl1\",\"ingressSecurityRules\":[{\"protocol\":\"6\",\"source\":\"0.0.0.0/0\",\"tcpOptions\":{\"destinationPortRange\":{\"min\":443,\"max\":443}}}],\"egressSecurityRules\":[{\"protocol\":\"all\",\"destination\":\"0.0.0.0/0\"}]}") + let subnet = one(text: "{\"id\":\"sn\",\"securityListIds\":[\"sl1\"]}") + oci_access_disagreement(subnet_row: subnet, lists: [good]) == "" + && oci_access_disagreement(subnet_row: subnet, lists: [no_ssh]) != "" + && oci_access_disagreement(subnet_row: subnet, lists: []) != "" +} + +// A held instance must be attached to the selected subnet: an otherwise identical instance on another +// subnet, or with no attached VNIC, stops (side-chat review on gunbc#13351). +test fn a_held_instance_must_be_attached_to_the_selected_subnet() -> Bool { + oci_attachments_disagreement(rows: json_items(text: "[{\"id\":\"a\",\"lifecycleState\":\"ATTACHED\",\"subnetId\":\"sn1\"}]"), subnet: "sn1") == "" + && oci_attachments_disagreement(rows: json_items(text: "[{\"id\":\"a\",\"lifecycleState\":\"ATTACHED\",\"subnetId\":\"sn2\"}]"), subnet: "sn1") != "" + && oci_attachments_disagreement(rows: json_items(text: "[{\"id\":\"a\",\"lifecycleState\":\"DETACHED\",\"subnetId\":\"sn1\"}]"), subnet: "sn1") != "" +} + +// THE LAUNCH RECORD'S LIFECYCLE (side-chat review on gunbc#13351). Only the run that created a record, +// on its first send, retires it on a definitive non-creation. A re-send never retires it: an auth +// rejection on a re-send after an unknown original keeps the record. A 5xx, a throttle or a conflict +// keeps it on any send. +test fn only_a_first_send_definitive_refusal_retires_the_record() -> Bool { + oci_answer_retires_record(first_send: true, status: 400, body: "{}") + && oci_answer_retires_record(first_send: true, status: 500, body: "{\"message\":\"Out of host capacity.\"}") + && !oci_answer_retires_record(first_send: false, status: 401, body: "{}") + && !oci_answer_retires_record(first_send: false, status: 403, body: "{}") + && !oci_answer_retires_record(first_send: false, status: 400, body: "{}") + && !oci_answer_retires_record(first_send: true, status: 409, body: "{}") + && !oci_answer_retires_record(first_send: true, status: 429, body: "{}") + && !oci_answer_retires_record(first_send: true, status: 503, body: "{}") +} + +// The record round-trips its first-send time and exact body, and a record outside the retry-token +// window is not re-sent. +test fn the_record_carries_its_send_time_and_expires_from_replay() -> Bool { + let round = match oci_parse_launch_record(content: oci_launch_record_text(sent_at: second(count: 1000), body: "{\"a\": 1}\n{\"b\": 2}")) { + OciLaunchRecorded { sent_at, body } => second_count(s: sent_at) == 1000 && body == "{\"a\": 1}\n{\"b\": 2}" + OciLaunchRecordMalformed { cause } => false + } + let malformed = match oci_parse_launch_record(content: "not-a-time\n{}") { + OciLaunchRecordMalformed { cause } => true + OciLaunchRecorded { sent_at, body } => false + } + round && malformed + && oci_replay_window_open(sent_at: second(count: 1000), now: second(count: 1000 + 3600)) + && !oci_replay_window_open(sent_at: second(count: 1000), now: second(count: 1000 + 82800)) + && !oci_replay_window_open(sent_at: second(count: 1000), now: second(count: 999)) +} + +// An SSH rule restricted to a source port does not admit a client connecting from an ephemeral port; +// the same rule without the source restriction does (side-chat review on gunbc#13351). +test fn a_source_port_restricted_ssh_rule_is_not_admitted() -> Bool { + let subnet = one(text: "{\"id\":\"sn\",\"securityListIds\":[\"sl1\"]}") + let egress = "\"egressSecurityRules\":[{\"protocol\":\"all\",\"destination\":\"0.0.0.0/0\"}]" + let restricted = one(text: join(["{\"id\":\"sl1\",\"ingressSecurityRules\":[{\"protocol\":\"6\",\"source\":\"0.0.0.0/0\",\"tcpOptions\":{\"sourcePortRange\":{\"min\":22,\"max\":22},\"destinationPortRange\":{\"min\":22,\"max\":22}}}],", egress, "}"], "")) + let full = one(text: join(["{\"id\":\"sl1\",\"ingressSecurityRules\":[{\"protocol\":\"6\",\"source\":\"0.0.0.0/0\",\"tcpOptions\":{\"sourcePortRange\":{\"min\":1,\"max\":65535},\"destinationPortRange\":{\"min\":22,\"max\":22}}}],", egress, "}"], "")) + oci_access_disagreement(subnet_row: subnet, lists: [restricted]) != "" + && oci_access_disagreement(subnet_row: subnet, lists: [full]) == "" +} diff --git a/dag/test/claim/systemd/service_exec_test.dag b/dag/test/claim/systemd/service_exec_test.dag new file mode 100644 index 00000000000..2b3494ee072 --- /dev/null +++ b/dag/test/claim/systemd/service_exec_test.dag @@ -0,0 +1,39 @@ +module test.claim.systemd.service_exec_test + +import std.types { String, List, Bool } +import extdeps.systemd.service_exec { + ExecLiteral, ExecVariable, ExecWordRendered, ExecWordRefused, exec_word_render, + InvocationIdVariable, ExitStatusVariable, + ExecCommandLineRendered, ExecCommandLineRefused, exec_command_line, +} +import extdeps.tools.gnu_coreutils { mkdir_create_only_command } + +test fn exec_word_spells_variables_as_braced_expansions() -> Bool { + match exec_word_render(parts: [ExecLiteral { text: "/spool/a+" }, ExecVariable { variable: InvocationIdVariable }, ExecLiteral { text: "+" }, ExecVariable { variable: ExitStatusVariable }]) { + ExecWordRendered { word } => (word as String) == "/spool/a+$\{INVOCATION_ID}+$\{EXIT_STATUS}" + ExecWordRefused { reason: _ } => false + } +} + +test fn exec_word_refuses_a_literal_that_would_be_read_as_syntax() -> Bool { + let dollar = match exec_word_render(parts: [ExecLiteral { text: "/spool/$HOME" }]) { ExecWordRefused { reason: _ } => true ExecWordRendered { word: _ } => false } + let space = match exec_word_render(parts: [ExecLiteral { text: "/spool/a b" }]) { ExecWordRefused { reason: _ } => true ExecWordRendered { word: _ } => false } + let percent = match exec_word_render(parts: [ExecLiteral { text: "/spool/%n" }]) { ExecWordRefused { reason: _ } => true ExecWordRendered { word: _ } => false } + dollar && space && percent +} + +test fn exec_command_line_admits_a_rendered_expansion_and_refuses_a_bare_one() -> Bool { + let admitted = match exec_command_line(command: mkdir_create_only_command(path: "/spool/a+$\{INVOCATION_ID}")) { + ExecCommandLineRendered { line } => (line as String) == "mkdir -- /spool/a+$\{INVOCATION_ID}" + ExecCommandLineRefused { reason: _ } => false + } + let bare = match exec_command_line(command: mkdir_create_only_command(path: "/spool/$EXIT_STATUS")) { + ExecCommandLineRefused { reason: _ } => true + ExecCommandLineRendered { line: _ } => false + } + let unmodeled = match exec_command_line(command: mkdir_create_only_command(path: "/spool/$\{HOME}")) { + ExecCommandLineRefused { reason: _ } => true + ExecCommandLineRendered { line: _ } => false + } + admitted && bare && unmodeled +} diff --git a/dag/test/claim/systemd_unit_file_witness_test.dag b/dag/test/claim/systemd_unit_file_witness_test.dag index b105e301f20..500e39912d1 100644 --- a/dag/test/claim/systemd_unit_file_witness_test.dag +++ b/dag/test/claim/systemd_unit_file_witness_test.dag @@ -6,6 +6,7 @@ import extdeps.systemd.unit_file { SystemdUnitFile, ServiceUnitFile, TimerUnitFile, + PathUnitFile, PathUnit, DirectoryNotEmpty, SystemdInstallSection, NotInstallable, Installable, @@ -231,6 +232,22 @@ test fn the_live_belt_timer_serializes_to_exactly_its_deployed_bytes() -> Bool { ) == "[Unit]\nDescription=gunbc roadmap belt tick cadence\n\n[Timer]\nUnit=gunbc-srv1-green-belt.service\nOnBootSec=2min\nOnUnitInactiveSec=60s\nAccuracySec=5s\n\n[Install]\nWantedBy=timers.target\n" } +// The path-unit form (systemd.path(5)): a [Path] section between [Unit] and [Install], with Unit= +// naming the activated unit and the condition rendered under its own key -- the shape the belt's exit +// inbox path unit is emitted in (gunbc.live_deploy.emit live_deploy_belt_exit_path_unit_file). +test fn a_path_unit_serializes_with_its_path_section() -> Bool { + serialize_systemd_unit_file( + file: PathUnitFile { + unit: [Description { text: "gunbc roadmap belt: a unit exit was recorded" as NonEmptyStr }], + path: [ + PathUnit { target: "gunbc-srv1-green-belt-exit.service" as NonEmptyStr }, + DirectoryNotEmpty { path: "/opt/gunbc/attempt-exit-spool/inbox" as NonEmptyStr }, + ], + install: Installable { directives: [WantedBy { target: "paths.target" as NonEmptyStr }] }, + }, + ) == "[Unit]\nDescription=gunbc roadmap belt: a unit exit was recorded\n\n[Path]\nUnit=gunbc-srv1-green-belt-exit.service\nDirectoryNotEmpty=/opt/gunbc/attempt-exit-spool/inbox\n\n[Install]\nWantedBy=paths.target\n" +} + // `TimerUnit` renders as `Unit=`, not `TimerUnit=` — the same variant-name-versus-wire-name trap // `ServiceType` carries, and the one most likely to leak here because [Unit] is also a section name. test fn the_timer_unit_directive_renders_as_the_bare_unit_key() -> Bool { diff --git a/dag/test/claim/tailscale_acl_phase2_design_witness_test.dag b/dag/test/claim/tailscale_acl_phase2_design_witness_test.dag index 45705eadc8e..ec661074565 100644 --- a/dag/test/claim/tailscale_acl_phase2_design_witness_test.dag +++ b/dag/test/claim/tailscale_acl_phase2_design_witness_test.dag @@ -3,13 +3,18 @@ module test.claim.tailscale_acl_phase2_design_witness data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly test fn witness_secret_homed_in_gunbai_secrets() -> Bool { - operator_tailscale_acl_secret_bindings.first().project == bmc_secrets_project_id - && operator_tailscale_acl_secret_bindings.first().secret == (tailscale_acl_ephemeral_token_secret_id as String) + match operator_tailscale_acl_secret_bindings.first() { + Present { value: b } => b.project == bmc_secrets_project_id && b.secret == (tailscale_acl_ephemeral_token_secret_id as String) + Absent => false + } } test fn witness_operator_user_principal_accessor_only() -> Bool { operator_bindings_are_user_principal_accessor_only(bindings: operator_tailscale_acl_secret_bindings) - && operator_tailscale_acl_secret_bindings.first().role == role_secretmanager_secret_accessor + && match operator_tailscale_acl_secret_bindings.first() { + Present { value: b } => b.role == role_secretmanager_secret_accessor + Absent => false + } && operator_principal_member() == fleet_operator_gcp_iam_member() && string_contains(s: operator_principal_member(), pattern: tailscale_acl_operator_email() as String) && tailscale_acl_operator_email() == fleet_operator_email diff --git a/dag/test/claim/target_invocation_witness_test.dag b/dag/test/claim/target_invocation_witness_test.dag index c919bfbf374..fb72e21a86a 100644 --- a/dag/test/claim/target_invocation_witness_test.dag +++ b/dag/test/claim/target_invocation_witness_test.dag @@ -507,8 +507,18 @@ test fn the_instrument_target_and_its_binding_are_the_same_identity() -> Bool { LabelNonCanonical { cause: _ } => false }) && (render_label(l: t.label) == differential_operand()) - && ((instrument_targets() |> count) == 8) - && ((instrument_bindings() |> count) == 8) + && registry_and_bindings_are_one_population() +} + +// THE REGISTRY AND ITS BINDINGS ARE ONE POPULATION, JOINED AT IDENTITY GRAIN. Completeness is an +// identity join and never a count (DESIGN section 5): a literal count of the live registry is a +// measurement copied from the tree it measures, and it went false the first time a row was added. +// Both directions are asserted, so a target with no binding AND a binding for no registered +// target each fail -- a count equality passes a registry that swapped one row for another. +fn registry_and_bindings_are_one_population() -> Bool { + let bound = instrument_bindings() |> map(b => b.target) + fold(instrument_targets(), init: true, f: fn(acc, t) { acc && label_in(labels: bound, wanted: t) }) + && fold(bound, init: true, f: fn(acc, l) { acc && label_in(labels: instrument_targets(), wanted: l) }) } // THE ABSENCE PROOF, MEASURED OVER THE DERIVED POPULATION RATHER THAN ASSERTED IN PROSE. @@ -524,15 +534,16 @@ test fn the_instrument_target_and_its_binding_are_the_same_identity() -> Bool { // // THE PLANNED SITE'S MODULE PATH MUST SIT INSIDE THE STATIC GATE. Since the 2026-08-29 bankruptcy // ruling, `v2.workflow.required_floor` `required_gate_prefixes` is a static roster and a site -// outside it dispositions `DeclinedOutsideRequiredGate` -- which is what happened to the original -// `test.claim.some_witness` fixture path: the presence conjunct above went silently red while the -// module itself sat off-gate and unexecuted. `v2.test.` is a rostered prefix, so the fixture's -// planned site plans again; a future roster shrink that drops the prefix reds this test loudly, -// which is the control working rather than a new fragility. +// outside it dispositions `DeclinedOutsideRequiredGate`, so the presence conjunct below goes +// silently red while the fixture itself sits off-gate. This fixture has been bitten twice: the +// original `test.claim.some_witness` path, and then `v2.test.some_witness`, whose `v2.test.` prefix +// the revert of gunbc#12582 (gunbc#12852) removed from the roster. The path now rides the +// `test.claim.infer_` family row, which has been in the roster throughout; a future roster change +// that drops it reds the planned-site conjunct, which is the control working. fn census_fixture_sites() -> List { [ DiscoveredSite { - module_path: "v2.test.some_witness", + module_path: "test.claim.infer_some_witness", function: "a_planned_site", reads_live_tree: SubstrateInputsOnly {} }, @@ -546,7 +557,7 @@ fn census_fixture_sites() -> List { fn planned_site_label() -> Label { Label { - package: PackagePath { segments: ["v2", "test", "some_witness"] } + package: PackagePath { segments: ["test", "claim", "infer_some_witness"] } target: TargetName { name: "a_planned_site" } } } diff --git a/dag/test/claim/tmux_session_list_outcome_witness_test.dag b/dag/test/claim/tmux_session_list_outcome_witness_test.dag index e6769e4ca2d..1fbe915252f 100644 --- a/dag/test/claim/tmux_session_list_outcome_witness_test.dag +++ b/dag/test/claim/tmux_session_list_outcome_witness_test.dag @@ -87,10 +87,22 @@ test fn witness_exit_ok_parses_sessions() -> Bool { match classify_tmux_session_list(exit_ok: true, stdout: output, stderr: "") { TmuxSessionsListed { entries } => count(entries) == 2 - && entries.first().session_name == "alpha" - && entries.first().windows == 1 - && entries.skip(n: 1).first().session_name == "beta" - && entries.skip(n: 1).first().windows == 2 + && (match entries.first() { + Present { value: v } => v.session_name == "alpha" + Absent => false + }) + && (match entries.first() { + Present { value: v } => v.windows == 1 + Absent => false + }) + && (match entries.skip(n: 1).first() { + Present { value: v } => v.session_name == "beta" + Absent => false + }) + && (match entries.skip(n: 1).first() { + Present { value: v } => v.windows == 2 + Absent => false + }) TmuxNoServer { evidence: _ } => false TmuxListUnknowable { stderr: _ } => false TmuxListStdoutRefused { line_number: _, raw: _, reason: _ } => false @@ -102,7 +114,10 @@ test fn witness_ls_output_parse_matches_classifier() -> Bool { match parse_tmux_ls_output(output: output) { TmuxLsParsed { entries } => count(entries) == 1 - && entries.first().session_name == "gunbc-dispatch-node-4" + && (match entries.first() { + Present { value: v } => v.session_name == "gunbc-dispatch-node-4" + Absent => false + }) TmuxLsParseRefused { line_number: _, raw: _, reason: _ } => false } } diff --git a/dag/test/claim/transport_emission_not_modeled_witness_test.dag b/dag/test/claim/transport_emission_not_modeled_witness_test.dag index 56ab6bc2bf3..129884b024d 100644 --- a/dag/test/claim/transport_emission_not_modeled_witness_test.dag +++ b/dag/test/claim/transport_emission_not_modeled_witness_test.dag @@ -58,6 +58,16 @@ data unmodeled_output_key_source: String = "module tfx_key\nservice Fk \{\n ope data payload_verb_without_content_source: String = "module tfx_payload\nservice Fp \{\n operation Read \{\n input \{ path: String \}\n output \{ content: String from \"content\" \}\n readonly\n transport file \{ path: \"\{path\}\" \}\n \}\n operation Seal \{\n input \{ path: String \}\n output \{ success: Bool from \"write_success\" \}\n transport file \{ path: \"\{path\}\", verb: \"write_owner_only\" \}\n \}\n\}\n" +// The rust shell handler now realizes the declared stderr capture channels +// (truncation + both byte counts) from WitnessStderrCapturePolicy. The mixed +// row keeps the unmodeled-key RED beside that emission so a handler that +// answered every sibling, or a wall that poisoned every sibling, still fails. +data shell_stderr_capture_source: String = "module tfx_cap\nimport std.shell_stream_capture \{ WitnessStderrCapturePolicy, BoundedTail \}\nimport std.measure \{ byte_size \}\nservice Bin \{\n operation Run \{\n input \{\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail \{ bytes: byte_size(count: 16) \}\n \}\n output \{\n success: Bool from \"exit_success\"\n stderr: String from \"stderr\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + +data shell_stderr_capture_beside_unmodeled_key_source: String = "module tfx_cap_mix\nimport std.shell_stream_capture \{ WitnessStderrCapturePolicy, BoundedTail \}\nimport std.measure \{ byte_size \}\nservice Bin \{\n operation Run \{\n input \{\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail \{ bytes: byte_size(count: 16) \}\n \}\n output \{\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n operation Weird \{\n input \{ bin_path: String \}\n output \{ digest: String from \"stderr_digest_hex\" \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + +data shell_stderr_capture_without_policy_input_source: String = "module tfx_cap_nop\nservice Bin \{\n operation Run \{\n input \{ bin_path: String \}\n output \{\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + fn not_modeled_blocking_count(source: String) -> Int { match compile_dag_diagnostic_census(source) { CensusObserved { rows: rows } => @@ -104,3 +114,19 @@ test fn w_unmodeled_output_channel_refuses_only_its_own_operation() -> Bool { test fn w_payload_verb_without_content_input_refuses_only_its_own_operation() -> Bool { not_modeled_blocking_count(source: payload_verb_without_content_source) == 1 } + +test fn w_declared_shell_stderr_capture_channels_emit() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_source) == 0 +} + +test fn w_declared_shell_stderr_capture_module_is_clean_of_all_blocking_diagnostics() -> Bool { + blocking_diagnostic_count(source: shell_stderr_capture_source) == 0 +} + +test fn w_unmodeled_shell_channel_refuses_beside_realized_capture_channels() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_beside_unmodeled_key_source) == 1 +} + +test fn w_capture_channel_without_stderr_capture_input_refuses() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_without_policy_input_source) == 1 +} diff --git a/dag/test/claim/v1_interpreter_primitive_roster_acceptance_test.dag b/dag/test/claim/v1_interpreter_primitive_roster_acceptance_test.dag index e75dbb81390..4ec30f6ed7a 100644 --- a/dag/test/claim/v1_interpreter_primitive_roster_acceptance_test.dag +++ b/dag/test/claim/v1_interpreter_primitive_roster_acceptance_test.dag @@ -42,7 +42,6 @@ fn known_dispatch_sites_contribute_rows() -> Bool { "eval_algebra_method_inner", "eval_call", "try_v2_std_collection_map_primitive_grounding", - "try_parse_table_memo_dispatch", ] symbols |> all(s => v1_interpreter_authored_roster_arms() |> any(r => r.dispatch_symbol == s)) } diff --git a/dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag b/dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag index ffcd82ce880..b166e16464d 100644 --- a/dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag +++ b/dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag @@ -60,7 +60,6 @@ test fn every_derived_site_contributes_rows() -> Bool { "eval_algebra_method_inner", "eval_call", "try_v2_std_collection_map_primitive_grounding", - "try_parse_table_memo_dispatch", ] symbols |> all(s => v1_interpreter_authored_roster_arms() |> any(r => r.dispatch_symbol == s)) } diff --git a/dag/test/claim/variant_literal_expected_application_witness_test.dag b/dag/test/claim/variant_literal_expected_application_witness_test.dag new file mode 100644 index 00000000000..eae2cec9a55 --- /dev/null +++ b/dag/test/claim/variant_literal_expected_application_witness_test.dag @@ -0,0 +1,18 @@ +module test.claim.variant_literal_expected_application_witness_test + +import std.types { Bool } + +// A variant literal of a generic owner takes the expected APPLICATION of that owner as its type +// (v1.compiler.infer variant_literal_expected_application). The specimen is the shape of +// extdeps.dns.domain_name parse_dns_label_list: a fold in result position seeded with a bare +// constructor, whose step pushes onto the accumulator's payload. Typed as the uninstantiated +// declaration, the payload binding carried the formal `ok` and the pipe reached the emitter as a +// plain `.list_push(` method no Rust type has (E0599). Typed as the declared return's application, +// the payload is a List and the pipe lowers to the runtime list primitive. The excluded +// `Outcome` keeps the declaration's formals from reappearing anywhere in the module. + +data vlea_fold_source: String = "module vlea_fold\n\ntype Outcome = Good \{ value: ok } | Bad \{ value: err }\n\nfn collect_nonempty(xs: List) -> Outcome, String> \{\n fold(xs, init: Good \{ value: [] }, f: (acc, x) => match acc \{\n Bad \{ value: e } => Bad \{ value: e }\n Good \{ value: items } => if x == \"\" \{ Bad \{ value: \"empty\" } } else \{ Good \{ value: items |> list_push(x) } }\n })\n}\n" + +test fn a_constructor_seeded_fold_in_result_position_takes_the_declared_application() -> Bool { + compile_dag_rust_emit_check(vlea_fold_source, "src/vlea_fold.rs", ["rc_list_push("], [".list_push(", "Outcome"]) +} diff --git a/dag/test/claim/vllm_engine_args_render_witness_test.dag b/dag/test/claim/vllm_engine_args_render_witness_test.dag index 78222a6b5c4..6b3faf74f80 100644 --- a/dag/test/claim/vllm_engine_args_render_witness_test.dag +++ b/dag/test/claim/vllm_engine_args_render_witness_test.dag @@ -55,7 +55,13 @@ test fn an_explicit_utilization_renders_the_flag_and_its_value() -> Bool { utilization: Present { value: basis_point(count: 9200) }, kv_cache_bytes: none, }) - length(argv) == 2 && argv.first() == "--gpu-memory-utilization" && argv.skip(n: 1).first() == "0.92" + length(argv) == 2 && (match argv.first() { + Present { value: v } => v == "--gpu-memory-utilization" + Absent => false + }) && (match argv.skip(n: 1).first() { + Present { value: v } => v == "0.92" + Absent => false + }) } // EXPLICIT KV BYTES RENDER AS AN EXACT INTEGER, never as a rounded rendering: this is the one term in @@ -65,7 +71,13 @@ test fn explicit_kv_bytes_render_exactly() -> Bool { utilization: none, kv_cache_bytes: Present { value: byte_size(count: 23372437156) }, }) - length(argv) == 2 && argv.first() == "--kv-cache-memory" && argv.skip(n: 1).first() == "23372437156" + length(argv) == 2 && (match argv.first() { + Present { value: v } => v == "--kv-cache-memory" + Absent => false + }) && (match argv.skip(n: 1).first() { + Present { value: v } => v == "23372437156" + Absent => false + }) } // ── PREFIX CACHING: THREE STATES, THREE RENDERINGS ───────────────────────────────────────────── @@ -79,10 +91,16 @@ test fn an_omitted_prefix_caching_request_renders_no_flag() -> Bool { test fn an_explicit_prefix_caching_request_renders_the_flag_the_pair_route_sends() -> Bool { let argv = vllm_prefix_caching_argv(r: PrefixCachingExplicit { enabled: true }) - length(argv) == 1 && argv.first() == "--enable-prefix-caching" + length(argv) == 1 && (match argv.first() { + Present { value: v } => v == "--enable-prefix-caching" + Absent => false + }) } test fn an_explicit_disable_renders_the_negative_flag_rather_than_silence() -> Bool { let argv = vllm_prefix_caching_argv(r: PrefixCachingExplicit { enabled: false }) - length(argv) == 1 && argv.first() == "--no-enable-prefix-caching" + length(argv) == 1 && (match argv.first() { + Present { value: v } => v == "--no-enable-prefix-caching" + Absent => false + }) } diff --git a/dag/test/claim/witness_deferral_freeze_witness_test.dag b/dag/test/claim/witness_deferral_freeze_witness_test.dag index efedc35db53..27d0b8e9276 100644 --- a/dag/test/claim/witness_deferral_freeze_witness_test.dag +++ b/dag/test/claim/witness_deferral_freeze_witness_test.dag @@ -273,15 +273,15 @@ test fn witness_deferral_freeze_live_roster_is_well_formed() -> Bool { test fn witness_deferral_freeze_commit_roster_enrollment_admits() -> Bool { witness_has_executing_consumer_for_row( - entry: "src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag", - function: "witness_content_key_subject_stable" + entry: "src/v2/test/claim/long/parse_binding_fidelity_witness_test.dag", + function: "witness_resolve_distinct_param_bindings_holds" ) } test fn witness_deferral_freeze_commit_roster_row_is_not_frozen() -> Bool { !frozen_path_deferral_covers( - entry: "src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag", - function: "witness_content_key_subject_stable" + entry: "src/v2/test/claim/long/parse_binding_fidelity_witness_test.dag", + function: "witness_resolve_distinct_param_bindings_holds" ) } diff --git a/dag/test/claim/worker_turn_dispatch_witness_test.dag b/dag/test/claim/worker_turn_dispatch_witness_test.dag new file mode 100644 index 00000000000..2280b8a2026 --- /dev/null +++ b/dag/test/claim/worker_turn_dispatch_witness_test.dag @@ -0,0 +1,172 @@ +module test.claim.worker_turn_dispatch_witness_test + +import std.types { Bool, List, NonEmptyStr, String } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.optional { Present, Absent } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.roadmap_launch_admission { launch_identity_exemplar } +import gunbc.roadmap_belt_actuate { belt_snapshot_disposition, SnapshotConsumedByChild, SnapshotDiscardRequired, SpawnFailed } +import gunbc.roadmap_dashboard_instance { HostDashboardInstance, srv1_live_dashboard_instance, srv2_lab_dashboard_instance } +import gunbc.roadmap_dispatch_actuator { + DispatchProviderInnerArgv, + DispatchProviderInnerArgvReady, + CodexProviderExecutableAbsent, + CursorProviderOffCustodyHost, + CursorProviderCredentialUnbound, + CodexProviderOffCustodyHost, + ClaudeProviderCredentialUnbound, + GunbcHarnessSpawnNeedsInstance, + CursorDispatchProvider, + CodexDispatchProvider, + DispatchCliProvider, + dispatch_provider_inner_argv_for_instance, + dispatch_credential_snapshot_path_for_instance, +} +import extdeps.llm.cli { ReasoningMedium, ProviderAccountDefaultModel } +import gunbc.cursor_harness_credential { cursor_worker_turn_api_key_host_path } +import gunbc.codex_harness_credential { codex_harness_turn_lock_path, codex_worker_turn_codex_home } +import gunbc.host_layout { srv2_roadmap_codex_home } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE PRODUCTION SPAWN FOLD for a dashboard instance, with nothing supplied but the instance: these +// claims exercise dispatch_provider_inner_argv_for_instance over the declared srv1 and srv2 instances, +// so a change that sends a Cursor or Codex turn to an ambient login, or to a host that does not hold +// the credential, reds here. +fn inner_for(instance: HostDashboardInstance, provider: DispatchCliProvider) -> DispatchProviderInnerArgv { + dispatch_provider_inner_argv_for_instance( + instance: instance, + provider: provider, + effort: ReasoningMedium, + model: ProviderAccountDefaultModel, + session_uuid: "00000000-0000-4000-8000-000000000000", + node_id: "node-worker-turn" as NonEmptyStr as RoadmapNodeId, + attempt_key: "0123456789abcdef", + worktree_path: "/tmp/w", + brief: "brief", + alignment_chain_fingerprint: "fp", + ) +} + +fn ready_argv(inner: DispatchProviderInnerArgv) -> List? { + match inner { + DispatchProviderInnerArgvReady { argv } => Present { value: argv } + CodexProviderExecutableAbsent { reason: _ } => none + CursorProviderOffCustodyHost { reason: _ } => none + CursorProviderCredentialUnbound { reason: _ } => none + CodexProviderOffCustodyHost { reason: _ } => none + ClaudeProviderCredentialUnbound { reason: _ } => none + GunbcHarnessSpawnNeedsInstance { reason: _ } => none + } +} + +fn first_word_is(argv: List, word: String) -> Bool { + match argv.first() { + Present { value: w } => w == word + Absent => false + } +} + +fn has_pair(argv: List, flag: String, value: String) -> Bool { + string_contains(s: join(argv, separator: "\0"), pattern: join(["\0", flag, "\0", value], "")) +} + +// THE CURSOR ROUTE on the custody host: the key is exported inside the child from the attempt's +// credential snapshot, never the mutable custody file (the shared sh wrapper is the first word and the +// snapshot path is its argument), the agent runs +// unattended in print mode against the attempt worktree, and no word of the argv is --api-key. +test fn a_cursor_turn_on_srv2_binds_the_custody_key_through_the_env_wrapper() -> Bool { + match ready_argv(inner: inner_for(instance: srv2_lab_dashboard_instance(), provider: CursorDispatchProvider)) { + Absent => false + Present { value: argv } => + first_word_is(argv: argv, word: "sh") + && any(argv, a => a == dispatch_credential_snapshot_path_for_instance( + instance: srv2_lab_dashboard_instance(), + node_id: "node-worker-turn" as NonEmptyStr as RoadmapNodeId, + attempt_key: "0123456789abcdef", + provider: CursorDispatchProvider, + ) as String) + && !any(argv, a => a == cursor_worker_turn_api_key_host_path as String) + && any(argv, a => a == "cursor-agent") + && any(argv, a => a == "--print") + && any(argv, a => a == "--force") + && any(argv, a => a == "--trust") + && has_pair(argv: argv, flag: "--sandbox", value: "enabled") + && has_pair(argv: argv, flag: "--workspace", value: "/tmp/w") + && !any(argv, a => a == "--api-key") + } +} + +test fn a_cursor_turn_off_its_custody_host_refuses() -> Bool { + match inner_for(instance: srv1_live_dashboard_instance(), provider: CursorDispatchProvider) { + CursorProviderOffCustodyHost { reason } => string_contains(s: reason as String, pattern: "srv2") + CursorProviderCredentialUnbound { reason: _ } => false + DispatchProviderInnerArgvReady { argv: _ } => false + CodexProviderExecutableAbsent { reason: _ } => false + CodexProviderOffCustodyHost { reason: _ } => false + ClaudeProviderCredentialUnbound { reason: _ } => false + GunbcHarnessSpawnNeedsInstance { reason: _ } => false + } +} + +// THE CODEX ROUTE on the custody host: the whole command runs under the exclusive non-blocking flock +// on the custody lock, with conflict exit 75, and CODEX_HOME is the custody directory -- never the +// instance's ambient roadmap login, which is the arm this replaced. +test fn a_codex_turn_on_srv2_runs_under_the_flock_on_the_custody_codex_home() -> Bool { + match inner_for(instance: srv2_lab_dashboard_instance(), provider: CodexDispatchProvider) { + DispatchProviderInnerArgvReady { argv } => + first_word_is(argv: argv, word: "flock") + && any(argv, a => a == "--exclusive") + && any(argv, a => a == "--nonblock") + && has_pair(argv: argv, flag: "--conflict-exit-code", value: "75") + && any(argv, a => a == codex_harness_turn_lock_path as String) + && any(argv, a => a == concat("CODEX_HOME=", codex_worker_turn_codex_home as String)) + && !any(argv, a => a == concat("CODEX_HOME=", srv2_roadmap_codex_home as String)) + CodexProviderExecutableAbsent { reason: _ } => false + CursorProviderOffCustodyHost { reason: _ } => false + CursorProviderCredentialUnbound { reason: _ } => false + CodexProviderOffCustodyHost { reason: _ } => false + ClaudeProviderCredentialUnbound { reason: _ } => false + GunbcHarnessSpawnNeedsInstance { reason: _ } => false + } +} + +test fn a_codex_turn_off_its_custody_host_refuses() -> Bool { + match inner_for(instance: srv1_live_dashboard_instance(), provider: CodexDispatchProvider) { + CodexProviderOffCustodyHost { reason } => string_contains(s: reason as String, pattern: "srv2") + DispatchProviderInnerArgvReady { argv: _ } => false + CodexProviderExecutableAbsent { reason: _ } => false + CursorProviderOffCustodyHost { reason: _ } => false + CursorProviderCredentialUnbound { reason: _ } => false + ClaudeProviderCredentialUnbound { reason: _ } => false + GunbcHarnessSpawnNeedsInstance { reason: _ } => false + } +} + +// THE CURSOR ABANDONMENT CONTROL. The snapshot the cursor spawn loads is the provider-labelled path +// the belt discards for every post-selection outcome but a launched child +// (gunbc.roadmap_belt_actuate belt_snapshot_owned_outcome), and the spawn's wrapper refuses the exec +// (exit 79) when it cannot remove that snapshot. If the cursor spawn read any other path, the belt's +// discard would miss it and an abandoned attempt would leave the key on disk. +test fn an_abandoned_cursor_attempt_leaves_no_snapshot_the_belt_does_not_discard() -> Bool { + let belt_path = dispatch_credential_snapshot_path_for_instance( + instance: srv2_lab_dashboard_instance(), + node_id: "node-worker-turn" as NonEmptyStr as RoadmapNodeId, + attempt_key: "0123456789abcdef", + provider: CursorDispatchProvider, + ) + let id = launch_identity_exemplar() + let abandoned_discards = match belt_snapshot_disposition( + outcome: SpawnFailed { node_id: "node-worker-turn", step: "cursor-provider-preflight", detail: "refused", provider: "cursor", launch: id }, + ) { + SnapshotDiscardRequired => true + SnapshotConsumedByChild => false + } + match ready_argv(inner: inner_for(instance: srv2_lab_dashboard_instance(), provider: CursorDispatchProvider)) { + Absent => false + Present { value: argv } => + abandoned_discards + && any(argv, a => a == belt_path as String) + && any(argv, a => string_contains(s: a, pattern: "|| exit 79")) + } +} diff --git a/dag/test/claim/worker_turn_standing_witness_test.dag b/dag/test/claim/worker_turn_standing_witness_test.dag new file mode 100644 index 00000000000..56d7e45f269 --- /dev/null +++ b/dag/test/claim/worker_turn_standing_witness_test.dag @@ -0,0 +1,192 @@ +module test.claim.worker_turn_standing_witness_test + +import std.types { Bool, List, NonEmptyStr, String, FilePath } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.optional { Present, Absent } +import std.claim_evidence { RecordedFactId } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.roadmap_dashboard_instance { srv1_live_dashboard_instance, srv2_lab_dashboard_instance } +import gunbc.codex_app_server_press { parse_account_trip_stdout_for_tests, CodexAccountStandingEvidence } +import gunbc.provider_standing_probe_bridge { provider_standing_bundle_from_codex_account_evidence } +import gunbc.provider_standing { + ProviderStanding, + ProviderStandingUnobserved, + ProviderStandingFromFact, + InstallationReady, + ToolEnvironmentReady, + LimitDispatchDrawAutomatic, + provider_standing_from_observed_bundle, + provider_standing_is_selection_eligible, +} +import gunbc.dispatch_selection { + ProviderInventory, + OfferCodex, + OfferClaude, + OfferCursor, + provider_inventory_for_instance, + inventory_with_observed_standing, + inventory_with_codex_state_root, + resolve_dispatch_selection, + provider_selection_request_for_kind, + DispatchSelectionResolved, + DispatchSelectionRefused, + NoDerivedSizingExpectation, + dispatch_codex_process_fingerprint_unobserved_selection_refusal, +} +import extdeps.llm.cli_lifecycle { CodexCliProvider, ProviderInstance } +import gunbc.codex_harness_credential { codex_harness_credential_context, codex_worker_turn_codex_home } +import gunbc.cursor_harness_credential { cursor_status_reading } +import gunbc.worker_turn_standing { cursor_standing_from_status, cursor_entitlement_trip_pending_reason } +import gunbc.roadmap_dispatch_actuator { + dispatch_actuator_worker_turn_selection_observed, + DispatchActuatorSelectionOk, + DispatchActuatorSelectionRefused, + CodexDispatchProvider, + CursorDispatchProvider, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUPPLIED AT THE TRIP BOUNDARY: the account trip's stdout, in the wire shapes the press witnesses +// already pin (test.claim.codex_app_server_press). The healthy trip is a chatgpt/pro account with +// capacity left in the codex bucket; the exhausted one carries the live 2026-08-06 rateLimits +// capture verbatim (usedPercent 100, rate_limit_reached). The inhabitance of the trip itself -- the +// app-server run against the custody CODEX_HOME -- is the live exercise once custody lands. +data initialize_line: String = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"userAgent\":\"x\"}}\n" +data account_read_line: String = "{\"jsonrpc\":\"2.0\",\"id\":2,\"result\":{\"account\":{\"type\":\"chatgpt\",\"planType\":\"pro\",\"email\":\"REDACTED@example.invalid\"},\"requiresOpenaiAuth\":true}}\n" +data rate_limits_capacity_line: String = "{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"rateLimits\":{\"limitId\":\"codex\",\"primary\":{\"usedPercent\":12},\"secondary\":{\"usedPercent\":3},\"spendControlReached\":false}}}\n" +data rate_limits_exhausted_line: String = "{\"jsonrpc\":\"2.0\",\"id\":3,\"result\":{\"rateLimits\":{\"limitId\":\"codex\",\"limitName\":null,\"primary\":{\"usedPercent\":100,\"windowDurationMins\":10080,\"resetsAt\":1786159940},\"secondary\":null,\"spendControlReached\":false,\"planType\":\"pro\",\"rateLimitReachedType\":\"rate_limit_reached\"}}}\n" + +fn trip_evidence(rate_limits_line: String) -> CodexAccountStandingEvidence { + parse_account_trip_stdout_for_tests(stdout: concat(initialize_line, concat(account_read_line, rate_limits_line))) +} + +fn observed_codex_standing(rate_limits_line: String) -> ProviderStanding { + match first_codex_instance(inventory: provider_inventory_for_instance(instance: srv2_lab_dashboard_instance())) { + Absent => ProviderStandingUnobserved { reason: "no codex offer on srv2 lab" as NonEmptyStr } + Present { value: provider } => + provider_standing_from_observed_bundle( + fact_id: "witness-codex-harness-trip" as RecordedFactId, + provider: provider, + bundle: provider_standing_bundle_from_codex_account_evidence( + evidence: trip_evidence(rate_limits_line: rate_limits_line), + credential: codex_harness_credential_context(), + installation: InstallationReady, + tool_environment: ToolEnvironmentReady, + ), + ) + } +} + +fn first_codex_instance(inventory: ProviderInventory) -> ProviderInstance? { + fold(inventory.offers, init: none, f: (acc, offer) => + match acc { + Present { value: _ } => acc + Absent => + match offer { + OfferCodex { offer: o } => Present { value: o.instance } + OfferClaude { offer: _ } => acc + OfferCursor { offer: _ } => acc + } + } + ) +} + +fn codex_selection_reason(standing: ProviderStanding) -> String { + match resolve_dispatch_selection( + inventory: inventory_with_codex_state_root( + inventory: inventory_with_observed_standing(inventory: provider_inventory_for_instance(instance: srv2_lab_dashboard_instance()), kind: CodexCliProvider, standing: standing), + account_state_root: codex_worker_turn_codex_home as FilePath, + ), + request: provider_selection_request_for_kind(kind: CodexCliProvider), + sizing_expectation: NoDerivedSizingExpectation, + ) { + DispatchSelectionResolved { receipt: _ } => "RESOLVED" + DispatchSelectionRefused { sizing_expectation: _, requested_selection: _, reason } => reason as String + } +} + +// THE OBSERVED STANDING REACHES SELECTION. A healthy custody trip makes the codex offer eligible, so +// selection passes eligibility and stops at the next gate, the unobserved process fingerprint (kept by +// operator ruling D1, 2026-10-05, until the wet tmux observation on srv2 is taken). The exhausted trip +// is refused at eligibility instead, with a different reason. If the observation were not reaching +// selection, the two would refuse for the same reason. +test fn a_healthy_custody_trip_passes_eligibility_and_an_exhausted_one_does_not() -> Bool { + let healthy = codex_selection_reason(standing: observed_codex_standing(rate_limits_line: rate_limits_capacity_line)) + let exhausted = codex_selection_reason(standing: observed_codex_standing(rate_limits_line: rate_limits_exhausted_line)) + healthy == (dispatch_codex_process_fingerprint_unobserved_selection_refusal as String) + && exhausted != healthy + && exhausted != "RESOLVED" +} + +test fn the_exhausted_custody_trip_is_not_selection_eligible_and_the_healthy_one_is() -> Bool { + provider_standing_is_selection_eligible( + standing: observed_codex_standing(rate_limits_line: rate_limits_capacity_line), + draw: LimitDispatchDrawAutomatic, + ) + && !provider_standing_is_selection_eligible( + standing: observed_codex_standing(rate_limits_line: rate_limits_exhausted_line), + draw: LimitDispatchDrawAutomatic, + ) +} + +// THE CODEX OFFER IS JUDGED ON THE CUSTODY CODEX_HOME, not the instance's ambient account state root, +// so changing the ambient login cannot change the verdict or the root the spawn binds. +test fn the_observed_codex_offer_names_the_custody_codex_home() -> Bool { + let inventory = inventory_with_codex_state_root( + inventory: inventory_with_observed_standing(inventory: provider_inventory_for_instance(instance: srv2_lab_dashboard_instance()), kind: CodexCliProvider, standing: observed_codex_standing(rate_limits_line: rate_limits_capacity_line)), + account_state_root: codex_worker_turn_codex_home as FilePath, + ) + any(inventory.offers, offer => + match offer { + OfferCodex { offer: o } => (o.account_state_root as String) == (codex_worker_turn_codex_home as String) + OfferClaude { offer: _ } => false + OfferCursor { offer: _ } => false + }) +} + +// CURSOR: a rejected key refuses naming the rejection; an accepted key is still not selectable, +// because entitlement waits on the one-word trip (D2). Neither arm is "available". +data observed_unauthenticated_status: String = "{\n \"status\": \"unauthenticated\",\n \"isAuthenticated\": false,\n \"hasAccessToken\": false,\n \"hasRefreshToken\": false,\n \"message\": \"Not logged in\"\n}" +data supplied_authenticated_status: String = "{\"status\":\"authenticated\",\"isAuthenticated\":true}" + +test fn cursor_status_never_makes_the_offer_selectable_before_the_entitlement_trip() -> Bool { + let accepted = cursor_standing_from_status(reading: cursor_status_reading(stdout: supplied_authenticated_status)) + let rejected = cursor_standing_from_status(reading: cursor_status_reading(stdout: observed_unauthenticated_status)) + let accepted_pending = match accepted { + ProviderStandingUnobserved { reason } => reason == cursor_entitlement_trip_pending_reason + ProviderStandingFromFact { fact: _ } => false + } + let rejected_named = match rejected { + ProviderStandingUnobserved { reason } => string_contains(s: reason as String, pattern: "rejects") + ProviderStandingFromFact { fact: _ } => false + } + accepted_pending && rejected_named + && !provider_standing_is_selection_eligible(standing: accepted, draw: LimitDispatchDrawAutomatic) +} + +// THE ROUTE, through the production actuator selection: on an instance that does not hold the +// credentials, both observed draws refuse before any observation runs or any spawn is planned. +test fn worker_turn_draws_on_a_non_custody_instance_refuse() -> Bool { + let codex = match dispatch_actuator_worker_turn_selection_observed( + instance: srv1_live_dashboard_instance(), + provider: CodexDispatchProvider, + node_id: "node-worker-turn" as NonEmptyStr as RoadmapNodeId, + attempt_key: "0123456789abcdef", + sizing_expectation: NoDerivedSizingExpectation, + ) { + DispatchActuatorSelectionRefused { reason: _ } => true + DispatchActuatorSelectionOk { provider: _, effort: _, model: _, process_fingerprint: _ } => false + } + let cursor = match dispatch_actuator_worker_turn_selection_observed( + instance: srv1_live_dashboard_instance(), + provider: CursorDispatchProvider, + node_id: "node-worker-turn" as NonEmptyStr as RoadmapNodeId, + attempt_key: "0123456789abcdef", + sizing_expectation: NoDerivedSizingExpectation, + ) { + DispatchActuatorSelectionRefused { reason: _ } => true + DispatchActuatorSelectionOk { provider: _, effort: _, model: _, process_fingerprint: _ } => false + } + codex && cursor +} diff --git a/dag/test/claim/workspace_checkpoint_instrument_witness_test.dag b/dag/test/claim/workspace_checkpoint_instrument_witness_test.dag index d3d9bfe29e1..ee9f77f24de 100644 --- a/dag/test/claim/workspace_checkpoint_instrument_witness_test.dag +++ b/dag/test/claim/workspace_checkpoint_instrument_witness_test.dag @@ -5,6 +5,7 @@ import std.optional { Present, Absent } import std.algebra { Cons, Empty } import std.measure { byte_size, measure_count, Nanosecond, nanosecond, nanosecond_count } import std.content_hash { Sha256Digest, sha256_hex_digest } +import v2.std.collection { Map, empty_map, map_insert } import std.fabric_blob { FabricBlobAddress } import std.fabric_storage { FabricObject, FabricStoredObject, FabricObjectFetched, FabricObjectGetRefused, FabricObjectMissing, fabric_object_ref_of, fabric_object_ref_eq } import std.durable_compare_and_set { cas_generation_first } @@ -24,12 +25,16 @@ import gunbc.auth.access_token_source { WorkloadIdentityToken } import gunbc.fabric_storage_r2_head { R2HeadCredential, r2_head_binding, R2HeadCasNotGrounded, R2HeadAdvanceKnown, R2HeadAdvancedTo, R2HeadStagingCustody } import gunbc.fabric.workspace_source { WorkspaceSourceRoot, workspace_credential_exclusions, workspace_exclusions, workspace_path_exclusion, workspace_source_tally, - workspace_pack_readback, WorkspacePackClean, WorkspacePackCarriesExcluded, + workspace_pack_readback, WorkspacePackReadback, WorkspacePackClean, WorkspacePackCarriesExcluded, WorkspacePackPlaceholderAbsent, WorkspacePackPlaceholderSizeDiffers, + WorkspacePackPlaceholderNotRegular, WorkspacePackPlaceholderUnread, WorkspacePackPlaceholderDigestDiffers, WorkspaceStagedPlaceholder, + workspace_transcript_caveat, + workspace_observed_agent_state_slots, workspace_agent_state_slot_admission, workspace_agent_state_slot_words, + AgentStateSlotAdmitted, AgentStateSlotAbsent, AgentStateSlotUnknown, } import gunbc.fabric.workspace_checkpoint_instrument { workspace_snapshot_capability, WorkspaceSnapshotBtrfs, WorkspaceSnapshotAbsent, WorkspaceSnapshotUnobserved, WorkspaceMeasuredPart, PartMeasured, PartUnmeasured, workspace_bound_verdict, BoundHolds, BoundExceeded, BoundUndecided, - workspace_quota_verdict, QuotaHolds, QuotaExceeded, + workspace_quota_verdict, QuotaHolds, QuotaExceeded, workspace_draft_verdict_lines, workspace_draft_numbers, workspace_instrument_commit, InstrumentCommitCasNotGrounded, WorkspaceCustody, workspace_custody_clean, workspace_custody_of_head_advance, workspace_exit_with_custody, WorkspaceRestoredFile, workspace_restore_compare, RestoreByteEqual, RestoreMissingFile, RestoreExtraFile, RestoreBytesDiffer, RestoreCarriesUncommittedChunk, @@ -180,22 +185,195 @@ test fn the_tally_admits_only_unexcluded_non_build_files() -> Bool { && map(t.excluded_credential_members, m => m as String) == ["home/u/gunbc/.env", "home/u/gunbc/keys/id_rsa"] && t.excluded_transcripts == 2 && measure_count(m: t.excluded_transcript_bytes) == 5700 && t.build_output == 1 && t.unparsed == [] + && map(t.transcript_placeholders, p => join([p.member as String, "=", to_string(measure_count(m: p.size))], "")) == ["home/u/gunbc/projects/p/0d3a.jsonl=5000", "home/u/gunbc/history.jsonl=700"] +} + +// A CREDENTIAL NEVER GETS A PLACEHOLDER, NOT EVEN ONE THAT ALSO LOOKS LIKE A TRANSCRIPT: credentials +// are matched first, and only the TranscriptExclusion class mints a placeholder. A transcript +// symlink carries no content to stand in for, so it gets none either. +test fn a_credential_class_member_never_gets_a_placeholder() -> Bool { + let listing = join([ + "f\t600\t.credentials.json\t\t450", + "f\t600\tprojects/p/.env\t\t30", + "f\t600\tprojects/p/id_rsa.jsonl\t\t0", + "f\t600\tprojects/.ssh/k.jsonl\t\t80", + "l\t777\tprojects/p/link.jsonl\tother.jsonl\t11", + "f\t644\tprojects/p/s.jsonl\t\t9", + ], "\n") + let t = workspace_source_tally(root: tally_root, exclusions: workspace_exclusions(), listing: listing) + t.excluded_credentials == 3 + && map(t.transcript_placeholders, p => p.member as String) == ["home/u/gunbc/projects/p/id_rsa.jsonl", "home/u/gunbc/projects/p/s.jsonl"] + && t.excluded_transcripts == 3 +} + +data readback_member: String = "home/u/.claude/projects/p/s 1.jsonl" + +data readback_placeholder: WorkspaceStagedPlaceholder = WorkspaceStagedPlaceholder { member: "home/u/.claude/projects/p/s 1.jsonl", size: byte_size(count: 5000), expected_digest: digest(c: "a") } + +data readback_names: String = "home/u/gunbc/README.md\nhome/u/.claude/projects/p/s 1.jsonl\n" + +fn readback_sizes(type_letter: String, placeholder_bytes: String) -> String { + join([ + "-rw-r--r-- 1000/1000 120 2026-10-05 12:00:01 home/u/gunbc/README.md", + join([type_letter, "rw-r--r-- 1000/1000 ", placeholder_bytes, " 2026-10-05 12:00:02 home/u/.claude/projects/p/s 1.jsonl"], ""), + "", + ], "\n") +} + +fn archived_digest(hex_char: String) -> Map { + map_insert(m: empty_map(), key: readback_member, value: digest(c: hex_char).hex as String) +} + +fn readback_of(placeholders: List, names: String, sizes: String, digests: Map) -> WorkspacePackReadback { + workspace_pack_readback(exclusions: workspace_exclusions(), placeholders: placeholders, archive_listing: names, size_listing: sizes, archived_digests: digests) } // THE PACK READBACK'S RED: an archive that carries a credential refuses the upload, whatever list // built it. test fn an_archive_carrying_a_credential_refuses_the_upload() -> Bool { - match workspace_pack_readback(exclusions: workspace_exclusions(), archive_listing: "home/u/gunbc/README.md\nsession-home/.claude/.credentials.json\n") { + match readback_of(placeholders: [], names: "home/u/gunbc/README.md\nsession-home/.claude/.credentials.json\n", sizes: "", digests: empty_map()) { WorkspacePackCarriesExcluded { member: m, by: x } => (m as String) == "session-home/.claude/.credentials.json" && (x.spelling as String) == "~/.claude/.credentials.json" WorkspacePackClean { members: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +// A TRANSCRIPT NAME WITH NO STAGED PLACEHOLDER IS REAL CONTENT AND REFUSES. +test fn an_archived_transcript_without_a_placeholder_refuses_the_upload() -> Bool { + match readback_of(placeholders: [], names: readback_names, sizes: readback_sizes(type_letter: "-", placeholder_bytes: "5000"), digests: archived_digest(hex_char: "a")) { + WorkspacePackCarriesExcluded { member: m, by: x } => (m as String) == readback_member && (x.spelling as String) == "projects/**/*.jsonl" + WorkspacePackClean { members: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +// THE CONFIDENTIALITY RED: a REAL transcript of the placeholder's exact name, type and size is +// refused, because its bytes do not hash to the staged filler's digest. +test fn a_same_size_real_content_member_refuses_the_upload_by_digest() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: readback_names, sizes: readback_sizes(type_letter: "-", placeholder_bytes: "5000"), digests: archived_digest(hex_char: "b")) { + WorkspacePackPlaceholderDigestDiffers { member: m, expected: e, archived: a } => (m as String) == readback_member && (e.hex as String) == (digest(c: "a").hex as String) && a == (digest(c: "b").hex as String) + WorkspacePackClean { members: _ } => false + WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + } +} + +// THE PLACEHOLDER'S SIZE RED: archived at any size but its planned one, the upload refuses. +test fn a_placeholder_whose_size_differs_refuses_the_upload() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: readback_names, sizes: readback_sizes(type_letter: "-", placeholder_bytes: "4999"), digests: archived_digest(hex_char: "a")) { + WorkspacePackPlaceholderSizeDiffers { member: m, planned: p, archived: a } => (m as String) == readback_member && measure_count(m: p) == 5000 && measure_count(m: a) == 4999 + WorkspacePackClean { members: _ } => false + WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +test fn a_placeholder_archived_as_a_non_regular_member_refuses_the_upload() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: readback_names, sizes: readback_sizes(type_letter: "c", placeholder_bytes: "5000"), digests: archived_digest(hex_char: "a")) { + WorkspacePackPlaceholderNotRegular { member: m, type_letter: t } => (m as String) == readback_member && t == "c" + WorkspacePackClean { members: _ } => false + WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false } } -test fn a_clean_archive_reads_back_its_member_count() -> Bool { - match workspace_pack_readback(exclusions: workspace_exclusions(), archive_listing: "home/u/gunbc/README.md\nhome/u/gunbc/src/a.rs\n") { +test fn a_placeholder_whose_archived_bytes_were_not_read_refuses_the_upload() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: readback_names, sizes: readback_sizes(type_letter: "-", placeholder_bytes: "5000"), digests: empty_map()) { + WorkspacePackPlaceholderUnread { member: m } => (m as String) == readback_member + WorkspacePackClean { members: _ } => false + WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +test fn a_planned_placeholder_missing_from_the_archive_refuses_the_upload() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: "home/u/gunbc/README.md\n", sizes: "-rw-r--r-- 1000/1000 120 2026-10-05 12:00:01 home/u/gunbc/README.md\n", digests: empty_map()) { + WorkspacePackPlaceholderAbsent { member: m } => (m as String) == readback_member + WorkspacePackClean { members: _ } => false + WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +// THE POSITIVE CONTROL: the placeholder at its name, regular, at size, hashing to its staged digest. +test fn a_clean_archive_with_its_placeholder_intact_reads_back_its_member_count() -> Bool { + match readback_of(placeholders: [readback_placeholder], names: readback_names, sizes: readback_sizes(type_letter: "-", placeholder_bytes: "5000"), digests: archived_digest(hex_char: "a")) { WorkspacePackClean { members: n } => n == 2 WorkspacePackCarriesExcluded { member: _, by: _ } => false + WorkspacePackPlaceholderAbsent { member: _ } => false + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => false + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => false + WorkspacePackPlaceholderUnread { member: _ } => false + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => false + } +} + +// ── THE AGENT-STATE SLOT: A DECLARED INPUT, A CLOSED CHOICE OVER THE OBSERVED SLOTS ─────────── + +data observed_home_dirs: List = ["/home/briansrls/.claude@gmail2", "/home/briansrls/.claude@gmail3", "/home/briansrls/.claude", "/home/briansrls/.claude@", "/home/briansrls/gunbc", "/home/briansrls/.codex"] + +test fn the_observed_slots_are_the_claude_at_directories() -> Bool { + workspace_observed_agent_state_slots(dirs: observed_home_dirs) == ["gmail2", "gmail3"] +} + +// A NAMED EXISTING SLOT IS ADMITTED, as that slot's root, and the receipt line carries slot and path. +test fn a_named_existing_slot_is_admitted_and_its_receipt_carries_it() -> Bool { + let a = workspace_agent_state_slot_admission(requested: Present { value: "gmail2" }, observed: workspace_observed_agent_state_slots(dirs: observed_home_dirs)) + let admitted = match a { + AgentStateSlotAdmitted { slot: s, root: r } => (s as String) == "gmail2" && (r.path as String) == "/home/briansrls/.claude@gmail2" && (r.label as String) == "claude-agent-state-gmail2" + AgentStateSlotAbsent { observed: _ } => false + AgentStateSlotUnknown { requested: _, observed: _ } => false } + admitted && workspace_agent_state_slot_words(admission: a) == "agent_state_slot=gmail2 path=/home/briansrls/.claude@gmail2" +} + +// NO DEFAULT AND NO FALLBACK: an absent (or empty) slot refuses, naming what was observed. +test fn an_absent_slot_refuses_naming_the_observed_slots() -> Bool { + let observed = workspace_observed_agent_state_slots(dirs: observed_home_dirs) + let absent = match workspace_agent_state_slot_admission(requested: none, observed: observed) { + AgentStateSlotAbsent { observed: o } => o == ["gmail2", "gmail3"] + AgentStateSlotAdmitted { slot: _, root: _ } => false + AgentStateSlotUnknown { requested: _, observed: _ } => false + } + let empty = match workspace_agent_state_slot_admission(requested: Present { value: "" }, observed: observed) { + AgentStateSlotAbsent { observed: _ } => true + AgentStateSlotAdmitted { slot: _, root: _ } => false + AgentStateSlotUnknown { requested: _, observed: _ } => false + } + absent && empty +} + +test fn an_unknown_slot_refuses_naming_the_observed_slots() -> Bool { + let a = workspace_agent_state_slot_admission(requested: Present { value: "gmail9" }, observed: workspace_observed_agent_state_slots(dirs: observed_home_dirs)) + let refused = match a { + AgentStateSlotUnknown { requested: r, observed: o } => r == "gmail9" && o == ["gmail2", "gmail3"] + AgentStateSlotAdmitted { slot: _, root: _ } => false + AgentStateSlotAbsent { observed: _ } => false + } + refused && string_contains(s: workspace_agent_state_slot_words(admission: a), pattern: "observed=gmail2,gmail3") } // ── VERDICTS ─────────────────────────────────────────────────────────────────────────────────── @@ -224,6 +402,20 @@ test fn measured_parts_over_the_bound_exceed_it_whatever_is_missing() -> Bool { } } +// WHILE THE SUBJECT IS A STATIC TREE WITH SYNTHETIC TRANSCRIPTS, THE 10 s / 60 s VERDICTS ARE +// WITHHELD -- even when every measured part is far under both bounds, no bound line says it holds -- +// and the quota decides over the static size, labelled static. +test fn the_draft_time_verdicts_are_withheld_and_the_quota_is_static() -> Bool { + let fast = [PartMeasured { label: "snapshot", elapsed: ms(n: 40) }, PartMeasured { label: "upload", elapsed: ms(n: 900) }, PartMeasured { label: "commit", elapsed: ms(n: 20) }] + let block = workspace_draft_verdict_lines(snapshot_parts: fast, scan_parts: fast, static_size: Present { value: byte_size(count: 1600000000) }, n: workspace_draft_numbers) + let lines = split(block, separator: "\n") + string_contains(s: block, pattern: workspace_transcript_caveat as String) + && length(filter(lines, l => string_contains(s: l, pattern: "=withheld: churn not measured"))) == 4 + && length(filter(lines, l => string_contains(s: l, pattern: "=holds") || string_contains(s: l, pattern: "=exceeded") || string_contains(s: l, pattern: "=undecided"))) == 1 + && string_contains(s: block, pattern: "quota_static=holds bytes=1600000000") + && (workspace_transcript_caveat as String) == "transcript content synthetic, sizes preserved, static tree; NOT a 10 s / 60 s verdict" +} + test fn the_quota_verdict_compares_bytes_to_the_quota() -> Bool { let under = match workspace_quota_verdict(size: byte_size(count: 1600000000), quota: byte_size(count: 10000000000)) { QuotaHolds { size: _, quota: _ } => true QuotaExceeded { size: _, quota: _ } => false } let over = match workspace_quota_verdict(size: byte_size(count: 10000000001), quota: byte_size(count: 10000000000)) { QuotaExceeded { size: _, quota: _ } => true QuotaHolds { size: _, quota: _ } => false } diff --git a/dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag b/dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag new file mode 100644 index 00000000000..4663e1efb0e --- /dev/null +++ b/dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag @@ -0,0 +1,110 @@ +module test.claim.workspace_source_pack_archive_real_execution + +import std.types { String, NonEmptyStr, Int, Bool, List, FilePath } +import std.measure { byte_size } +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.shell +import gunbc.fabric.workspace_source { + WorkspaceTranscriptPlaceholder, WorkspaceStagedPlaceholder, workspace_stage_placeholders, WorkspacePlaceholdersStaged, WorkspacePlaceholdersRefused, + workspace_pack_archive, workspace_pack_refusal_words, WorkspacePackBuilt, WorkspacePackRefused, + WorkspacePackClean, WorkspacePackCarriesExcluded, WorkspacePackPlaceholderAbsent, WorkspacePackPlaceholderNotRegular, + WorkspacePackPlaceholderSizeDiffers, WorkspacePackPlaceholderUnread, WorkspacePackPlaceholderDigestDiffers, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE PACK'S TWO-ROOT TAR COMPOSITION AND ITS ARCHIVE READBACK, BY REAL EXECUTION, inside a directory +// the witness mints and removes. A real tree (one source file, and a real transcript the plan +// excludes) and a staged placeholder for that transcript are packed into one tar by the production +// gunbc.fabric.workspace_source workspace_pack_archive, which lists the archive back, extracts the +// placeholder member and hashes it. The positive control reads back clean. The red is the +// confidentiality boundary itself: the staged placeholder's bytes are swapped for real content of +// the SAME size before packing -- what a real transcript standing at the placeholder's path would +// look like -- and the archive is refused by digest. The pure readback arms are discriminated in +// test.claim.workspace_checkpoint_instrument; this witness is their inhabitance on the real route. + +fn with_temp_dir(scenario: fn(String) -> Bool) -> Bool { + let dir = shell.Mktemp.Dir() + if dir.success == false { + false + } else { + let observed = scenario(dir.path as String) + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + observed && cleaned.success + } +} + +fn relative_to_root(path: String) -> String { + substring(s: path, start: 1, end: length(path)) +} + +data transcript_text: String = "{\"type\":\"user\",\"message\":\"a real transcript line of exactly sixty-four bytes\"}\n" + +// The real tree under /real and the placeholder plan for its one transcript, at the +// transcript's own size. Returns the plan only if the tree was written. +fn plant(dir: String) -> List { + let real = join([dir, "/real/.claude/projects/p"], "") + let made = shell.Mkdir.Parents(path: real as FilePath).success + let readme = Filesystem.Write(path: join([dir, "/real/README.md"], ""), content: "readme\n").success + let transcript = Filesystem.Write(path: join([real, "/s.jsonl"], ""), content: transcript_text).success + if made && readme && transcript { + [WorkspaceTranscriptPlaceholder { member: relative_to_root(path: join([real, "/s.jsonl"], "")) as NonEmptyStr, size: byte_size(count: length(transcript_text)) }] + } else { + [] + } +} + +fn pack(dir: String, plan: List, swap_in_real_content: Bool) -> String { + let staging = join([dir, "/staging"], "") + let root = join([staging, "/placeholders"], "") + match workspace_stage_placeholders(root: root, placeholders: plan, staged: []) { + WorkspacePlaceholdersRefused { why: w } => concat("staging-refused: ", w) + WorkspacePlaceholdersStaged { placeholders: staged } => { + let swapped = if swap_in_real_content { + Filesystem.Write(path: join([root, "/", relative_to_root(path: join([dir, "/real/.claude/projects/p/s.jsonl"], ""))], ""), content: transcript_text).success + } else { true } + if swapped == false { + "swap-failed" + } else { + match workspace_pack_archive(staging: staging, members: [relative_to_root(path: join([dir, "/real/README.md"], "")) as NonEmptyStr], placeholders: staged, placeholder_root: root) { + WorkspacePackRefused { refusal: v, why: w } => join(["pack-refused: ", workspace_pack_refusal_words(refusal: v), " ", w], "") + WorkspacePackBuilt { archive: _, readback: r } => + match r { + WorkspacePackClean { members: n } => concat("clean ", to_string(n)) + WorkspacePackCarriesExcluded { member: _, by: _ } => "carries-excluded" + WorkspacePackPlaceholderAbsent { member: _ } => "placeholder-absent" + WorkspacePackPlaceholderNotRegular { member: _, type_letter: _ } => "placeholder-not-regular" + WorkspacePackPlaceholderSizeDiffers { member: _, planned: _, archived: _ } => "placeholder-size-differs" + WorkspacePackPlaceholderUnread { member: _ } => "placeholder-unread" + WorkspacePackPlaceholderDigestDiffers { member: _, expected: _, archived: _ } => "placeholder-digest-differs" + } + } + } + } + } +} + +test fn the_two_root_archive_carries_the_placeholder_and_reads_back_clean_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let plan = plant(dir: dir) + length(plan) == 1 && pack(dir: dir, plan: plan, swap_in_real_content: false) == "clean 2" + }) +} + +test fn same_size_real_content_at_the_placeholder_path_is_refused_by_digest_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let plan = plant(dir: dir) + length(plan) == 1 && pack(dir: dir, plan: plan, swap_in_real_content: true) == "placeholder-digest-differs" + }) +} + +// A WORKSPACE WITH NO TRANSCRIPTS: one ordinary file and zero placeholders packs clean through the +// same two-list route, its second list empty. +test fn one_ordinary_file_and_zero_placeholders_packs_clean_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let made = shell.Mkdir.Parents(path: join([dir, "/real"], "") as FilePath).success + let readme = Filesystem.Write(path: join([dir, "/real/README.md"], ""), content: "readme\n").success + made && readme && pack(dir: dir, plan: [], swap_in_real_content: false) == "clean 1" + }) +} diff --git a/dag/test/claim/workspace_transcript_rate_real_execution_witness_test.dag b/dag/test/claim/workspace_transcript_rate_real_execution_witness_test.dag new file mode 100644 index 00000000000..59ae0b07514 --- /dev/null +++ b/dag/test/claim/workspace_transcript_rate_real_execution_witness_test.dag @@ -0,0 +1,87 @@ +module test.claim.workspace_transcript_rate_real_execution + +import std.types { String, NonEmptyStr, Int, Bool, List, FilePath } +import std.measure { measure_count } +import std.algebra { Cons, Empty } +import std.checkpointed_workspace { WorkspaceAgeOf, WorkspaceAgeUndefined, workspace_age_between } +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.shell +import gunbc.fabric.workspace_source { WorkspaceSourceRoot } +import gunbc.fabric.workspace_transcript_rate { + workspace_transcript_rate_sample_wet, workspace_transcript_rate_interval, TranscriptAppendInterval, + TranscriptRateSampled, TranscriptRateSampleRefused, TranscriptRateSample, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE INHABITANCE OF THE PURE BOUNDARIES IN test.claim.workspace_transcript_rate (DESIGN 3: supplied +// inputs never remove the last execution of the real path). A real agent-state tree is planted in a +// directory the witness mints and removes, and the production sampler -- the real find walk, the +// pack's real matcher -- reads it, is written to, and reads it again. The route is asserted, not only +// the answer: the transcript grows by exactly the bytes written, a new transcript appears at its +// whole size, a file outside the transcript class is never observed however much it grows, and a +// transcript rewritten shorter is a truncation. A root that does not exist refuses through find's +// own status. + +fn with_temp_dir(scenario: fn(String) -> Bool) -> Bool { + let dir = shell.Mktemp.Dir() + if dir.success == false { + false + } else { + let observed = scenario(dir.path as String) + let cleaned = shell.Remove.RecursiveForce(path: dir.path) + observed && cleaned.success + } +} + +fn root_at(dir: String) -> WorkspaceSourceRoot { + WorkspaceSourceRoot { label: "claude-agent-state-witness", path: join([dir, "/.claude"], "") as NonEmptyStr, build_output: [] } +} + +fn interval_of(before: TranscriptRateSample, after: TranscriptRateSample) -> List { + match before { + TranscriptRateSampleRefused { refusals: _ } => [] + TranscriptRateSampled { at: a, files: fa } => + match after { + TranscriptRateSampleRefused { refusals: _ } => [] + TranscriptRateSampled { at: b, files: fb } => + match workspace_age_between(from: a, to: b) { + WorkspaceAgeUndefined { order: _ } => [] + WorkspaceAgeOf { age: span } => [workspace_transcript_rate_interval(before: fa, after: fb, span: span)] + } + } + } +} + +test fn a_real_tree_grows_gains_a_transcript_and_truncates_one_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + let projects = join([dir, "/.claude/projects/p"], "") + let planted = shell.Mkdir.Parents(path: projects as FilePath).success + && Filesystem.Write(path: join([projects, "/s.jsonl"], ""), content: "0123456789").success + && Filesystem.Write(path: join([projects, "/t.jsonl"], ""), content: "0123456789").success + && Filesystem.Write(path: join([dir, "/.claude/settings.json"], ""), content: "{}").success + let first = workspace_transcript_rate_sample_wet(roots: [root_at(dir: dir)], label: "transcript-rate-witness-first") + let written = Filesystem.Write(path: join([projects, "/s.jsonl"], ""), content: "0123456789abcdefghij").success + && Filesystem.Write(path: join([projects, "/t.jsonl"], ""), content: "0123").success + && Filesystem.Write(path: join([projects, "/u.jsonl"], ""), content: "01234").success + && Filesystem.Write(path: join([dir, "/.claude/settings.json"], ""), content: "{\"a\":\"a much longer settings file\"}").success + let second = workspace_transcript_rate_sample_wet(roots: [root_at(dir: dir)], label: "transcript-rate-witness-second") + planted && written && (match interval_of(before: first, after: second) { + Cons { head: row, tail: Empty } => + measure_count(m: row.bytes_appended) == 15 + && row.files_grown == 1 && row.files_created == 1 && length(row.truncations) == 1 && row.active_writers == 3 + Cons { head: _, tail: Cons { head: _, tail: _ } } => false + Empty => false + }) + }) +} + +test fn a_root_that_does_not_exist_refuses_by_real_execution() -> Bool { + with_temp_dir(scenario: dir => { + match workspace_transcript_rate_sample_wet(roots: [root_at(dir: dir)], label: "transcript-rate-witness-absent") { + TranscriptRateSampleRefused { refusals: rs } => length(rs) == 1 + TranscriptRateSampled { at: _, files: _ } => false + } + }) +} diff --git a/dag/test/claim/workspace_transcript_rate_witness_test.dag b/dag/test/claim/workspace_transcript_rate_witness_test.dag new file mode 100644 index 00000000000..a73788bf019 --- /dev/null +++ b/dag/test/claim/workspace_transcript_rate_witness_test.dag @@ -0,0 +1,205 @@ +module test.claim.workspace_transcript_rate + +import std.types { String, NonEmptyStr, Int, Bool, List } +import std.measure { InstantBefore, InstantSame, byte_size, measure_count, nanosecond, nanosecond_count, ObservationInstant, ProcessMonotonic, WallClock, CpuClock, InstantAfter, InstantIncomparable } +import std.optional { Present, Absent } +import std.algebra { Cons, Empty } +import extdeps.shell.exec { ProcessRunOutcome, ProcessRan, ProcessNotSpawned, ProcessObservation } +import extdeps.tools.findutils { find_regular_files_sha256_command, find_command_runs_another_program } +import gunbc.fabric.workspace_source { WorkspaceSourceRoot, workspace_source_fixed_roots, workspace_agent_state_slot_admission } +import gunbc.fabric.workspace_transcript_rate { + TranscriptFileStat, TranscriptFileMtime, TranscriptRateClockUndefined, workspace_transcript_rate_root_command, workspace_transcript_rate_root_reading, + TranscriptRootRead, TranscriptRootRefused, workspace_transcript_rate_interval, workspace_transcript_rate_mtime, + TranscriptRateSampled, TranscriptRateSampleRefused, TranscriptRateSample, workspace_transcript_rate_window_of, + TranscriptRateObserved, TranscriptRateRefused, TranscriptRateRootSelection, TranscriptRateObservation, workspace_transcript_rate_receipt, + workspace_transcript_rate_select_roots, workspace_transcript_rate_roots, TranscriptRateRootsSelected, TranscriptRateRootUndeclared, TranscriptRateSlotRefused, workspace_transcript_rate_fixed_root_labels, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE TRANSCRIPT APPEND-RATE INSTRUMENT AT ITS PURE BOUNDARIES, inputs supplied. Each claim has a +// discriminating red beside its positive control: the walk is a reader of metadata and the +// content-reading sha256 walk is not; a shrinking file is a truncation and never a negative append; +// an unreadable root refuses the sample and the window. The real walk over a real tree -- growth, +// creation, truncation and a missing root through find itself -- is the inhabitance claim in +// test.claim.workspace_transcript_rate_real_execution. + +data root: WorkspaceSourceRoot = WorkspaceSourceRoot { label: "claude-agent-state-test", path: "/r/.claude", build_output: [] } + +fn stat(member: String, size: Int, mtime: Int) -> TranscriptFileStat { + TranscriptFileStat { member: member as NonEmptyStr, size: byte_size(count: size), mtime: TranscriptFileMtime { since_epoch: nanosecond(count: mtime) } } +} + +// One clock for every sample of a window, as the wet sampler uses; a second process name or a CPU +// basis is a different clock. +fn at(ns: Int) -> ObservationInstant { + ObservationInstant { domain: ProcessMonotonic { process: "workspace-transcript-rate" }, basis: WallClock, since_origin: nanosecond(count: ns) } +} + +fn ran(code: Int, stdout: String, stderr: String) -> ProcessRunOutcome { + ProcessRan { observation: ProcessObservation { exit_code: code, stdout: stdout, stderr: stderr } } +} + +test fn the_walk_runs_no_other_program_while_the_sha256_walk_does() -> Bool { + let walk = workspace_transcript_rate_root_command(root: root) + (walk.program.invocation as String) == "find" + && find_command_runs_another_program(command: walk) == false + && find_command_runs_another_program(command: find_regular_files_sha256_command(root: "/r/.claude")) +} + +test fn a_shrinking_file_is_a_truncation_and_not_a_negative_append() -> Bool { + let row = workspace_transcript_rate_interval( + before: [stat(member: "r/.claude/projects/p/a.jsonl", size: 100, mtime: 1), stat(member: "r/.claude/projects/p/b.jsonl", size: 50, mtime: 1)], + after: [stat(member: "r/.claude/projects/p/a.jsonl", size: 40, mtime: 2), stat(member: "r/.claude/projects/p/b.jsonl", size: 60, mtime: 2)], + span: nanosecond(count: 10000000000), + ) + measure_count(m: row.bytes_appended) == 10 + && row.files_grown == 1 + && row.active_writers == 2 + && (match row.truncations { + Cons { head: t, tail: Empty } => (t.member as String) == "r/.claude/projects/p/a.jsonl" && measure_count(m: t.from) == 100 && measure_count(m: t.to) == 40 + Cons { head: _, tail: Cons { head: _, tail: _ } } => false + Empty => false + }) +} + +test fn growth_creation_removal_and_a_same_size_rewrite_are_counted_apart() -> Bool { + let row = workspace_transcript_rate_interval( + before: [stat(member: "a", size: 10, mtime: 1), stat(member: "b", size: 10, mtime: 1), stat(member: "c", size: 10, mtime: 1), stat(member: "d", size: 10, mtime: 1)], + after: [stat(member: "a", size: 15, mtime: 2), stat(member: "b", size: 10, mtime: 2), stat(member: "c", size: 10, mtime: 1), stat(member: "e", size: 7, mtime: 2)], + span: nanosecond(count: 100), + ) + measure_count(m: row.bytes_appended) == 12 + && row.files_grown == 1 && row.files_created == 1 && row.files_removed == 1 && row.files_rewritten_same_size == 1 + && row.active_writers == 3 && nanosecond_count(n: row.span) == 100 +} + +test fn the_walk_keeps_only_the_packs_transcript_class_and_reads_mtime_to_the_nanosecond() -> Bool { + match workspace_transcript_rate_root_reading(root: root, run: ran(code: 0, stdout: "64\t1728123456.1234567890\tprojects/p/s.jsonl\n9\t1728123456.5\tsettings.json\n5\t1.0\t.credentials.json\n3\t2.25\thistory.jsonl\n", stderr: "")) { + TranscriptRootRefused { root: _, detail: _ } => false + TranscriptRootRead { files: fs } => + length(fs) == 2 + && any(fs, f => (f.member as String) == "r/.claude/projects/p/s.jsonl" && measure_count(m: f.size) == 64 && nanosecond_count(n: f.mtime.since_epoch) == 1728123456123456789) + && any(fs, f => (f.member as String) == "r/.claude/history.jsonl" && nanosecond_count(n: f.mtime.since_epoch) == 2250000000) + } +} + +test fn an_unparsed_mtime_or_line_refuses_rather_than_dropping_the_file() -> Bool { + (match workspace_transcript_rate_mtime(text: "1.2.3") { Absent => true Present { value: _ } => false }) + && (match workspace_transcript_rate_root_reading(root: root, run: ran(code: 0, stdout: "64 projects/p/s.jsonl\n", stderr: "")) { + TranscriptRootRefused { root: _, detail: _ } => true + TranscriptRootRead { files: _ } => false + }) +} + +fn never(k: Int) -> TranscriptRateSample { + TranscriptRateSampleRefused { refusals: ["not sampled"] } +} + +test fn an_unreadable_root_refuses_the_sample_and_the_window() -> Bool { + let reading = workspace_transcript_rate_root_reading(root: root, run: ran(code: 1, stdout: "", stderr: "find: '/r/.claude': Permission denied")) + let not_spawned = workspace_transcript_rate_root_reading(root: root, run: ProcessNotSpawned { program: "find", cause: "no such program" }) + (match reading { TranscriptRootRefused { root: _, detail: _ } => true TranscriptRootRead { files: _ } => false }) + && (match not_spawned { TranscriptRootRefused { root: _, detail: _ } => true TranscriptRootRead { files: _ } => false }) + && (match workspace_transcript_rate_window_of(baseline: TranscriptRateSampleRefused { refusals: ["claude: denied"] }, remaining: 3, next: never, pause: k => true, rows: []) { + TranscriptRateRefused { reason: _, rows: rows } => length(rows) == 0 + TranscriptRateObserved { rate: _ } => false + TranscriptRateClockUndefined { from: _, to: _, order: _, rows: _ } => false + }) +} + +fn second_sample(k: Int) -> TranscriptRateSample { + if k == 2 { + TranscriptRateSampled { at: at(ns: 10000000000), files: [stat(member: "a", size: 1010, mtime: 2)] } + } else { + TranscriptRateSampled { at: at(ns: 20000000000), files: [stat(member: "a", size: 1030, mtime: 3)] } + } +} + +test fn a_completed_window_receipts_every_interval_and_the_summary_with_its_caveat() -> Bool { + match workspace_transcript_rate_window_of(baseline: TranscriptRateSampled { at: at(ns: 0), files: [stat(member: "a", size: 1000, mtime: 1)] }, remaining: 2, next: second_sample, pause: k => true, rows: []) { + TranscriptRateRefused { reason: _, rows: _ } => false + TranscriptRateClockUndefined { from: _, to: _, order: _, rows: _ } => false + TranscriptRateObserved { rate: r } => { + let receipt = workspace_transcript_rate_receipt(head: "", observation: TranscriptRateObserved { rate: r }) + length(r.rows) == 2 + && contains(receipt, "interval 1 span_ms=10000 bytes_appended=10 ") + && contains(receipt, "interval 2 span_ms=10000 bytes_appended=20 ") + && contains(receipt, " mean_bits_per_s=12 mean_bytes_per_s=1 ") + && contains(receipt, " p95_bits_per_s=16 p95_bytes_per_s=2 ") + && contains(receipt, "verdict=observed") + && contains(r.caveat as String, "ONE SAMPLE OF WORKLOAD") + } + } +} + +test fn a_failed_pause_refuses_the_window() -> Bool { + match workspace_transcript_rate_window_of(baseline: TranscriptRateSampled { at: at(ns: 0), files: [] }, remaining: 2, next: second_sample, pause: k => false, rows: []) { + TranscriptRateRefused { reason: _, rows: _ } => true + TranscriptRateObserved { rate: _ } => false + TranscriptRateClockUndefined { from: _, to: _, order: _, rows: _ } => false + } +} + +// The Claude root comes from the pack's slot admission, supplied here with an observed slot set; the +// Codex root is the pack's fixed row. An unknown slot refuses the selection. +fn roots_for(requested: String) -> TranscriptRateRootSelection { + workspace_transcript_rate_roots( + slot: workspace_agent_state_slot_admission(requested: Present { value: requested }, observed: ["gmail2", "gmail3"]), + fixed: workspace_transcript_rate_select_roots(labels: workspace_transcript_rate_fixed_root_labels, declared: workspace_source_fixed_roots), + ) +} + +test fn the_observed_roots_are_the_admitted_slot_and_the_codex_row_and_an_unknown_slot_refuses() -> Bool { + (match roots_for(requested: "gmail2") { + TranscriptRateRootsSelected { roots: rs } => length(rs) == 2 && any(rs, r => (r.path as String) == "/home/briansrls/.claude@gmail2") && any(rs, r => (r.path as String) == "/home/briansrls/.codex") + TranscriptRateRootUndeclared { label: _ } => false + TranscriptRateSlotRefused { slot: _ } => false + }) + && (match roots_for(requested: "gmail9") { + TranscriptRateSlotRefused { slot: _ } => true + TranscriptRateRootsSelected { roots: _ } => false + TranscriptRateRootUndeclared { label: _ } => false + }) + && (match workspace_transcript_rate_select_roots(labels: ["no-such-root"], declared: workspace_source_fixed_roots) { + TranscriptRateRootUndeclared { label: l } => (l as String) == "no-such-root" + TranscriptRateRootsSelected { roots: _ } => false + TranscriptRateSlotRefused { slot: _ } => false + }) +} + +fn sample_at(instant: ObservationInstant) -> fn(Int) -> TranscriptRateSample { + k => TranscriptRateSampled { at: instant, files: [] } +} + +fn window_after(instant: ObservationInstant) -> TranscriptRateObservation { + workspace_transcript_rate_window_of(baseline: TranscriptRateSampled { at: at(ns: 10), files: [] }, remaining: 1, next: sample_at(instant: instant), pause: k => true, rows: []) +} + +// THE SPAN IS DERIVED BY ORDERING TWO INSTANTS OF ONE CLOCK. The positive control is the ordered pair +// on the same domain and basis; the reds are a later instant that does not follow (InstantAfter) and +// an instant on another process's clock or another basis (InstantIncomparable) -- each its own typed +// refusal carrying the order, never a zero span read as a quiet interval. +test fn ordered_same_clock_instants_derive_a_span_and_reversed_or_foreign_instants_refuse_by_their_order() -> Bool { + (match window_after(instant: at(ns: 25)) { + TranscriptRateObserved { rate: r } => (match r.rows { Cons { head: row, tail: Empty } => nanosecond_count(n: row.span) == 15 Cons { head: _, tail: Cons { head: _, tail: _ } } => false Empty => false }) + TranscriptRateRefused { reason: _, rows: _ } => false + TranscriptRateClockUndefined { from: _, to: _, order: _, rows: _ } => false + }) + && (match window_after(instant: at(ns: 5)) { + TranscriptRateClockUndefined { from: _, to: _, order: o, rows: rows } => length(rows) == 0 && (match o { InstantAfter => true InstantIncomparable { left: _, right: _ } => false InstantBefore => false InstantSame => false }) + TranscriptRateObserved { rate: _ } => false + TranscriptRateRefused { reason: _, rows: _ } => false + }) + && (match window_after(instant: ObservationInstant { domain: ProcessMonotonic { process: "another-process" }, basis: WallClock, since_origin: nanosecond(count: 25) }) { + TranscriptRateClockUndefined { from: _, to: _, order: o, rows: _ } => (match o { InstantIncomparable { left: _, right: _ } => true InstantAfter => false InstantBefore => false InstantSame => false }) + TranscriptRateObserved { rate: _ } => false + TranscriptRateRefused { reason: _, rows: _ } => false + }) + && (match window_after(instant: ObservationInstant { domain: ProcessMonotonic { process: "workspace-transcript-rate" }, basis: CpuClock, since_origin: nanosecond(count: 25) }) { + TranscriptRateClockUndefined { from: _, to: _, order: o, rows: _ } => (match o { InstantIncomparable { left: _, right: _ } => true InstantAfter => false InstantBefore => false InstantSame => false }) + TranscriptRateObserved { rate: _ } => false + TranscriptRateRefused { reason: _, rows: _ } => false + }) +} diff --git a/dag/test/claim/xorriso_path_list_witness_test.dag b/dag/test/claim/xorriso_path_list_witness_test.dag index a4c49552407..3a76751b147 100644 --- a/dag/test/claim/xorriso_path_list_witness_test.dag +++ b/dag/test/claim/xorriso_path_list_witness_test.dag @@ -85,6 +85,7 @@ test fn nested_seed_directories_pin_owner_from_the_top_and_mode_on_every_level() Mtcollins1CensusImageDerived { input: i } => { let nested = UbuntuSeededInstallMediaBuildInput { stock_artifact: i.stock_artifact, nocloud_dir_on_iso: "nocloud/srv3", user_data_body: i.user_data_body, seed_files: [], + seed_artifacts: [], grub_kernel_cmdline: i.grub_kernel_cmdline, volume_id: i.volume_id, pinned_dates: i.pinned_dates, image_stem: i.image_stem, builder_revision: i.builder_revision, } install_media_remaster_seed_directory_levels(nocloud_dir_on_iso: "nocloud/srv3") == ["/nocloud", "/nocloud/srv3"] diff --git a/dag/test/fixture/board_fru_observation.dag b/dag/test/fixture/board_fru_observation.dag new file mode 100644 index 00000000000..b7169ec50c1 --- /dev/null +++ b/dag/test/fixture/board_fru_observation.dag @@ -0,0 +1,26 @@ +module test.fixture.board_fru_observation + +import std.optional { Present } +import extdeps.board_authority { BoardFruIdentity, BoardFruObservation, BoardFruRead } + +// FRU board-area readings as SUPPLIED VALUES for witnesses that need a host's observed board. The +// two committed reads carry exactly the fields of the captured `ipmitool fru print` texts +// (artifacts/bmc/mtcollins1-controller-fru-2026-09-12.txt and +// artifacts/bmc/mtjade1-ipmitool-fru-print-192.168.1.246-2026-10-04T110207Z.txt); the claim that +// the real producer reads those texts into these shapes is gunbc board_authority_binding's +// inhabitance witness. The ASRock read is a board with no binding row. +data collins_fru_read: BoardFruObservation = BoardFruRead { fru: BoardFruIdentity { + manufacturer: "FOXCONN", product: "Mt. Collins", part_number: Present { value: "02030A800-600-G" } +} } + +data jade_fru_read: BoardFruObservation = BoardFruRead { fru: BoardFruIdentity { + manufacturer: "WIWYNN", product: "Mt.Jade Motherboard", part_number: Present { value: "B81.03010.0041" } +} } + +data asrock_fru_read: BoardFruObservation = BoardFruRead { fru: BoardFruIdentity { + manufacturer: "ASRockRack", product: "ALTRAD8UD-1L2T", part_number: none +} } + +data gigabyte_fru_read: BoardFruObservation = BoardFruRead { fru: BoardFruIdentity { + manufacturer: "GIGABYTE", product: "MP72-HB0", part_number: none +} } diff --git a/dag/test/fixture/recorded_required_floor_log_excerpt.dag b/dag/test/fixture/recorded_required_floor_log_excerpt.dag new file mode 100644 index 00000000000..6c738127189 --- /dev/null +++ b/dag/test/fixture/recorded_required_floor_log_excerpt.dag @@ -0,0 +1,22 @@ +module test.fixture.recorded_required_floor_log_excerpt + +import std.types { Int, String } + +// REAL BYTES FROM ONE RECORDED REQUIRED-FLOOR JOB LOG, NOT A DESIGNED FIXTURE. The source is GitHub +// Actions run 35048059968, job 104642384772 (required-witnesses-floor, conclusion success, runner slot +// actions-runner@srv4-18.service), read through GitHub's "Download job logs for a workflow run" on +// 2026-10-04. The excerpt is every `[floor-phase]` and `[floor-cgroup]` line of that log, verbatim and +// in log order with GitHub's timestamp prefix kept, plus a handful of the log's untagged and +// differently tagged lines (an `[over-cost]` preview row among them) that a reader of the two tags +// must pass over. Nothing is edited inside a line; the .dag string escapes are the only change. +// +// IT IS AN EXCERPT BECAUSE THE WHOLE LOG IS 1.5 MB, and that size is itself a recorded fact the +// extraction relies on: it sits well under the seed REST handler's whole-body read, so the network +// read of a log like this one is not refused for size. +data recorded_floor_log_run_id: Int = 35048059968 + +data recorded_floor_log_job_id: Int = 104642384772 + +data recorded_floor_log_slot_unit: String = "actions-runner@srv4-18.service" + +data recorded_required_floor_log_excerpt: String = "2026-09-16T02:36:29.9676382Z [floor-cgroup] when=floor-entry path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:36:29.9692531Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=8949473280 peak=10444185600 events=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.00,total=2,full,avg10=0.00,avg60=0.00,avg300=0.00,total=2]\n2026-09-16T02:36:29.9698651Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=196170059776 peak=364675248128 events=[low,0,high,53814358,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.02,total=13867330781,full,avg10=0.00,avg60=0.00,avg300=0.02,total=13729467014]\n2026-09-16T02:36:29.9703909Z [floor-cgroup] when=floor-entry level=/sys/fs/cgroup/system.slice max=max high=max current=212249088000 peak=380169330688 events=[low,0,high,53814358,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.00,avg60=0.00,avg300=0.02,total=13841256370,full,avg10=0.00,avg60=0.00,avg300=0.02,total=13703013143]\n2026-09-16T02:36:29.9709147Z [floor-phase] phase=strict-preparation state=started\n2026-09-16T02:40:20.9989392Z [floor-phase] phase=gate-closure state=completed wall_ms=22194 prefixes=0 seeds=1 closure=168 bare_pulled=36 outside_closure=5688 corpus=5856\n2026-09-16T02:40:32.0494666Z [floor-phase] phase=expr-var-classification expr_var_occurrences=13930 free_reference_edges=3081 bound_occurrences_suppressed=10836 qualified_chain_heads=13 classification_refusals=0\n2026-09-16T02:40:32.0501531Z [floor-phase] phase=expr-var-classification BoundByParameter=7869 BoundByLet=1153 BoundByMatchPattern=1814 DeclaredInThisModule=1964 DeclaredElsewhere=1117 QualifiedChainHead=13\n2026-09-16T02:40:32.0506107Z [floor-phase] phase=reference-closure-index state=completed wall_ms=63 modules=168 names=6680 subject=93ea90191edaf50d\n2026-09-16T02:40:32.7858463Z [floor-phase] phase=touched-entry-compile-subject seeds=5 modules=[\"extdeps.tmux\", \"gunbc.roadmap_dashboard_instance\", \"gunbc.roadmap_dashboard_instance_apply\", \"test.claim.roadmap_dashboard_instance_apply_witness\", \"test.claim.tmux_session_list_outcome_witness\"]\n2026-09-16T02:40:32.7866444Z [floor-phase] phase=arm-set-changed-consumers state=completed changed_declarations=0 consumers_added=0 flat_channel_consumers=0 outside_floor_roots=0 base=db2257679fb454fcf80ddb4b901d422e8afa5d51 head=84085164947f2c2116ecd8060368c2cf35d22c8c\n2026-09-16T02:41:38.2496524Z [floor-phase] phase=gate-closure state=completed wall_ms=65296 prefixes=32 seeds=1027 closure=3021 bare_pulled=71 outside_closure=2835 corpus=5856\n2026-09-16T02:46:29.9985719Z [floor-cgroup] when=beat-10 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:46:29.9992653Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16104755200 peak=16106577920 events=[low,0,high,3848,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,3848,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=5.62,avg60=3.64,avg300=1.66,total=8053481,full,avg10=5.62,avg60=3.64,avg300=1.66,total=8053205]\n2026-09-16T02:46:30.0042734Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=227828391936 peak=364675248128 events=[low,0,high,53837371,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=1.16,avg60=1.36,avg300=0.72,total=13872120203,full,avg10=1.16,avg60=1.36,avg300=0.72,total=13734253335]\n2026-09-16T02:46:30.0074968Z [floor-cgroup] when=beat-10 level=/sys/fs/cgroup/system.slice max=max high=max current=243909996544 peak=380169330688 events=[low,0,high,53837371,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=1.15,avg60=1.39,avg300=0.72,total=13846043407,full,avg10=1.15,avg60=1.39,avg300=0.72,total=13707796653]\n2026-09-16T02:48:21.6671401Z [floor-phase] phase=module-path-index-warm state=completed cpu_ms=0 wall_ms=0 rss_growth_bytes=8192 modules=5856 provenance=built-by-preparation\n2026-09-16T02:48:21.6828893Z [floor-phase] phase=shared-index-warm state=completed cpu_ms=0 wall_ms=0 rss_growth_bytes=61440 modules=5856 provenance=built-by-preparation\n2026-09-16T02:48:22.6822520Z [floor-phase] phase=expr-var-classification expr_var_occurrences=178167 free_reference_edges=47772 bound_occurrences_suppressed=129176 qualified_chain_heads=1210 classification_refusals=0\n2026-09-16T02:48:22.6831385Z [floor-phase] phase=expr-var-classification BoundByParameter=84278 BoundByLet=17473 BoundByMatchPattern=27425 DeclaredInThisModule=21920 DeclaredElsewhere=25852 QualifiedChainHead=1210 NamesNothingKnown=9\n2026-09-16T02:48:22.7377365Z [floor-phase] phase=expr-var-classification unclassified=9 distinct=7 specimens=[\"extdeps.cloud.gcp.iam::access_token\", \"extdeps.cloud.gcp.secret_manager::access_token\", \"extdeps.cloudflare.account_api_tokens::auth_token\", \"extdeps.github.pulls::List_PullRequest\", \"extdeps.github.pulls::List_PullReview\", \"extdeps.github.pulls::List_ReviewComment\", \"std.resources::ReadWrite\"]\n2026-09-16T02:48:22.7385985Z [floor-phase] phase=reference-closure-index state=completed wall_ms=926 modules=3019 names=68559 subject=eb8fa8db5887fb3b\n2026-09-16T02:48:24.2494508Z [floor-phase] phase=pool-root-index-warm state=completed cpu_ms=524 wall_ms=777 rss_growth_bytes=7786496 producer=v2.lens.registry.completeness.lens_registry_completeness_live_facts provenance=built-by-preparation\n2026-09-16T02:48:25.1817029Z [floor-phase] phase=languages-consumer-census-warm state=completed cpu_ms=729 wall_ms=785 rss_growth_bytes=0 decl_rows=72 provenance=built-by-preparation\n2026-09-16T02:48:27.8950074Z [floor-phase] phase=prepared-effect-input-acquire state=completed acquisition=gunbc.roadmap_authority.roadmap_acceptance_event_history_load checkout_input=dag/gunbc/roadmap/roadmap_acceptance_event_history.jsonl content_digest=ac7a97b227a758c0 disposition=RoadmapAcceptanceEventHistoryLoaded cpu_ms=13 wall_ms=23 rss_growth_bytes=0\n2026-09-16T02:48:28.1831332Z [floor-phase] phase=prepared-effect-input-warm state=completed producer=gunbc.roadmap_authority.roadmap_authority_projection input=gunbc.roadmap_authority.roadmap_acceptance_event_history_load disposition=Stored cpu_ms=250 wall_ms=287 rss_growth_bytes=1867776\n2026-09-16T02:48:30.3776259Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.native_decl_selection.collision_resolved disposition=Stored cpu_ms=2150 wall_ms=2194 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:30.3818798Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.extdeps.languages.bash_command_fold.bash_fold_formal_productions disposition=Stored cpu_ms=4 wall_ms=4 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:30.3826228Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.extdeps.languages.bash_command_fold.bash_fold_lex disposition=Stored cpu_ms=0 wall_ms=0 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:30.4161518Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.compiler.program_assembly.dag_prepared_grammar disposition=Stored cpu_ms=32 wall_ms=32 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:31.1141726Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_produced_module_equals_eval.produced_add_module_source disposition=Stored cpu_ms=694 wall_ms=697 rss_growth_bytes=237568 provenance=built-by-preparation\n2026-09-16T02:48:31.6563401Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_classical_not_ingested_equals_eval.ingested_classical_not_arrow_with_body disposition=Stored cpu_ms=529 wall_ms=531 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:31.8729253Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.long.emit_host_classical_not_ingested_equals_eval.ingested_classical_not_swapped_arrow_with_body disposition=Stored cpu_ms=224 wall_ms=225 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:32.2808714Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.compiler.self_host.direct_rust_door_fixture.direct_rust_door_specimen_resolved disposition=Stored cpu_ms=405 wall_ms=408 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:32.6503107Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.execution.emit_on_demand_match_loop_fold_family_witness.mlf_family_primary_emitted disposition=Stored cpu_ms=358 wall_ms=369 rss_growth_bytes=487424 provenance=built-by-preparation\n2026-09-16T02:48:33.0043494Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.execution.emit_on_demand_family_crate_witness.logic_family_primary_emitted disposition=Stored cpu_ms=341 wall_ms=353 rss_growth_bytes=221184 provenance=built-by-preparation\n2026-09-16T02:48:33.3686481Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.self_host.stage0_production_target.stage0_boundary_profile_emitted_source disposition=Stored cpu_ms=358 wall_ms=364 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:33.5046083Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.self_host.stage0_production_target.stage0_boundary_base_emitted_source disposition=Stored cpu_ms=11 wall_ms=11 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.1446122Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add disposition=Stored cpu_ms=744 wall_ms=764 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.3435250Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add2 disposition=Stored cpu_ms=180 wall_ms=198 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.6980018Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_emission_add_then_add2 disposition=Stored cpu_ms=342 wall_ms=354 rss_growth_bytes=114688 provenance=built-by-preparation\n2026-09-16T02:48:34.7349575Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declarationless_refuses disposition=Stored cpu_ms=35 wall_ms=36 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:34.7437180Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_unwired_two_declaration_refuses disposition=Stored cpu_ms=9 wall_ms=9 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:34.7516227Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declaration_count_add disposition=Stored cpu_ms=6 wall_ms=6 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.7582621Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.claim.self_host.rust_module_emission_population.population_declaration_count_add_then_add2 disposition=Stored cpu_ms=8 wall_ms=8 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:34.9354974Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_reproducer_parsed disposition=Stored cpu_ms=166 wall_ms=173 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:35.0535117Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_bare_parsed disposition=Stored cpu_ms=116 wall_ms=121 rss_growth_bytes=90112 provenance=built-by-preparation\n2026-09-16T02:48:35.1540672Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_where_control_parsed disposition=Stored cpu_ms=97 wall_ms=100 rss_growth_bytes=49152 provenance=built-by-preparation\n2026-09-16T02:48:35.3384595Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.d1_declaration_grammar_parse.d1_cache_identity_where_parsed disposition=Stored cpu_ms=183 wall_ms=184 rss_growth_bytes=4096 provenance=built-by-preparation\n2026-09-16T02:48:35.5009300Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.where_refinement_clause_parse.where_refinement_bare_parsed disposition=Stored cpu_ms=161 wall_ms=162 rss_growth_bytes=16384 provenance=built-by-preparation\n2026-09-16T02:48:35.6112723Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.expression_bodied_fn_parsed disposition=Stored cpu_ms=109 wall_ms=110 rss_growth_bytes=16384 provenance=built-by-preparation\n2026-09-16T02:48:35.6769819Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.expression_bodied_literal_fn_parsed disposition=Stored cpu_ms=65 wall_ms=65 rss_growth_bytes=20480 provenance=built-by-preparation\n2026-09-16T02:48:35.7771869Z [floor-phase] phase=pure-producer-share-warm state=completed producer=v2.test.parse.expression_bodied_fn_decl_parse.braced_fn_control_parsed disposition=Stored cpu_ms=99 wall_ms=100 rss_growth_bytes=0 provenance=built-by-preparation\n2026-09-16T02:48:35.7781034Z [floor-phase] phase=pure-producer-share-scope state=completed rostered_modules_outside_subject=1 modules=[test.claim.live_deploy.emit]\n2026-09-16T02:48:55.2023750Z [floor-phase] phase=bare-reference-edge-index-warm state=completed roots=BareReferenceEdgeIndexBuild/source-roots cpu_ms=10124 wall_ms=17621 rss_growth_bytes=0 source_files=5856 bare_eligible=637 provenance=built-by-preparation\n2026-09-16T02:48:55.2034410Z [floor-phase] phase=bare-reference-edge-index-warm state=completed roots=BareReferenceEdgeIndexBuild/witness-layer-roots cpu_ms=6 wall_ms=6 rss_growth_bytes=0 source_files=5856 bare_eligible=637 provenance=already-warm-on-entry triggered_by=a-site-ahead-of-floor-preparation\n2026-09-16T02:48:55.2046185Z [floor-phase] phase=strict-preparation state=completed wall_ms=745230 modules_resolved=3019 modules_excluded=2 digest=eb8fa8db5887fb3b\n2026-09-16T02:48:55.4049682Z [floor-phase] phase=module-index state=completed wall_ms=202 modules=4460\n2026-09-16T02:48:55.7569513Z [floor-phase] phase=declarer-discovery state=completed wall_ms=350 declarers=18\n2026-09-16T02:48:56.1087890Z [floor-phase] phase=whole-tree-graph-facts state=completed wall_ms=353\n2026-09-16T02:48:56.1231903Z [floor-phase] phase=declarer-closure state=completed wall_ms=14 sources=19\n2026-09-16T02:48:56.2786170Z [floor-phase] phase=closure-strict-resolve state=completed wall_ms=155 sources=19\n2026-09-16T02:48:56.3076153Z [floor-phase] phase=published-mock-projection state=completed wall_ms=1105 keys=91\n2026-09-16T02:50:27.9817767Z [floor-phase] phase=discovery-authority state=completed wall_ms=90265 authority=v2.workflow.floor_discovery_source_authority sources=5856 rows=19891 entries=2194 full_inventory_release_rss_kb_before=15307380 full_inventory_release_trim_reclaimed_kb=731228 full_inventory_release_rss_kb_after=14577200\n2026-09-16T02:50:28.9533919Z [floor-claim-ceiling] grandfathered_identities=3779 grandfathered_budget_steps=361500 new_witness_budget_steps=72300\n2026-09-16T02:50:29.0124493Z [floor-phase] phase=site-projection state=completed wall_ms=91735 declared=19891 sites=4879 files=2194 claims=3971 declined_long=401 declined_fixture=0 declined_outside_gate=177 declined_gate_closure=15007 declined_discovery_excluded=5 declined_cost_debt=330\n2026-09-16T02:50:31.9575417Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9580122Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9626843Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=5 subject=c01458db5a196a34\n2026-09-16T02:50:31.9645327Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=0 bound_occurrences_suppressed=1 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9649696Z [floor-phase] phase=expr-var-classification BoundByParameter=1\n2026-09-16T02:50:31.9653062Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=92107e64fdc80bbc\n2026-09-16T02:50:31.9704793Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=1 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9707855Z [floor-phase] phase=expr-var-classification DeclaredElsewhere=1\n2026-09-16T02:50:31.9712213Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=4 names=7 subject=9b51427d706ae1d9\n2026-09-16T02:50:31.9898466Z [floor-phase] phase=expr-var-classification expr_var_occurrences=1 free_reference_edges=1 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9903147Z [floor-phase] phase=expr-var-classification DeclaredInThisModule=1\n2026-09-16T02:50:31.9905070Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=4 names=7 subject=3bd800efac0c7530\n2026-09-16T02:50:31.9923594Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9926724Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9930122Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:50:31.9993760Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:31.9995823Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:31.9999837Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:50:32.0033070Z [floor-phase] phase=expr-var-classification expr_var_occurrences=0 free_reference_edges=0 bound_occurrences_suppressed=0 qualified_chain_heads=0 classification_refusals=0\n2026-09-16T02:50:32.0037327Z [floor-phase] phase=expr-var-classification \n2026-09-16T02:50:32.0039559Z [floor-phase] phase=reference-closure-index state=completed wall_ms=0 modules=3 names=4 subject=fd4346f64dfada17\n2026-09-16T02:56:30.0047177Z [floor-cgroup] when=beat-20 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T02:56:30.0056010Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16078217216 peak=16106631168 events=[low,0,high,7254,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,7254,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=2.17,avg60=1.80,avg300=1.62,total=26620107,full,avg10=2.17,avg60=1.80,avg300=1.62,total=26619735]\n2026-09-16T02:56:30.0068529Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=159988195328 peak=364675248128 events=[low,0,high,53855074,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.24,avg60=0.28,avg300=0.47,total=13878228854,full,avg10=0.24,avg60=0.28,avg300=0.47,total=13740334938]\n2026-09-16T02:56:30.0079198Z [floor-cgroup] when=beat-20 level=/sys/fs/cgroup/system.slice max=max high=max current=176071000064 peak=380169330688 events=[low,0,high,53855074,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.21,avg60=0.26,avg300=0.45,total=13852086330,full,avg10=0.21,avg60=0.26,avg300=0.45,total=13713813769]\n2026-09-16T02:59:58.3859791Z [floor-fold-time] scope_build=143.5s over 695 construction(s) (mean=206ms) frame_build=2.2s over 3971 construction(s) (mean=0.5ms)\n2026-09-16T02:59:58.3860774Z [floor-scope-cost] max=0.13GB at=v2.test.claim.staging (1596 modules) total_built=1.29GB over 695 construction(s)\n2026-09-16T03:00:30.6968920Z [floor-phase] phase=terminal-ledger-publish state=completed resolve_ms=31249 total_ms=32635 rows=3971\n2026-09-16T03:00:30.8700701Z [over-cost] v2.test.claim.enforcement.determinism_transitive_witness.determinism_live_intra_holds observed_wall_ms=325 observed_cpu_ms=317 eval_steps=165094 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8705737Z [over-cost] v2.test.lens_test_migration_debt.test_migration_debt_test.retained_rust_kernel_wall_holds_against_live_tree observed_wall_ms=295 observed_cpu_ms=292 eval_steps=3128 line_ms=100 outcome=fail\n2026-09-16T03:00:30.8711745Z [over-cost] v2.test.claim.body_lowering.arrow_body_form_witness.arrow_body_form_eval_value_vertical_holds observed_wall_ms=287 observed_cpu_ms=232 eval_steps=90747 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8717111Z [over-cost] v2.test.claim.enforcement.lens_module_gate_witness.question_zero_verdict_live_holds observed_wall_ms=274 observed_cpu_ms=212 eval_steps=63825 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8723183Z [over-cost] v2.test.emit.emit_module_contribution.emc_all_three_arms_are_constructed_by_the_emitter_holds observed_wall_ms=211 observed_cpu_ms=203 eval_steps=110579 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8724944Z [over-cost] v2.test.parse.d5_expression_grammar_parse.cause_i_match_infix_parses_holds observed_wall_ms=204 observed_cpu_ms=203 eval_steps=76436 line_ms=100 outcome=pass\n2026-09-16T03:00:30.8726657Z [over-cost] v2.test.emit.emit_module_contribution.emc_origins_locate_the_unobserved_producer_holds observed_wall_ms=208 observed_cpu_ms=201 eval_steps=110307 line_ms=100 outcome=pass\n2026-09-16T03:06:30.0271620Z [floor-cgroup] when=beat-30 path=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service\n2026-09-16T03:06:30.0274812Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice/actions-runner@srv4-18.service max=17179869184 high=16106127360 current=16106008576 peak=16112607232 events=[low,0,high,10824,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,10824,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=9.69,avg60=6.15,avg300=2.73,total=39674404,full,avg10=9.69,avg60=6.15,avg300=2.73,total=39670316]\n2026-09-16T03:06:30.0279569Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice/system-actions\\x2drunner.slice max=max high=max current=172097458176 peak=364675248128 events=[low,0,high,53882643,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.61,avg60=1.03,avg300=1.39,total=13890030741,full,avg10=0.51,avg60=0.99,avg300=1.38,total=13752007203]\n2026-09-16T03:06:30.0283751Z [floor-cgroup] when=beat-30 level=/sys/fs/cgroup/system.slice max=max high=max current=188181905408 peak=380169330688 events=[low,0,high,53882645,max,0,oom,0,oom_kill,0,oom_group_kill,0] events_local=[low,0,high,0,max,0,oom,0,oom_kill,0,oom_group_kill,0] pressure=[some,avg10=0.61,avg60=1.00,avg300=1.35,total=13863806856,full,avg10=0.51,avg60=0.97,avg300=1.34,total=13725404851]" diff --git a/dag/test/probe/qualification_collection_relabel_probe.dag b/dag/test/probe/qualification_collection_relabel_probe.dag new file mode 100644 index 00000000000..1df99eb5bdc --- /dev/null +++ b/dag/test/probe/qualification_collection_relabel_probe.dag @@ -0,0 +1,19 @@ +module test.probe.qualification_collection_relabel_probe + +// THIS MODULE MUST NOT RESOLVE. Collecting outside the token join is minting a collection the join +// never admitted: relabel another collection's run and floor job as dispatch d's. The carrier is +// sealed, so the one function that mints it -- gunbc.runner.runner_qualification_dispatch +// collect_qualification_run, whose reads sit in the arm the token join admits -- is the only route. +// Reached only through test.claim.runner_qualification_delivery_rewrap_witness. + +import gunbc.runner.runner_qualification_dispatch { DispatchedQualificationRun, CollectedQualificationRun } + +fn relabel(d: DispatchedQualificationRun, c: CollectedQualificationRun) -> CollectedQualificationRun { + CollectedQualificationRun { + dispatched: d, + run: c.run, + floor_job: c.floor_job, + conclusion: c.conclusion, + instruments_unextracted: c.instruments_unextracted, + } +} diff --git a/dag/test/probe/qualification_delivery_rewrap_probe.dag b/dag/test/probe/qualification_delivery_rewrap_probe.dag new file mode 100644 index 00000000000..4d81ba655b4 --- /dev/null +++ b/dag/test/probe/qualification_delivery_rewrap_probe.dag @@ -0,0 +1,19 @@ +module test.probe.qualification_delivery_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. Each function is a route by which a holder of two legitimate JIT +// deliveries would relabel one: keep delivery B's sealed credential and the runner id GitHub returned +// for B, and claim dispatch A produced them -- so a qualification subject over A would carry B's +// runner. The witness test.claim.runner_qualification_delivery_rewrap_witness hands this source to the +// compiler and requires the refusal at the sealed carrier by name. dag/test/probe/ sits under the +// whole-tree strict-resolve exclusion, so this source reaches the compiler only through that witness. + +import std.types { Int } +import gunbc.runner.runner_jit_perform { + AuthorizedJitMintDispatch, BoundJitCredential, JitCredentialDelivery, JitCredentialBoundToAttempt, +} + +fn rewrap(a: AuthorizedJitMintDispatch, b: BoundJitCredential) -> JitCredentialDelivery { + JitCredentialBoundToAttempt { + bound: BoundJitCredential { dispatch: a, credential: b.credential, runner_id: b.runner_id }, + } +} diff --git a/dag/test/probe/qualification_generate_answer_rewrap_probe.dag b/dag/test/probe/qualification_generate_answer_rewrap_probe.dag new file mode 100644 index 00000000000..d710d3e0c2f --- /dev/null +++ b/dag/test/probe/qualification_generate_answer_rewrap_probe.dag @@ -0,0 +1,15 @@ +module test.probe.qualification_generate_answer_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. The generate fold pairs a dispatch with a 201 body it is handed, so +// an outside caller would mint a delivery for dispatch A from any created body. Only the perform +// function, whose body is GitHub's answer to dispatch.request, is admitted. Reached only through +// test.claim.runner_qualification_delivery_rewrap_witness. + +import extdeps.transports.rest { RestAnswered } +import extdeps.github.actions_jit_runner { JitConfigCreatedBody } +import gunbc.runner.runner_jit_perform { JitMintDispatch } +import gunbc.github_effect_perform { JitMintPerformance, jit_mint_performance_of_generate } + +fn mint_from_a_supplied_body(a: JitMintDispatch, body: JitConfigCreatedBody) -> JitMintPerformance { + jit_mint_performance_of_generate(dispatch: a, outcome: RestAnswered { answer: body }) +} diff --git a/dag/test/probe/qualification_job_log_receipt_forged_probe.dag b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag new file mode 100644 index 00000000000..5e0c7f066a8 --- /dev/null +++ b/dag/test/probe/qualification_job_log_receipt_forged_probe.dag @@ -0,0 +1,45 @@ +module test.probe.qualification_job_log_receipt_forged_probe + +// THIS MODULE MUST NOT RESOLVE. Each function below is a route by which a caller outside +// gunbc.runner.runner_qualification_dispatch would otherwise hold a job-log receipt for rows it never +// read from a run, or read a run's outputs under a token or run id the collector never admitted: (a) +// writing the sealed receipt from invented readings, (b) calling the one function that mints it from +// outside the collect stage, (c) calling the reads past the read plan, (d) reading the claim-cost +// artifact for a free token and run id. The witness +// test.claim.qualification_job_log_receipt_forged_probe_witness hands this source to the compiler and +// requires each refusal at its subject by name. dag/test/probe/ sits under the whole-tree +// strict-resolve exclusion, so this source reaches the compiler only through that witness. + +import std.content_hash { content_hash_atom } +import std.types { Int, NonEmptyStr } +import std.resources { Network } +import gunbc.github_effect_perform { ControllerInstallationToken } +import gunbc.runner.runner_host_unit_slot { HostUnitSlot } +import gunbc.runner.runner_qualification_dispatch { + CollectedQualificationRun, QualificationJobLogReceipt, QualificationLogReadings, + JobLogInstrumentStanding, AttemptJobLog, job_log_standing, + QualificationCollectOutcome, collect_qualification_instruments_under, + ClaimCostArtifactRead, read_claim_cost_artifact, +} + +// (a) AN OUTSIDE-AUTHORED RECEIPT. Refuses at QualificationJobLogReceipt. +fn forged_receipt(c: CollectedQualificationRun, h: HostUnitSlot, rd: QualificationLogReadings) -> QualificationJobLogReceipt { + QualificationJobLogReceipt { collected: c, slot: h, log_code_points: 1, log_digest: content_hash_atom(value: "invented"), readings: rd } +} + +// (b) THE MINTER CALLED FROM OUTSIDE THE COLLECT STAGE, over a log this caller supplies. Refuses at +// job_log_standing. +fn sneaked_receipt(c: CollectedQualificationRun, h: HostUnitSlot, l: AttemptJobLog) -> JobLogInstrumentStanding { + job_log_standing(collected: c, slot: h, log: l) +} + +// (c) THE READS CALLED PAST THE READ PLAN: a genuine collection under a token the plan never admitted. +// Refuses at collect_qualification_instruments_under. +fn reads_past_the_plan(c: CollectedQualificationRun, t: ControllerInstallationToken) -> QualificationCollectOutcome uses net: Network { + collect_qualification_instruments_under(collected: c, token: t) +} + +// (d) THE CLAIM-COST READ UNDER A FREE TOKEN AND ANOTHER RUN'S ID. Refuses at read_claim_cost_artifact. +fn claim_cost_for_another_run(t: ControllerInstallationToken, owner: NonEmptyStr, repo: NonEmptyStr, other_run_id: Int) -> ClaimCostArtifactRead uses net: Network { + read_claim_cost_artifact(token: t, owner: owner, repo: repo, run_id: other_run_id) +} diff --git a/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag b/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag new file mode 100644 index 00000000000..ed6688ed778 --- /dev/null +++ b/dag/test/probe/qualification_receive_jit_mint_rewrap_probe.dag @@ -0,0 +1,21 @@ +module test.probe.qualification_receive_jit_mint_rewrap_probe + +// THIS MODULE MUST NOT RESOLVE. It is the rewrap that needs no forged literal: hold an authorized +// dispatch A and a legitimate delivery B, and hand B's credential and runner id to receive_jit_mint +// under A. Unsealed, the result carries bound.dispatch == A and every equality on the dispatch admits +// it. The witness test.claim.runner_qualification_delivery_rewrap_witness requires the refusal at +// receive_jit_mint's admission. dag/test/probe/ sits under the whole-tree strict-resolve exclusion, +// so this source reaches the compiler only through that witness. + +import gunbc.runner.runner_jit_perform { + AuthorizedJitMintDispatch, BoundJitCredential, JitCredentialDelivery, JitMintDispatchAuthorized, + receive_jit_mint, +} +import extdeps.github.actions_jit_runner { JitConfigMinted } + +fn rewrap_through_the_receiver(a: AuthorizedJitMintDispatch, b: BoundJitCredential) -> JitCredentialDelivery { + receive_jit_mint( + dispatch: JitMintDispatchAuthorized { dispatch: a }, + outcome: JitConfigMinted { config: b.credential, runner_id: b.runner_id }, + ) +} diff --git a/dag/test/probe/seed_artifact_fetch_forged_probe.dag b/dag/test/probe/seed_artifact_fetch_forged_probe.dag new file mode 100644 index 00000000000..154d0010fa5 --- /dev/null +++ b/dag/test/probe/seed_artifact_fetch_forged_probe.dag @@ -0,0 +1,34 @@ +module test.probe.seed_artifact_fetch_forged_probe + +// THIS MODULE MUST NOT RESOLVE. Each function below is a route by which a caller outside the +// seeded-media remaster would otherwise write a seed artifact somewhere its admission never +// checked: replaying an admitted artifact into a seed directory of its choosing (where its name +// might collide with a text seed the admission never saw, or point outside the seed), or minting +// an admitted artifact without the admission. The witness +// test.claim.seed_artifact_fetch_forged_probe_witness hands this source to the compiler and requires +// each refusal at its subject by name. dag/test/probe/ sits under the whole-tree strict-resolve +// exclusion, so this source reaches the compiler only through that witness. + +import std.types { List, NonEmptyStr, String } +import gunbc.host_command_binding { CommandHost } +import extdeps.provisioning.ubuntu_seeded_install_media { AdmittedSeedArtifact, SeedArtifactBasename } +import extdeps.provisioning.ubuntu_seeded_install_media_remaster { install_media_remaster_fetch_artifact, install_media_remaster_fetch_artifacts } + +// (a) ONE ADMITTED ARTIFACT, REBOUND TO ANOTHER SEED DIRECTORY. Refuses at install_media_remaster_fetch_artifact. +fn rebound_one(host: CommandHost, artifact: AdmittedSeedArtifact) -> String? { + install_media_remaster_fetch_artifact(host: host, nocloud_dir: "/tmp/work/gunbc-runner" as NonEmptyStr, artifact: artifact) +} + +// (b) THE ADMITTED POPULATION, REBOUND. Refuses at install_media_remaster_fetch_artifacts. +fn rebound_all(host: CommandHost, artifacts: List) -> String? { + install_media_remaster_fetch_artifacts(host: host, nocloud_dir: "/tmp/work/../../boot" as NonEmptyStr, artifacts: artifacts) +} + +// (c) AN ADMISSION NOBODY RAN. Refuses at SeedArtifactBasename and AdmittedSeedArtifact. +fn forged() -> AdmittedSeedArtifact { + AdmittedSeedArtifact { + basename: SeedArtifactBasename { value: "runner-host-up.sh" }, + source_url: "https://example.invalid/x", + sha256: "0000000000000000000000000000000000000000000000000000000000000000", + } +} diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index 49916065be5..9a64fed5c47 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -16,7 +16,9 @@ the green positive controls of the accumulator-copy lens's fold-shape evidence ( interpreted file_gate ratchets over lean.dag, bash.dag, swift.dag, 00_compile.dag, analyze.dag, and glob_discovery_law.dag from v2.test.claim.complexity.accumulator_copy_roster_gate_*: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of interpreted file_gate as 300s census test fns over those live files; family remains operator-ruled offline (gunbc.offline_local_recipe policy accumulator_copy_roster_gate_exclusion_note)). Population: v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash roster_lean_file_gate_walk, v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash roster_bash_file_gate_walk, v2.test.claim.complexity.accumulator_copy_roster_gate_swift roster_swift_file_gate_walk, v2.test.claim.complexity.accumulator_copy_roster_gate roster_compile_stage_file_gate_walk, v2.test.claim.complexity.accumulator_copy_roster_gate roster_lens_traversal_file_gate_walk, v2.test.claim.complexity.accumulator_copy_roster_gate roster_glob_discovery_file_gate_walk. Restored when: an executing census or lane with a declared ceiling can ingest those six live subjects as claims (native ingest, or interpreted ingest whose per-file cost fits the 300s census and whose glob walk fits the 8s changed-witness wall) so the six identities are test fns that call file_gate and reach PASS or a discriminating RED on that run. Passing under claim_batch is the declared state, not the trigger. -### native route enforces admit_callers: a call from a declaration outside a fn's admitted caller list refuses — declared 2026-09-22 +### native route enforces admit_callers: a call from a declaration outside a fn's admitted caller list refuses — declared 2026-09-22 · RETIRED + +**RETIRED — TRIGGER FIRED.** 2026-10-03 -- the capability, observed on the native route (v2.test.claim.construction_confinement.construction_confinement): v2.compiler.construction_confinement, entered from v2.compiler.infer infer, checks caller admission from the fn's declared admit_callers list and refuses construction_confinement_reason_caller_not_admitted, located at the reference, for a reference from any declaration outside it (cc_real_route_refuses_exactly_the_outside_introductions_holds counts exactly one on the whole native path, beside an admitted call that raises none; cc_non_admitted_caller_refuses_holds and cc_admitted_caller_is_admitted_holds hold it at the stage's interface) -- the seed's v1 constructor_call_admission_diags. THE CARRIER DIVERGES FROM THE ONE THIS ROW NAMED, with its reason: the list is not carried on the Arrow. It is a fact ABOUT the declaration, read by a stage that needs the closure, so it rides the same route the sole_constructor property does -- captured at normalize (v2.compiler.body_lowering_fold body_lower_fn_decl_declared_admitted_callers onto NormalizedTree declared_admitted_callers), filled into the closure index as v2.std.symbol_index CallersAdmitted -- and v2.std.node arrow_signature_edges_conform is unchanged. The carrier lands with its consumer in one change, which is the capability the trigger asked for. A clause entry that is not decl_ref(module_path, decl_name) refuses at normalize (body_lowering_reason_admit_callers_entry_not_decl_ref), as v1 AdmitCallersEntryNotDeclRef does; the empty-list refusal is kept. native route enforces admit_callers: a call from a declaration outside a fn's admitted caller list refuses: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: a v2 stage checks caller admission; that stage adds the Arrow carrier with its consumer). Population: the native lane (std.compiler_entry SourceRootEvalDriver) -- every fn declaring admit_callers, the clause parsed and discarded at fn lowering. Restored when: a v2 stage checks caller admission on the native route from the fn's declared admit_callers list, refusing a call from a declaration outside it; that stage adds the Arrow carrier together with its consumer. Parsing the clause, or carrying the list with nothing reading it, does not retire this row. @@ -50,6 +52,10 @@ Builtin signature grounding -- **RUNG DROP, DECLARED (2026-09-01).** PREVIOUS RU **THE CITED-SYMBOL CENSUS IS NO LONGER A REQUIRED CHECK, AND THIS ROW DECLARES THE RUNG IT DROPS (2026-08-23).** It sits beside the CI entry above rather than inside it, for the reason the regen row already gives: that paragraph narrates the floor and regen cuts, this is a third drop on its own clock, and a declaration wedged into the repository's most-edited prose collides with every unrelated edit to it. WHAT WAS HERE: a second `witnesses.yml` job, `cited-symbol`, running `claim_executor --required-cited-symbol --source-root dag --source-root src/v2` and stopping the line when any authored `DeclarationRef` named a symbol that does not resolve — the executing half of §3's cite-the-symbol-not-the-position rule. **IT IS DELETED** — the job, its command authority (`gunbc.fabric_witness_run` `cited_symbol_run_command`), the job row and capability wiring in `gunbc.witness_floor_workflow`, and the closure witness that covered it. REASON: operator directive, 2026-08-23. This is not a defect finding and the mechanism was not failing; it was executing and green, and the directive is that it does not belong in CI as a separate corpus-wide job. PREVIOUS RUNG: *mechanically preventable* — a real wall, armed on every push, measured green at `checked=390` on run `32664434197` (`f49886339a5`, 2026-08-23T20:26Z). TEMPORARY RUNG: *mitigatable* — the rule survives as review diligence, which is strictly weaker in the exact way §6 already records for the deleted inert-lens census: a newly authored citation naming a symbol that does not exist is writable again, and nothing detects it. MEASURED POPULATION AT THE CUT: 390 authored references, on the last green run of the job. FUTURE EXPOSURE, STATED AS A SECOND FACT BECAUSE JOINING THE TWO IS THE DENOMINATOR ERROR THIS DOCUMENT KEEPS FINDING: unbounded. Nothing counts a citation authored after the cut and nothing refuses one, so §4b(3)'s BOUNDED-POPULATION REQUIREMENT IS NOT SATISFIED HERE, and this row does not pretend otherwise — the drop is an operator-approved exception to that clause rather than an instance of it, and saying so is the whole point of writing the row down. An earlier revision of this row read *the 390 references, plus every citation authored after it* and called that the bounded population; an exact measurement joined to an open future set is not bounded, and calling it so is the same move §5 forbids when a count copied from the current tree is asked to serve as an oracle. The measured half also rots the way §3 says positional citations do: it is the population's size AT THE MOMENT OF THE DROP, never a standing figure. WHAT WAS NOT CLAIMED WHILE THE DROP STOOD: the capability survived at its own entry point — `--required-cited-symbol` was still a flag on `claim_executor` — but a mode no workflow invokes guards nothing, and calling the surviving flag a mitigation would have been exactly the inflation §4b(1) forbids. **THE RESTORATION TRIGGER FIRED ON 2026-08-25 AND THIS ROW IS RETIRED AS A DROP, WITH ITS EXPOSURE CLOSED RATHER THAN RE-DECLARED.** The trigger read: this row retires when the citation wall is re-derived where the operator's own framing puts it — *you would just make them a normal compiler error* — checked at ingestion, on the module whose source carries the citation, from that module's own text, rather than reconstructed corpus-wide by a second job; and it named §6's module-authorship trigger as the same rung, to be landed together rather than each rebuilding a corpus walk. Both landed on one construction, `v1_compiler.declaration_index`, inside the `parse` phase of `claim_executor --required-ci`: the sweep that already parses every authored module now derives one record per module and resolves every authored `DeclarationRef` against it by keyed lookup. `--required-cited-symbol` is DELETED with the same change, so there is one route and not two. RUNG: back to *mechanically preventable*, and STRICTLY WIDER than what was dropped — the enrolled population is every authored citation in a non-fixture module (measured 1441 authored, 161 in fixture carriers, 79 naming namespaces no `.dag` module declares), not the five carriers the deleted lens's population named, and test modules are INDEXED, so the outside-index disposition those exclusions forced is not needed. WHAT THE FIRST EXECUTION FOUND, because a wall that lands green over a corpus nobody checked for two days would be the more suspicious result: 46 sites over 38 distinct targets, every one a citation naming a declaration that does not exist — the exact class this rule names, accumulated in the unguarded window this row declared unbounded. They are NOT repaired here and NOT silently excluded: they are enumerated at identity grain in `PRE_EXISTING_CITATION_DEBT` as a §5 monotone debt contract whose universe is discovered by the index itself and whose rows REFUSE once their citation stops refusing, so the roster can only shrink. WHAT IS STILL OPEN, stated so this retirement is not read as wider than it is: `v2.lens.cited_symbol_resolution` is DELETED as of 2026-08-26, and the disposition was three-way rather than the two this row admitted. The count was stale on arrival — sixteen was the file's size at #7707 and it had grown to 27 `test fn` identities by the time this sentence was written — and "dead" was true of the lens and false of a third of its witnesses. Measured against the seven symbols that file imported FROM the lens, only 6 of the 27 touch one; those 6 died with it. Six more call `resolve_declaration_ref`, which lives in `v2.std.decl_ref_resolution` and SURVIVES with four other consumers, so they are the only executing evidence for a live authority's five-arm refusal and §4b(4) rehomed them to `test.claim.long.decl_ref_resolution_witness_test` rather than deleting them with the machinery that climbed. The remaining 15 are population and projection claims about the carriers that PROJECT `DeclarationRef`s and moved to `test.claim.long.carrier_reference_integrity_witness_test`. What the corpus-wide census subsumes is TYPED-LITERAL citations specifically — `DeclarationRef` record literals and the `decl_ref`/`decl_field_ref` constructors — not "citations" in general; a prose reference inside a `String` is covered by nothing, before or after. The per-PR witness that survives was renamed `test.claim.doc_graph_reference_partition_witness_test`, because its subject is a bucket partition over `gunbc.doc_graph_roots` and never was resolution, and under the old name it would have been the only cited-symbol-named thing left in the tree — a misreading two readers made independently from exactly that surface; and the wall is host Rust rather than a modeled ingestion operation, declared as seed growth in `gunbc.declaration_index_seed_growth`. +### std.algebra collection_count_shape: the result type of a bare count over a free monoid in v2 — declared 2026-10-05 + +std.algebra collection_count_shape: the result type of a bare count over a free monoid in v2: RUNG DROP, structurally impossible -> mitigatable (replacement staged: collection_count_shape returns std.nat Nat (the template every other size row carries), with integer literals at the consuming positions elaborating at Nat). Population: v2.std.node: every bare count( call site (sizes of edge and child lists), each bound to collection_count_shape by the collection roster, v2.std.node_query: every bare count( call site, each bound to collection_count_shape by the collection roster, every other bare count( call in src/v2 that binds to collection_count_shape (a bare head no in-scope declaration claims), each typed Int rather than Nat, the SEED's method typing: v1.compiler 04_method reads algebra_templates_for_profile, which includes free_monoid_collection_templates, so a seed method-form count (`xs |> count`, `xs.count()`) over a FreeMonoid is typed by this row's Int (review 76274). The seed grounds std.nat Nat to the kernel integer (gunbc.structural_realization_bindings kernel_grounding_rows), so the REALIZED type of that result is unchanged; the declared type is what is narrowed. Restored when: BOTH capabilities: v2 infer elaborates an integer literal at its expected type through the declared elaboration tables of std.literal_elaboration (a carrier's kernel grounding with its declared range, or a literal homomorphism, whichever row the carrier has), AND v2 infer derives and judges a call's result type when the callee returns a declared (non-kernel) type (gunbc.recurring_failure_mode call_result_of_a_declared_return_type_unjudged_in_v2_infer) -- SUFFICIENT FOR collection_count_shape to return std.nat Nat with every count result judged at its consuming position and every literal at those positions written as it is today. Restoration: the row returns Nat and this drop retires. + ### A compiler change that adds or tightens a refusal can refuse code in admitted modules that no required lane compiles on that change, so the refusal lands on main while every required lane reports SUCCESS — declared 2026-09-24 A compiler change that adds or tightens a refusal can refuse code in admitted modules that no required lane compiles on that change, so the refusal lands on main while every required lane reports SUCCESS: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of the whole-corpus compile-clean pass of the required floor, deleted by the 2026-08-15 floor cut (gunbc.rung_drop.floor_cut_effect_gates names compile-clean among its outstanding gates); the replacement floor compiles its nominal gate closure plus diff-derived seeds, and a compiler change seeds none of the modules its new refusal reaches). Population: every blocking diagnostic the whole-root compile reports in a module no required lane compiled on the change that introduced it, keyed by (module, verbatim message, occurrence); the standing members are enumerated by the instrument, not by this row: `gunbc compile --source-root dag --source-root src/v2 --target dag --repository gunbc --measured-root-demands tools/whole_corpus_compile_measured_root_demands.json --output-dir ` on a host its admission admits, decoded by gunbc.emit_diagnostic_observation read_emit_diagnostics, the modules the whole-root compile enters in the name census only are not compiled by it, so a refusal in one of them is outside this population as well as outside the lanes; that gap belongs to gunbc.rung_drop.required_lanes_do_not_resolve_product_layer_modules and is not claimed here, designed must-not-resolve probes under dag/test/probe/ and dag/test/fixture/ appear in that compile's blocking population because the whole-root compile does not apply floor_prepared_subject_exclusions; the roster that lands atomically with the join will carry them, so the join does not report them as new, and whether each still refuses with its designed class is gunbc.compile_diagnostic_census's question, not this row's. Restored when: THE CAPABILITY, AT TWO RUNGS WITH TWO DIFFERENT CONSEQUENCES FOR THIS ROW. The join: a whole-root blocking-diagnostic identity join compiles every admitted dag-primary source and joins its blocking diagnostics at (module, verbatim message, occurrence) grain against a committed debt roster that lands atomically with it. A NEW row is a red, attributed observation naming the row and the revisions since the last green run, and a DEPARTED row needs a typed disposition (repaired, module deleted, or now compiled by a required lane). (a) RUN ON A SCHEDULE OR AFTER MERGE, the join is MITIGATION: the refusal still lands on main and is only reported after it, so the rung stays at mitigatable and THIS ROW STAYS Standing. It is a real improvement in visibility, and it is not the restoration. (b) RUN AS A REQUIRED, MERGE-BLOCKING JOIN on the change, the same join RESTORES mechanically preventable, the declared previous rung, and only that retires this row. COMPLETENESS IS PART OF THE CAPABILITY. The compiler can stop early, so a reading may be a PREFIX of the true blocking population: gunbc.emit_diagnostic_observation read_emit_diagnostics distinguishes EmitPopulationComplete from EmitPopulationCompletenessUnestablished for exactly that reason. The join is sufficient only when each reading is either Complete, or built iteratively or per phase so that a diagnostic newly visible because an earlier refusal stopped masking it is recorded with a typed PreviouslyMasked disposition. Such a diagnostic is not reported as NEW, and it is not attributed to the revision that unmasked it. WHAT DOES NOT RETIRE THIS ROW: a scheduled or post-merge join, which is arm (a); a one-off whole-root run or a receipt outside a schedule or gate, which is the 2026-09-23 specimen this row was declared on; a roster landing with no join executing it, which is the inert tier DESIGN section 6 names; a count comparison in place of the identity join (DESIGN section 5); a join over completeness-unestablished readings with no PreviouslyMasked disposition, which would misreport unmasked standing debt as new; burning the standing rows down to zero, which empties the population without adding the capability; or gunbc.rung_drop.required_lanes_do_not_resolve_product_layer_modules retiring on its changed-declaration arm, which this row exists because it can. Arm (b) needs a runner above the whole-root compile's measured demand, and whether to fund it is the operator's call. @@ -204,7 +210,13 @@ CI — **RUNG DROP, DECLARED (2026-08-15, the floor cut).** WHAT WAS HERE: one c **NO REQUIRED LANE COMPARES A SECOND GENERATION AGAINST THE FIRST, AND THIS ROW DECLARES THAT RUNG (2026-09-03).** Split out of `floor_cut`, which named this loss in prose on 2026-08-15 and then enumerated five siblings that do not include it. PREVIOUS RUNG: 2 (mechanically preventable) -- a required run compared a second emit against the first and refused a difference. TEMPORARY RUNG: 1 (mitigatable): the capability survives as `v1_compiler` `--required-regen-fixed-point`, which has an entry point and no required consumer, so a non-deterministic emitter is caught only when a human runs it. REASON: the 2026-08-15 floor cut ended the enrolment of three capabilities at once -- second-generation agreement and the two behavioral-receipt instrument targets -- and `floor_cut` records that no workflow invokes them. The three are counted together and are not one kind: second-generation agreement is NOT itself a behavioral-receipt target, and an earlier revision of this row said it was. WHY IT IS NOT COVERED BY THE REGEN PHASE THAT DOES RUN: `RequiredCiPhase::Regen` adjudicates the committed mirrors against what the authority emits ONCE -- byte agreement between two artifacts -- which is a different question from whether a SECOND generation from the same authority transaction agrees with the referenced first. The receipt model already separates those two as different variants, and this row is scoped to the second. **WHAT THIS PHASE DOES NOT DETECT, CORRECTED 2026-09-03 AND KEPT RATHER THAN QUIETLY DELETED (bright-ram-778 on gunbc#10154).** An earlier revision of this row claimed that an emitter wrong the same way twice satisfies the phase that runs and fails the one that does not. THAT IS FALSE, and it is false in the direction that inflates the missing capability: if both generations produce the same wrong bytes W, the one-shot committed comparison may pass when the committed bytes are W, and the second-generation comparison passes too, because W equals W. A repeatability comparison cannot see deterministic semantic wrongness; the only thing it sees is a GENERATION DISAGREEMENT. Naming the property correctly narrows what this row is owed, which is the point of stating it: the missing capability is agreement between generations, not correctness of either one. BOUNDED POPULATION, WIDENED 2026-09-04 AND ENUMERATED RATHER THAN OBSERVED: one capability -- refusal of a non-deterministic emit on the merge path -- over EVERY REQUIRED PHASE THAT REGENERATES A COMMITTED POPULATION, which is TWO of the five the roster declares. The enumeration is the point and is recorded so a later reader can re-run it rather than trust it: gunbc.required_ci_phase_roster declares parse, namespace-wave-admission, regen, generated-artifact and floor; the discriminator is whether the phase holds a committed artifact population it regenerates, not whether it writes files. REGEN qualifies (src/v1/stage0/src/required_regen_host.rs reaches emitted_population, whose list carries 157 mirror paths). GENERATED-ARTIFACT qualifies (its population is gunbc.generated_artifact_registry committed_generated_artifacts, 38 paths, of which 4 are seed mirrors under src/v1/stage0/src). PARSE and NAMESPACE-WAVE-ADMISSION hold no such population and emit nothing (namespace_wave_admission.rs performs zero file writes). FLOOR DOES WRITE FILES AND IS STILL OUT: they are run receipts, not a regeneration of a committed baseline, and the phase holds no committed artifact population at all -- which is exactly why 'writes files' is the wrong discriminator and 'regenerates a committed population' is the right one. HOW MANY FILES FLOOR WRITES IS DELIBERATELY NOT STATED HERE. Three independent readings disagreed on it within one review -- uploaded artifact names in the emitted workflow, fs::write sites in cli_run/required_floor_runner.rs, and a real run's artifact set -- because they measure three different populations, and the number is not what excludes floor. Transcribing any of them would put a stale count in the one sentence whose whole purpose is that the count does not decide. The instrument is the runner and the emitted workflow; re-derive it there if a count is ever needed. THE EARLIER POPULATION NAMED NEITHER PHASE, and that is the defect this widening repairs rather than a restatement: a lane restoring second-generation agreement for REGEN ALONE would have discharged the trigger while the generated-artifact phase's 38 stayed unguarded -- a trigger naming less than the capability it restores, satisfied while the capability stays dead for part of its subject, which is the failure DESIGN 4b(3) names in terms. RESTORATION TRIGGER, A CAPABILITY AND DELIBERATELY NOT A LIST OF PHASES: a required lane producing a second generation from the same authority transaction and REFUSING a byte difference against the referenced first generation, FOR EVERY PHASE THAT REGENERATES A COMMITTED POPULATION -- stated as the property so that adding a sixth such phase and skipping it does not satisfy this row, which enumerating today's two would have allowed. SUFFICIENT FOR a merge whose emitter disagrees with itself across two generations being refused, demonstrated by a red on a fixture that produces generation_1 != generation_2 UNDER THE EXACT EXECUTION MODEL THE RESTORED PHASE USES -- a fixture that is unstable only under some other harness establishes nothing about this phase. THE BOUND ON WHAT THAT RED ESTABLISHES, stated so the row is not later cited past it: one two-generation experiment shows the phase catches the generation-instability class it deliberately exercises. It does NOT show that every nondeterministic emitter is rejected, and this row must not be read as claiming it. Restoring the flag's invocation without that red does not discharge it: the flag already exists and its existence is what this row records as insufficient. **`floor_cut` CANNOT RETIRE WHILE THIS ROW STANDS.** **AMENDED 2026-10-04 BY OPERATOR RULING (paraphrased: the regen need not run multiple times, and v1 determinism is no longer a concern).** Between 2026-09-23 and 2026-10-04 the stage0 mirror half of this capability WAS enrolled, as the `regen-fixed-point` phase of the build lane (a same-binary second emit compared against the first generation's receipt); the registry-projection half never was, so this row stood throughout. The ruling deletes that phase AND the `--required-regen-fixed-point` mode behind it, so nothing now detects a nondeterministic v1 emitter on any path. That is not a lower rung -- the ladder has none below mitigatable -- it is a scope decision: BY OPERATOR RULING 2026-10-04, v1 SEED-EMIT NONDETERMINISM IS OUTSIDE THE MODELED GUARANTEE. REASON: the second generation cost a whole-closure emit on every required run and on every author regen while v1 is frozen-semantics seed maintenance, and the operator judged determinism of that seed not worth the price. RESTORATION TRIGGER: an operator ruling that re-requires v1 emit determinism, at which point this row's original trigger above applies unchanged. A waiver of DETERMINISM only: CONVERGENCE (the committed mirrors equal one emission by the seed built from them) stays enforced by the generated-artifact phase. The row is NOT retired -- its trigger has not fired, and a ruling that the capability is not owed is not that trigger. -### native G0 path records sole_constructor as a type-item property, the way v1 parsed_sole_constructor_properties does — declared 2026-09-18 +### new-witness eval-step cost gate over the kotlin and rust extended-function grammar parse claims (test.claim.parse_test_kotlin_grammar, test.claim.parse_test_rust_grammar): they still execute, eval_steps stay recorded, a semantic red and a wall-clock crossing still block; only the eval-step cost-gate rung is lowered — declared 2026-10-05 + +new-witness eval-step cost gate over the kotlin and rust extended-function grammar parse claims (test.claim.parse_test_kotlin_grammar, test.claim.parse_test_rust_grammar): they still execute, eval_steps stay recorded, a semantic red and a wall-clock crossing still block; only the eval-step cost-gate rung is lowered: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: honest per-token parse cost of the interpreted parser over a foreign grammar; owner: grammar program (stern-bear-500)). Population: test.claim.parse_test_kotlin_grammar.claim_kotlin_extended_function_parses_holds: over the new-witness eval-step budget, rostered in v2.workflow.floor_eval_step_cost_drop floor_eval_step_cost_drop_foreign_grammar_extended_parse_rows; measured by v2.test.parse census phase 4, claim_batch BuildBuddy invocations 2c8f9d0b and 9241511f on main of 2026-10-05 plus this change: verdict pass, the claim's own marginal over the new-witness budget even with its grammar preparation billed apart; re-derived by claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/parse/parse_test_kotlin_grammar_test.dag --functions claim_kotlin_extended_function_parses_holds. The billed work is one tokenize and one real parse of an extended function with the language's own grammar, test.claim.parse_test_rust_grammar.claim_rust_extended_function_parses_holds: over the new-witness eval-step budget, rostered in v2.workflow.floor_eval_step_cost_drop floor_eval_step_cost_drop_foreign_grammar_extended_parse_rows; measured by v2.test.parse census phase 4, claim_batch BuildBuddy invocations 2c8f9d0b and 9241511f on main of 2026-10-05 plus this change: verdict pass, the claim's own marginal over the new-witness budget even with its grammar preparation billed apart; re-derived by claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/parse/parse_test_rust_grammar_test.dag --functions claim_rust_extended_function_parses_holds. The billed work is one tokenize and one real parse of an extended function with the language's own grammar. Restored when: THE CAPABILITY: a precedence-climbing tree contract (one node per operator application, not one per level), or parse claims evaluated on the native route -- the capability gunbc.rung_drop dag_text_round_trip_nested_bind_new_witness_eval_step_cost names. WHAT IT MUST BE SUFFICIENT FOR: both identities, still tokenizing and parsing their extended-function source once through their own language's grammar, measuring under the NewWitnessTier budget that v2.workflow.required_floor claim_ceiling_eval_step_budget derives. Deleting an identity, shrinking its source below the constructs its claim names, supplying its tokens or parse, or bundling it into a shared producer does not satisfy this trigger. + +### native G0 path records sole_constructor as a type-item property, the way v1 parsed_sole_constructor_properties does — declared 2026-09-18 · RETIRED + +**RETIRED — TRIGGER FIRED.** 2026-10-03 -- the capability, observed on the native route (v2.test.claim.construction_confinement.construction_confinement): the captured sole_constructor terminal (v2.compiler.body_lowering_fold TypeDeclModifier, on NormalizedTree type_declaration_modifiers) is minted as a construction property -- v2.compiler.normalized_tree normalized_tree_confinements projects it to v2.std.symbol_index ConstructionSealedToModule, which v2.compiler.symbol_index_fill symbol_index_fill_module_confinements writes into the closure index at the declaring path -- and v2.compiler.construction_confinement, entered from v2.compiler.infer infer, refuses construction_confinement_reason_sole_constructor_outside_declaring_module, located, for a record construction AND for a cast into the type from any module other than its declaring one: the wall v1 sole_constructor_construction_diags enforces on its record-literal and cast arms. cc_real_route_refuses_exactly_the_outside_introductions_holds runs the whole native path over one program holding admitted and refused introductions together and counts exactly two such refusals (red with the infer gate removed, where the same program refuses only for unrelated grounding and coercion reasons), so the inside construction and the checked-constructor call beside them are admitted; the decision rows cc_outside_construction_refuses_holds, cc_outside_cast_refuses_holds, cc_inside_construction_is_admitted_holds and cc_inside_cast_is_admitted_holds hold it at the stage's interface. nominal_opaque still mints nothing, as in v1. native G0 path records sole_constructor as a type-item property, the way v1 parsed_sole_constructor_properties does: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: v2 parse mints the sole_constructor property from the captured modifier terminal). Population: v2.compiler.body_lowering_fold body_lower_type_decl_declared_modifiers — the slot is CARRIED (NormalizedTree type_declaration_modifiers) but no stage checks construction against it, native emit of type decls that carry sole_constructor — the construction property is not minted. Restored when: v2 parse mints the sole_constructor property from the captured modifier terminal on a type decl, sufficient for the construction wall v1 parsed_sole_constructor_properties enforces. Consuming the token without minting that property does not retire this row. Token consumption of nominal_opaque matches v1 and is not this drop. @@ -372,7 +384,7 @@ the renderer hop's structural arm (v1.compiler.emit_rust provenance_declares_str **RUNG DROP, DECLARED (2026-08-29, the CI bankruptcy, operator ruling in session neat-tern-658).** PREVIOUS RUNG: mechanically preventable for every class the required run carried -- eight phases over two lanes, the floor folding every discovered witness (11,996 on the last green run) through a whole-corpus strict preparation, plus v2-emission, emit-compile, partition-crates and generated-artifact drift. TEMPORARY RUNG: mechanically preventable ONLY for what the static gate names -- parse, namespace-wave-admission, regen, generated-artifact drift, and a floor over `v2.workflow.required_floor` `required_gate_prefixes` prepared as that roster's import closure -- and MITIGATABLE for everything else: those witnesses and phases are still runnable (the floor with the gate widened, `claim_executor --required-v2-emission`, `--required-emit-compile`; and -- CORRECTED 2026-08-31, re-verified against main AFTER the generated-artifact partial climb below, because a drop whose stated mitigation cannot be executed is not mitigated but a rung-honesty defect in the drop row itself, and one cited as coverage -- NOT `--required-partition-crates`, which is ABSENT from claim_executor's argument parser: that build is projected by `gunbc.repo.repo_self_build` `repo_self_build_partition_crates_command`. `--required-generated-artifact` is absent from that parser too, and the climb below restores the CHECK without restoring that spelling, so the generated-artifact projection is regenerated through the .dag entry point `tools.generated_artifact_gate` `main_wet_one`, invoked per artifact path via `gunbc run` on a gunbc BUILT FROM SOURCE -- the installed binary predates the section 4c annotation channel and cannot parse the corpus. Both flag spellings went with this cut's deleted phases and the prose naming them was not updated, so this sentence recited two remedies nobody could run) and their receipts are produced only when someone runs them, so a regression there is caught at the next manual run, not at merge. REASON: the last green run was 87 wall minutes (`gh run view 33140387402`), strict preparation alone 27 of them on one core, against ~50 squash-merges a day; main sat red for fifteen consecutive runs on two integration collisions no PR could have seen (#9662), and the CI-cost lane's change-denominated selection did not land in time to prevent it. A gate no merge can wait for validates nothing, which is a lower rung than a small gate that runs. POPULATION: every witness outside the roster (reported per run as `declined_outside_required_gate` in the disposition TSV), the three deleted required phases v2-emission, emit-compile, and partition-crates, and the v1-compiler lib tests that carry the `live-corpus` ignore reason -- enumerated by `cargo test -p v1-compiler --lib -- --ignored --list` -- each of which prepares or builds over the live tree and is run only by the receipts lane; AND -- POPULATION AMENDED 2026-09-02, because this row was cited as coverage for a member it did not name -- every module OUTSIDE THE REQUIRED FLOOR GATE CLOSURE, which no required phase compiles at all: the floor prepares the import closure of `required_gate_prefixes`, not the corpus, so a module that no rostered prefix transitively imports is never checked by any required run, and a green required run has therefore never entailed that the corpus holds the floor. That member is a FOURTH thing the original population sentence did not name -- not a witness outside the roster, not a deleted phase, not an ignored lib test -- while the RESTORATION TRIGGER below already covers it verbatim (the whole corpus as its universe). The omission was a rung-honesty defect in THIS ROW rather than a missing class, so it is amended here instead of filed as a second row: two rows behind one trigger is the same single-authority fork this project rejects in every other subject, and the more dangerous form when the row is quoted as evidence of what CI checks. the first-ever CI run of `cargo test -p v1-compiler --lib` (run 33238828500) was cancelled by its own 60-minute timeout with 204 of 682 tests finished, which is how that class was measured into existence. The workflow's two-lane shape, the aggregate context, and the per-claim cost receipt are unchanged, and the drop is countable from that receipt every run. RESTORATION TRIGGER: the CAPABILITY of a required run whose population is DERIVED from the change -- the roadmap's 'decide what matters before paying to prepare everything' -- executing on the required path with the whole corpus as its universe; a widening of the static roster does not retire this row, it only moves the population. Widening the roster is a wall-clock decision priced by the cost receipt and recorded on the roster's own header. GENERATED-ARTIFACT PARTIAL CLIMB (2026-08-31): restoring generated-artifact drift widens the static required roster by one exact class and does NOT retire this drop; the population-derived restoration trigger has not fired. INCIDENT RECEIPT (2026-08-31), recorded because a drop whose population has demonstrably fired ranks differently for restoration than one that never has: the generated-artifact drift member produced a real stale artifact on main -- two correct regens composed a `docs/design-ledgers.md` missing the frontier-receipts projection of `gunbc.self_host_compile_phase_frontier`, with no conflict and no author error -- detected only sideways by an adjacent-diff review question, and bounded to exactly one file by a dispatched whole-population `main_wet` regen on pristine main (repair and measurement: PR #9804). The window between drift landing and detection is the cost this row's REASON accepted, now materialized once. INCIDENT RECEIPT (2026-09-02), recorded on the same principle as the one above -- a population that has demonstrably fired ranks differently for restoration than one that never has: a whole-corpus compile over dag plus src/v2 found 11 NON-EXHAUSTIVE MATCH SITES STANDING ON GREEN MAIN, closed variants that do not eliminate exhaustively, which is the ordinary compiler floor this document names first; every one of them sits in a module the gate closure never compiles, so no required run has ever been able to see them. Measured as the BASELINE arm of the nested-pattern exhaustiveness checker in gunbc#10028 (whose own change additionally exposes a larger population of ordinary floor defects that the OLD checker could not see -- that population is counted by that PR and its repair program, deliberately NOT transcribed here, because a number copied into this row rots the moment the instrument that produced it is re-run; it is not evidence about this gate in any case, since those sites were invisible to the CHECKER rather than to CI, and only the 11 that main already carries under today's checker measure what the gate does not compile). Those 11 are the first number anyone has put on the gap between main is green and the corpus holds the floor, and they are exactly the population this amendment names. INCIDENT RECEIPT (2026-10-07), a further firing of this drop: the EqualityOptionalityMismatch wall (failure mode optional_compared_with_required_value) landed with its consumers outside the gate, so every site it refuses stayed broken while every required lane reported success, and was found only by a whole-population emit census run as a scratch script (not an instrument, and no count is carried here); gunbc#13549 repaired the sites by matching on the optional. Standing, trigger and gate prefixes are unchanged. -AMENDED 2026-10-01: the srv1 probe of the withdrawn v2.test. family (neat-boar-16, scratch commit eff2d73b14 = main c95f906 plus the family, artifacts on branch artifact/v2-gate-probe) measured what this row's population holds for that family: 841 v2.test.* modules declaring 5,030 test fns at c29acd041e1 have no required route; with the family restored, 4,265 were planned, 4,192 passed, 17 reached no verdict (route gaps already rostered by v2.workflow.floor_route_gap), and 44 were red on main with no lane observing them. Preparation dominated, about 3,569 modules resolved and 3,096 s wall against about 101 s of claim evaluation, the shape of this row's 2026-09-19 nominal-roster cut, so the family stays out until the trigger below. One module, v2.test.claim.cargo_build_run_argv_witness, did not typecheck on main and refused the whole floor in strict preparation when the family was restored; gunbc#12860 repaired it; of the 44 measured red identities, those still red are named here with their owners, and each leaves this amendment's list when its fix lands, so restoring the family does not inherit them as silent reds. Owner deep-ferret-305 (floor lane), fixed by work item adhoc-e6c8990b-ccf: v2.test.emit.rust_variant_construct_emit.rust_variant_construct_emit_holds_accepts (returned false); v2.test.emit.rust_variant_construct_emit.rust_variant_construct_emit_wrong_value_discriminates (returned false); v2.test.name_resolve.test_code_reference_wall.supplied_root_is_what_normalize_emits_holds (returned false). Owner calm-fox-43 (grammar lane), measured red in a SEPARATE measurement from the srv1 probe: gunbc run --claim-run on main eb60ee176d0 on 2026-10-01 (remote 48GB runner), identical on main's grammar and on gunbc#12920, so not caused by that change; cause not yet derived: v2.test.execution.emit_ingest_grammar_relation_round_trip.same_grammar_parse_ingest_bridge_holds (returned false); v2.test.execution.dag_add_emit_round_trip.dag_add_domain_coerce_accepts_holds (returned false); v2.test.execution.dag_add_emit_round_trip.dag_add_domain_translate_accepts_holds (returned false). None is an expected-red row: that roster asserts the identity executes, and these do not while the family is outside the gate. The restoration trigger above stands whole. +AMENDED 2026-10-01: the srv1 probe of the withdrawn v2.test. family (neat-boar-16, scratch commit eff2d73b14 = main c95f906 plus the family, artifacts on branch artifact/v2-gate-probe) measured what this row's population holds for that family: 841 v2.test.* modules declaring 5,030 test fns at c29acd041e1 have no required route; with the family restored, 4,265 were planned, 4,192 passed, 17 reached no verdict (route gaps already rostered by v2.workflow.floor_route_gap), and 44 were red on main with no lane observing them. Preparation dominated, about 3,569 modules resolved and 3,096 s wall against about 101 s of claim evaluation, the shape of this row's 2026-09-19 nominal-roster cut, so the family stays out until the trigger below. One module, v2.test.claim.cargo_build_run_argv_witness, did not typecheck on main and refused the whole floor in strict preparation when the family was restored; gunbc#12860 repaired it; of the 44 measured red identities, those still red are named here with their owners, and each leaves this amendment's list when its fix lands, so restoring the family does not inherit them as silent reds. Owner deep-ferret-305 (floor lane), fixed by work item adhoc-e6c8990b-ccf: v2.test.name_resolve.test_code_reference_wall.supplied_root_is_what_normalize_emits_holds (returned false). The two rust_variant_construct_emit identities that work item named left this list when proud-wren-900 grounded unit-variant True on the binding-ref literal ladder (dag_binding_type_variant_true stays a coproduct arm, never a bool keyword), stopped treating BindingRef match as the int-bool octet channel, and retargeted complement/meet/join goldens and emit_host allocators onto dag_token_kw_true/false; emit_host_variant_construct_equals_eval left when its eval stub read ConstructTagEdge instead of a named Holds child the construct node does not carry. Owner calm-fox-43 (grammar lane), measured red in a SEPARATE measurement from the srv1 probe: gunbc run --claim-run on main eb60ee176d0 on 2026-10-01 (remote 48GB runner), identical on main's grammar and on gunbc#12920, so not caused by that change; cause not yet derived: v2.test.execution.emit_ingest_grammar_relation_round_trip.same_grammar_parse_ingest_bridge_holds (returned false); v2.test.execution.dag_add_emit_round_trip.dag_add_domain_coerce_accepts_holds (returned false); v2.test.execution.dag_add_emit_round_trip.dag_add_domain_translate_accepts_holds (returned false). None is an expected-red row: that roster asserts the identity executes, and these do not while the family is outside the gate. The restoration trigger above stands whole. ### Required lanes do not resolve product-layer modules: a module outside the nominal gate closure and the drift gate's entry closure can stop resolving on main while every required lane reports SUCCESS — declared 2026-09-20 diff --git a/docs/plans/accumulator-copy-roster-gate-census-ledger.md b/docs/plans/accumulator-copy-roster-gate-census-ledger.md deleted file mode 100644 index 3cdcba2b658..00000000000 --- a/docs/plans/accumulator-copy-roster-gate-census-ledger.md +++ /dev/null @@ -1,17 +0,0 @@ -# Ledger: accumulator_copy roster gate (300s census) - -Parent: sunny-bat-82 / snappy-stag-806 silent reds. Subject: `complexity/accumulator_copy_roster_gate_*` as one roster-scan cost. Cap not raised. Not enrolled in the required gate. - -Instrument: out-of-gate 300s census of those entries. Classification: **(d)** — real standing and scan-cost defects, not stale greens. - -Typed standing for priced-out lens walks: `gunbc.rung_drop.accumulator_copy_priced_out_file_gate_ratchets`. Executing consumer for those walks: `gunbc.offline_local_recipe` `RecipeFunctions` rows (not the drop's population strings). Exclusion rationale stays `gunbc.ci.ci_layer_roots` `accumulator_copy_roster_gate_exclusion_note` (unchanged from main). - -| Row | Class | Cause | Repair | -| --- | --- | --- | --- | -| TIMEOUT lean/bash/swift entries | (d) | Interpreted `file_gate` of `src/v2/extdeps/languages/{lean,bash,swift}.dag`. | Freeze identities deleted (meaning fork: ratchet name on a weaker contract). Walks: `roster_{lean,bash,swift}_file_gate_walk` in `*_walk.dag`, named by `gunbc.offline_local_recipe`. | -| TIMEOUT `accumulator_copy_roster_gate_test.dag` | (d) | Same scan packed `00_compile.dag` and `analyze.dag`. | Ordinary `fn`s `roster_compile_stage_file_gate_walk`, `roster_lens_traversal_file_gate_walk`. Executing `file_gate` `test fn`s: snippets plus `roster_std_algebra_zero_suspects_within_ratchet`, `roster_machine_code_zero_suspects_within_ratchet`, `roster_lens_cost_model_zero_suspects_within_ratchet`. | -| RED `red_control_planted_copy_still_alarms` | (d) | Control used `list_append(left: acc, right: x)` — linear non-copy polarity (`noncarrier_name_snippet`). Fixture `planted_copy.dag` and `quadratic_snippet` use `left: x, right: acc`. | Snippet polarity aligned with the planted fixture. | -| RED `roster_glob_discovery_zero_suspects_within_ratchet` | (d) | Path moved to `src/v2/test/workflow/glob_discovery_law.dag` (#9637). Interpreted `file_gate` also interrupted the 8s changed-witness wall. | Freeze identity deleted. Walk: `roster_glob_discovery_file_gate_walk` named by `gunbc.offline_local_recipe`. | -| RED `roster_compile_stage_zero_suspects_within_ratchet` | (d) | `00_compile.dag` is not a 300s interpreted subject. | Renamed `roster_compile_stage_file_gate_walk`; named by the recipe. | - -Freeze rows for demoted walks were deleted so they cannot rot as `StaleFrozenPathDeferral`. CI exclusion substring `claim/complexity/accumulator_copy_roster_gate` is unchanged. diff --git a/docs/plans/anonymous-record-literal-design.md b/docs/plans/anonymous-record-literal-design.md deleted file mode 100644 index 7cb43d6b562..00000000000 --- a/docs/plans/anonymous-record-literal-design.md +++ /dev/null @@ -1,305 +0,0 @@ -# Anonymous record literals: the tag-absent construct, elaborated from the expected type - -Status: PR1 of two (the model and its consumer census). PR2 is the cut. Owner lane: sleek-fox-423, -parent gentle-koi-724, reviewer neat-boar-16. This builds on the construct carrier of -`docs/plans/construct-tag-reference-design.md` (gunbc#12701, lively-eagle-657) and does not restate -it. - -## Symptom and the chain (DESIGN 6b) - -A headless brace literal, `data x: T = { f: v }` or a nested `f: { g: w }`, file-refuses at native -ingest with `body_lowering_reason_unsupported_form`. A refused file drops its declarations from the -native index, so every importer then fails as unbound. The survey (lively-eagle-657, quiet-hawk-702 -probe on the #12420 tree) names seven files. - -The slice, read from the grammar forward: - -- **Grammar.** `v2.extdeps.languages.dag` `dag_grammar_primary_expr_core` has ONE headless-brace - alternative, `{ field_init_list }`. `dag_grammar_field_init_expr` keys each item on EITHER a - binding name OR a string literal. The record literal `{ f: v }` and the map literal - `{ "k": v }` are the same production. Only the lexical kind of each key tells them apart. -- **Lowering.** `v2.compiler.body_lowering_fold` `body_lower_try_record_literal` needs a head tag - from `body_lower_record_literal_tag_optional`. A headless brace has none, so the primary is - unreadable and refuses at the primary (the XL-2 refusal arm, - `unrecognized_primary_expression_lowers_to_its_first_atom_at_v2_body_lowering`). This refusal is - CORRECT at this link. Lowering runs before resolve and before infer, so it cannot know the - constructor. The earliest unjustified boundary is therefore not lowering. It is the construct - carrier, which has no tag-absent case, together with the absence of any stage that carries a - declared type DOWN into the expression it types. -- **No stage pushes a declared type down.** v2 infer is a bottom-up fold. Its declared-type - judgments run AFTER a body is synthesised: `infer_arrow_body_inhabits_declared_return` compares a - derived body type with a declared return (cause `arrow_body_does_not_inhabit_declared_return`), - and only for a return the language join denotes (`dag_binding_denotation`). Call arguments are - judged the same way (`position_direct_call_argument`). A tag-elided construct cannot be - synthesised at all, because its tag is what types it. So its tag must be supplied BEFORE infer's - fold reaches it, by a stage that walks top-down with the declared type as context. -- **The seed is not an oracle for the choice.** v1 chooses the struct in its Rust emitter by - matching field names: `v1.compiler.emit_rust` `anonymous_record_struct_candidates`, and it - refuses `AmbiguousAnonymousRecordLiteral` ("shape matches N structs -- add a nominal type"). - That is the selection this lane is forbidden to make. So seed-vs-native agreement on the chosen - tag is not evidence. The oracle is content equality with the authored headed form (control 1). - -## The model - -**The carrier.** Form B is the tag-ABSENT case of the one construct carrier of #12701. It is not a -new node kind. After #12701 a construct is `Conj { : , fields.. }`. -The tag-absent construct is `Conj { : , fields.. }`. - -- The tag edge is always present, so every construct reader keys on the same first edge and asks - only whether its target is a reference or the elision marker. The alternative, a construct with - no tag edge, is a bare `Conj` of Named edges. That is also the anonymous record TYPE - (`body_lower_anonymous_record_type_optional`, `-> { success: Bool }`) and the shape every - "unmarked Conj" reader already means something else by. Presence-by-absence would be a second, - implicit representation. It is refused for the same reason #12701 keeps `construct_tag_marker` - distinct from `declaration_reference_marker`. -- `construct_tag_elided_marker` is a structural core marker declared beside `construct_tag_marker` - in `v2.std.node`, in the same form, and counted once in the #12473 `EdgeLabel` census. It - carries no spelling and no guess. -- **Key kind is recorded, not interpreted.** Each field edge keeps its authored key kind. A name key - is `Named { name }`, as today. A string key has no field-edge form yet (see Scope). Lowering does - not decide "record" versus "map". It records what was written. - -**Where elaboration happens: in resolve, at the construct-tag writer, with an expected-type -context threaded down from declared types.** (Revised after the first review. The first draft -placed it in infer "before synthesis". Infer is a bottom-up fold, so that would have been a second, -top-down pass beside it.) - -Choosing the tag reads exactly two facts, and both are NAME facts that resolve already has: - -- The expected type's HEAD resolves to a declaration. Resolve resolves every type expression. -- Whether that declaration is a single record, and, for nesting, its fields' declared types. Resolve's - `ResolveContext.namespace.symbol_index` returns the declaration node through `symbol_index_lookup`, - and `v2.std.type_binder` `type_decl_view` reads its shape. - -Resolve is also, after #12714, the ONE writer of construct tags: `resolve_construct_walk` rewrites -the authored tag spine into `declaration_reference_node(path)`. Writing the elided tag anywhere else -would give one fact, "the declaration this construct constructs", two writers (§3). So the elided -case is a second arm of that same writer, with the reference derived from the expected type rather -than from an authored spine. Infer then sees only ordinary resolved constructs, and checks them -with its existing construct typing and declared-return judgment. - -**The expectation is a parameter, not ambient context.** An expectation (`ResolveExpectation`: the -authored type expression, and the position its names are read at) is handed, as an explicit -argument, by exactly the four position rules below to the one child each rule owns -(`resolve_expected_edge`). It is never a `ResolveContext` field. Every other walk goes through -`resolve_node_walk`, which has no expectation to give, and a headless literal there refuses. That -makes review condition 3 structural rather than a clearing discipline: a field on the context would -have been inherited by every child walk that copies the context (a call's arguments, for one), and -each would have had to remember to clear it. - -**The expectation is set ONLY from an AUTHORED type annotation** (ruling, gentle-koi-724): a data -declaration's declared type, a fn's declared return, a record field's declared type, or the element -type of an annotated list. Resolve never infers, unifies or propagates a type it computed. Where no -annotation reaches an elided construct, it refuses located (`resolve_anonymous_record_no_expected_type`) -and never guesses. A record field's declared type is an annotation authored on the record -declaration, so nesting stays within this rule. **Syntactic peel only** (review condition 1, neat-boar-16, binding both arms). The expectation is peeled -syntactically from the authored annotation. Where reaching a closed record head would need -inference, type-variable instantiation, alias unfolding beyond resolve's existing lookup, or any -head that is not a closed record (or `Map`, for the map arm), the construct REFUSES located. There -is no unification and no substitution in resolve. A record field whose declared type is one of the -record's own binders (`top: T` of `BoundedLattice`) therefore gives no record head, and a headless -literal there refuses. The four positions: - -- **data initializer**: `data x: T = e` lowers to `Arrow(, T, T, body: e)`, so the - body edge of an Arrow is walked with its declared return as the expectation; -- **declared return**: the same Arrow rule covers `fn f(..) -> T { e }`, reaching `e`'s tail - expression through blocks and `let .. in` bodies (the value position, not the statements); -- **record field**: under a construct resolved to record `R` (authored OR elaborated), field `f`'s - init is walked with the declared type of `R.f` as the expectation, as authored (no substitution). This is how nesting works: `compile_stage_memo`'s - `key_derivation: { .. }`, and `host_transport`'s `RuntimePrimitive { value: { .. } }`; -- **list element**: under a list literal whose expected head is `List` (through its alias - authority), each element is walked with the element type argument as the expectation. - `dag/std/algebra`'s 88 `AlgebraFieldTemplate` rows need this. - -At a tag-elided construct, the elided arm of the writer: - -1. Takes the expectation. If there is none, refuse `resolve_anonymous_record_no_expected_type`. -2. Takes its HEAD's resolved declaration, through resolve's existing lookup only. Type arguments - (`BoundedLattice`) never choose the constructor, and they are not substituted - anywhere. If the head does not resolve, the head's own resolve refusal stands, and no second - cause is minted for it. A head that is a type binder, or that reaches a record only through - alias unfolding the lookup does not already do, refuses - `resolve_anonymous_record_expected_type_not_record`. -3. Requires the declaration to be a single record (`type_decl_view` `PlainTypeDecl` whose member is a - product), or `Map` (the MAP arm, which this lane refuses located; see Scope). Anything else, - whether a multi-variant coproduct, a primitive, or `List`, refuses - `resolve_anonymous_record_expected_type_not_record`. -4. Writes `declaration_reference_node(path)` of that declaration through `resolved_reference_node`, - the same carrier the authored arm writes. -5. Walks the fields under the record-field rule above. - -The choice reads the expectation only. The field set is never an input to steps 1-3. - -**Resolve's choice is not proof** (review condition 2). Infer CHECKS the elaborated construct -against the declared type exactly as it checks an authored tag: field names and field value types -through its existing construct typing, and the declared return through -`infer_arrow_body_inhabits_declared_return`. A wrong field or a wrong value type refuses in infer, at -the field. - -**Refusal causes** (typed, located at the literal's `{`, each with an ownership row in -`v2.workflow.compile_door_cause_ownership`, per -`refusal_producer_lands_without_its_ownership_row`): - -- `resolve_anonymous_record_no_expected_type`: the construct sits in a position with no declared - type. Examples: a `let` without an annotation, a call argument, a match-arm body, a list literal - that itself has no expected type. These are not guessed. Call arguments stay here on purpose: - their formal's declared type is a fact about the CALLEE, whose resolution this position does not - own. Widening to them is a named follow-up, not an implicit one. -- `resolve_anonymous_record_expected_type_not_record`: the expected head resolves to something other - than a single record or `Map`. -- `map_literal_construction_not_modeled`: the expected head is `Map`. This is the MAP arm, and it - plugs into the same writer. - -A tag-elided construct that survives resolve is unwritable downstream. The emit, eval and target_model -readers gate on a reference target, and the elision marker is not one. So a missed elaboration -refuses at their existing gates, and cannot emit an anonymous struct. - -## As built in PR2 (gunbc#12740) - -The model above was refined while the cut was reviewed. Each refinement below is the authority that -the PR2 code cites by section name. - -### One reader of the construct tag - -`v2.std.node_query` `construct_tag_reading` answers -`ConstructTagAuthored { reference } | ConstructTagElided | NotAConstruct`. Every reader (body -lowering's core-substrate test, resolve's construct-tag writer, the claims) matches on that one -answer. `construct_tag_optional` is its projection. The elision is carried as an EDGE LABEL -(`construct_tag_elided_marker`) on the tag's target, the mechanism the tag marker itself uses. A -marker carried as an atom's identity did not survive to resolve. And body lowering must count an -elided construct as core substrate, or its fold re-lowers the literal to its first atom. - -### The string key is a grammar production - -`dag_grammar_field_init_expr` is a choice between a name alternative and a production of its own, -`^dag_production_field_init_string_key` (emitted `^dag_surface_field_init_string_key`), whose key is -the class-stamped string terminal that gunbc#12759 decodes. So the key's kind is a fact of the parse -tree, read by its production identity (`body_lower_field_init_is_string_keyed`), and never recovered -from a spelling. A record literal refuses a string key at the item (`field_init_unlowered`). The -occurrence-role row of the new production reads its names as references, and the field-init reader -answers `NoNameHere` for it. The map arm (gunbc#12758) dispatches on the same identity. Admitting a -quoted key whose DECODED text names a declared field is a declared frontier. It is a follow-up in the -record arm, and it also carries the quoted-key round trip; the first lane that admits quoted keys -(gunbc#12758) carries that round trip for maps. - -### The head resolves the way the annotation resolves - -An annotation written in the literal's own module resolves its head through `resolve_atom` in the -literal's context, the same route the annotation itself takes, so the two cannot bind different -declarations. A field type is written in the record's own module, so it is read at the record's path -through the index's lexical lookup. - -### The list-element rule - -A list literal passes its element type to its elements only when the annotation's head reaches the -declaration infer types list literals with (`v2.std.list_introduction` -`list_introduction_head_path`, `std.algebra.FreeMonoid`). - -### Pure-renaming aliases - -`List` is an alias (`type List = FreeMonoid`), so the list rule needs one kind of -alias to be followed. Exactly one kind is followed: a PURE RENAMING, `type A = H` -(the aliased expression is a head applied to exactly the alias's own binders, in declared order) or -`type A = H`. Following one substitutes nothing, because the annotation's arguments stay in the -positions they were written. - -- Purity is decided ONCE, on the declaration (`v2.std.type_binder` `type_alias_renaming`). It is - recorded when the index records the declaration (`v2.std.symbol_index` `renaming_aliases`, - `symbol_index_renaming_alias_at`), and resolve reads the recorded fact, never re-deriving it at a - use. -- Chains are followed transitively, each renamed head read at the alias's own position. A revisited - path refuses located (`resolve_anonymous_record_alias_cycle`). -- A reordering, constant or partially applied alias is not a renaming. It is the declaration the head - names, so the literal refuses. - -## Consumers (producers, readers, rewriters) - -Stated as a delta on #12701's census. Every row there still applies; the rows here are the ones -the elided case adds. - -| Module | Symbol | Role | Change | -|---|---|---|---| -| v2.std.node | `construct_tag_elided_marker` | vocabulary | New structural core marker beside `construct_tag_marker`. | -| v2.std.node_query | `construct_node` | producer | Takes the tag target as the reference OR the elision marker (one parameter; a typed `ConstructTag = Authored { reference } \| Elided` at the call boundary so a caller cannot pass an arbitrary node). | -| v2.std.node_query | `construct_tag_optional` | reader | Answers the reference only. For an elided tag it answers Absent. `construct_tag_is_elided` is the one reader of the elided case. | -| v2.std.node | `arrow_body_record_construct_conforms`, `classify_arrow_body_form` | well_formed gate | Admits the elided marker ONLY before resolve. After resolve the well_formed gate refuses it. | -| v2.compiler.body_lowering_fold | `body_lower_try_record_literal` | producer | A headless `{ field_init_list }` whose every key is a name lowers to `construct_node(Elided, ..)`. The field inits go through the existing `body_lower_field_init_edge`. | -| v2.compiler.resolve | `ResolveExpectation`, `resolve_expected_edge`, `resolve_arrow_body_or_named_edge`, `resolve_record_field_edge`, `resolve_list_literal_expected` | position rules | The expectation parameter and the four rules that hand it to one child each. `ResolveContext` is unchanged. | -| v2.compiler.resolve | `resolve_construct_walk`, new `resolve_construct_walk_with_tag`, `resolve_elided_construct_walk` | writer | The tag is walked once before the fields, and its resolved path is handed to the field rule, so a headless literal nested under an authored head elaborates. The elided arm writes the tag from the expectation (steps 1-5). In `resolve_node_walk`, an elided construct refuses `resolve_anonymous_record_no_expected_type`. | -| v2.compiler.resolve | `resolve_pattern_node_walk` | reader | A tag-elided PATTERN does not arise (the pattern grammar requires a head). No change. | -| v2.compiler.infer | Conj gather arms | reader | No change. A tag-elided construct cannot reach infer, because resolve either writes its tag or refuses. | -| v2.workflow.compile_door_cause_ownership | three rows | ownership | One row per refusal cause. | -| v2.test.claim.namespace_xl0.reference_conservation_accepted_drops | `a_map_literal_value_refuses_at_the_literal_holds` | control | Today it asserts `unsupported_form` for `Map = { "k": .. }`. UNCHANGED by this lane: a string-keyed item still refuses at lowering here. It moves with the map arm (gunbc#12734, jolly-boar-246), which gives string keys an edge form and must then read the RESOLVE outcome. | -| v2.compiler.eval, v2.std.compilers.target_model, emit | construct gates | reader | No change. They already refuse a non-reference tag after #12701. PR2 adds a control proving an elided construct cannot reach them. | - -## Scope: which of the seven Form B admits - -THE READING THAT SET THIS LANE'S SCOPE, not a standing fact. It comes from a one-off probe on main -7f144278c57: each file's source was run through the parse and normalize route of -`v2.test.claim.namespace_xl0.reference_conservation` `conservation_subject`, and the first fatal was -rendered through the parse's `SpanIndex`. Only the FIRST fatal per file is shown, so a file can have -later blockers. The standing instrument is the #12550 base-vs-head native census, run on PR2's head. -It re-derives which of these files are admitted, and PR2 cites that census rather than this table -(DESIGN §6: name the instrument, never transcribe its output). - -| File | First fatal refusal (line) | Form | Form B admits it? | -|---|---|---|---| -| dag/std/algebra | 359, `{ name: "filter", .. }` | the body of `fn collection_filter_shape() -> AlgebraFieldTemplate`; 88 more rows of the same record inside list literals | yes: declared return, and list element | -| dag/std/primitives | 17, `data char_at_contract: PrimitiveContract = {` | data initializer | yes | -| dag/std/termination | 45, `data .. : BoundedLattice = {` | data initializer, generic, newline-separated fields | yes, by HEAD lookup: `BoundedLattice` resolves to a single record. Its type argument is not substituted and chooses nothing, and none of its field inits is headless. The newline separator is to be confirmed in PR2. | -| dag/extdeps/realization/compile_stage_memo | 39, `key_derivation: {` | record field under a headless data initializer (lowering refuses the inner brace first) | yes: data initializer plus record field | -| dag/extdeps/realization/parse_table_memo | 50, `key_derivation: {` | same | yes | -| dag/std/types | 5, `data kernel_type_set: Map = {` | MAP literal | **no: MAP arm.** Reaches the elided-tag writer in resolve and refuses located `map_literal_construction_not_modeled`, as a declared population (below). | -| src/v2/std/host_transport | about 119, `reason: ^emit_host_runtime_row_..` | **caret symbol literal** (`body_lowering_reason_caret_symbol_not_lowered`), not Form B | **not by Form B alone.** Its first blocker is the caret form. Its Form B site (`RuntimePrimitive { value: { .. } }`, record field) is admitted by this lane, but the file stays refused until caret symbols lower. That is #12420 (vivid-ant-536), so PR2 lists it as depending on #12420. | - -**The map arm (ruling, gentle-koi-724).** This lane builds the ONE elided-tag writer (in resolve) and its -RECORD arm. At that same site, a headless brace whose expected head is `Map` takes the MAP arm, which -refuses located `map_literal_construction_not_modeled`, with its ownership row. It is not left as -`unsupported_form`, and there is no second site later: map construction plugs into this arm. - -For lowering to reach the site, a string-keyed item needs a field-edge form. It lowers to the same -tag-elided construct, with each item carried as a `map_literal_entry_marker` edge to a -`(key, value)` pair. Lowering still does not pick map or record; a construct that mixes name keys -and string keys refuses at lowering, located at the first key of the other kind. - -The refusing population is DECLARED, as a `gunbc.rung_drop` row. Population: `dag/std/types` plus -every other map-literal file the base census locates, named. Restoration trigger: map construction -is modeled, so a `Map` expected type elaborates the literal to a value that inhabits it, linked -to the open question on the XL-2 row -(`unrecognized_primary_expression_lowers_to_its_first_atom_at_v2_body_lowering`). - -If `dag/std/types` staying refused still blocks the native seven transitively, the map arm is on the -critical path; the parent dispatches it. - -**The seed.** The seed choosing by field names is not an oracle. If it chooses WRONGLY for a real -file, meaning the chosen struct differs from the declared type, that is a silent seed defect, and PR2 -files a `gunbc.recurring_failure_mode` receipt for it. - -## PR2 (the cut) and its controls - -1. `data x: T = { f: v }` lowers, resolves and elaborates to a node whose `content_hash` equals that - of `data x: T = T { f: v }`. Content equality with the headed form. The authored-tag token occurrence - is the one expected difference. `v2.std.node` `content_hash` folds only kind, edge labels and - children, so it should fall outside the hash. PR2 confirms this by execution. If it does not, the - control compares the occurrence-erased projection, and says so. -2. A tag-elided literal with no expected type (a `let` without an annotation) refuses - `resolve_anonymous_record_no_expected_type`, located at its `{`. -3. The expected type is a two-constructor coproduct: refuses `resolve_anonymous_record_expected_type_not_record`. -4. Fields that do not match the chosen record refuse at the field, through the existing construct - typing, and not as "no constructor". -5. **Mutation: choose by field names.** Two records share the literal's field set. The expected - type names the one declared SECOND. The control asserts the declared type's path is chosen. A - field-name chooser picks the first or refuses as ambiguous, so it goes red either way. -6. Nesting: `host_transport`'s form, and a two-level `compile_stage_memo` form, each equal to their - hand-headed form. -7. **Infer still checks** (review condition 2): elaboration succeeds, and infer then refuses a field - value of the wrong type, located at the field. -8. **The expectation does not leak** (review condition 3): an anonymous literal passed as a call argument - under an annotated data declaration refuses `resolve_anonymous_record_no_expected_type`. A second - case puts it in a match-arm body under an annotated fn return. -9. **Syntactic peel** (review condition 1): a headless literal at a record field declared as the - record's own binder (`top: T`) refuses `resolve_anonymous_record_expected_type_not_record`. -10. An elided construct cannot pass the post-resolve well_formed gate or the target_model gate - (a discriminating pair with the headed form). -11. Evidence: the #12550 base-vs-head native census shows each Form B file of the table admitted, - with every other outcome change explained. Seed claims show no true->false. diff --git a/docs/plans/approval-enrolment-handshake.md b/docs/plans/approval-enrolment-handshake.md deleted file mode 100644 index 8b9087f647a..00000000000 --- a/docs/plans/approval-enrolment-handshake.md +++ /dev/null @@ -1,129 +0,0 @@ -# Approval device enrolment as a handshake - -Status: design (node://adhoc-19a8196b-f67, operator scope 2026-09-30). Authority once landed: -`gunbc.auth.approval_enrolment_handshake`. This page states the design; the model is the fact. - -## The defect this replaces - -Today one POST both consumes the enrolment code and mints the enrolment -(`gunbc.auth.approval_device_redemption` `commit_enrolment`: slot generation 1 `code` → 2 -`enrolled`). A phone whose POST gets no decodable answer holds `SubmissionUnknown`, and the only way -out is a readback authenticated by the *enrolled* App Attest key. For an enrolment that never -existed, that read can only refuse, and a bare refusal is not proof of absence. So the phone can't -safely discard its keys, can't reuse its code, and has no typed exit. On 2026-09-30 an operator's -phone sat in exactly that state after a front-door 404 (the device routes were unrouted), with an -expired code the server had never spent. - -The operator's requirement: **enrolment must never be a death sentence.** Every phone state has a -typed exit, and the phone may always start over safely. - -## The one invariant - -> **An enrolment can decide only if it is ACTIVE, and it becomes ACTIVE only on a signature by its -> decision key over a server challenge issued for that enrolment.** - -Everything else follows. A phone that discards its prepared keys can't produce that signature, so -nothing it discarded before sending the ACK can ever become ACTIVE. The worst case is a PENDING -record nobody holds, and PENDING lapses at its deadline on its own. **So a handshake ends by expiry -or by either party walking away, and the phone never has to tell the server it is leaving** -(operator refinement, 2026-09-30). Cancel is a local discard of the prepared keys. The one state -where a discard can leave something behind is after the ACK may have landed (see -*ConfirmUnknown*). What it leaves is an ACTIVE enrolment whose decision key no longer exists -anywhere, so it can never sign a decision. - -## The server record: one CAS slot per code - -The code stays the identifier (one thing typed, one slot). The slot's generations form a line, and -each write is a compare-and-set expecting the generation before it -(`std.durable_compare_and_set`), so two writers can never both win: - -| gen | record | written by | reached from | -|---|---|---|---| -| 1 | `IssuedCode { code, login, issued_at, expires_at }` | code issue verb (operator, via fleet-converge) | absent | -| 2 | `Pending { enrollment_id, login, platform, decision_key, attestation, submitted_at, pending_deadline, confirm_nonce }` | SUBMIT (SYN) | 1, code live | -| 3 | `Active { …pending, confirmed_at }` | CONFIRM (ACK) | 2, before `pending_deadline` | -| 4 | `Revoked { …active, revoked_at, reason }` | operator revocation | 3 Active | - -**The PENDING deadline is read, not swept.** A `Pending` observed at or after `pending_deadline` -*reads* as `Lapsed`, and CONFIRM refuses it. Nothing is ever written to end a handshake. This is the same pattern -the code already uses for its own window (`expires_at` compared at observation by -`utc_instant_before`). No background writer is needed and none can be forgotten. - -*Stated divergence (DESIGN §3b, leasing):* the brief said "std.temporal_effect lease". Its -`HeldLease` models an observed running process, `std.durable_exclusive_hold` has no deadline, and -`std.scoped_authorization` `OperatorGrant.expires_at` is an operator's grant, not a party's -provisional hold. So the deadline follows the enrolment code's existing `expires_at` pattern in -this module. If a deadline-bearing hold is ever added to std, this record should inhabit it. - -## The legs - -| leg | route | authenticated by | effect | -|---|---|---|---| -| SYN | `POST /approve/device/enrol` `{code, platform, decision_key, evidence, push}` | the code (bearer secret) + App Attest attestation over the transcript | gen 1→2 `Pending`; answers SYN-ACK | -| SYN-ACK | (the SYN's answer) `{enrollment_id, confirm_challenge {nonce, pending_deadline}}` | — | — | -| ACK | `POST /approve/device/confirm` `{enrollment_id, signature}` | **decision key** signature over `confirm_signing_input(enrollment_id, nonce)` (one Face ID at enrolment; operator-confirmed 2026-09-30) | gen 2→3 `Active`; answers `{standing: active}` | -| STATUS | `POST /approve/device/enrolment-status` `{code, requested_at}` | possession of the code | none; answers the closed status | - -**SYN is idempotent.** A SYN against a `Pending` slot whose stored decision key and attest key id -equal the request's answers the *same* SYN-ACK (same `enrollment_id`, same nonce, same deadline) -and writes nothing. A SYN against a slot already at gen 2+ with a *different* key refuses with -`code_already_used`. That's the only way a spent code refuses, and it can't be reached by the phone -that spent it. - -**STATUS is authenticated by the code.** Every state has a code, and the phone holds it from the -moment it's typed. So one authentication covers every state, including states where the server -holds no key for the phone (gen 1, absent). The answer is a closed set: - -``` -NeverSubmitted gen 1, code live → phone may SYN (again) -CodeExpired gen 1, code past expires_at → phone discards, needs a new code -CodeNotIssued slot absent → phone discards, needs a new code -Pending { enrollment_id, confirm_challenge } → phone confirms (ACK), or walks away -Lapsed → gen 2 past its deadline; phone discards -Active { enrollment_id, decision_key_matches: Bool } → phone adopts iff its key matches -Revoked → terminal, phone discards -``` - -`Active` answers whether the stored decision key equals the one in the request's proof. That's -why STATUS also carries the phone's decision key: a code holder learns only whether *its own* key -is the active one. - -## Phone states and their exits (every one typed) - -| phone state | how it's left | -|---|---| -| `Unenrolled` | code typed → `Prepared` | -| `Prepared` (keys made, nothing sent) | SYN → `SubmissionUnknown`; Cancel → discard → `Unenrolled` (nothing left the phone) | -| `SubmissionUnknown` (SYN may have landed) | SYN-ACK → `Pending`; STATUS: `NeverSubmitted` → resend SYN; `Pending` → `Pending`; `CodeExpired`/`CodeNotIssued`/`Lapsed` → discard → `Unenrolled`; `Active`+match → `Enrolled` (impossible without an ACK, answered for completeness) | -| `Pending` (SYN-ACK held) | ACK → `ConfirmUnknown`; Cancel → local discard → `Unenrolled` (the PENDING lapses unaided) | -| `ConfirmUnknown` (ACK may have landed) | `{standing: active}` → `Enrolled`; STATUS: `Active`+match → `Enrolled`; `Pending` → re-ACK; `Lapsed` → discard → `Unenrolled`. Cancel → local discard. If the ACK had landed, the ACTIVE record is left with a decision key that no longer exists: inert for deciding, and removed by the operator's existing revocation. The phone shows that consequence before discarding here. | -| `Enrolled` | revocation, key invalidation (existing) | - -Cancel is the operator's existing local Cancel button, and it stays local: no route, no signature, no server write. The phone never has to revoke or abandon anything (operator refinement, 2026-09-30); a server-confirmed withdraw route is explicitly not the design. - -## RED per transition (a lost answer at each leg) - -Each row is a claim in the PR. It names the state the phone holds, what was lost, what the server -holds, and the typed exit the phone must take. The RED is the mutation that must make it fail. - -| # | lost | server holds | phone exit | RED | -|---|---|---|---|---| -| R1 | SYN never arrived | gen 1 live | STATUS→`NeverSubmitted`→resend | STATUS on gen 1 answers anything but `NeverSubmitted` | -| R2 | SYN-ACK lost | gen 2 `Pending` | resend SYN → same SYN-ACK, one record | a repeated identical SYN writes or answers a different nonce | -| R3 | SYN with another key | gen 2 | `code_already_used` | a second key's SYN is admitted or overwrites | -| R4 | ACK never arrived | gen 2 | STATUS→`Pending`→re-ACK | a PENDING record decides a redemption | -| R5 | ACK answer lost | gen 3 `Active` | STATUS→`Active`+match→`Enrolled` | STATUS answers `Active` without the key match, or matches a foreign key | -| R6 | ACK after deadline | gen 2 past deadline | refuse → STATUS `Lapsed` → discard | a lapsed PENDING confirms | -| R7 | phone walked away (Cancel) | gen 2 | deadline lapses → reads `Lapsed`; a new code enrols afresh | a lapsed PENDING reads `Pending`, or blocks a new code | -| R8 | two ACKs (retry after a lost answer) | gen 2 or 3 | the second answers `active` idempotently | a repeated ACK writes a second record or refuses a confirmed enrolment | -| R9 | code expired unspent | gen 1 past expiry | STATUS→`CodeExpired`→discard | an expired code reads `NeverSubmitted` | -| R10 | ACK with a key other than the pending one | gen 2 | refuse | a signature by any key but the pending decision key activates | - -## Migration - -This replaces generations 2 and 3 of the enrolment slot (`enrolled`, `revoked` → `pending`, -`active`, `revoked`). It's a replacement migration cut at the root (DESIGN §3): the -store on srv1 holds **no** enrolled record today (only two expired gen-1 codes), so there's no -persisted population to carry. The redemption path (`device_redemption_admission`) reads `Active` -where it read `EnrolmentCodeConsumed`, and every enrolment-only route that took the enrolled -standing takes `Active` and nothing else. diff --git a/docs/plans/arrow-contract-edges.md b/docs/plans/arrow-contract-edges.md deleted file mode 100644 index 3763fc13eff..00000000000 --- a/docs/plans/arrow-contract-edges.md +++ /dev/null @@ -1,89 +0,0 @@ -# Arrow contract edges (XL-2 PR2a) - -Parent design: [service-interface-and-uses-carriers.md](service-interface-and-uses-carriers.md) (#12851). Ruling: the side chat, 2026-10-01, option A, admitted with no operator sign-off needed. The ruling forbids four things, none of which this change does: a new connective or behaviour, an annotation bag, unknown labels, and a second carrier for one fact. - -## What lands - -An Arrow may carry three optional **contract edges**. The first two (effect claims and execution mode) landed in PR2a. The third (resource requirements) landed with D13 step (a); see the section on it below. - -| Edge | Target shape | Vocabulary home | Admitted labels | -| --- | --- | --- | --- | -| `^arrow_effect_claims_edge` | NONEMPTY Conj of self-named childless Atoms, no label twice (no claims has one form: the absent edge) | `std.effects` `EffectClaim` (`ReadonlyClaim`, `IdempotentClaim`), new here | `readonly`, `idempotent` | -| `^arrow_execution_mode_claim_edge` | one childless Atom | `std.execution_mode` `ExecutionMode` (existing) | `hermetic` (only `Hermetic` is claimable) | -| `^arrow_resource_requirements_edge` | one of THREE declared forms: a NONEMPTY Conj, one Named edge per member (label = spelling, target = type reference); the childless Atom `^requirements_declared_none` (`requires none`); the childless Atom `^requirements_opaque` (`requires opaque`). An ABSENT edge is an undeclared operation, read as Undecided (see the reversal below) | the open set of `resource` declarations, checked at resolution (`v2.std.symbol_index` `symbol_index_declares_resource_at`) | any declared resource, each at most once by resolved declaration | - -`hermetic` is kept off the effect claims: it says where the Arrow may run, not what its effect does. The surface `OperationModifier` stays one syntax vocabulary (`v2.extdeps.languages.dag` `dag_grammar_op_modifier_expr`), and lowering (PR2c) projects each arm to its home. - -**Three layers, one fact each:** -- **Structure: `v2.std.node`.** - - `arrow_named_edge_is_contract`. - - `arrow_named_edge_is_non_binder`, the one predicate every count of an Arrow's binders reads, here and in `v2.std.type_binder` `arrow_named_labels_conform`, which no longer restates an allowlist. - - `arrow_signature_edges_conform`: at most one of each contract edge, and never a binder. This module names no claim or mode. -- **Vocabulary: the homes.** `std.effects` `EffectClaim` and `std.execution_mode` `ExecutionMode`. -- **Spelling and check: `v2.std.arrow_contract`.** - - `effect_claim_label` and `execution_mode_claims_label` are exhaustive matches over the home types. They are the only place the surface spellings appear, so a variant added to a home must be decided there. - - `arrow_contract_conforms` checks each contract edge's target against the homes. `v2.std.type_binder` `type_binder_node_conforms` runs it on every Arrow. - -**Declared residue (🟡).** A label is admitted by comparing it against each claimable variant's projection (`effect_claim_label_is_admitted`, `execution_mode_claim_label_is_admitted`). That is a hand list of the variants, because the language cannot fold over a closed coproduct's constructors. No decoded variant is produced, because nothing consumes one: lowering (PR2c) projects variants to labels, never the reverse. A variant missing from the list is refused, never admitted, so the residue fails closed. **Bound:** two claims and one claimable mode. **Owner:** XL-2. **Trigger:** a language capability to enumerate a closed coproduct's constructors; the list is then derived from the projections and deleted. For the same reason, the edge target is a label checked against the home rather than a value of the home type: a Node edge carries Symbols, and making a wrong claim unwritable would need typed node targets. That is a substrate capability beyond this ruling, which forbids a new connective without operator sign-off. - -Properties required by the ruling, and where each is established: -- **Contract edges are not binders.** `arrow_named_edge_is_non_binder`. Test: `admitted_contract_edges_conform_and_are_not_counted_as_a_binder`. -- **Duplicates and unknown labels refuse.** Tests: `a_malformed_effect_claim_refuses` and `a_malformed_execution_mode_claim_refuses`. The reds include an empty claims edge (a second identity for "no claims"), `hermetic` among the effect claims, and `wet`, a real `ExecutionMode` that is not claimable. Every test checks both walls, the structural one and `v2.std.type_binder` `type_binder_node_conforms`, so a positive control goes red if `arrow_named_labels_conform` omits either label. -- **Canonical identity is order-independent, and a claim is part of identity.** Test: `contract_edge_identity_is_order_independent`, over `v2.std.node` `content_hash`. -- **A claim-free Arrow keeps its identity.** It carries neither edge, so nothing about it changes. - -All tests are in `v2.test.claim.arrow_contract_edge_conformance`. - -## Readers that must not consume the edges - -Audit of every Arrow reader outside `src/v2/test`. "Metadata" means: handled exactly as the declared-order edge is. - -| Reader | Today with an unknown named edge | Owed | -| --- | --- | --- | -| `v2.std.node` `arrow_signature_edges_conform` | counted as the binder | **this change** | -| `v2.std.type_binder` `arrow_named_labels_conform` | refuses | **this change**: reads `arrow_named_edge_is_non_binder` | -| `v2.std.type_binder` `type_binder_node_conforms` | — | **this change**: runs `v2.std.arrow_contract` `arrow_contract_conforms` | -| `v2.std.type_binder` `node_inferred_subtree_nodes` | walked as inferable | skip every `arrow_named_edge_is_contract` edge. **Landed with D13 step (a)** (the first producer) | -| `v2.compiler.resolve` `resolve_arrow_node_in` | resolved under the body scope | effect claims and execution mode: carried unwalked. Resource requirements: walked in the Arrow's type scope (no value params), each member must name a declared resource, at most once (`resolve_arrow_resource_requirements`). **Landed with D13 step (a)** | -| `v2.compiler.infer` `infer_gather_fold_step` / `infer_arrow_signature_order_edge` | inferred as a value child | skip every contract edge. **Landed with D13 step (a)** | -| `v2.compiler.infer` `infer_product_child_evidence_edges` (the Arrow's derived TYPE) | its type enters the derived type | every contract edge rides as metadata. **Landed with D13 step (a)** | -| `v2.compiler.translate` `translate_grounding_derived_gate_subtree` | requires grounding for every subtree node | exempt (metadata). **Landed with PR2c-i**: the walk skips a contract edge under an Arrow (`translate_edge_is_arrow_contract`, reading `arrow_named_edge_is_contract`), so a claim payload is never required to carry grounding. Discriminator: `v2.test.claim.translate_underived_refusal` `translate_gates_a_positional_payload_but_not_a_contract_payload_holds` | -| `v2.std.node` content hash (`canonicalize_arrow_labeled`) | hashes every edge, named edges sorted by label | in the hash, order-independent (tested here) | - -Readers that ignore a named edge safely: the domain, codomain and body readers in resolve, infer, eval, translate and `v2.std.compilers.target_model`; `v2.std.node_query`; `v2.std.decl_index`; `v2.compiler.symbol_index_fill`; `v2.compiler.emit_produced`; `v2.compiler.compile`; `v2.std.node` `cost_edge_role`; `v2.lens.cost.copied_port_derivation`. - -## Standing: DESIGN §3c, per edge family - -The two families have different producers and different consumers, so each has its own standing. - -### Effect claims and execution mode (PR2a, produced by PR2c-i): consumer a declared frontier - -- **Consumed by execution:** the conformance wall (`v2.std.node` `arrow_signature_edges_conform`, reached by every `well_formed` check of an Arrow) and `v2.test.claim.arrow_contract_edge_conformance`. The pipeline reader arms (resolve carries both unwalked; infer and `type_binder` skip them) landed with D13 step (a), the first producer of any contract edge. -- **Produced by execution (PR2c-i):** the operation-modifier lowering (`v2.compiler.body_lowering_fold` `body_lower_operation_modifier`) emits both edges from `readonly`, `idempotent` and `hermetic`, and refuses a repeated modifier at the second (`v2.test.claim.normalize.operation_modifier`). `translate_grounding_derived_gate_subtree` carries them as metadata in the same change. -- **Consumer of the claims themselves, still a declared frontier:** no stage yet reads an effect claim or the execution mode to decide anything (retry admission, caching, hermetic placement). Trigger: the first such reader lands and consumes the edge off the operation Arrow; until then the conformance wall and the reader arms are the consumers. -- **Why ahead of the consumer:** the 2026-10-01 ruling asked for the substrate change to be reviewable on its own diff. - -### Resource requirements (D13 step a): producer landed, demand consumer a declared frontier - -- **Produced and consumed by execution in step (a):** `body_lower_operation_requires` emits the edge from an operation's `requires` clause. `v2.compiler.resolve` `resolve_arrow_resource_requirements` reads it on every resolved operation Arrow and refuses a member that is not a declared resource or that repeats one. Controls: `v2.test.claim.normalize.operation_requires_edge`. That is a conformance consumer: it establishes that the requirement names real resources. It does not use the requirement. -- **Production consumer of the requirement itself, a named later change:** D13 step (b), the DependencyDemand derivation. A function's demand is the union of three parts: its direct resource operations; for each service operation it calls, that callee Arrow's `^arrow_resource_requirements_edge`; and its resolved callees' demand. That is how a `net: Network` requirement becomes derived instead of authored, which is the restoration trigger of `gunbc.rung_drop` `network_requirement_unrepresented_after_uses_cut`. -- **Transition trigger, at executable grain:** step (b) lands, and its derivation reads this edge off the callee operation's Arrow into the calling function's derived demand. Executed evidence is a control in which a function calling an operation that `requires Network` derives demand naming `std.resources.Network`, and that demand disappears when the clause is removed. Reading the edge only for conformance, or adding Network to demand by spelling, does not satisfy the trigger. -- **If step (b) is abandoned:** the `requires` grammar member, `body_lower_operation_requires`, the `^arrow_resource_requirements_edge` label and its at-most-one count, its target check in `v2.std.arrow_contract`, and `resolve_arrow_resource_requirements` with its two refusal reasons are deleted. The resource declaration's index mark (`symbol_index_declares_resource_at`, from #12898) then has no reader left, so it is deleted with them, along with its `resource_declarations` carrier. - -## Not in this change: input defaults - -Per the ruling, a default belongs to the binder and not to its type. One binder representation (a required type, an optional single default, closed labels, one reader and builder) serves fn parameters, service io fields and record fields. That changes the existing name-to-type binder shape, so it is a replacement migration (DESIGN §3) and is PR2b: its producers and readers are measured first, then all move together. - -## Third edge: resource requirements (D13 step a) - -`^arrow_resource_requirements_edge` (`v2.std.node` `arrow_resource_requirements_label`) is the operation's declared `requires R, ..` (`v2.extdeps.languages.dag` `dag_grammar_op_requires_expr`), lowered by `v2.compiler.body_lowering_fold` `body_lower_operation_requires`. Its target is a nonempty Conj with one Named edge per member: the label is the member's spelling, and the target is its type reference. Unlike the two claim edges, it names DECLARATIONS from an open set, so `v2.compiler.resolve` walks it and checks each member against `v2.std.symbol_index` `symbol_index_declares_resource_at`. It is the first producer of any contract edge, so the pipeline reader arms above landed with it, for all three edges. Controls: `v2.test.claim.normalize.operation_requires_edge`. - -### Reversal: an absent requirements edge is UNDECLARED, not "none" (D13 ruling B) - -**This reverses #12911**, where "no requirements has one form, the absent edge". D13 step (b) derives a function's demand from the operations it calls. Under the old reading, every operation nobody audited, and every one added later, would silently read as requiring nothing. That is the fail-open default DESIGN section 5 forbids, so the lane owner ruled it out (option B). Every operation now declares one of three forms, all on this one edge, with no second carrier: - -- `requires R, ..`: the named resources, checked at resolution as above. -- `requires none`: **no requirement in the audited vocabulary.** Today that vocabulary is `Network` only (the D13 Network audit, `docs/plans/d13-network-audit.md`). It is not a claim about Filesystem, Clock or Entropy: their demand derives from direct capability calls and is never authored. When the audited vocabulary grows, the meaning of `none` grows with it, and the rows are re-audited. -- `requires opaque`: the operation's demand is decided by runtime values (a generic exec, `shell.exec Run`, `systemd-run`, a runtime-program runner), so a caller's derived demand is **Undecided**, a located refusal at the uses-wall, never empty. - -An operation with no clause carries no edge. Step (b) reads that as **Undecided**, exactly like `opaque`, so silence can never mint "none". The grammar (`dag_grammar_op_requires_expr`) makes `none` and `opaque` whole clauses: a member after either refuses. The v1 seed carries the same three forms (`v1.compiler.parse` `operation_requires_declaration`: `RequiresUndeclared | RequiresNone | RequiresOpaque | RequiresResources`). Controls: `v2.test.claim.normalize.operation_requires_edge` and `test.claim.v1_operation_requires_parse_witness_test`. diff --git a/docs/plans/arrow-elimination-model.md b/docs/plans/arrow-elimination-model.md deleted file mode 100644 index 44614d3bd65..00000000000 --- a/docs/plans/arrow-elimination-model.md +++ /dev/null @@ -1,125 +0,0 @@ -# Arrow elimination in v2 infer — model for ruling - -Status: RULED by v2 foundation (neat-boar-16), 2026-09-26, and implemented in the PR that -carries this file. Rulings: Int's value type is owned by `v2.std.integer` -(`integer_int_type_node`) as Bool's is by `v2.std.logic` (`bool_node`); `dag_binding_denotation` -stays the one binding→type join and its rows point at those authorities. Elimination reads the -DECLARED return atom, never the composed evidence. The composed-evidence defect is rostered as -`gunbc.recurring_failure_mode` `function_type_evidence_carries_its_body`. - -Found while building: the evaluator carried the same second representation for Bool -(`v2.extdeps.runtimes.v2_evaluator` `v2_eval_bool_literal_pin` as every Bool runtime value's -type), so eval's resolved-type acceptance refused the derived Bool application with -`eval_rejected_resolved_type_mismatch`. Bool runtime values now carry `bool_node()`; the pin -remains only as the evaluator's literal-shaped node for `true`. The evaluator's Int copy -(`v2_eval_int_type_node`) is deleted in favour of `integer_int_type_node`. -Trigger: work item "v2: infer derives a Bool-returning application", parent `deep-bee-18`, -consumer `v2.compiler.refinement_discharge`. - -## What was measured (main `21f4d0c390`, a locally built `gunbc run` over a probe entry) - -| application `Transform[Arrow(Conj{x: Int}, R, body), 1]` | infer | root facts | eval | -|---|---|---|---| -| R = Int, body = int literal | Accepted | GroundingNotDerived | `eval_rejected_grounding_not_derived` | -| R = Bool, body = `true` | Accepted | GroundingNotDerived | `eval_rejected_grounding_not_derived` | -| R = Int, body = `true` | Accepted | GroundingNotDerived | — | -| R = Bool, body = int literal | Accepted | GroundingNotDerived | — | - -`infer_compatible_argument_admits` asserts only `Accepted`, so it never witnessed derivation. - -## The slice, re-derived (DESIGN §6b) - -1. **Return type atom → its grounding.** `v2.compiler.infer` `infer_node_facts` grounds a - binding atom through `v2.extdeps.languages.dag` `dag_binding_denotation`, which declares - only `^dag_binding_type_int` and maps it to `dag_int_inhabitant_node` — the *roster record - describing* Int (inhabitant atom + surface spelling), not the Int type. Bool has no row, so - a Bool return atom stays on the frontier, and so does its Arrow (the product row needs every - child). -2. **Arrow introduction.** Nothing compares an Arrow's body type to its declared return. The - Arrow product row composes children's evidence — including the body's — into the Arrow's - "type". -3. **Arrow elimination — the earliest unjustified boundary.** - `infer_transform_derived_optional` derives a type only for binary Int add and casts. No rule - types `f(a)` as `f`'s codomain, so NO application derives, Int included. Argument - inhabitance (`infer_application_argument_inhabitance`) is judged, then the node falls to - the frontier. - -## One Int, not two (the unification) - -Census of what each form means today: - -- `Atom(^dag_binding_type_int)` is the Int **value type** at every consumer: the Int literal - rule (`infer_branch_int_binding_type_node`), binary add's unified type, match/branch - unification (`infer_branch_type_is_int`), parameter operand types (the declared domain atom - via `infer_find_arrow_domain_type_in_tree`), argument inhabitance (declared formal compared - structurally to the argument's type), and the evaluator's runtime primitive type - (`v2.extdeps.runtimes.v2_evaluator` `v2_eval_int_type_node`). -- `dag_int_inhabitant_node` (a Conj) is the dag language roster's record about Int. Its only - use as a *type* is `dag_binding_denotation` → the binding atom's derived type. - -So the fork is `dag_binding_denotation` returning the record. The model: - -- **`dag_binding_denotation(sym)` is the single binding → value-type authority** and returns - the value type the binding names: Int → `Atom(^dag_binding_type_int)`, Bool → - `v2.std.logic` `bool_node()` (the node the Bool literal rule already uses). The Bool row is - one more row of the same join, not a special case. -- **The literal rules consume it** instead of minting their own nodes: an Int literal's type is - `dag_binding_denotation(^dag_binding_type_int)`, a Bool literal's is - `dag_binding_denotation(^bool_node_symbol)` (bool_node's own identity since gunbc#12785, which - derives every kernel row from `dag_kernel_value_type_roster` and deleted the second Bool name). `infer_branch_int_binding_type_node` - deletes. (The evaluator's `v2_eval_int_type_node` is the runtime's own copy of that node, and - is left to a later cut; it is equal by structure today.) -- **A type-expression atom's OWN grounding is its kind**, not its denotation: - `kind_node(TypeDenotationKind)` (`std.kind`), exactly as a roster type member derives today. - This is forced, not chosen: the Int denotation is structurally the atom itself, so taking it as - the atom's derived type hits `std.constraints` `canonical_grounding_from_derived_type`'s - self-evidence wall — and it was always a category error (the type of the expression `Int` is - a type-kind, not `Int`). - -## The two rules - -**Arrow introduction (body/return check), at the Arrow product row.** When an Arrow carries a -body edge (`^arrow_body_edge`) and both the body's facts and its declared return atom's -denotation are derived: the body's value type must equal the denotation, structurally with -provenance stripped (`infer_type_equal_ignoring_provenance`, the list rule's authority). A -mismatch refuses with the new reason `^arrow_body_does_not_inhabit_declared_return`, located at -the body. A body or return that is not derived leaves the Arrow on the counted frontier, never -admitted. - -**Arrow elimination, in `infer_transform_derived_optional`.** For `Transform[callee, args…]` -whose callee is an Arrow and whose argument inhabitance was admitted: the application's derived -type is `dag_binding_denotation` of the callee's declared return atom. The derivation requires -the callee Arrow itself derived (so the introduction check has run). A non-Arrow callee (an Atom -operator — `FormalUnresolved`), an underived callee, or a return with no denotation stays on the -counted frontier. - -Red and controls owed: -- Accepting: `R = Bool, body = true` derives Bool at the application; eval of it equals eval of - the literal `true` and differs from eval of `false`. -- Accepting: `R = Int, body = 1` derives Int at the application (same rule, not a Bool case). -- Red: `R = Bool, body = 1` and `R = Int, body = true` refuse - `^arrow_body_does_not_inhabit_declared_return`. -- The existing argument-inhabitance reds stay red. - -## Consumers that see the newly derived facts - -- `v2.compiler.eval` `inferred_facts_for_eval` — applications now pass its grounding gate. -- `v2.compiler.translate` — the `translate_rejected_grounding_not_derived` gate, same. -- `v2.std.coercion` via `infer_transform_cast_optional` — a cast whose operand is an - application is now judged by `coercion_cast_crossing` instead of staying on the frontier. -- `v2.compiler.refinement_discharge` (deep-bee-18) — the intended consumer. -- infer's own branch/match/loop unification — an arm that is an application now has a type, so - a previously frontier arm pair can now unify or refuse `arm type mismatch`. This is the - population the floor has to census. -- Readers of an Arrow's evidence or a binding atom's resolved type: the return atom's evidence - changes from the inhabitant record to the type kind. `v2.test.execution.dag_binding_denotation` - counts derivation only and holds either way. - -## Questions for the ruling - -1. Is `dag_binding_denotation` the right single authority for binding → value type, or should - Int's value type live in `std.integer` (as Bool's lives in `std.logic`), with the language row - pointing at it? -2. The Arrow's composed evidence includes the body's type (and `dag_arrow_with_body_node` lists - the return atom twice). A function type is domain → codomain. Out of scope here unless you - rule otherwise; noted so it is not mistaken for part of this model. diff --git a/docs/plans/belt-event-driven-obligations.md b/docs/plans/belt-event-driven-obligations.md new file mode 100644 index 00000000000..9c60c00fc94 --- /dev/null +++ b/docs/plans/belt-event-driven-obligations.md @@ -0,0 +1,125 @@ +# Belt demotion: event-driven attempt obligations + +Status: implemented on gunbc#13125. This is the design for items #6 and #4 of [dogfood-route-manual-interventions](dogfood-route-manual-interventions.md), and it executes the owner ruling "The belt demoted: wake-up and anti-entropy only" (2026-09-22, [harness-work-lifecycle](harness-work-lifecycle.md)). + +The model's homes are the factory controller, which owns wake-ups and event delivery, and the factory attempt lifecycle. `factory-dogfood-route` (gunbc#13077) only consumes their receipts and carries a gating edge on them. + +## The defect, re-derived (DESIGN §6b) + +`gunbc.roadmap_belt_actuate` `belt_tick_for_instance` was one process that ran, in order: +- launch admission, spawn and teardown; +- commit, then verify, then review and audit, then publish, then metering, over every attempt; +- then, in `gunbc.roadmap_belt_tick_cli`, the result return, the served observation, the dogfood start record and the snapshot census. + +The earliest unjustified boundary was not a slow stage. It was the claim, implicit in that one function, that these are one fact. They are separate facts: +- each attempt's capture, verify, review, audit and publish is an obligation of that attempt alone; +- launch is an obligation of the frontier; +- each instance fold is an obligation of the instance. + +Fusing them made every stage's latency every other stage's latency. On srv2, from 06:50 to 16:30 on 2026-10-03, one SCM capture held every tick at its 90-minute bound, and nothing behind it ran for any attempt. + +## What was built + +### Obligations (`gunbc.roadmap.roadmap_belt_obligation`) + +- **Per-attempt kinds:** capture, verify, review, audit, publish. +- **Instance kinds:** launch, served observation, result return, metering, dogfood start record, snapshot census. +- **Outstanding is derived, never stored.** `belt_attempt_outstanding` reads the attempt's projected segments, so no second record repeats what the receipts say (ruling Q1). +- **A failed or refused stage is not outstanding.** The line stops for that attempt only, and the operator resumes it, per the 2026-10-03 manual-resume ruling. + +### One unit per attempt, one per instance obligation + +- **Why per attempt.** Every `gunbc run` loads the corpus, measured at about 60 s and about 8 GB peak RSS for a typecheck. A unit per (kind, attempt) would pay that once per kind. +- **What the attempt's unit does.** It is `gunbc-obligation-attempt--`, running `belt_attempt_obligations_cli`. It runs the attempt's outstanding kinds in eligibility order (`belt_attempt_obligations_run`), re-observing after each one, and stops at the first that waits on something outside the process. +- **Each kind is still its own ensure** over the core the tick used to call per attempt. Each core is idempotent over unchanged evidence. +- **The unit name is the exclusive hold.** `belt_obligation_hold_decision` reads the manager for that one name: a loaded unit is held, an unreadable answer refuses, anything else starts. Another attempt's unit never reaches the decision. That is the #6 isolation. +- **Deadline.** `RuntimeMaxSec` is the obligation's deadline, and `CollectMode=inactive-or-failed` lets the name be reused after a failure. +- **Grain ruling.** bold-bee-114 agreed to the per-attempt grain on 2026-10-06. + +### Exit records (`gunbc.roadmap.roadmap_attempt_exit_spool`) + +- **Who writes them.** Worker, reviewer and auditor units end with a create-only exit record. Their `ExecStopPost` is a typed argv: `+mkdir -- /+++${INVOCATION_ID}+${SERVICE_RESULT}+${EXIT_CODE}+${EXIT_STATUS}`. + - No shell is involved. This follows bold-bee-114's ruling against shell control text in minted commands. + - The `+` prefix runs only the hook outside the unit's sandbox, so a confined reviewer cannot forge records. + - The stop-time variables and the command-line rendering are modeled in `extdeps.systemd.service_exec`. +- **RemainAfterExit is removed from worker, reviewer and auditor units.** This reverses the 2026-09-05 choice. That choice kept a successful exit readable as `active (exited)`, which is manager memory: it is lost on reboot, and while it stands the unit never stops, so no stop hook can fire at exit. The record replaces it. +- **The supervisor keeps RemainAfterExit.** Moving its convene gate onto the record is a follow-up. +- **Observer conditions (bold-bee-114):** + - A collected unit is read from its record. + - A collected unit with no record is unobserved, never "never ran". The start receipt (the G5 placement record for workers, the metering roster entry for reviewers and auditors) separates "never started" from "exited without a record". + - A failed hook surfaces as that located unobserved arm. + - One wet control reads the exit verdict from the spool alone. +- **Signals.** A signaled worker reads as the signal number, which is exactly what `ExecMainStatus` reported, so no consumer's decision moves. A distinct signaled arm on `WorkerProcessEvidence` is a follow-up. + +### Events, not a timer (`gunbc.live_deploy`) + +- **No belt timer.** The deployment member is `BeltEventPathUnit`, which installs: + - the exit path unit, a `DirectoryNotEmpty=` level on the exit inbox and on `queued/`, which starts the drain service (`belt_exit_drain_cli`); + - the publication-answer path unit, a `PathChanged=` edge on the answer directory, which starts the discovery service (`belt_discover_once_cli`); + - the spool stages: inbox, queued, running, done and refused. +- **The drain** hands each recorded exit to its attempt's unit and moves it to `queued/`, or to `refused/` when it cannot be handed off, so the inbox path goes quiet. It then **sweeps `queued/`**: every attempt that still has an unclaimed entry there is enqueued again (a held unit is a no-op; a gone unit is started). A worker exit also queues the launch obligation. +- **Queued is a level, and acknowledgment is bound to a run** (bold-bee-114, 2026-10-09). The exit path unit watches `inbox/` and `queued/` with `DirectoryNotEmpty=`. An attempt's run first **claims** its population, the create-only move `queued -> running` of every entry for the attempt (an entry whose exit was recorded before the run's first observation, so the run is guaranteed to have seen it). The receipt names the claim (`claimed_exits`), and only after it is written does the run move exactly that claim `running -> done`. A later arrival is never in the claim, so it is never acknowledged by a run that did not observe it; it stays `queued`, the level starts the drain, and the sweep hands the attempt to a new run. A held unit is not proof the active run includes a new event. A run that finds `running` entries for its attempt adopts them (the unit name is the exclusive hold, so they are a dead predecessor's). The run makes at most `belt_attempt_obligation_pass_bound` passes, re-claiming when an arrival is already queued; the bound leaves the rest to the level. Controls: `test.claim.roadmap.roadmap_belt_exit_drain_wet_witness_test` cuts A (exit after the final observation, before settlement), B (after settlement, before the unit exits) and C (unit or host gone while queued, or dead claim). +- **A claim orphaned with no new event is recovered** (bold-bee-114, 2026-10-09). The path unit does not watch `running/`: a long verify legitimately holds it, so a level there would re-trigger while the run lives and fail the path unit at its rate limit. Two routes instead. + - **Claimant-terminal rescue.** The attempt unit's own `ExecStopPost` (typed argv, no shell) records an `obligation`-role entry in the inbox whenever the unit ends, including at its `RuntimeMaxSec` bound. The inbox level starts the drain; the drain reconciles every `running` entry whose unit is not live back to `queued`, consumes the obligation entry (inbox to done, never enqueued, only once the unit is gone) and its sweep starts the attempt again, which adopts the claim. A settled run leaves nothing to reclaim, so this cannot loop. + - **Boot reconciliation.** The drain service is `WantedBy=multi-user.target`, so a host or user-manager restart (no stop hook ran) runs one drain that does the same reconcile. + - Controls in `roadmap_belt_exit_drain_wet_witness_test`: a lone orphaned claim, a claim held by a live unit not reclaimed, a claimant killed at its run bound, and a settled run's hook entry consumed without starting anything. +- **Stated cost.** An unclaimed arrival that lands while its attempt's unit is held restarts the drain (a corpus load) repeatedly until that unit's pass re-claims or the unit exits; this wastes work and loses nothing. A stuck `running` or `queued` entry is the backlog row's `StaleBacklog`. +- **Launch** is the demoted tick: admission, spawn, teardown and bounded discovery. It has one unit name per instance. The deployment's belt service only queues it (`belt_launch_enqueue_cli`). +- **Deadlines need no wake-up timer.** Every awaited unit (worker, reviewer, auditor) has its own run bound, and ending at that bound writes an exit record like any other ending. The deadline is therefore an event too. + +### Discovery, bounded + +`belt_discover_for_instance` reads every current attempt's evidence and hands at most `belt_discovery_attempt_bound` owed attempts to their units, plus the instance obligations a moved record makes owed. It never does the work, and nothing requires it to run on a cadence. + +### Receipts and the page + +- **Tick receipt v7:** spawn, teardown and discovery passes. +- **Per-attempt obligation receipt:** `receipts/obligations--.json`, consumed by `/workflow.json`. Its decoder is total and subject-bound: node and attempt must equal the requested subject, `observed_at`, the claim and the standing are validated, every step is decoded (each kind at most once, no unknown kind, no malformed pass) before verify is projected. A pending verification now shows that attempt's own verify step. + +### The RLM launch receipt + +bold-bee-114 ruled on 2026-10-06: +- **Timer row replaced.** The two event path units must be loaded, enabled, armed and aimed at the emitted target. +- **Tick freshness replaced by an event backlog row.** An exit-spool entry unconsumed past its stage's bound refuses with `StaleBacklog`, naming the oldest entry. An armed path unit proves the watcher is armed; the backlog row proves events are consumed. +- **Declared rung drop.** The publication-answer half has no consumption record yet, so it is declared as `gunbc.rung_drop` `belt_liveness_publication_answers_unconsumed`. + +## REDs, and where they run + +- **#6:** `test.claim.roadmap.roadmap_belt_obligation_witness_test` `a_long_verify_does_not_delay_another_attempts_publish_or_the_observation`. +- **#4, deployment half:** `the_deployment_wakes_the_drain_on_an_exit_and_installs_no_belt_timer` in the same file. +- **#4, hand-off half:** `test.claim.roadmap.roadmap_belt_exit_drain_wet_witness` `a_recorded_worker_exit_is_handed_to_its_attempt_and_leaves_the_inbox`. This runs a real spool on the local-repo wet lane. +- **Worker hook:** `test.claim.roadmap.dispatch_worker_confinement_witness_test` `the_worker_unit_stops_with_its_process_and_writes_its_exit_record`. + +## Conformance (DESIGN §3b) + +- **Leasing / locking:** the hold is the manager's unit-name uniqueness. It diverges from `std.durable_exclusive_hold`, for a stated reason: the subject is the manager's own unit population, and a second store would be a second authority for which unit is running. +- **Materialization / realization:** obligations reuse the existing per-attempt cores, with no second copy. +- **Process observability:** each obligation writes its own receipt, the page reads the attempt's own receipt, and the launch receipt carries liveness against events. + +## Follow-ups, stated + +- **One PR per work item.** Publication still keys the PR by the attempt branch. The publish obligation is idempotent per attempt (it observes before creating), not per work item. Raised with bold-bee-114 on 2026-10-06. +- **Publication helper.** It still runs on its own timer (`PublicationHelperTimerUnit`), under a separate principal. +- **Supervisor units** keep RemainAfterExit. +- **`WorkerProcessSignaled`.** The process model has no signaled arm yet. +- **Publication-answer consumption record.** This is the rung drop's restoration trigger. + +## Rulings (bold-bee-114) + +- **2026-10-03, Q1:** no `std.obligation` carrier. +- **2026-10-03, Q2:** + - Exits are recorded in an instance-local spool, not `gunbc.roadmap_event_log` (the issue history) or `gunbc.fabric_event_log`, which needs a head compare-and-set per append. + - The hook never runs the interpreter. + - The hook is a typed `mkdir` argv, with no shell. +- **2026-10-03, homes and PR identity:** + - Homes are the factory controller and the factory attempt lifecycle. + - There is one PR per work item. Not yet built; see follow-ups. +- **2026-10-06, process grain:** per attempt. +- **2026-10-06, exit records:** + - Reviewers and auditors get exit records in this PR. + - The deadline is only a typed timeout, never the completion route. +- **2026-10-06, RemainAfterExit:** removed, with three conditions: + - A unit gone with no record reads as unobserved. + - A failed hook write is a located refusal. + - One control produces the exit verdict from the spool alone. +- **2026-10-06, RLM receipt:** event path rows, plus the backlog row or a declared drop. diff --git a/docs/plans/binder-node-representation.md b/docs/plans/binder-node-representation.md deleted file mode 100644 index b69d1070400..00000000000 --- a/docs/plans/binder-node-representation.md +++ /dev/null @@ -1,81 +0,0 @@ -# The binder node (XL-2 PR2b-3) - -Parent design: [service-interface-and-uses-carriers.md](service-interface-and-uses-carriers.md). - -Rulings (side chat, 2026-10-01): -- A default belongs to the BINDER, never to its type. -- One binder representation (a required type, an optional default, closed labels, one reader and one builder) is shared by fn parameters, service io fields and record fields. -- For the representation, option A: a `Conj` told apart in the way `v2.std.type_binder` tells type-parameter binders apart. That needs no new connective, so no operator sign-off. - -## The representation - -A binder is still the Named edge `name -> target` inside an Arrow domain, io payload or record payload. Its **target** is now a **binder node**: - -``` -Conj { - -> T (exactly one) - -> v (at most one) -} -``` - -- **Self-identifying labels.** Both labels are interned from lexemes no source can spell, as `type_params_marker` is. A binder node therefore identifies itself from its own labels, with no parent context and no guessing. No authored record field can collide with it: a field spelled `binder_type` interns a different symbol. -- **One reader.** `v2.std.node_query`: `binder_node_parts`, `declared_field_from_edge` (which now carries `default`), `member_edge_type_node` (now `Optional`), `find_binder_type`. -- **One builder.** `binder_edge`, `binder_edge_with_default`, `binder_relabelled`. 2b-1 (#12899) and 2b-2 (#12904) routed every reader and every producer through this pair, so the representation changed in these functions alone. -- **Fail-closed.** - - A Named member whose target is not a binder node is **not** a binder. `declared_field_from_edge` answers Absent, `member_edge_type_node` answers Absent, and `find_binder_type` refuses with `^named_child_not_a_binder`. - - The **binder wall**, `v2.std.type_binder` `arrow_domain_members_are_binders`, run by `type_binder_node_conforms` on every Arrow, refuses a hand-built domain member at the Arrow. - - A record payload the wall does not reach still fails closed at its first reader. `type_decl_view` cannot tell a record type from a module body structurally, so the wall cannot be stated there without guessing. - -## Walkers that would have misread a binder node, and what each does now - -The inventory covered every Arrow, domain and payload reader outside `src/v2/test`. **None needed parent context** (the ruling's stop condition): - -| Walker | Now | -| --- | --- | -| `v2.compiler.infer` formation (`infer_formation_facts_from_entries`) | A binder node has its own arm, `infer_binder_node_facts_from_entries`. Its derived type is a binder node over the type's derived type with **no default**, so defaults are not type identity, and derived types and source types share one binder shape. A binder's default is judged at the binder's declared type (`infer_binder_default_check`, position `PositionBinderDefault`; XL-2 PR2c-ii). | -| `v2.compiler.symbol_index_fill` `symbol_index_fill_containment_edge` | Indexes a binder at its **type** (`symbol_index_binder_type_or_target`), so a parameter or field path resolves to its declared type. ``/`` never become path segments. | -| `v2.compiler.translate` `translate_algebra` | A binder node translates **as its type** (`TypeExprTranslateBinder`, `translate_binder_node_step`). The default's fold result is discarded. A non-binder record member refuses (`translate_binder_member_type`). Arrow type expressions read parameter types through `find_binder_type`. | -| `v2.compiler.resolve` declaring path | Does not extend across `binder_node_label`s. | -| `v2.std.bounded_lattice_completeness` | Reads a binder field's shape from its type (`bounded_lattice_field_subject`). | -| Lenses (unit modeling, lifecycle, residency) | Through `member_edge_type_satisfies`. A non-binder answers no. | -| Type-name indexes (`node_query`, `concept_index`) | Through `member_edge_type_name`. A non-binder renders ``, never a fabricated name. | -| `target_model` parameter rendering | Through the reader. A non-binder refuses. | -| `lens/application` paths | A path that addressed a field's type now addresses its binder node. An edit replacing it with a bare type produces a hand-built binder, which the readers and the wall refuse. Loud, not silent. | -| Subtree-fold lenses (effect reach, determinism, cost, fn index, mandatory tag, decl-facts skeleton) | Will see a default as code once defaults exist. No default exists before 2c, which decides count-or-skip per lens. | -| Structural equality / anti-unification | Compare binder nodes structurally. Derived types carry no default, so two Arrows differing only in a default compare equal once defaults are judged in 2c. Nothing to change before then. | - -## Expected content-hash churn (one-time, intended) - -Every binder's target changes from `T` to `Conj { -> T }`. So the content hash (`v2.std.node` `content_hash`) of **every Arrow with a parameter, every record type with a field, every variant arm with a payload field, and every node containing one** changes once in this PR. The ruling anticipated this ("a claim-free Arrow keeps its identity except for an intentional one-time binder migration"). - -What moves with it: -- `v2.lens.interface_summary` `signature_fingerprint_of_node`. Every declaration with a parameter or field gets a new fingerprint. No API change is implied. -- `v2.lens.affected_set` `canonical_boundary_hash_receipt`. -- Any committed fixture or receipt that pins such a hash. - -A nullary Arrow, an Atom type, and a payload-free variant keep their hashes. - -## Defaults (XL-2 PR2c-ii) - -- **Lowering.** Every default lowers onto its binder through `v2.compiler.body_lowering_fold` `body_lower_binder_edge_read` (`binder_edge_with_default`): - - a fn or pattern parameter's from its own tail (`body_lower_typed_param_optional`); - - a record field's and a service io field's from the field tail (`body_lower_io_field_default_optional`). - - The value lowers through the one value reader (`body_lower_value_read`) and refuses at the value under that reader's cause. `body_lowering_reason_default_value_unmodeled` has no producer and is deleted. A field's **wire key** is a realization fact and still refuses (records) or is set aside (io blocks) until the realization binding (XL-2 PR3). -- **Judgment.** `v2.compiler.infer` `infer_binder_default_check` judges the default at the binder's declared type through `infer_judge_declared_position`, at a fifth position, `v2.std.inhabitance` `PositionBinderDefault`. That is the same relation an argument meets at its formal. A non-inhabiting default refuses as `^infer_reason_default_does_not_inhabit_binder_type`, located at the default. `^infer_binder_default_unjudged` is retired. -- **Scope.** A default sits under the Arrow's domain, which resolve walks in the type-parameter frame over the OUTER scope, never the Arrow's own value parameters. So a default cannot read a sibling parameter. -- **Emission.** A target with no parameter or field defaults refuses a defaulted binder, located at the default (`^produced_decl_render_param_default_unrealized`, `^target_semantic_decl_field_default_unrealized`), rather than dropping it. -- **Subtree lenses.** A default is code that runs when its argument is omitted, so the subtree-fold lenses (effect reach, determinism, cost, fn index, mandatory tag, decl-facts skeleton) COUNT it as code of the declaration it sits in. No lens skips it. - -## Declared frontier: omission at a call or a construction - -A caller may not yet OMIT a defaulted argument, and a construction may not yet omit a defaulted field. Both bind through one binding plan, which still requires every formal, so an omitting call or construction refuses at the application or construct, loudly: -- calls: `v2.std.arrow_signature` `application_binding_plan`, used by infer and eval; -- record construction: `application_binding_plan_over`, used by `v2.compiler.infer` `infer_record_field_binding`. - -**Trigger:** one shared binding plan that reads each formal's binder metadata (via `declared_field_from_edge`) and admits a defaulted formal as optional, together with default materialization, so that eval, and emission where the target admits it, supply the default for the missing slot or field. Until then a default is lowered, judged and carried, and an omitting site refuses. - -## Scope and generics (side-chat review on #12948) - -- A default resolves in the ENCLOSING value scope, with the declaration's type parameters only as its type scope (`v2.compiler.resolve` `resolve_arrow_domain_walk`). An outer value is visible; a sibling parameter is not; a type parameter's spelling does not resolve as a default value. -- A default under a generic declaration (an Arrow or type declaration with type parameters) refuses at the default as `^infer_binder_default_generic_unmodeled` (`v2.compiler.infer` `infer_generic_default_refusal`), until a default is judged with its declaring type parameters in scope. diff --git a/docs/plans/buganizer-visual-contract.md b/docs/plans/buganizer-visual-contract.md deleted file mode 100644 index c12af449091..00000000000 --- a/docs/plans/buganizer-visual-contract.md +++ /dev/null @@ -1,59 +0,0 @@ -# Buganizer visual contract (captured 2026-09-22) - -Authority for the issue-tracker frontend's look and feel. Owner's directive: anchor -on Google's Issue Tracker — no homegrown UX. Provenance per section. The anatomy sections -are MEASURED (2026-10-04): the modeled values are in `extdeps.google.issue_tracker`, and the -captures and probe output they were read from are named below. - -## Captures on disk -- `~/Assigned to me - Issue Tracker.html` (259KB — client-rendered DOM, empty state + advanced search open) -- `~/Assigned to me - Issue Tracker_files/` (assets incl. main CSS bundle `m=c` 6.3MB) -- `~/issue-tracker-shots/` (2026-10-04: issues.chromium.org signed out at 1440x1100; screenshots, - computed-style probes, the instrument, and `element-spec.md`, the element-by-element comparison - and the owner's rulings). Shared copy of the screenshots: - https://drive.google.com/drive/folders/14rcDZQtJAHzrAYimoRNmm7rBS1HqV_9b?usp=sharing - -## Status vocabulary (VERIFIED — the product's own query builder) -open · new · assigned · accepted · closed · fixed · verified · duplicate · inactive · -infeasible · intended behavior · not reproducible · obsolete - -## Sidebar roster (VERIFIED — owner's session) -Assigned to me · Starred by me · Upvoted by me · CC'd to me · Collaborating · -Reported by me · To be verified · Bookmark groups · Saved searches · Hotlists -(Create hotlist) · Create bookmark group · Browse components - -## App shell (VERIFIED) -- Header: product wordmark "Issue Tracker"; search input with "Search help" + "Save"; - "Create Issue" primary button; right side: theme switcher, help, Google Account block. -- Content region: search builder (Match all/any, + OR rows), "Issue search results" heading. -- Empty state: "No issues match your search." + "Learn more about searching" link. - -## Design tokens (VERIFIED — extracted from the CSS bundle) -- Text: `#202124` primary · `#5f6368` secondary · `#3c4043` tertiary -- Borders/dividers: `#dadce0` · `#bdc1c6` · `#e8eaed` -- Action blue: `#1a73e8` · hover `#1967d2` · pressed `#185abc` -- Error/red: `#c5221f` · `#ea4335` -- Backgrounds: `#f1f3f4` · `#f8f9fa` · highlight blue `#e8f0fe` · `#d2e3fc` -- Type: Roboto, Arial, sans-serif · Google Sans, Roboto, Arial, sans-serif (headings) - -## Issue-row anatomy (MEASURED 2026-10-04 — captures `06-grid`, `14-grid-default`, `03-deps-tree`) -Results grid columns: select · star · P · Type · Title · Assignee · Status · 7d views · ID · -Last modified; rows are one dense line and the title links to the detail page. Row, header and -relation-table heights are `issue_tracker_measured_metrics`; the served column roster and its -owner amendments are `issue_results_columns`. - -## Issue detail anatomy (MEASURED 2026-10-04 — captures `01-detail-comments`, `11-ancestor-menu`, `15-status-edit`) -Header: component path and ancestor chain as chips, then the title and the action buttons. Tabs: -Comments · Dependencies · Duplicates · Blocking · Resources, each with a count. Main column: a chip -row (status, type, priority), then cards (status update, description, comments). Field rail at -the trailing edge. Type scale, palette (light and dark), metrics, card, status chip and button -anatomy are the `issue_tracker_measured_*` rows; the dark token row of 2026-09-23 is replaced by -`issue_tracker_measured_palette_dark`. Our Work tab maps onto the History half + execution -receipts. - -## Mapping notes for the frontend (workflow policy, not extdeps) -- Issue = roadmap node; Component = domain placement (owner/lane); Blocking edges = - dependency edges; Stars/CC/Upvotes = per-user subscription facts (new modeled state). -- Status = our derived present-tense explanation mapped to the product's standings - (open/new/assigned/accepted/fixed/verified), never a flattened label. -- "To be verified" (stock view) = our verification-owed candidates. diff --git a/docs/plans/canonical-content-order-draft.md b/docs/plans/canonical-content-order-draft.md deleted file mode 100644 index 6d83f0a77ec..00000000000 --- a/docs/plans/canonical-content-order-draft.md +++ /dev/null @@ -1,77 +0,0 @@ -# Canonical content order — draft declaration for fit check (node adhoc-77383faf-d07) - -Decision (deep-ferret-305): declare in std now; #12895 portable_value_cmp becomes its interpreter realization. - -## 1. Inhabit std.algebra, no parallel vocabulary -- `type TotalOrder { compare: fn(T, T) -> Ordering }` — result is the existing `std.algebra Ordering`. -- `AlgebraProfile` gains `TotalOrderProfile`; `total_order_templates()` = [compare]. -- `OrderedRing` / `Field` COMPOSE it: `ordered_ring_templates()` / `approximate_field_templates()` - take their `compare` row from `total_order_templates()` instead of restating it (one compare concept). - -## 2. The canonical order is a DERIVED inhabitance, not a function over a dynamic value -No `.dag` carrier for "any value" exists (PortableValue is seed Rust), and typed `.dag` map keys are -one type K. So the authority is a structural derivation rule — the same shape as -`algebra_profile_equality_extensional` — saying how every admissible key type inhabits TotalOrder: -- Bool: false < true. Int: numeric. String: Unicode-scalar lexicographic (= UTF-8 byte order). -- Float: IEEE 754-2019 §5.10 totalOrder, cited through the binary64 authority of #12547 - (lively-newt-480). Realized by `f64::total_cmp`. NOT raw u64 bit compare (that inverts negatives). -- List: lexicographic by element, then length. Map: lexicographic over entries in canonical key order, - comparing (key, value). Set: lexicographic over members in canonical order. -- Record: fields compared in canonical field order. Variant: by arm key, then fields as a record. -- Cross-kind rank (Null < Unit < Bool < Int < Float < Str < List < Map < Set < Record < Variant) is - only reachable in the interpreter's untyped value space; it is the interpreter realization's - extension and is never observable on a well-typed key. - -## 3. Record/variant key: spelling now, declared identity is the climb -Declared identity (owner module + declaration, arm ordinal) is the right key, but it is not carried by -`Value::Variant` / `PortableValue::Variant` today — `gunbc.guarantee_stall.variant_owner_identity_stall` -(NS-0B). Spelling is the only key BOTH realizations carry, and it is total on what they carry. -The residual (two same-spelled owners compare Equal) is exactly that stall's population; the order -switches to declared identity when NS-0B's trigger fires. Stated, not silent. - -## 4. Realizations (the one authority, two realizations) -- Interpreter: portable_value_cmp (#12895) realizes §2; to_string/interpolation of Value::Map render - entries in that order. -- Emitted Rust: map rendering sorts entries by the emitted compare for K (generated from the same - derivation rows), never by im::HashMap iteration. -- Control: a fixture spanning every kind rank (incl. -0.0/+0.0/negative/NaN floats, same-length lists, - nested maps) — interpreter and emitted compare agree; a multi-entry map renders byte-identically - across 2 processes, interpreted and emitted. - -## 5. Gate -v2.lens.determinism classifies every remaining host-order path (iteration, Debug formatting) as -HostUnspecifiedOrder; red on any unclassified one. - -## Fit check (neat-boar-16) — conditions accepted -1. Interpreter and emitted comparisons are both DERIVED from the rule rows (kind rank, float rule, field/arm - key), not two hand comparators; control = same multi-entry map, identical bytes, 2 processes, both. -2. OrderedRing/Field DELETE their restated compare in the same change. -3. Ordering reused; no new carrier. -4. Spelling key is a named stand-in on the carrier, trigger = variant_owner_identity_stall (NS-0B); - spelling is an ordering key only, never identity. OPEN — see below. -5. Float controls: -0/+0, NaN payloads, negatives (totalOrder via #12547). -6. #12895 consumes this authority; sign-off from jolly-boar-500 / sleek-ibex-207 before either lands. - -### Condition 4 is not realizable in the interpreter before NS-0B -Value::Variant / PortableValue::Variant carry no owner identity, so the interpreter cannot detect "two -distinct declarations, same spelling": it can neither refuse nor tiebreak by declaring module. In the -emitted Rust the case cannot arise for a typed key (distinct declarations are distinct Rust types; an -enum key distinguishes them by arm). So the residual is interpreter-only and is exactly the stall's -population. - -### Ruling on condition 4 (neat-boar-16): option A -- Variant/record order = spelling, THEN full payload by the same canonical order. Keys tie only when they - render to identical bytes, so tie order is unobservable in output. -- Control: two same-spelled variants with DIFFERENT payloads render in a fixed order across 2 processes. -- §4b drop row, interpreter path only. Population: variant_owner_identity_stall's two members, plus - same-spelled variant keys from distinct declarations. The interpreter conflates those for ordering AND, - pre-existing (not introduced here), for equality. Trigger: owner identity carried on Value::Variant and - PortableValue::Variant (NS-0B), the capability, not a PR. Emitted Rust: no row (distinct types). - -### jolly-boar-500 sign-off (conditions) -(a) Kind rank derived from the declaration order of the portable value model's variants. PortableValue's - order (Null, Unit, Bool, Int, Float, Str, List, Map, Set, Record, Variant) matches the list exactly; no change. - Correction: Set is OrdSet, so "canonical member order" equals byte-lexicographic member strings. - #12895's Set rule was already conformant; only the float rule diverges. -(b) Differential control: interpreter vs emitted compare agree on a generated corpus covering every kind - pair, NaN, -0.0/+0.0, empty vs non-empty collections, nested maps and sets. diff --git a/docs/plans/carrier-realization-arbiter-repair-design.md b/docs/plans/carrier-realization-arbiter-repair-design.md deleted file mode 100644 index e085e4336b1..00000000000 --- a/docs/plans/carrier-realization-arbiter-repair-design.md +++ /dev/null @@ -1,646 +0,0 @@ -# Repair design: shadow census, then an A/B/C/D counterfactual over the one fork (2026-08-21) - -**Session:** `royal-dove-436`. **Work item:** `node://adhoc-c735d227-60b`. -**Authorized by:** `smart-ram-730`, after the arm census in -[`t2_t3_realization_route_2026-08-21.md`](../probes/t2_t3_realization_route_2026-08-21.md). -**Status: DESIGN. Nothing here is built.** -**This revision supersedes an earlier one in the same PR** that proposed deleting the short-circuit -as the first experiment. That is still the right eventual repair and it is the wrong first move; §2 -says why, and the reason is not caution — it is that a deletion cannot measure the quantity the -decision turns on. - -## What the measurement forces, before any design choice - -| arm | T2 | T3 | -|---|---:|---:| -| **A** — generic carrier signature (`list_append`, `length`, `is_empty`) | **25** | 0 | -| **B** — declared-structure constructor | 4 | 17 | -| **C** — direct carrier-to-carrier assignment | 3 | 0 | -| UNALIGNED — not attributed | 2 | 1 | - -Arm A makes this a forced conclusion rather than a menu: - -> `list_append(left: FreeMonoid, right: FreeMonoid)` is emitted **once**, generically, so it -> has exactly **one** host parameter type. Its `.dag` callers pass values declared `String` — the same -> modeled type, by `std.string_type` `String = FreeMonoid`. With two host realizations the -> generic function can only be one of them, and every call from the other is an E0308 **no call-site -> edit can remove**. - -So the landed shape is **one exact identity query → one base target realization → position-specific -reference-layer rendering**, and two candidate repairs are already refuted: - -- **Constructor-side only** closes arm B — 21 of 52 — and leaves arm A's 25, the largest arm. -- **Anything relying on monomorphization** cannot help: the type renderer's test is - `rust_host_text_carrier_elem_name(n) == "Char"`, a syntactic read of the authored element spelling - decided *before* instantiation. A generic `T` is never spelled `Char`. - -## The authority exists, is unreachable, and is MIS-KEYED - -*An earlier revision of this heading said "is correct". That was wrong, and wrong in the -direction that matters: the authority's **logic** was checked and its **key** was not. A total -function over a wrong key does not fail — it returns a confidently wrong answer. The key defect -is `type_reference_decl_file`'s fallback arm, traced in the section beginning "the identity key -is a LOCATION" below; the retraction is restated here because a reader who stops at this heading -must not leave with the superseded claim.* - -`type_realization_decision` **does have consumers** — `v1.compiler.coercion` `lookup_checkpoint` is a -thin derivation of it for every `decl_file != ""` caller. (An earlier revision of this lane's PR body -claimed zero consumers; that was an error, from grepping the type name rather than the function.) - -`structural_declaration_modules_for("String")` = `["src/v2/std/text.dag", "dag/std/string_type.dag"]`, -so the strict query **would** refuse the native spelling. But six type renderers — `render_rust_type`, `render_rust_type_without_applied_binding`, -`render_rust_applied_type`, `render_rust_type_with_applied_binding`, `render_rust_decl_type`, -`render_rust_fn_sig_type` — each **return on their first line** via -`is_host_text_carrier_type` → `"String"`, unconditional on `decl_file`. **No `String`-spelled -reference in type position can reach the authority.** An unreachable wall, not a missing one (DESIGN -§6 coverage-by-illusion). Established from those six renderers' control flow — each call verified to be the statement -immediately following its own `fn` declaration, not merely early in the body; **not** established by -a discriminating execution. (This count read *five* until 2026-08-21: -`render_rust_type_with_applied_binding` was missed when the first sweep's grep output was truncated -before reaching it. The full derivation is in gunbc#8805; the correction is recorded rather than -silently applied because the earlier count was relayed upward and a review restated it as fact.) - -## §2 — Why the experiment is not "delete the short-circuit" - -Two quantities are being confused, and only one is answerable without emitting: - -- **Decision divergence** — how many occurrences get a different answer from the short-circuit than - from the authority. Statically computable. -- **Diagnostic conversion** — which rustc failures appear once the authority controls emitted Rust. - Only observable by emitting and compiling. - -A deletion produces the second with no record of the first, so every downstream question ("was this -error caused by the change, or exposed by it?") becomes narration. The census below produces the -first *before* any byte moves, which is what makes the second adjudicable. - -## Step 1 — Shadow census, no output change - -At every affected type and value renderer, compute **both** answers and keep emitting the legacy one. -Write a **sidecar receipt** — explicitly **not** comments into the emitted Rust (an earlier -suggestion, withdrawn: DESIGN §4c makes an annotation unreadable by any `Accepted` program, so a -receipt emitted as commentary is a receipt no consumer can join on). - -One row per occurrence, keyed by: source `DeclarationRef`; source occurrence / use site; emitted file -+ enclosing declaration; **position kind** (declaration / fn signature / value constructor / applied -type argument); legacy base realization; authority decision; reference-layer decision; identity -available? - -Two constraints that are load-bearing, not stylistic: - -1. **Query the strict `type_realization_decision`, never `lookup_checkpoint`.** `lookup_checkpoint` - deliberately preserves the bare-name fallback when `decl_file == ""`, so censusing through it - measures the bypass against itself. -2. **Outcomes are `Agrees` / `DivergesWithExactIdentity` / `IdentityUnavailable`, and - `IdentityUnavailable` is never folded into structural rendering.** `Unrealized` (a known - structural declaration) and `Refused` (identity not supplied) are different answers. Merging them - reproduces the bypass under a newer name — the exact defect this work exists to delete. - -### Step 1 shape: RULED — (b), a separate substrate walk, plus a mandatory calibration control - -Reconnaissance surfaced a fork the brief did not settle, and `smart-ram-730` ruled it. Both the fork -and the ruling are recorded, because the reasoning constrains how step 1 may report. - -**What was never the obstacle.** Both inputs the census needs are pure and exist today: -`v1.compiler.coercion` `type_realization_decision` is importable (three modules already import from -that module), and `v1.compiler.05_emit_rust` `is_host_text_carrier_type` is pure — but **private to -that module**, imported by nothing. - -**The fork was where the census runs.** - -- **(a) Inside the emitter** — the brief's literal shape: both answers at each of the six renderers, - legacy one emitted, receipt routed out. The v1 Rust emit path's file writing is host-driven, so - routing a receipt out means a **second output channel through a load-bearing file**. -- **(b) A separate substrate walk** — a `v2.workflow` census module over the same assembled closure, - a probe entry, and a thin host driver: the shape `v2.workflow.realization_sweep` and - `realization_sweep_survey.rs` already establish, substrate analysing and host transporting. - -**Ruled: (b).** On the merits: (a) obliges step 1 to prove its own inertness before it can report -anything about its subject, and *an instrument that must first prove it did not disturb the thing it -measures is a worse instrument than one that cannot disturb it*. Under (b), acceptance condition 8 — -bytes outside the divergence population unchanged — is true **by construction** for step 1 rather -than something step 1 establishes. - -**The objection against (b), and what neutralises it.** A parallel walk can drift from the control -flow it claims to describe, and *this lane exists because a parallel answer path was authoritative in -the wrong place*. That suspicion is correct, but the drift risk is not uniform across the two things -(b) borrows, and separating them is what decides it: - -- **The predicate cannot drift**, because `is_host_text_carrier_type` is **imported, not - re-derived**. A re-derived copy would be the same §3 fork again and is an immediate refusal; - importing it is the whole reason (b) is admissible. -- **The traversal can drift.** The walk may visit occurrences the emitter never renders, or miss ones - it does. That is the real exposure, and it is not hypothetical: a subtly wrong occurrence set makes - every count wrong while looking perfectly well-formed. - -**So the calibration control is a precondition of the census being reportable, not a nice-to-have:** - -> The walk must **reproduce the known 25 as its diagnostic-producing divergence subset.** Partition -> the walk's `DivergesWithExactIdentity` rows by whether the occurrence currently produces a rustc -> diagnostic; that subset must equal the 25 sites of arm A — joined by source declaration + enclosing -> emitted declaration + operation, **never by line**. **If it does not, the census is WRONG and its -> divergence count must not be published. Report the mismatch instead.** - -This converts the drift objection from an argument into a measurement, which is the only way to -settle it. Note what it does **not** require: the walk need not *explain* the 25, only **find** them. -It composes with the cross-tab already committed to — the calibration is one cell of it, and the -interesting cell (`DivergesWithExactIdentity` at zero diagnostics) is trustworthy only once the -calibrated cell checks out. - -### Correction to (b) as ruled: the v2 assembly is the WRONG TREE, so (b) is v1-side - -Found while building it, and recorded because it falsifies a premise both the recommendation and the -ruling rested on — not the ruling's *reasoning*, which survives intact, but the concrete artifact it -named. - -**(b) was described as "a `v2.workflow` census module over the same assembled closure", reusing the -`v2.workflow.realization_sweep` pattern. That is not implementable as stated.** That pattern's -`assemble_program_from_ingest` returns an `Outcome` over **`v2.std.node.Node`**, while -`v1.compiler.05_emit_rust` — the module whose decisions the census exists to measure — operates on -**`v1.std.core.Node`**, imported by name at the top of that file. They are different types. A walk -over the v2 assembly would be measuring a different tree from the one the emitter renders, which is -the traversal-drift failure the calibration control exists to catch, built in at the foundation. - -**(b′), the corrected shape, is v1-side and is implementable today.** `v1.compiler.compile` -`front_end_sources(sources: List) -> FrontendResult` is a public entry returning -`FrontendResult { graph: ModuleGraph?, newline_indices, intern_table, … }` — the v1 tree the emitter -consumes, plus the `newline_indices` that `authored_name_at` needs. So: - -1. the host supplies the closure's `SourceFile` rows — **transport only**, the same role - `realization_sweep_survey.rs` plays for its probe; -2. the census module calls `front_end_sources` and walks each module's items for type-reference - occurrences; -3. per occurrence: `authored_name_at`, `v1.compiler.coercion` `type_reference_decl_file`, position - kind; -4. legacy answer from the **imported** `is_host_text_carrier_type`; authority answer from the strict - `type_realization_decision`; -5. rows out as TSV. - -**Everything the ruling actually reasoned about survives this correction**, which is why it is a -correction and not a re-open: the predicate is still imported rather than re-derived, no output -channel is added to the emit path, acceptance condition 8 is still true by construction for step 1, -and the calibration control is unchanged and now matters more — a v1-side walk *can* still drift from -the emitter's control flow, and reproducing the 25 is still what settles it. - -**REFUTED BY CI, 2026-08-21 — the census module DOES have to live in `src/v1/`.** This paragraph -claimed the module need not, on the grounds that import direction is not a layer rule (DESIGN §3 -makes acyclicity the only structural law and folders a browsing convention), and concluded that this -"removes the v1-growth question from step 1 entirely". - -**The premise is true and the conclusion is false.** Import direction is indeed not a layer rule, but -that is not the constraint that binds. The required floor discovers subjects by **path**, over the -roots `dag` and `src/v2` (`witness_layer_roots`), and resolves that closure as one program — -`modules_resolved=3820`. A module authored at `src/v2/workflow/` is therefore swept into discovery -because of **where it sits**, not because of what imports it, and its `v1.*` imports are unresolvable -inside that envelope. Measured, on PR #8816: - -``` -src/v2/workflow/carrier_realization_census.dag:3:1: error: unresolved import: - module 'v1.compiler.compile' not found -required-ci: floor refused: subject=7b5536161546187e modules_resolved=3820 modules_excluded=4 -``` - -This is the same structural unresolvability `dag/test/claim/checkpoint_identity_keying_witness_test.dag` -already documents about itself; I read that precedent and still authored outside `src/v1/`. - -The module now lives at `src/v1/tests/claim/carrier_realization_census.dag` and produces -**byte-identical** results there, so nothing measured is invalidated — only this placement claim is. - -**So the v1-growth question is REOPENED, and it is an admission to state rather than one to assume.** -Step 1 adds one module to the seed tree. Under DESIGN §3's v1 maintenance standing the admission test -is PURPOSE — *"anything in support of v2 self host is safe"* — and the E0308 board is that program, -so it reads as admissible. It is recorded here as an open admission, not as a settled one: this -document does not get to grant itself the exception, and the paragraph it replaces is exactly what -happens when a design talks a question out of existence instead of answering it. - -### There is no visibility change: (b′) is zero-touch on `05_emit_rust` after all - -This heading previously read *"(b) is NOT zero-touch on `05_emit_rust`, stated plainly"* and -disclosed a visibility change on `is_host_text_carrier_type` as the one seed edit step 1 needs. -**That disclosure was wrong, and it is corrected in the direction of touching less, which is exactly -the direction a disclosure must not be left wrong in.** - -The `.dag` language has **no visibility syntax at all** — corpus-wide there is no `export`, `pub` or -`private` form; every top-level `fn` in a module is importable by name. `is_host_text_carrier_type` -is an ordinary top-level `fn`, exactly like `rust_scalar_checkpoint_render_base` — which -`src/v1/tests/claim/checkpoint_identity_keying_witness_test.dag` **already imports from this same -module** today. `v1.compiler.05_emit_rust` is likewise already imported by `v1.compiler.compile` and -`v1.compiler.stage0_crates`. - -So **step 1 requires no edit to `v1.compiler.05_emit_rust`, and no edit to the v1 seed at all.** The -predicate is imported, not re-derived — the property the ruling actually depended on — and nothing is -widened to achieve it. - -**The lifetime question dissolves with the change that raised it.** It asked whether a widened -surface survives the repair or is scoped to the census. There is no widened surface: the predicate's -importability is a property of the language, not a grant this lane made. If the terminal repair -deletes the spelling-keyed short-circuits and the predicate with them, the census's import fails -loudly at that point — which is the correct coupling, and is a live dependent rather than residue. -Recorded rather than dropped, because an unasked question and a dissolved one look identical in six -weeks. - -### The census's executing consumer is its own driver, because the batch it would have used is gone - -`src/v1/tests/claim/checkpoint_identity_keying_witness_test.dag` documents its own execution as -`gunbc.ci_layer_roots` `v1_claim_scoped_witness_entries` → `v1_claim_scoped_witness_batch`, "whose -source-root envelope is dag plus src/v1". **That batch is deleted** — `ci_layer_roots` -`v1_claim_scoped_witness_batch_deleted_note` records the deletion (2026-08-15), and -`witness_fold_src_v1_coverage_gap_note` carries the resulting declared coverage gap. - -So a census module cannot inherit that enrollment, and step 1 does not try to. Its executing consumer -is **its own host driver**, the "local recipe" standing `realization_sweep_survey.rs` already -occupies for its probe — the driver supplies the `dag` + `src/v1` source-root envelope for the census -module itself, and separately supplies the subject closure's sources as `SourceFile` **data**. Those -are two different roles for two different populations and the design keeps them apart deliberately: -confusing them is how a census ends up measuring its own pool instead of its subject. - -## Step 2 — Four counterfactual crates over one pinned tree - -Same toolchain, same assembly path, same manifest, same source population, separate fresh output -directories: - -| | type position (decl + fn sig) | value position | -|---|---|---| -| **A** baseline | legacy | legacy | -| **B** | strict | legacy | -| **C** | legacy | strict | -| **D** | strict | strict | - -A factorial over exactly the fork the census measured. It answers what no total and no single -treatment can: whether arm A's 25 are controlled by the **type** short-circuit, whether arm B's 21 -are controlled by the **value** short-circuit, and whether converging both produces agreement or -merely relocates the disagreement. - -**Publish a site-conversion ledger, not four totals:** baseline site, treatment site, source -declaration, old expected/found, new expected/found, old category, new category. **Join by source -declaration + enclosing emitted declaration + operation — never by line.** One error block can split -into several sites, and lines move. - -**Do not land the dual renderer.** It is experimental quarry: leaving both answers in production -creates the second authority this work exists to delete (DESIGN §3). - -### Ledger requirement: diff the full warning histogram — and the probe's column CANNOT carry it - -**The requirement, and the near-miss that produced it.** `smart-ram-730`, from another lane: a repair -keyed on the emitted pattern string moved its board 31 → 32 — noise — while underneath it -`unreachable_pattern:6` appeared. **Those are errors here, not lints.** Two arms of the same variant -emitted different patterns, the first lost its guard and shadowed the second into dead code at six -sites. The headline moved by one; six arms died. A realization change is exactly the edit that can -make one arm subsume another: if a carrier's base realization changes, arms distinguishable by its -host type may cease to be. - -**An earlier revision of this section said to satisfy that by diffing the probe's histogram column. -That check cannot work, and the defect is in the instrument, not the diff.** Verified first-hand in -`docs/probes/curated_cargo_probe_one.sh`: `ERROR_HISTOGRAM` greps `'^error\[E[0-9]+\]'` and -`HISTOGRAM_SUM` greps `'^error(\[E[0-9]+\])?:'`, over a plain `cargo build --release --lib` with no -`--message-format=json`. `warning: unreachable pattern` matches **neither**. The class can never -appear in that column. - -**So absence of that row is blindness, not zero.** Reporting the class clean from that column would -not be a measurement — it would be an instrument that cannot express the class, which is the -absorbing fallback (DESIGN §5) relocated into the measuring apparatus. (Older readings that *did* -carry the class legitimately came from a `rustc --message-format=json` instrument, which includes -lints. Same question, different instrument, different answer.) - -**What the A/B/C/D ledger does instead:** read the kept cargo log directly and diff the **full sorted -`warning:` histogram** across all four crates, in one dispatch covering every arm. - -**And it carries the honest bound, because at a refusing baseline nobody can do better:** the emitted -crate fails to build, so rustc stops before typechecking most items and never runs reachability on -them. A zero from that log means *"none among what rustc reached, and unchanged between arms"* — it -does **not** mean the crate has none. That bound is published with the number, not left to a reader. - -**NARROWING, verified first-hand after `bright-dove-741` refuted the general form.** The emitted -crate carries `#![deny(unreachable_patterns)]` — confirmed in `v1.compiler.stage0_crates`'s emitted -crate attributes and in the seed's own `lib.rs`. A **denied** lint renders as `error: unreachable -pattern`, which **does** match the probe's uncoded-suffix grep. So the blindness above is real for -**warning-form** lints and **not** for denied ones, and `bright-dove-741` holds the positive control -in band: 6-then-0 across their own defective and shipped runs, on this instrument and entry. - -**So the standing rule is narrower than first stated, and this is the corrected form: whether an -instrument can emit a class is a property of the EMITTED ARTIFACT's lint attributes, answered per -artifact — not a fixed property of the probe.** The cheap way to answer it is to find a run where -that class was nonzero; a class no run has ever produced is a class you cannot report clean. The -log-level warning reading above stays the right mechanism for warning-form classes; the rule around -it is what changes. - -## Step 3 — Pre-registered acceptance - -A rising rustc total is neither proof of progress nor proof of failure. The repair is semantically -correct only if **all** hold: - -1. Every changed occurrence was already in the shadow divergence population. -2. The selected base equals `type_realization_decision` for the exact declaration. -3. Type and value positions consume the **same** base answer for one declaration. -4. The reference layer stays a **separate axis**; `Rc` is never inferred from base spelling. -5. Same spelling + different declaration identity can still give different answers. -6. Unavailable identity **refuses** rather than taking the legacy bare-name answer. -7. A generic `FreeMonoid` is not classified as text because some instantiation later uses `Char`. -8. **Emitted bytes outside the divergence population remain byte-identical.** - -Condition 8 is the discriminator this lane did not previously have, and it is why it is stated as a -test rather than a hope: **a new error in an emitted file whose bytes did not change is not "debt -exposed by the repair" — it is contamination or an unrelated tree delta.** It converts a vague worry -into a mechanical check. - -Then classify every treatment-only error as `ExpectedSuccessor` / `UnrelatedRegression` / -`Unclassified`. `ExpectedSuccessor` requires a **carried** causal relation, not a narrated one: it -occurs at the changed occurrence or a direct use of the changed declaration; its expected/found -contains the authority-selected realization or its independently derived reference layer; the -baseline emitted a different type at that exact semantic position; and the obligation could not have -arisen until that representation was selected. - -**Acceptance: wrong-authority decisions = 0 over the scoped population, unrelated regressions = 0, -unclassified = 0.** Only then does the raw total speak to convergence at all. - -## The Root B precedent, used for what it actually establishes - -Root B's forced `HostNative` changed **two independent axes at once** — primitive realization *and* -import/use-line synthesis. So its 693 → 807 established neither "the rise is hidden debt" nor the -opposite; it established that the repr caused its target family, and that the switch was not a valid -repair because it carried a fused unrelated decision. **Use Root B as precedent for isolating axes — -which is exactly what A/B/C/D does — never as precedent for admitting an increase.** This experiment -can be cleaner than its precedent: the identity authority already exists, and the short-circuit -varies without touching import policy, closure selection, or Cargo config. - -## Instrument lesson this design adopts for its own ledgers - -`smart-ram-730`, on the RT-builtin misattribution: **a classifier that folds its discriminating input -into its derived label destroys the ability to re-adjudicate later**, and the cost lands on whoever -needs a different partition than the one that was authored. The 2026-08-21 partition consumed each -block's `note: function defined here` into its `root` column, so 5 sites coded `RT-builtin` cannot be -re-split from the TSV — the callee note is a good **field** and was a bad **label**. - -This design's ledgers therefore carry the raw discriminating inputs beside every derived label, as -`routes.tsv` and `arbiter_arms.tsv` already do (raw `expected`/`found` beside `expected_route` / -`found_route` / `carrier` / `verdict`). Concretely, the step-2 ledger carries position kind, identity -availability, and both decisions as **fields**, never only the conversion verdict computed from them. - -## Population, pinned so it cannot drift - -The scoped population is the **52 shared-root sites** in `arbiter_arms.tsv`, and the first deliverable -is the A/B/C/D conversion receipt over **arm A's 25**: largest measured arm, constructor-only already -falsified against it, mechanism located, authority present, and its failure is that the authority is -*unreachable* rather than unspecified. - -The 5 `RT-builtin` sites in `v2_std_compilers_target_model.rs` (lines 6183 ×2, 6198, 6242, 6267) are -**outside** this population and stay outside it. Two of them plausibly belong to this root and three -to the cross-realization signature-drift root, but this lane's spelling-keyed classifier cannot -discriminate `HashMap`-from-inhabitant-row from `HashMap`-from-`v1_rt::lookup`-signature, so the -adjudication waits for the callee-note field rather than being made by an instrument that cannot make -it. Two sites either way do not touch the 25. - -## What this design does not establish - -- **It is not costed.** No estimate is offered, because the A/B/C/D receipt is what produces the - blast-radius number any estimate would need. -- **It does not claim the arms share one fix.** B and C are treatments precisely so that question is - measured rather than assumed. -- **It does not pick structural vs native `String`.** That is a policy about the seed's own - representation; the experiment produces the data it should be decided on, and - `checkpoint_table_bypasses_identity_note` records what picking a direction without that data cost - last time (the over-broad `Bool` row: 5 fixture refusals plus `required-regen` drift). -- **It does not touch the v1 freeze question.** Every step edits the v1 seed emitter, admissible - under the DESIGN §3 purpose test only insofar as it serves the v2 self-host program — which the - E0308 board is, but the admission is the operator's, not this document's. - -### The census is permanent, and it is NOT enrolled — declared, not solved - -`smart-ram-730` asked whether the census is a permanent lens or a one-measurement instrument, -because the second is a scaffold and DESIGN §6 lets no scaffold land on author declaration alone. - -**It is permanent, by design and not by relabelling.** The question it answers — *does the legacy -short-circuit still diverge from the identity authority at this occurrence* — does not retire when -T2/T3 is repaired; it **inverts**. Today the divergence set is the defect population. After the -repair, the same walk over the same subject must produce an **empty** divergence set, and any row -reappearing is the regression. That is DESIGN §4b(4) exactly: a climb deletes the redundant -production machinery (the short-circuit in the six renderers) and **keeps the discriminating -evidence enrolled**, because deleting the evidence alongside the machinery recreates -specification-without-execution one rung up. The census carries both halves — the diverging rows are -the discriminating RED, the `Agrees` rows the accepted positive control. - -So no scaffold admission is owed and no dissolution trigger is owed. - -**But "permanent" must not be allowed to do the work of "enrolled", and today it cannot.** CI's only -invocation is `claim_executor --required-ci --source-root dag --source-root src/v2`. `src/v1` is not -a source root; it is reached only by the `.dag` **parse sweep** (`v1_src_dag_parse`), which parses -and does not evaluate. A module at `src/v1/tests/claim/` is therefore **parsed and never executed** -by the required run. Claiming it as an enrolled regression control would be precisely the tier -DESIGN §6 names — the machinery exists and nothing gates on it — and an inert lens is itself a lie. - -Stated at the honest grain instead: - -| | | -|---|---| -| **rung now** | *mitigatable* — a hand-invoked probe; its evidence is real when run, and runs only when a human runs it | -| **ceiling** | *mechanically preventable* — a witness the required floor executes on every PR | -| **next-rung trigger** | an execution route from the required run into `src/v1`, **or** relocation to a home the floor already executes | - -Which of those two is right is not decided here, and the second one is now known to be **closed**: -relocating out of `src/v1` is exactly what CI refused, because the `v1.*` imports are unresolvable -inside the `dag` + `src/v2` envelope. So the trigger is the first: **`src/v1` becomes an executed -root, or an execution route reaches it.** That is a CI-scope change affecting every lane, it is not -this lane's to land, and it is owned by `smart-ram-730`. - -**One connection is withdrawn as wrong, and it is recorded here even though it never reached a -shipped revision of this document** — it was the reasoning behind the trigger, so a reader who -re-derives the trigger would re-derive the error. Replying to the permanence ruling I named the -seven-witnesses-in-a-declined-home lane (`royal-tern-339`) as the lane whose answer this trigger -would consume. It is not, and the difference is mechanism rather than degree: those witnesses live -inside `src/v2`, which **is** a source root, so they are **discovered and then declined by home -policy**, and their fix is a move between homes inside an already-discovered root. This census is -never discovered at all. Same symptom, different mechanism — the surface-versus-mechanism error this -whole lane exists to measure, committed by me at the level of coordination, and caught by -`smart-ram-730` only because the connection was stated out loud rather than assumed silently. - -For the record, and because it re-sizes the trigger rather than merely relocating it: the objection -to adding `src/v1` as a root is **not** roster blast radius. The whole `src/v1` tree currently holds -14 test fns in one file — the checkpoint-identity assertions that an emitter authority note cites -**by name** and that have never executed — and this census would make it two files. The real open -question is **resolution**, not population: the floor resolves its roots as one program -(`modules_resolved=3820`), so whether `v1` and `v2` module namespaces collide when unified is the -thing to measure. That measurement is a one-command experiment and it belongs to the owner above. - -## THE REPAIR IN THIS DOCUMENT DOES NOT FIX ARM A — the identity key is a location (2026-08-21) - -Measured, then confirmed from the code. Both halves are below, and the second one invalidates the -design above rather than qualifying it. - -### The measurement: `decl_file` is where the reference SITS, not where the type is DECLARED - -The calibration control ran over the 22-module import closure of `src/v2/compiler/01_tokenize.dag` — -the module that emits `src/v2_compiler_tokenize.rs`, where all 25 arm-A sites live. It **failed**, and -the failure is the finding: - -``` -1142 rows: 1134 Agrees, 8 DivergesWithExactIdentity, 0 IdentityUnavailable -arm x outcome: 1036 fallback|Agrees 98 resolved|Agrees 8 fallback|Diverges -``` - -Every one of the **111 `String` references took the fallback arm**; the resolved arm fired zero times -for this carrier. `decl_file` came back as the referencing module's own file in every case — -`std.types` → `dag/std/types.dag`, `v2.compiler.tokenize` → `src/v2/compiler/01_tokenize.dag`, -`v2.std.text` → `src/v2/std/text.dag`. The value `src/v2/std/text.dag` is produced **only** from -inside `v2.std.text` itself; no cross-module reference to `String` ever yields it. - -So the 8 divergences are **true by accident**: they are exactly the references that happen to sit -inside their own declaring module, where "the file I am in" and "the file it is declared in" are the -same string. - -This also dissolves the `decl_file` partition recorded earlier in this lane (41 `types.dag` / -6 `algebra.dag` / 4 `string_type.dag`, "zero counterexamples"). That was never a partition by -declaring module — it was a histogram of which file each occurrence sits in. Its zero counterexamples -were **structural**: a column that is a function of the row's own location cannot produce one. - -### The mechanism, named as the class DESIGN already names - -`v1.compiler.coercion` `type_reference_decl_file` projects the resolved node bound to the reference -and otherwise **falls back to the reference node's own `ident_span`**. Its authority note describes -that arm as firing *"when the reference IS the declaration"* — but the arm also fires whenever -inference is simply unavailable, with no way to tell the two apart from outside. That is: - -- **state-space conflation** (§5) — *the reference is its own declaration* and *inference was - unavailable* are different states with different remedies, collapsed into one arm; -- **the absorbing fallback** (§5) — a failure arm that **widens instead of refusing**. The note itself - warns that an empty identity yields no realization and silently renders structurally; the fallback - avoids the empty string by substituting a **wrong non-empty one**, which is strictly worse — it is - undetectable rather than merely absent, and its deficit frequency is zeroed by construction. - -Splitting that arm into two named outcomes is a **prerequisite** for the census, not a cleanup after -it: until they are split, 1036 of 1142 rows are `fallback` and cannot be scored correct or wrong. - -### Why this invalidates the repair above - -`type_realization_decision` **takes `decl_file` as a parameter** — - -``` -fn type_realization_decision(target: RenderTarget, dag_name: String, decl_file: String) -``` - -— and derives nothing declaration-shaped internally. Every production caller supplies it identically: -`v1.compiler.emit` and `v1.compiler.emit_rust` both pass `type_reference_decl_file(n: n)`, and -`lookup_checkpoint` is a thin derivation of the same call, so it inherits the key too. - -**Therefore routing the six renderers through the authority changes which function computes the answer -and leaves the basis of the answer identical.** The authority is not a second opinion about identity; -it is the same location-valued key wearing a better name. Arm A survives the repair with its 25 sites -intact, and an A/B counterfactual would return `converted=0` for a reason unrelated to whether routing -was the right idea. - -**One retraction this forces, on the finding that opened this lane.** It was reported that -`type_realization_decision` is *"present, correct, and unreachable"* for the text carrier in type -position. *Present* and *unreachable* hold. **Correct does not** — its logic was checked and its key -never was. A total function over a wrong key is not a correct authority that happens to be bypassed; -reaching it would have produced a confidently wrong answer instead of no answer. That word is what -made routing look sufficient, so it is retracted here rather than left standing as the premise. - -### Sequencing, replacing the step order above - -1. Split the fallback arm into two named outcomes — prerequisite. -2. Establish **why** `n.inferred` is not `Resolved` at these reference sites. This is likely the defect - itself rather than something to route around, and the fallback is what has been hiding it. -3. Only then ask whether routing to the authority is the right repair, on a key that means something. - -No production change is made on any of this yet. The A/B/C/D counterfactual crates are **not** built -on the current design, and no divergence count is published. - -**One thing deliberately not claimed:** `v2.compiler.tokenize` has exactly 25 `String` rows and there -are exactly 25 arm-A sites. The relation should be many-to-one, so equal counts are as consistent with -coincidence as with a join. Upgrading it requires a join on the **same occurrences**, not on both sets -having 25 members. - -## THE MEASUREMENT ABOVE IS PRE-RECONCILE — the subject claim is withdrawn (2026-08-21) - -Everything above about `0 of 111` is a real measurement of the **wrong phase**, and the sentence that -made it look like a production finding is withdrawn here rather than amended in place. - -**Verified first-hand in `v1.compiler.compile` `compile_to_resolved_with_options`:** - -``` -let frontend = front_end_sources(sources: sources) <- what the census walks -let norm = normalize_graph(graph: graph, ...) -let typed = reconcile_with_census_extra(graph: norm.graph, ...) -ResolvedPipelineResult { graph: typed, ... } -``` - -and `emit_artifact(typed: ResolvedGraph, artifact: Artifact)` consumes the **typed** graph. So -`front_end_sources` returns the **input** to normalization and inference — not the output. - -| | | -|---|---| -| **what `0 of 111` establishes** | type references in the **pre-reconcile** tree do not carry the inference answer that reconcile is responsible for producing | -| **what it does NOT establish** | that production emission takes the fallback at those sites | - -The second sentence was published in this document and in this lane's PR body. **Withdrawn.** - -This is the same shape as the other errors recorded in this lane: the instrument ran, the number is -correct, and it answers a *neighbouring* question. The earlier (b′) correction moved the census onto -`front_end_sources` because the v2 assembly was the wrong **tree** — it fixed the tree and missed the -**phase**. - -**What survives unconditionally:** the helper's shape defect. `type_reference_decl_file` cannot -distinguish *this node IS the declaration* from *this is a reference whose declaration was not -recovered here*, and in the second case returns the file containing the reference. That is -state-space conflation on any tree. What is **unmeasured** is whether the production typed graph -reaches the bad arm for the T2 population. The three-case cross-module trace recorded above is built -on the withdrawn premise and is likewise unconfirmed for production. - -### The corrected grain - -`compile_to_resolved` → `ResolvedGraph.modules` → `TypedModule.items` + `TypedModule.type_env`. - -Every `TypedModule` carries both, which is exactly the pair the census needs, and `05_emit_rust.dag` -**already imports `lookup_type_for`** — so this is an existing dependency, not a new abstraction. The -renderers visibly receive `env` while the current identity helper ignores it. - -At every occurrence, record all three answers **independently**, never letting one substitute for -another before comparison: - -| column | source | -|---|---| -| `inferred_declaration` | `n.inferred` → `Resolved` node, if present | -| `environment_declaration` | `lookup_type_for(m.type_env, n)`, if present | -| `legacy_file_key` | `type_reference_decl_file(n)` | - -Two constraints on the carrier. `ReferenceIsDeclaration` may be produced **only** when the resolved -declaration is demonstrably the same declaration node — never merely because inference was absent, -since that is the conflation under measurement and the instrument must not reproduce it. And the raw -reference-file fallback appears only as `LegacyFallbackUsed { reference_file }`, an observation of -current behaviour; it is **never** labelled declaration identity. - -### The four outcomes, fixed before the data - -- **A — inference resolves and agrees with the TypeEnv.** The identity key is *not* the active T2 - root; the first-line text shortcut is the primary defect, and routing proceeds after recalibration - on the typed tree. -- **B — inference absent but `lookup_type_for` resolves correctly.** The defect is narrower than - "inference is broken": `type_reference_decl_file` reads the **wrong carrier** for declaration - identity. Repair the helper to consume the TypeEnv or an already-resolved declaration; do **not** - modify global inference to populate a redundant field. Most plausible, given the renderers already - carry `env`. -- **C — both fail.** The binding/inference defect is genuinely upstream and becomes the primary - front; T2 is downstream. Refusal stays explicit — no falling back to reference file or bare - spelling. -- **D — they resolve different declarations.** Strongest possible finding: two resolution authorities - disagreeing inside the production typed tree. Realization work stops and that fork closes first. - -### T3 is not wholly downstream of an identity repair - -Recorded as a correction to this document's own framing. The collection paths are decided by separate -mechanisms — `rust_seed_host_container_base`, `node_is_keyed_collection`, `node_is_set_collection`, -`emit_map_type`, element-collection rendering, declared-structure record construction — which select -`Vec` / `HashMap` / `BTreeSet` / `PartialFunction` / `PointwisePower` independently of the checkpoint -decision. A correct declaration key is **necessary infrastructure** for a unified realization -authority, but will not by itself make `HashMap`↔`PartialFunction` or `BTreeSet`↔`PointwisePower` -converge. - -So **"52 sites are one root" reads from here on as one _non-confluence class_** — target -representation selected by position — **and not as one implementation repair.** Likely decomposition: -(I) exact reference→declaration identity, (II) T2 text-carrier base realization, (III) T3 parametric -collection realization, (IV) shared reference-layer projection. - -### What is blocked and what is not - -The **production routing edit** is blocked; the lane is not. Proceeding: refresh the 52-site -population against the current 399-error board, map each site to its type/value rendering consumer, -separate T2 from T3, and prepare the counterfactual comparison. **The six shortcuts are not deleted -until the identity census has a trustworthy key.** diff --git a/docs/plans/census-part-2-inference-design.md b/docs/plans/census-part-2-inference-design.md deleted file mode 100644 index 2371f9aec71..00000000000 --- a/docs/plans/census-part-2-inference-design.md +++ /dev/null @@ -1,213 +0,0 @@ -# Census part 2 — inference (and emit) census below resolve - -**Status: design only.** The build waits until census part 1 lane A lands: #13026, -deep-badger-684, reconciled here against its final head `9b7d2adc5c`. This doc does not mint a row -format. A's member already carries a stage, so part 2 **adds two arms to A's stage coproduct** and -adds no new field. Items marked **[lane C]** are a declared frontier, because lane C has not been -dispatched (§8). - -Governing sections: DESIGN §3 (one row format, not two), §3c (every declaration added here names its -consumer), §4b (rung honesty at a declared subject grain), §5 (a refusal is a row, never a widened -pass), §6 (name the instrument; never transcribe its output). - -## 1. What part 1 already establishes (reused, not restated) - -- **The population authority.** `v2.compiler.compile` `native_census_modules` maps an ingest to - `NativeCensusModule { module, path }`, one row per file that declares a module line. -- **The resolve decision.** `native_census_module_resolution` returns one of - `NativeCensusModuleFileRefused | NativeCensusModuleResolveRefused { first, rest, observation } | - NativeCensusModuleResolved`. The lane and the census both read this one decision. -- **The residual grain.** `NativeCensusResidualRow { module, path, chain }` has one row per failure - chain, so one module can produce several rows. The fatal is *derived* with - `v2.std.diagnostic diagnostics_fatal(chain)` and is never stored. -- **The file-refusal row.** `v2.compiler.native_test_vocabulary` `NativeTestFileRefusal { path, - module, head_reason, fatal_reason }`. The rendering in swift-lynx-592's #13005 groups by **fatal** - and treats **head** as an advisory field, because `parse_grammar_choice_overlap_residue` heads - every file. -- **Locus.** swift-lynx-592's path renders a chain's located fatal through - `lens_verdict_diagnostics_located_chain_text_in_spans`. Part 2 uses that renderer and adds no - second one. -- **Lane A (#13026).** The carrier is `v2.compiler.compile` `NativeCensusCauseMember - { stage: NativeCensusCauseStage, path, module, head_reason, … }`, grouped into - `NativeCensusCauseGroup { fatal_reason, members }` by `native_census_cause_groups_add`. The - partition check is `native_census_cause_partition_holds`. The stage coproduct is - `NativeCensusCauseFrontEnd | NativeCensusCauseResolve`. The instrument row is - `//gunbc/instruments:v2-native-census` (`V2NativeCensusProducer`), and it runs the driver verb - `census-resolve`. **Line:col is not part of A.** It waits on swift-lynx-592's path, which is held - on #12506, so it is a dependency of this doc (§8) and not an A field. - -## 2. Subject population - -The population is **every module that `native_census_module_resolution` answers -`NativeCensusModuleResolved`** over the same ingest part 1 censused. The ingest is the same source -roots at the same revision. Below that boundary, a module is a subject of: - -1. **infer**: `native_test_infer_resolved`, that is `infer_and_discharge`, over the module's - `ResolvedTree`. -2. **emit**: `compile_inferred_translate`, that is `v2.compiler.emit` `emit`, over the - `InferredTree` for the target the instrument row declares. Only modules that infer are subjects - of emit. - -The population is **derived, never re-enumerated**. Part 2 reads part 1's per-module decision and -filters to the resolved arm. A module that part 1 left in the file-refused or resolve-refused arms -is **not a part 2 subject**. It still appears in the receipt as an `UpstreamRefused` disposition -(§5), so the identity join over `native_census_modules` stays complete (DESIGN §5: completeness is -an identity join, not a count). - - -### 2a. The infer demand is shared (sharp-raven-357 ruling) - -The per-module infer step is **one demand subject in the native drain**, not a walk owned by this -census. The working name is `NativeInferCensusSubject { entry }`. It resolves via -`native_demand_resolved_tree`, then runs `native_test_infer_resolved`, and yields -`Inferred { InferredTree } | InferRefused { chains }`. Two folds read that one result: - -- This census reads the `InferRefused` arm: one row per chain, with cascade attribution (§4). -- M0 of the nominal-type plan (gunbc#13024) reads the `Inferred` arm: type declarations and - `as` sites. On `InferRefused`, M0 records each of the module's `as` sites as - `OperandTypeUndecided` with the infer cause, counted and never dropped. - -Each fold measures a different fact, so each keeps its own line kind (refusal rows vs -classification rows) in one stdout. They share the subject. They do not share a row format. - -The driver verb is **`census-infer`**. Widening `census-resolve` would make that word mean two -things, and §3 forbids a meaning fork. `census-resolve` stays part 1's verb. Its resolve rows and -`census-infer`'s rows are the one carrier of §5, distinguished by `stage`. - -## 3. What a refusal at each stage means - -| stage | refusal means | row stage | -|---|---|---| -| file (front end) | The file never parsed into a module. This is part 1's `NativeTestFileRefusal` and is not re-reported. | — | -| resolve | Some reference in the module did not bind. Part 1 reports this. | — | -| **infer** | The module resolved, but `infer_and_discharge` refused. Causes include no grounding derived, a fact-lookup miss, incoherent canonical grounding, a branch-shape mismatch, or an undischarged obligation. Each is a located `Diagnostic` from `v2.compiler.infer`. | `Infer` | -| **emit** | The module inferred, but the target realization refused. The refusal is the target's inability to realize a semantically legal program (DESIGN §4: the target never decides legality). | `Emit` | - -**Part 2 needs a discriminator that does not exist yet.** The native lane folds resolve and infer -into one `NativeTestStagePrepare` (`native_test_prepare_module` binds them). A census whose stage -column read `Prepare` would attribute an infer refusal to resolve, which is the fabricated -attribution that the `NativeLaneModuleResolution` comment already refuses at the context/resolve -split. So the census does **not** read the lane's `Prepare` verdict. It calls the two halves -separately, using part 1's resolved tree, and stamps the stage from which call refused. Whether -`NativeTestStagePrepare` should itself be split into `Resolve | Infer` is a lane-vocabulary change -that is out of scope for this doc. It is flagged here because the census makes that conflation -visible. - -An infer refusal is also **per chain**, as it is for resolve. If `infer_and_discharge` returns -several `NonEmptyDiagnostics`, the module yields one row per chain. If infer stops at the first -fatal, the row set for that module is a **lower bound**. The receipt says so with the same -`ObservationCompleteness` arm resolve uses (`ObservationIncomplete { reason }`), and the census does -not report it as complete. - -## 4. Cascade attribution - -Part 1 lane C collapses import cascades to their roots. If module M fails to resolve only because an -import target T was refused, the row belongs to T and M is a cascade member. - -Below resolve, the same structure applies along a different edge. **Infer runs per module over a -resolved tree whose imported declarations come from other modules.** When M's infer refuses at an -occurrence whose fact is owned by an imported declaration in module T, and T itself refused infer, -M's row is a **cascade** of T's root row. - -- **The attribution edge.** The refused fatal's locus resolves to a declaration. If that - declaration's owning module is not M and that owning module has its own infer row, M's row is - attributed `CascadeOf { root: }`. Otherwise it is `Root`. -- **The rule is reused, not reinvented.** Lane C's collapse rule (root = earliest refused module on - the import path) is applied with the infer-stage row table substituted for the resolve-stage one - **[lane C]**. Part 2 adds no second cascade authority. If lane C's rule cannot take a stage - parameter, that rule is the thing to generalize. -- **Cross-stage cascades are not collapsed.** A module whose import target refused at *resolve* is - not an infer subject at all (§2), so it never produces an infer row to attribute. -- **Emit cascades.** Emit runs over one module's inferred tree. A cascade at emit is only possible - if emit reads another module's emitted artifact. Until a measurement shows such an edge, every - emit row is `Root`. A cascade reported at emit is then a **red signal** that the edge exists. It - is never silently attributed. - -## 5. Receipt shape — A's carrier, two more stage arms - -``` -NativeCensusCauseStage - = NativeCensusCauseFrontEnd -- A - | NativeCensusCauseResolve -- A - | NativeCensusCauseInfer -- part 2 - | NativeCensusCauseEmit -- part 2 -NativeCensusCauseMember -- A's member, unchanged apart from the stage arms -NativeCensusCauseGroup { fatal_reason, members } -- A's grouping key, unchanged -attribution: Root | CascadeOf{root} -- [lane C] declared frontier -``` - -- **Grouping stays on `fatal_reason` alone,** as A has it. Stage is a member field and never a key. - This is the same rule A applies to head: a fatal shared across stages is one cause group with - members at several stages, and a reader filters on the field. Heads are carried and never tallied. -- **The partition check is A's.** `native_census_cause_partition_holds` covers infer and emit - members without change. A member that lands in no group, or in two, breaks the partition at every - stage. -- **Per-module disposition** is one coproduct per module, so the join over `native_census_modules` - is total: `UpstreamRefused { stage: FrontEnd | Resolve }` | `InferRefused` | `EmitRefused` | - `Emitted`. Rows hang off the refused arms. -- **Completeness** is a coproduct, as in part 1 (`Xl2CensusCompleteness` pattern). An incomplete - infer observation or an unjoinable cascade root each refuses completeness. Neither is a zero. -- **No parallel carrier.** An `InferCensusRow` beside A's member would be the §3 fork. Adding - arms makes every exhaustive match over `NativeCensusCauseStage` fail to compile until it handles - them, which is the enrollment we want. - -## 6. Instrument label — extend `v2-native-census`, no second label - -Part 2 is **the same label**: `//gunbc/instruments:v2-native-census`. The label measures one fact, -the native census's cause groups over the self-host roots. Part 2 extends the stages the census -reaches; it does not measure a different fact. A second label would give one measurement two -routes, which DESIGN's "Building & checks" forbids (the `--self-host` precedent A also cites). - -- **Realization.** `V2NativeCensusProducer` switches from running `census-resolve` to running - `census-infer` (§2a). That verb reaches infer and emit through the shared demand subject and - prints A's `cause_group` lines, whose members now include infer and emit members. `census-resolve` - stays as a verb for part 1's grain. It is a realization, not a peer route, so this is not a - second label. -- **Exit codes are A's, unchanged.** 0: the partition held. 1: the partition broke. 2: the run did - not complete. Refusals are the census's data and do not change the exit code. Completeness below - complete is reported on the status line, as A reports `cause_groups` and `partition_holds`. -- **Emit target.** The target is the instrument's own fact (the row names it), never a CLI option, - so that one label cannot quietly measure a different target. -- **Cost.** This is a whole-tree run: CI or `ctrl-build --remote`, never in a session. The infer - pass reuses the single `ResolutionContext` per ingest (#11401). Inferring per module must not - rebuild that context. - -## 7. Discriminating controls - -Each control must go red when the behavior it guards is wrong. These are fixtures handed to the -compiler (DESIGN §4b: a compiler's probes are invalid programs). - -1. **Stage discrimination.** In a two-module fixture, A resolves and fails infer (a branch-shape - mismatch), and B fails resolve. The expected rows are A with `Infer` and B with `Resolve`. - Rendering both as `Prepare`, or giving A `Resolve`, must red. -2. **Per-chain grain.** One module with two independent infer refusals yields two rows. Folding - them to one must red. If infer cannot produce two, the control instead asserts - `ObservationIncomplete`. -3. **Cascade.** T fails infer at a declaration that M uses, and M's only infer failure is at that - use. Expected: M is `CascadeOf T` and T is `Root`. Removing T's defect must make **both** rows - disappear. That is the route assertion, not only the answer (DESIGN §3 pairing). -4. **Non-cascade.** M has its own infer defect and also imports a refused T. M's own row must be - `Root`. This catches over-collapse. -5. **Emit.** A module that infers but uses a construct the target refuses yields one `Emit` row. - The positive control is a module that emits, with disposition `Emitted` and no rows. -6. **Join completeness.** Delete one module from the disposition table. The identity join must - refuse, and a count comparison must not pass it. -7. **Inhabitance.** One control runs the real whole-tree route (the instrument row itself, on CI) - and asserts that at least one `Infer`-stage row was produced **by the infer call** and not by a - supplied fixture. Removing the infer call from the route must red it. - -## 8. Open dependencies - -- **Line:col on members.** swift-lynx-592 owns this path (#13005 follow-up), and it is held on - #12506. A's member shape takes the field once that lands. Part 2's infer and emit members inherit - it with no second locus path. Until then, a member's position is whatever its chain's diagnostics - carry, rendered by nothing new. -- **[lane C], declared frontier.** The cascade carrier (`Root | CascadeOf`) and its collapse rule - belong to lane C, which has not been dispatched. Its trigger is lane C landing a carrier that takes - the stage as a parameter. If C's rule is resolve-only, it is generalized in C's module, not copied - here. Until then, part 2 members carry no attribution, and the census reports `attribution - unobserved` as a completeness refusal, never as `Root`. -- **The shared infer subject** (§2a), with tidy-koi-264's M0. Whichever lane builds first owns the - subject and the `census-infer` verb arm. -- **`NativeTestStagePrepare` split.** This is a lane-vocabulary question (§3) that this doc raises - and does not decide. diff --git a/docs/plans/ci-throughput-subject-census.md b/docs/plans/ci-throughput-subject-census.md deleted file mode 100644 index 7ebf77fc75c..00000000000 --- a/docs/plans/ci-throughput-subject-census.md +++ /dev/null @@ -1,190 +0,0 @@ -# CI throughput comparability — a whole-path census of the current tree - -Stage 3 of the throughput-qualified convergence design, which named the CI side a frontier rather than -a bound subject and required its census to read the CURRENT tree. **That parent does not exist in this -tree: it is open as gunbc#11540 and this census must land with or after it** — the link below resolves -only once that PR lands, and naming it here is the declared frontier's trigger (§3c) rather than a -citation that silently dangles: [throughput-qualified-convergence-design.md](throughput-qualified-convergence-design.md). -That instruction was specific and is honoured literally here: `docs/plans/ci-humming.md` describes the -June slot/cgroup program and an older control/apply architecture, and deriving axes from it would -model a fleet that has since been replaced. - -This is a census, not a design. It answers one question — **which properties, if they change, make a -prior CI rate inapplicable** — and it answers it by naming carriers that exist today. - -## The finding that motivates the rest - -**No fold anywhere derives an aggregate CI rate** — completed homogeneous work per unit time, bound to -an admitted subject, a declared protocol and an observed realization. Durations DO exist and are -derived (`gunbc.superseded_run_starvation_census` `timestamp_diff_seconds`); a rate does not. - -The runner modules under `dag/gunbc/runner/` model slot identity, width admission, microVM sizing, -placement, connectivity repair, and receipts for nearly every step of an attempt's life. The query -behind the claims below is named rather than its output copied (§6): a case-insensitive search of -those modules for a rate or duration vocabulary — `throughput`, `per_hour`, `jobs_per`, `duration`, -`elapsed`, `wall_clock` — re-derives what this census read, and a reader who runs it today sees -whatever is true today rather than what was true when this was written. **Naming the producer instead -of its count is not a formality here**: an earlier revision of this census copied a hit count out of a -narrower search and used it as the evidence for a claim this document has since retracted (see the -corrections below), so the copied number outlived the search that produced it by exactly the margin -that makes transcription dangerous. - -What that search surfaces in the runner modules is not a rate: `runner_unit_file` `InvocationLocalCargo { jobs_per_slot }` is a **cargo invocation -parameter** — how many compile jobs one slot may spawn — and `runner_slot_allocation` carries a prose -aside about oversubscription degrading throughput. Both are inputs to a configuration; neither is an -observation of delivered work per unit time. - -So the CI side is in exactly the state the parent design describes for serving: a configuration -derived with care, converged, verified, and never put to the question it was chosen to answer. - -## Candidate axes, each with its carrier in the current tree - -An axis is a property whose change makes a prior number inapplicable. Each row names where the -property lives today, so the eventual subject type is assembled from existing authorities rather than -re-coined. - -| candidate axis | carrier in the tree today | -|---|---| -| host class | `gunbc.runner.runner_host_tpm_observation` and the Mt Collins extdeps briefs distinguish host populations; no single host-class carrier joins them yet | -| requested and observed slot width | `gunbc.runner_capacity_plan` `RunnerCapacityHostPlan { requested_width, observed_width }` | -| microVM shape | `gunbc.runner.runner_microvm` `RunnerMicroVmSize { vcpu_count, memory }`, with `GuestMemoryRequest` and `GuestResources` beside it | -| guest image | `gunbc.runner.runner_microvm` `RunnerGuestImage { distribution, release_series, arch, runner }` | -| actions-runner revision | `RunnerGuestImage.runner` (`ActionsRunnerRelease`) | -| cores per slot / jobs per slot | `gunbc.runner_slot_allocation` `gunbc_runner_cores_per_slot`, consumed by `runner_unit_file` `InvocationLocalCargo` | -| memory envelope per slot | `gunbc.memory_per_vcpu_convention`, with the microVM sizing relation | -| kernel and rootfs | `gunbc.runner.runner_microvm` config resolution (`kernel_path`, `rootfs_path`) | -| toolchain revision | not carried by the runner subsystem; the closest authority is the repo's own toolchain coherence module | -| cache topology and cache state | **no carrier** — see below | -| exact Work and source-tree identity | `gunbc.compute.work_request` `WorkOperation` is the nearest home; the join from a CI attempt to the exact tree it built is not modelled | -| dispatch policy | `gunbc.runner_attempt_launch` and the broker modules carry attempt admission; no policy-identity carrier | - -## What the census found missing, which is the useful half - -**1. Cache state has no carrier, and it is the axis most able to produce a meaningless comparison.** -`sccache` appears in the build environment and in `extdeps.cache` as a general notion, but nothing -models whether a given attempt ran against a warm or cold cache, or against which cache population. A -cold-cache run and a warm-cache run over the same tree on the same hardware differ by a large factor, -so without this axis two honest readings can disagree wildly and neither is wrong. Any CI floor set -before this exists is a floor over an unstated variable. - -**2. The actions-runner revision already diverges across the fleet, and it is declared.** -`runner_microvm` states that the guest image pins 2.337.0 while the fleet's host slots pin 2.336.0 — -found in review rather than by anything in the repository, and resolved forward deliberately. This is -a live specimen of exactly what an axis is for: two attempts on "the same fleet" can differ on runner -revision depending on which population served them, so a rate compared across that boundary is -comparing two subjects. - -**3. The rate INPUTS largely exist and nothing folds them — which an earlier revision of this census -missed, and the correction makes the CI probe cheaper rather than more expensive.** -`extdeps.github.workflow_runs` `WorkflowJobRun` carries `created_at`, `started_at`, `completed_at`, -`run_attempt`, `labels`, `status` and `conclusion`, and `gunbc.public_workload_census` already holds -observed execution rows with those timestamps populated. So the claim to make is narrower and sharper -than "nothing measures a rate": **every current consumer uses those timestamps as ORDERING -PREDICATES, never as an interval.** `gunbc.run_disposition` asks whether `run_started_at` is strictly -later than `created_at` to prove execution began; `gunbc.pr_base_freshness` asks whether a base commit -landed after a run started — both use the timestamps as ordering predicates rather than intervals. -That is a fact about THOSE TWO consumers and not about the tree, which is where an earlier revision of -this census overreached. - -**And the sentence above was itself wrong when first written, which is the correction worth reading.** -A revision of this census asserted that "neither subtracts; no fold in the tree derives a duration, a -queue delay or a rate". That is false. `gunbc.superseded_run_starvation_census` declares -`timestamp_diff_seconds(end, start) -> DurationComputed { duration_seconds: Second }` and uses it for -dead time under hold and for group hold after jobs — real timestamp arithmetic into a typed -`std.measure` quantity, over live workflow-run and job API calls with paging and terminal -classification. It is an executing precedent for acquisition and interval derivation, not a declared -shape. The claim was over-stated from an incomplete grep and asserted as established, which is the -§4d failure of asserting as deduced what was only inferred. - -The surviving claim is narrower and is the one that matters: **no fold derives an aggregate RATE** — -completed homogeneous work per unit time, bound to an admitted subject, a declared protocol and an -observed realization. Durations exist; a rate does not. - -Two consequences. A CI rate needs a fold over a substrate that exists, with an interval precedent -already executing — a materially smaller job than this census first implied. And because that -substrate is a census of REAL production jobs, some CI rate questions can be answered without a -synthetic probe at all, as a reading over observed work carrying `WindowSharedWithDeclaredTraffic` -rather than an isolated window: a weaker standing, much cheaper, and for a trend possibly the right -instrument. - -What remains genuinely absent is the **join from an attempt to the exact Work identity and source tree -it built**. A rate needs an operation ledger over homogeneous work (a one-minute no-op and a -fifteen-minute clean build are not interchangeable units), and job labels plus a run attempt do not -establish what was compiled. That, not the timestamps, is the precondition still missing. -(Correction raised by the side-chat review, 2026-09-17.) - -**4. Host class is implied by module paths rather than carried.** Mt Collins facts live in extdeps -briefs and the runner modules observe individual hosts; nothing names "this host belongs to class C" -in a way a measurement could cite. - -## Corrections after review (2026-09-17) - -**The census searched the wrong closure.** It read the modules under `dag/gunbc/runner/` and drew a -conclusion about CI, while the measurement substrate lives largely outside that subtree: -`extdeps.github.workflow_runs::WorkflowJobRun` (exact run attempt, status, conclusion, observed runner -labels, three timestamps, with exact-attempt and latest-attempt listings), -`gunbc.public_workload_census::ObservedExecution` (which joins a job to repository, workflow path, -workflow-blob SHA at the observed head, head SHA and an observed correctness standing — much closer to -a homogeneous operation ledger than this census allowed), and -`gunbc.superseded_run_starvation_census` (live API acquisition plus the interval derivation above). A -subtree is not a closure, and "I searched `runner/`" does not ground a claim about CI. - -**A second run-record authority already exists.** `extdeps.github.actions_runs` is another -workflow-runs projection with run timestamps and a `gh run list` transport, and `workflow_runs` -already names the pair as a meaning fork awaiting consolidation. A CI probe must CHOOSE or consolidate -that authority; introducing a third run record would make the fork three-way (§3). - -**The axis table conflates three layers.** Its test — does changing this make a prior rate -inapplicable — identifies a comparability dimension but does not decide where the dimension lives, and -the parent design already partitions subject / protocol / realization / window. Putting exact Work in -the subject makes the runner a new subject every commit; putting cache POPULATION in the subject makes -it change on every fill or eviction. Both are comparability facts and neither is stable configuration. - -| dimension | layer | -|---|---| -| host hardware class; configured slot width and resource envelope | subject | -| runner agent, guest environment, toolchain identities | subject | -| cache implementation, namespace/topology, configured policy | subject | -| exact Work, source tree, workflow definition | protocol (operation identity) | -| offered concurrency, stopping rule, required warm/cold condition | protocol | -| exact cache population encountered; exact host, VM, runner and attempt population | realization | -| ambient or foreign work during the window | window/isolation standing | - -So the cache finding splits three ways rather than being one missing axis: topology and policy are -subject, the required condition is protocol, the observed hit state is realization. - -**The Work-identity gap is narrower than stated, and the repair is different.** -`gunbc.runner_attempt_launch::plan_attempt_launch` already RECEIVES exact Work, Demand and Offer -identities and calls `grant_authorizes_reservation` before planning the VM — so Work identity reaches -the launch planner. The loss is downstream: `LaunchAuthorized` retains the coarse `GrantAuthorization` -verdict and not the admitted identities, so later receipts cannot rejoin the attempt to the exact Work. -The missing piece is a BRIDGE — admitted fabric Work identity → launched microVM attempt → observed -`WorkflowJobRun` and terminal receipt — and the repair carries the existing admitted identity forward -rather than coining a second CI-work identity. - -**Two "existing axes" are locators, not execution identities.** `RunnerGuestImage` carries -distribution, release series, architecture and actions-runner release; none of that content-identifies -the guest rootfs bytes, whatever its own comment calls itself. The launch planner likewise takes -`firecracker_binary`, `kernel_source` and `rootfs_source` as paths, and one path can name different -bytes across hosts or across time. So exact rootfs content identity, exact kernel content identity, -Firecracker/jailer release identity, and a host execution class covering host kernel, CPU class/policy -and storage realization are all REMAINING GAPS, not settled axes. - -## What this census does NOT do - -It does not propose the CI subject type. The gaps above are modelling obligations -(DESIGN §6) rather than conformance rows (§3b), because a row whose home does not exist is an -obligation and not a domain — and coining a subject over carriers that do not exist would be the -re-invention DESIGN §2's test names. The order is: close the cache-state and work-identity gaps, then -assemble the subject from carriers that exist, then probe. - -It also does not rank hosts, shapes or widths. The moment anything chooses among configurations, -`std.decision` is the home and §3d binds. - -## Standing - -A census, not enrolled in DESIGN, governing nothing. Its consumer is stage 3 of the parent design — -which is gunbc#11540 and is NOT YET IN THE TREE, so this document's only consumer is a declared -frontier and its merge dependency is that PR, -and its finding — that CI models its configuration thoroughly and its rate not at all — is the same -finding the parent made for serving, arrived at independently on the other side of the fleet. diff --git a/docs/plans/cli-invocation-emission-design.md b/docs/plans/cli-invocation-emission-design.md deleted file mode 100644 index 5f71cedd681..00000000000 --- a/docs/plans/cli-invocation-emission-design.md +++ /dev/null @@ -1,1418 +0,0 @@ -# CLI invocation emission — the tree is the authority, argv is emitted - -**Status: DESIGN NOTE PLUS ITS FIRST CUT.** An earlier revision read "No code lands from this -note yet" while the branch already carried a carrier, a seed realization, witnesses and one -migrated production site — the stale-citation class in the lane's own authority. What has landed -is stated at each wave below; what has not is stated as not. Design-note-first, per the -hollow-alias precedent. - -**Lane scope, narrowed (raised by the `shell → dag` session, accepted):** this note governs -**CLI-backed host effects only** — effects realized as a process invoked with an argument vector. -It is *not* a universal effect model: a Redfish/REST path reaches a modeled request and must reach -**no** `JqInvocation`, `CliSurface`, `ProcessArgvExpansion` or shell target. Treating every host -effect as a CLI invocation would repeat at the effect layer the mistake this lane removes at the -argv layer — one realization mistaken for the interface. A negative falsifier witness proving a -REST path reaches none of these carriers is **owed by this lane and not yet built.** - -**Lane:** supersedes the Lane B destination of `gunbc.plans.transport_argv_anemia_dissolution`. -Its *diagnosis* stands — "the model is the request; the curl argv is one derived realization of -it" — and its Lane A (HTTP → `transport rest`) is substantially right. Its Lane B destination -terminates at typed `extdeps/tools/*` operations and `ArgvCommand` builders, both still containing -literal argv — one layer short. - ---- - -## 1. The defect - -An invocation is authored today as a list of opaque strings: - -``` -argv: ["jq", "-er", "--argjson", "index", "{index}", ".data[0] | keys | .[$index]"] -``` - -Three independent facts are welded into that one list: - -1. **what jq is** — a program operand, a raw-output mode, typed variable bindings, an exit policy; -2. **how we reached it** — locally, or over `sshpass`+`ssh` to a BMC; -3. **what we asked it** — an OpenBMC-specific projection over fan PID zones. - -Welded, every new question is a new hand-typed list re-deciding all three. A flag nobody -modeled is trivially droppable — which is how `--fail` went missing from eleven curl sites and -made a 401 read as success. - -### The census (measured in-tree, not inherited from the plan) - -| fact | count | -|---|---| -| `argv:` lines | 597 across 129 files | -| `transport shell` blocks | 250 | -| `transport rest` blocks | 95 | -| `operation` declarations | 322 | -| operations declaring `success: Bool from "exit_success"` | 173 | -| operations declaring `exit_code: Int from "exit_code"` | 69 | -| `shell_quote` call sites | 36 | -| argv lines with a literal program head | 282 | -| …of those, heads that are **wrappers**, not tools | 78 | -| `sh -c` / `bash -c` embedded-language leaves | 14 | - -**Every count in this section is a measurement of one commit, `5c106b8a30` (2026-09-04), and is -not re-derived as the tree moves.** It is recorded that way deliberately rather than hand-refreshed: -a number edited to match today's tree is unreachable from whatever produced it and rots silently at -both ends (§6). Re-derive against a named ref before relying on any figure here; what the section -argues does not depend on the exact values. - -Two precision bounds: 315 of 597 argv lines have computed heads and are unclassifiable by -this method, so 78 is a **lower bound** on wrapper-hidden tools; the metacharacter count is -a text match, not a parse. - -**The plan's own census is stale by ~2.3×** — it records 262 argv lines across 37 files, and -148/51 for the success/exit_code split. Both measured above. - -### The census cannot currently be taken - -Counting by argv head yields `git` 43, `sshpass` 40, `sh` 13, `sudo` 9, `env` 9. The middle -four are wrappers, not tools; the real program sits at some positional offset. All seventeen -jq-over-ssh sites count as `sshpass`, not jq. - -**Moved since that measurement: #11061 binds the 40 `sshpass` heads to the computed -`extdeps.exec.command` `sshpass_binary_name`**, in `extdeps.bmc.openbmc_password_ssh_transport` and -`extdeps.ssh.password_session`. The invocations are unchanged — those operations still exec sshpass, -so the tool count of 40 still holds — but they have crossed from the literal-head population into -the computed-head one, so a re-derivation by matching the literal now answers zero for sshpass. That -is the shape this section is arguing about: the count was never wrong, the METHOD is what the corpus -moved out from under. - -This is itself the argument: tool identity is a positional accident in a flat string array, so -the corpus cannot answer "what do we invoke" without knowing `argv[11]` is the real program -because `argv[0..10]` happened to be an ssh prefix. - ---- - -## 2. Facts, separated by kind - -### 2a. Substrate facts (read from source) - -- `v2.std.compilers.target_model` `TargetModel` is target-agnostic in its bundle/lex/spellings - shape, but **text-specialized** in three fields: `binding_spellings: Map`, - `token_class_emit_transforms: Map String>`, `authority_source_text: String`. -- The emit carrier is `v2.std.compilers.target_model` `TargetText { source: String }` — one - String→atom introduction, concatenation as the only join, whole-value render as the only exit. -- `v2.extdeps.formats.sql_target` proves a **non-programming-language** target registers through - `target_model_make_with_emit_transforms`. It does **not** prove carrier independence: its - serialization node is still source-text shaped. -- The public emit seam commits to `Medium`. `Medium` itself is generic; the text lock - is in `TargetModel`, `serialize_target` and `emit`, not in `Medium`. -- `dag/extdeps/languages/` holds 32 language authorities. **None is a CLI-invocation authority.** -- `extdeps.exec.command` renders argv as `join(map(argv, shell_quote), " ")`, and is anchored to - the **SSH** manual while defining `curl`, `mkdir`, `tar`, `make`, `sed`, `cp` builders — plainly - a cross-tool realization utility, not the authority for those tools' semantics. -- `v2.std.host_transport` `ProcessInvocation` / `InvocationArg` exist with 72 consumer references. -- `v2.extdeps.posix` `Command`, `PosixArgument`, `SpawnProcessRequest` exist with **zero consumers - outside the declaring module**. -- `extdeps.posix.shell_command_language` exists as its own subject. -- `gunbc.command_runner` carries `command_runner_scaffold: Disposition = Scaffold`. - -### 2b. Upstream authority facts (cited) - -- **IEEE Std 1003.1-2024, Base Specifications Issue 8, §12** — 14 numbered Utility Syntax - Guidelines; `--` ends options; options precede operands (G9); `-` denotes stdin (G13); - option grouping behind one `-`; option-arguments as separate tokens. -- **POSIX `sed`** declares an explicit deviation: the relative order of `-e` and `-f` is - significant. So POSIX is the *base* grammar, never the whole tool grammar. -- **RFC 4254 §6.5** — the SSH `exec` channel request carries a single `command` field of type - **string**. No argv vector exists in the request, so argv structure cannot cross that boundary - as structure. The protocol does *not* define that string as shell source; shell re-parse is an - OpenSSH implementation fact, as `extdeps.ssh.session` already records. -- **IEEE Std 1003.1 Shell Command Language** — grounds the shell leg. Already cited by - `extdeps.posix.shell_command_language`, whose own note reserves it for exactly this use: - a POSIX-shell target "registers against THIS declaration and needs no new grounding." -- **jq manual** (`jqlang.org/manual/`) — `--raw-output`/`-r`, `--exit-status`/`-e`, - `--argjson name JSON-text`, `--arg name value`, `--` terminator, file operands vs stdin. -- **RFC 7950 (YANG 1.1)** — abstract data model held separate from concrete encoding; one model - renders as NETCONF XML or RESTCONF JSON. Cisco IOS XR ships "Model-Driven CLI" on that basis. - Prior art: the networking industry ran this exact migration from CLI-scraping to modeled trees. -- **PowerShell 7.6** — parameters bind to typed .NET objects; parameter metadata (`Position`, - `Required`, `Accepts multiple values`, `Accepts pipeline input`) is machine-readable. Evidence - that a per-tool option table can be data rather than prose. - -### 2c. Domain facts discovered during design (these constrain the model) - -- `extdeps.bmc.openbmc_fan_control` declares `OpenBmcSensorValueAbsent` — a **third state** - beside Refused and Observed. `openbmc_sensor_integer_projection_result` decodes empty stdout - to it, and `dag/test/claim/bmc_typed_operations_witness_test.dag` witnesses it. - **jq producing no output is a modeled value, not an error.** Any exit partition folding - "no result" into failure breaks working, witnessed code. -- `gunbc.host_effect_realize` `bmcweb_token_extraction_verdict` refuses blank stdout on its own. - So at that site `-e` was never the mechanism; it is transport *loudness*. -- Of the four jq operations in `openbmc_fan_control`, **all are live**: - `ProjectFanConfig`, `ObjectMapperServiceCount`, `ObjectMapperServiceAt`, `SensorIntegerValue`. - Only `extdeps.tools.jq` `jq.Json.ExtractRaw` is zero-consumer tree-wide. - ---- - -## 3. The layering - -A layer is only real if something varies at it. Checked against three CLI families: - -| layer | owns | POSIX | PowerShell | Cisco | -|---|---|---|---|---| -| **intent** | the tree: subject, named params, operands | *identical across all three* | -| **vocabulary** | which params exist, types, cardinality, positional, exclusivity | tool's manual | cmdlet parameter metadata | YANG module | -| **surface grammar** | how a bound param renders | `-a`, `--long=v`, bundling, `--` | `-Name value` / `-Name:value` | keyword paths in a mode hierarchy | -| **value encoding** | how a typed value becomes a param value | bytes; nested languages embedded | .NET objects — no serialization | typed per YANG leaf → XML/JSON | -| **carrier** | what the emitted thing physically is | argv vector *or* shell text | in-process object pipeline | NETCONF RPC document | -| **transport** | where it runs | local exec, ssh, enable-mode session, REST | - -The families agree at *intent* and disagree at *surface grammar*, *value encoding* and -*carrier* — so intent is the authority and everything below is a rendering. - -### Two consequences - -**Carrier ≠ surface grammar.** Emitting to shell text when the destination is `execve` invents -a quoting problem that does not exist. `shell_quote` at 36 sites is the current mitigation for -that conflation, not a requirement. - -**Value encoding nests.** A jq program is a language embedded as a leaf in a POSIX argument; so -is `sh -c` — 14 sites. Such a leaf should be a typed sub-tree, not an opaque String. Modeling -jq's *expression* language is a separate vertical from its *invocation* structure and is not a -prerequisite here. - ---- - -## 4. The model - -``` -JqInvocation semantic axes only; no flag spellings reachable - | - | jq cited option rows + shared CLI grammar rows - v -CliInvocationTree options grouped with their values; operands; terminator - | - | emission — roles erased here, correctly - v -List argument boundaries structural; no quoting - | - +-- local: tool resolution -> extdeps.posix Command -> spawn - | - +-- ssh: -> shell simple-command tree -> shell text -> RFC 4254 command string -``` - -### 4a. Semantic layer - -``` -type JqProgram { source: NonEmptyStr } // opaque, upstream-grounded, for now - -type JqBinding - = JqJsonBinding { name: JqBindingName, value: Json } - | JqTextBinding { name: JqBindingName, value: String } - -// JqBindingName is jq-owned, not a bare NonEmptyStr. Duplicate-name policy is declared, -// not left to jq's last-wins behaviour: the admission fold refuses a repeated name. - -type JqInput - = JqInputFile { path: FilePath } - | JqInputStdin { content: String } - -type JqOutputEncoding = JqJsonOutput | JqRawOutput // -r writes STRING results raw; - // non-string results stay JSON-formatted - -type JqExitPolicy = JqProgramExit | JqLastResultTruthiness - -type JqInvocation { - program: JqProgram - bindings: List - input: JqInput - output_encoding: JqOutputEncoding - exit_policy: JqExitPolicy -} -``` - -`JqInput` is where stdin stops pretending to be an option: `JqInputFile` produces one file -operand, `JqInputStdin` produces **no operand** and sets process stdin. Load-bearing: the four -local sites are stdin-fed, the seventeen remote ones file-operand. - -### 4b. Observation: two orthogonal axes, sealed - -Two earlier revisions of this section put on one axis a fact that lives on two. - -**Termination and output presence are orthogonal.** The live `SensorIntegerValue` specimen holds -both at once: exit status 0 **and** zero results. It runs `-r` without `-e`, its filter takes the -`empty` branch for a non-number, and exits zero with empty stdout. So -`JqExitZero | JqProducedNoResult` is a product, not a coproduct; written as a sum, the live case -is unrepresentable. - -Verified against `jqlang.org/manual/`: - -``` -default exit 0 when the program runs successfully, REGARDLESS of output ---exit-status / -e exit 0 last output neither false nor null - exit 1 last output false or null - exit 4 no valid result was ever produced -halt_error program-chosen, default 5 usage 2 compile error 3 -``` - -``` -type JqExitDisposition - = JqExitZero - | JqExitFalseOrNullUnderExitStatus // JqLastResultExit only - | JqExitNoResultUnderExitStatus // JqLastResultExit only - | JqExitRefused { code: Int } - -type JqOutputPresence - = JqOutputAbsent - | JqOutputPresent { stdout: String } - -type JqObservation sole_constructor { - process: ProcessObservation // raw evidence, retained - exit: JqExitDisposition // decoded projection, bound to it - output: JqOutputPresence - stderr: String -} -``` - -| case | exit disposition | output presence | -|---|---|---| -| `SensorIntegerValue`, numeric | `JqExitZero` | `JqOutputPresent` | -| `SensorIntegerValue`, non-numeric | `JqExitZero` | **`JqOutputAbsent`** | -| `ObjectMapperServiceAt`, found | `JqExitZero` | `JqOutputPresent` | -| truthiness policy, no result | `JqExitNoResultUnderExitStatus` | `JqOutputAbsent` | -| jq execution error | `JqExitRefused` | independently observed | - -**The decoder is policy-indexed and is the only mint.** -`decode_jq_observation(invocation, process) -> JqObservation` — not a global `exit_code -> JqExit` -lookup, because codes 1 and 4 mean nothing under `JqProgramExit`, and zero results under -`JqProgramExit` still produce code zero. - -**Sealing is required, not decorative.** Rejecting `success: Bool` beside `exit_code` removes one -writable contradiction; a freely constructible `JqObservation` recreates it under richer names — -`{ exit: JqExitZero, exit_code: 4 }` would be authorable. `sole_constructor`, minted only by the -policy-aware decoder, closes it: the raw process observation is retained as evidence and the -decoded facts are a projection **bound to it**, never two caller-authored fields. This is the first -concrete place where a construction wall is available today. - -**A stdout `String` cannot generically prove "exactly one nonempty raw string".** jq emits a -*stream*; under `--raw-output` a raw string may contain newlines, which is why `--raw-output0` -exists ("Like `-r` but jq will print NUL instead of newline after each output"). `foo\nbar\n` is -ambiguous between one string with a newline and two results. This does not block the first -vertical — an integer has an unambiguous textual grammar — but **this note promises no generic -`decode_exactly_one_nonempty_string`.** Lifting that requires one of: a single JSON envelope -parsed as JSON; `--raw-output0` once jq version/capability is modeled; or a program collecting its -result into a one-result container the domain decoder checks. - -**No `success: Bool`.** The seed derives it as exactly `exit_code == 0` — one fact twice. -`extdeps.git.git` `ls_remote_reports_termination_not_success_note` establishes this; 173 -operations still carry the Bool. - -### 4c. Two-level row derivation — the keystone - -Both the **selection** of an option and its **spelling** must be row-derived: - -``` -JqLastResultTruthiness -> JqCliOptionExitStatus (prevents omission) -JqCliOptionExitStatus -> canonical "--exit-status", alias "-e" (prevents bespoke spelling) - -JqJsonBinding { name, value } - -> [ argument "--argjson", argument emit(name), argument emit(value, Json) ] -``` - -If only the second level is data-driven while a function still says "when policy is X, append -option Y," the droppable-option class survives one layer down — the difference between this design -and centralizing 597 literals into N helper functions. - -The precise statement replacing "`-e` is part of the contract, not a flag": - -> `JqLastResultTruthiness` is part of the contract. `-e` and `--exit-status` are concrete -> spellings of that property in the jq CLI realization. - -### 4d. Grammar decomposition - -``` -shared CLI grammar option, option-argument, operand, repeated group, - terminator, short-option cluster, long spelling, - joined vs separate value - -+ per-tool cited rows jq options and operand order - sed's -e/-f ordering exception - git subcommand grammar - tar mode-word grammar - ssh -o forms -``` - -Named `CliSyntax`, not `PosixCli`: real tools deviate (POSIX `sed` provably), and the name must -not assert conformance the corpus cannot claim. - -### 4e. The structured CLI tree — why roles live here and not in the arg vector - -``` -type CliElement - = CliOption { option: CliOptionRef, values: List } - | CliOperand { value: CliValue } - | CliOptionTerminator -``` - -An option's values cannot detach from their option: `--argjson`, its binding name and its JSON -value stay one subtree until the grammar lowers them. - -**Role and provenance are orthogonal axes and must not share a coproduct.** A workspace path may -be an option value *or* an operand; a produced artifact either; a literal an option, subcommand, -option value, mode word or operand. Fusing them makes `OptionValue ∧ WorkspacePath` -unrepresentable, and expanding to `WorkspacePathOptionValue`, `WorkspacePathOperand`, … is the -combinatorial growth the grammar exists to remove. - -**`CliOption { values: List }` is too permissive as a *public* carrier.** Freely -constructible, it admits `--argjson` with zero, one or three values; a flag with a value; a -non-repeatable option twice; illegal order; another tool's option; operands before options where -forbidden. So the cited row must carry option identity, canonical spelling, aliases, argument -arity, repeatability, placement and joining discipline — and the emitter accepts only a -**normalized, admitted** tree: - -``` -type CliOptionSchema { - option: CliOptionRef - arguments: CliArgumentSchema - repeatability: CliOptionRepeatability - spelling: CliCanonicalSpelling - placement: CliOptionPlacement -} - -type AdmittedCliInvocation { tool: CliToolRef, options: List, operands: List } -``` - -Domain callers receive **no constructor** for `AdmittedCliOptionUse`. The jq semantic-to-syntax -projection selects the row and supplies values; the admission fold checks the schema — the -`sole_constructor` + `admit_callers` shape that sealed `TransportScript`, and what raises the rung -for the migrated path (§9). - -Roles are then **deliberately erased** at emission, because the OS receives an ordered vector of -strings. That erasure is emission, not anemia; the present defect is only that no preceding tree -exists — authors write the vector directly. - -### 4e-bis. The process plan is not the CLI grammar - -`CliSyntax` owns how options and operands become argument boundaries and **nothing else**: -stdin, environment, working directory and descriptor routing belong to the *process plan*. - -``` -type JqProcessPlan { command: CliCommandSurface, stdin: ProcessInput } - -JqInputFile(path) -> one CLI operand, no jq-data stdin -JqInputStdin(content) -> no file operand, stdin = content -``` - -This is why `SensorIntegerValue` is a good first proof: input content stays **outside argv** while -the jq program stays **one argv argument** — two properties an argv-only model cannot state, and -the gap `extdeps.llm.cursor_cli` hit when an argv-shaped carrier could not hold an -environment-passed credential. - -### 4f. Carrier: argv is native, text is the derived special case - -POSIX utility syntax operates over *arguments*; shell syntax is the preceding language that turns -words and expansions into them, with quote removal before the utility sees them. So argv is the -higher-structure carrier and shell text a serialization of it through an additional language. - -Making text canonical and re-deriving argv by parsing is backwards: it forces the strongest local -transport through an unnecessary shell grammar, quoting discipline and injection surface because -one weaker remote transport loses structure. - -**Local and SSH are not two jq targets.** They are one utility target, two realization carriers, -and one *additional nested target* on the SSH path — the POSIX shell grammar, modeled -independently and already grounded by `extdeps.posix.shell_command_language`. - -Counterfactual proving command text is not intrinsic: a remote receiver accepting a structured -request and calling `execve` itself would eliminate the shell leg while keeping SSH as transport. - -**Precedent for the composition shape:** `effect_plan_bash_materialize` obtains an argv vector, -converts it to Bash command nodes, then serializes the Bash grammar. Its argv source is still the -old declaration-owned materializer, but the shape is right. - -### 4g. Physical layer - -Lower into `v2.extdeps.posix` `Command` / `PosixArgument`, **not** `v2.std.host_transport` -`ProcessInvocation`. The latter is an emit-harness carrier whose `InvocationArg` variants -(`LiteralArg | WorkspacePath | ProducedArtifact`) classify build-workspace provenance, not CLI -roles; `build_transport_admission` only asks whether a step reads a declared workspace resource, -and `emit_host` flattens all three variants back to text. - -Its provenance vocabulary is hand-applied, not derived: `python.dag` models its script as -`WorkspacePath`, `go.dag` models `main.go` as `WorkspacePath`, and `c.dag` models `fixture.c`, -`-o` and `fixture` as three undifferentiated `LiteralArg`s — so C's compiler read of `fixture.c` -is invisible to the admission fold. A real harness defect, and an independent reason not to -promote this carrier to the universal process boundary. - -`ProcessInvocation` also lacks process-wide axes needed elsewhere: **no environment, no stdin, no -working directory, no file-descriptor mapping.** `extdeps.llm.cursor_cli` already forked around -this, minting `CursorProcessInvocation { program, argv, environment }` rather than reuse it; its -note states the principle with a security consequence — - -> an argv is not a process. A process is a program, an argv AND an environment, and a credential -> passed by environment is invisible in a model whose output stops at the argv — there was no -> field for it to appear in. - -A fork in `dag/extdeps/llm` against a carrier in `src/v2/std` would be unnecessary if -`host_transport` `ProcessInvocation` were the general authority its name suggests — independent -corroboration of the name collision from a different lane. - -**What `ProcessInvocation` remains good for.** Not deleted by this lane. It stays as a downstream -adapter for the emit-host harness — fixed option spelling to `LiteralArg`, workspace file operand -to `WorkspacePath`, generated binary operand to `ProducedArtifact` — keeping effect provenance -*after* CLI emission. Two constraints: no jq or domain caller constructs those variants directly, -and jq does not route through the emit-host harness merely because the type exists. The longer-term -cleanup is renaming it and `InvocationArg` to their real scope (`EmitHostProcessInvocation`, -`EmitHostArgumentMaterialization`) — a separate replacement migration, not bundled here. - -**Generalizing that subsystem is NOT a prerequisite for the first vertical.** Landing stdin, cwd -and fd mapping on `Command`/`SpawnProcessRequest` before any jq migration turns one proof vertical -into a process-runtime project. The stable boundary is `CliCommandSurface + ProcessInput`; it -binds first through one narrow local jq realization whose entire content is the jq executable -identity, a splice of already-emitted arguments, and the process channel wiring — **no option -spelling and no domain filter**. An honest realization boundary, not another argv authority. Its -dissolution trigger: the same emitted surface binds to `extdeps.posix` `Command`. Emit a -`CliToolRef` and let each realization resolve `"jq"` or an absolute path. - -`v2.extdeps.posix` `Command` is the right conceptual layer, with four bounded deficiencies: - -1. `program: AbsolutePath` — a semantic invocation names a *tool*; resolution must produce the - path at realization time. -2. no stdin or file-descriptor actions on `Command`/`SpawnProcessRequest`. -3. `PosixByteString = FreeMonoid` does not visibly exclude embedded NUL, which POSIX - process arguments require. -4. no executed proof that the active `dag/` lane can consume this `src/v2` carrier. - ---- - -## 5. Ownership - -| owner | owns | -|---|---| -| `extdeps.tools.jq` | what raw output means, what exit-status mode means, that `--argjson` takes a name and a JSON value, which options repeat — and **which spellings exist and which is canonical** (`long = exit-status`, `short alias = e`, canonical preference, value cardinality) | -| shared `CliSyntax` | **how a chosen form is constructed** — a long name renders `--` + name, a short renders `-` + character, clustering where the tool row permits, joined vs separate values, option/operand/terminator order | -| `gunbc.bmc_fan_projection` | the `TEMP_SOC` policy, the fan curve, controller cardinality, the desired topology | -| `extdeps.bmc.*` | programs projecting a documented OpenBMC response or config field | -| realization | local vs SSH vs another handler | -| **no caller** | `-e`, `-r`, `--argjson`, `--`, their positions, or the choice between `-e` and `--exit-status` | - -The tool row owns *which forms exist and which is canonical*; the grammar owns *how a chosen form -is constructed*. An earlier revision gave both "canonical spelling" — two authorities for one -decision, the §3 violation this note is about, committed inside it. - -The split is by **what fact the program encodes**, not that jq executes it. `ProjectFanConfig` -embeds `TEMP_SOC`, the aggregate fan PID entry, desired inputs, minimum and failsafe duties, -hysteresis and exact-one-controller policy — gunbc's chosen topology, so it moves. -`SensorIntegerValue`, `ObjectMapperServiceCount` and `ObjectMapperServiceAt` interpret **upstream -OpenBMC response shapes** and stay under `extdeps.bmc` even though jq realizes them. The several -*remote* fan-query programs carrying a literal `TEMP_SOC` are the ambiguous middle: they belong -beside the projection authority, or should derive that value from it, not keep the literal in -`extdeps`. - -Filters leave both `extdeps.tools.jq` (business policy in extdeps is a layer inversion) and -`openbmc.PasswordSshTransport` (SSH does not own what the remote program means). They do not all -land in one generic "workflow layer": policy-bearing ones go to `gunbc.bmc_fan_projection`, which -already owns the board, firmware, curve, config path, controller entry and tach inputs. - ---- - -## 6. Rules - -1. `-e`, `-r`, `--argjson`, `--` and their aliases may appear **only** in cited concrete-syntax rows. -2. Semantic modules construct no option node and no argv string. -3. Per-tool lowering contains no bespoke option-selection fold; semantic variants select grammar - rows exhaustively. -4. Transport consumes an inner invocation. SSH may **not** be modeled as `append(ssh_prefix, inner.argv)`. -5. The CLI inverse consumes `List`, not shell text — and it is **partial and - ambiguity-aware**, not bijective: clustered short options, optional option-arguments, operands - beginning with `-` and tool deviations admit multiple parses, so it answers - `CliParseUnique | CliParseAmbiguous | CliParseRefused`. It is **not** a Wave 1 acceptance - criterion; current argv remains an offline behavioral oracle. Shell text is parsed by the shell - grammar first; only a static literal simple command projects back to a CLI surface. -6. Every vertical deletes its prior argv authoring site **in the same cut**. "New tree plus old - helper for compatibility" creates two authorities. -7. No new production function returns `List` or `ArgvCommand` containing tool flag literals. -8. `ArgvCommand` builders and `transport shell { argv: [...] }` are migration substrates, not the - final authority. - ---- - -## 7. The cut - -Per the §3 replacement-migration rule, stated at exact identity grain. - -**Authority being replaced:** argv-as-authored-authority for jq invocations. - -**Root consumer × subject population:** the four live `openbmc.JsonProjection` operations -(`ProjectFanConfig`, `ObjectMapperServiceCount`, `ObjectMapperServiceAt`, `SensorIntegerValue`) -plus the seventeen `openbmc_password_ssh_transport` remote jq operations. - -**Recut: the first vertical is LOCAL only.** An earlier revision put local execution and SSH in -one "smallest complete" vertical. SSH adds a second language target, RFC 4254 string loss, -portable-word refusal, quoting, and the latent `sshpass -d 0` collision. The unit is **one local -jq vertical, demonstrated by one feature-rich migration and one discriminating counterexample.** - -**Wave 0 — carrier probe.** Stacked with its first production consumer, never mergeable alone. -A probe-only non-text serializer beside the text one; sealed `CliSurface`; existing text emission -byte-identical; argument boundaries observable *by execution*; direct/cast/unadmitted-mint REDs; -explicit refusal when no CLI serializer is wired. See §10 — the probe must execute, because it -will not refuse at compile time. - -**WAVE ORDER CORRECTED BY EXECUTION: 1B CUT FIRST, and the note was wrong, not the branch.** -Wave 1A was named first because it exercises the most axes at once, including a typed JSON binding -through `--argjson` — exactly why it cannot be first: that lowering is **not built**, and -`jq_invocation_lower` refuses it as `JqBindingLoweringUnwired`. `SensorIntegerValue` (Wave 1B) -migrated first because every axis is wired: program operand, stdin input, raw output, and -`JqProgramExit` selecting no exit-status option. Its falsifier role is undiminished by going first -— a counterexample does not require the thing it falsifies to exist yet. 1A follows once -`--argjson` lowers. - -**Wave 1A — `ObjectMapperServiceAt`.** The strongest exemplar: its argv exercises nearly every -load-bearing axis — `-e` truthiness exit, `-r` raw output, a typed JSON binding through -`--argjson` (an option with **two** values), stdin input, a jq program operand, exact -nonempty-string domain decoding, and a live recursive consumer. After migration the OpenBMC caller -stops reading `success`/`stdout`/`stderr` and matches -`OpenBmcServiceObserved | OpenBmcServiceProjectionRefused`. No layer above jq's cited rows can -name `-e`, `-r`, `-er` or `--argjson`. - -**Wave 1B — `SensorIntegerValue`, the required falsifier.** Immediately stacked, *before* the -abstraction is advertised as ready for repetition. The counterexample to a design overfit to `-e`, -proving four things Wave 1A cannot: `JqProgramExit` selects **no** `JqCliOptionExitStatus`; -`JqOutputAbsent` is not automatically a refusal; termination and output presence are orthogonal in -the observation model; one lowering supports two opposite domain policies unchanged. Its -acceptance population: - -``` -{"data": 41.6} -> Observed 42 {"data": "41"} -> Absent -{"data": 41} -> Observed 41 {"data": null} -> Absent -{} -> Absent invalid JSON -> Refused -two numeric inputs -> Refused, never first-pick jq nonzero -> Refused -``` - -**Wave 2 — first SSH migration.** Only after the local vertical is green: `CliSurface` → shell -simple-command tree → shell grammar emission → RFC 4254 command string, reusing the grammar-owned -quoting the effect-plan Bash path demonstrates rather than retaining `shell_quote`. Owns the -portable-word limitation, file-input-only admission, and a typed refusal for stdin-fed remote jq -while `sshpass -d 0` holds fd 0. Then the remaining remote population is mechanical. - -Emission-side witnesses, proven separately from semantics at every wave: - -``` -the semantic property selects the option identity (row level 1 perturbation) -that identity selects its canonical spelling (row level 2 perturbation) -JqProgramExit emits NO exit-status option -the jq program is exactly one argument -stdin content appears NOWHERE in argv -argument order is stable -direct / cast / unadmitted-mint construction refuses -the old transport argv site is absent in the same cut -no success: Bool survives on the new path -``` - -Both row levels need independent perturbation controls: a spelling perturbation proves spelling -is authoritative but says nothing about whether a required semantic property can be omitted. - -**Do not make the first vertical solve SSH.** Local exemplar and transport composition are -separate proofs. - -**First vertical (smallest complete):** - -1. `JqInvocation` and its semantic axes. -2. `CliInvocationTree`. -3. Shared CLI grammar rows + jq's cited option rows. -4. Emission to `List`. -5. Local resolution into `extdeps.posix` `Command`. -6. A process-input realization for jq stdin, separate from argv. -7. One live jq population cut over, with its prior argv authoring site deleted. - -The SSH handler is **Wave 2 and deliberately absent from this list.** An earlier revision carried -it as item 7, contradicting "Do not make the first vertical solve SSH" above and the recut in §7. -SSH adds a second language target, RFC 4254 string loss and the `sshpass -d 0` collision; none is -needed to prove a local vertical. - -The carrier lands **with** its first live consumer, or in an immediately preceding stacked PR -containing an executing consumer. A standalone carrier PR would reproduce the unconsumed -`v2.extdeps.posix` situation exactly. - -**Deleted at cutover:** `extdeps.tools.jq` `jq.Json.ExtractRaw` (zero consumers — deleted first, -not kept beside the new authority); the migrated sites' `transport shell` argv literals; the -seventeen re-inlined `sshpass`/`ssh` prefixes. - -**Explicitly NOT in this cut:** adding variants to `InvocationArg`; updating its 72-reference -population; renaming the harness carrier to `EmitHostProcessInvocation`; repairing the C/Go/Python -descriptor inconsistency; making `HostTransportDescriptor` a general process authority; modeling -jq's expression language; the other 575 argv lines. Those are independent lanes. - -**Deliberately not built:** a coverage lens over argv arrays. Under the replacement-migration -rule that is a census over a structure being deleted — the deletion is the census. The plan -proposes one; this note declines it. - ---- - -## 8. Witnesses - -Semantics and serialization are witnessed separately: - -- **semantic** — the caller requested the correct jq behavior. -- **emission** — that semantic tree lowers to the expected argument vector. -- **perturbation** — changing the jq option row changes or refuses the emitted vector; the - control proving the rows are load-bearing, not decorative. -- **domain** — absent, null, false, blank, wrong-type and multiple outputs cannot reach the write. -- **positive control** — one valid token still does. -- **absence control** — a non-numeric sensor reading still decodes to `OpenBmcSensorValueAbsent` - and does not become a refusal. - ---- - -## 9. Rung, by exact subject - -An earlier revision said "mechanically preventable, because a caller can still construct a raw -`List` and nothing yet refuses it." **If nothing refuses it, it is not mechanically -prevented** — it is measured or mitigated. It also *understated* what the migrated path can reach. -One number for several populations was the error. - -| exact subject | honest rung after the first vertical | mechanism | -|---|---|---| -| forging a `CliSurface` **by record literal or cast** | structurally impossible | `sole_constructor`, whose cross-module refusal is witnessed by the corpus `sole-constructor-cross-module` probes — not re-proved here | -| minting a `CliSurface` **whose content did not come from rows** | **mitigatable only** | *no wall.* `serialize_cli_arguments` is public and accepts caller-supplied `lex` and fragments, so any module can obtain a well-typed `CliSurface` carrying arbitrary text | -| omitting `--exit-status` from an accepted truthiness invocation | **structurally impossible within the new path** | exhaustive semantic→option selection; zero-or-many rows refuse; no mint on miss | -| hand-authoring flags inside a migrated route | **structurally impossible after cutover** | handler takes a semantic invocation, emits internally, old argv site deleted | -| reintroducing the exact old operation transport | mechanically preventable | structural deletion witness | -| a computed argument vector losing its argument boundaries at the host edge | **mitigatable only** | the `ProcessArgvExpansion` arm makes boundaries survive *on the carrier path* and refuses every malformed shape it is handed, but the guessing path it sits in front of is untouched for every value that is not this carrier — 21 operations still splice a declared `List` param. This is a repair at ONE seam, not a wall over the class | -| a domain module authoring jq program source | **mitigatable only** | §12 — `JqProgram` is public; nothing prevents it | -| decoding a jq exit code against the wrong contract | **structurally guaranteed on the new path** | §13 — the policy travels on the plan and `jq_classify_observation` is the only reader; a domain module has no exit code to misread | -| writing another raw jq argv elsewhere | **still writable** | unrelated raw shell/process routes remain | -| arbitrary raw argv corpus-wide | outside this vertical | later process/transport confinement | - -So: **the first migrated jq path can be structural now; the repository-wide raw-jq class stays -open; the repository-wide raw-argv class is a later migration.** - -**The seal is on the shape, not on the provenance — corrected 2026-08-19.** The row above -previously read *structurally impossible now — only the row emitter, serializer and decoder may -mint*: false, and the §4b inflation this document warns about, asserted about its own subject. -`sole_constructor` closes the record literal and the cast; it says nothing about a **public fold -that takes caller-supplied inputs**, which `serialize_cli_arguments` is. An earlier claim that -`CliSurface` was therefore *stronger* than `TransportScript` had it backwards: `TransportScript`'s -mint is `admit_callers`-sealed to two named production declarations; `CliSurface`'s is not sealed. - -**Ceiling and trigger.** The obvious repair — `admit_callers` on the serializer — is refused on §3 -grounds: `v2.std.compilers.cli_surface` would name every tool module that may emit a CLI, dispatch -fused into the interface. So the wall belongs on the **input**: a sealed `AdmittedCliInvocation` -that only a tool's own row-derived lowering can produce, with the serializer total over it. That -is **not built**, and its mint design is genuinely open — whatever seals it faces the same -recursion one level up. Until it lands, provenance sits at *mitigatable*; the practical -containment is that the only public jq entry point is `jq_invocation_process_plan` — a -convention, not a wall. - -**Application-argument typechecking is not the terminal trigger.** The `04_infer` gap is real — -`explicit_return_conformance_note` records that conformance is judged only by the -ground-kernel-scalar and ground-element-collection discipline — but it is neither *necessary* for -sealing this path nor *sufficient* to eliminate alternate raw routes. It matters when a sealed -carrier crosses an open function boundary and correctness rests on argument conformance; the first -vertical avoids that: no caller receives a public `execute(surface: CliSurface)` to smuggle into. -The domain caller passes a `JqInvocation`; the handler emits and consumes the sealed surface -internally. - -The real terminal trigger for the broad jq class is a **dominator condition**: - -> every production path capable of executing jq is dominated by the semantic jq handler, and raw -> process/shell routes cannot name jq except through an explicitly retained escape population. - -General argument typechecking strengthens the boundary; it does not prove that property. - -## 10. Open questions - -1. **Carrier parameterization — and the probe will NOT refuse.** The current answer is not - "unknown diagnostic" but: **there is no carrier-mismatch refusal on this path, so a compile-only - probe false-greens.** `emit`, `serialize_target` and `serialize_source_for_emitted` are fixed - to `Medium`, and underneath them `target_serialize_source_from_model_bounded` returns - `TargetText` rendered through `target_source_medium`. Passing a `cli_target` still takes the - text serializer. And `v1.04_infer` `explicit_return_conformance_note` records that declared - return conformance is judged only by the ground-kernel-scalar and ground-element-collection - discipline, so a structured mismatch such as `Outcome>` vs - `Outcome>` yields no diagnostic. **Do not treat a green compile as a - positive result.** - - The smallest discriminating probe is a **sibling concrete seam**, executed rather than compiled - — `emit_cli_probe` binding `translate`'s target tree to a non-text serializer — asking: can - `translate`'s output be consumed by a non-text serializer while the text serializer stays - byte-identical? It does not ask the current `emit` to produce what its signature cannot - express, and does **not** start by genericizing `TargetModel`. - - Controls: existing `emit` returns the exact prior bytes; the probe returns a sealed `CliSurface` - whose argument boundaries are inspectable **by execution**; swapping the CLI serializer for the - text one must *fail the test*; direct, cast and unadmitted-mint construction of `CliSurface` - refuse; a missing serializer raises a typed `TargetSurfaceSerializerUnwired` rather than - falling back to source text. - - Only after that receipt is the seam chosen. Prior: `TargetSurfaceSerializer` is the smaller - terminal seam, with the existing `emit` kept as a compatibility wrapper selecting the text - serializer — parameterizing every `TargetModel` consumer before a non-text target has proven it - necessary is the larger, less reversible move. - -2. **`sshpass -d 0` and stdin.** The password occupies fd 0; a remote stdin-fed jq would collide. - Latent, not live — all seventeen remote sites use file operands. Must be a typed refusal, not - an accidental limitation. Options: remote file-input only; password delivery on another - descriptor; a framed remote receiver. -3. **Portable-word allowlist.** `gunbc.typed_argv_exec` bounds what may cross SSH. jq programs - contain spaces, pipes, brackets, parentheses and quotes — outside that alphabet. The shell leg - must own canonical shell-word serialization with injection controls, or refuse. -4. Which of the 315 computed-head argv lines hide further wrappers. -5. **The remote jq rows do not preserve the jq program as one argument. Source-proven.** - Traced end to end, with the local half verified in this tree: - - - `v1_interpreter.rs` `push_shell_argv_tokens` pushes each evaluated `String` **verbatim** — no - splitting, no quoting — and both exec branches call - `Command::new(&argv[0]).args(&argv[1..])`. Despite its name, `transport shell` here is - **direct process execution**: no local shell, no `shell_quote`, no command-string renderer. - - `sshpass` parses its own options, copies the remaining argument pointers, and `execvp`s them - unchanged. - - OpenSSH's client consumes the `--` after the destination as its **own option terminator** - (not a remote quoting construct) and builds the remote command by appending each remaining - argv member separated by one literal space, with no escaping. - - OpenBMC's default SSH server is **Dropbear**, not OpenSSH — its `run_shell_command` - invokes the user's login shell with `-c`. - - So `["jq", "-er", "[.zones[].pids[] | select(...)] | length", path]` arrives remotely as - `jq -er [.zones[].pids[] | select(...)] | length /path`, and the remote shell reads the - unquoted `|` as pipeline operators. The law the code needs is - `shell_parse(join(map(shell_quote, argv), " ")) == argv`; it assumes - `shell_parse(join(argv, " ")) == argv`, which holds only inside the portable-word alphabet - these filters are outside — the alphabet `gunbc.typed_argv_exec` exists to bound. Dynamic values - such as `config_path` are unquoted too; the current path happens to be shell-safe, but - `NonEmptyStr` does not establish that. - - **Split the judgments rather than calling the whole thing broken:** - - ``` - RemoteJqArgumentBoundaryPreservation Violates — source-proven - DeployedSshClientIsStandardOpenSsh Unverified - DeployedBmcServerUsesDropbearShellExec StronglySupported, not observed on this firmware - AffectedOperationReachability Unverified - ObservedProductionManifestation Unverified - ``` - - The saving conditions are enumerable and none is visible in the repository: a custom binary - named `ssh` that shell-quotes (most plausible, since `sshpass` resolves through `PATH`); filters - already carrying canonical outer quoting; a forced-command or structured receiver decoding an - encoded argv; a row whose program is entirely portable words. - - **The executed control** uses the exact current prefix and direct argv execution — not a local - shell — with `jq -n`, so it needs no stdin or file and cannot mutate BMC state: - - ``` - portable positive ["jq","-n","-r","7"] -> exit 0, stdout "7" - current form ["jq","-n","-r","[1,2] | length"] -> intended "2" - serialized form ["jq","-n","-r","'[1,2] | length'"] -> expected "2" - ``` - - Discriminating result: portable and serialized controls succeed while the current form does not - produce the same exit/stdout observation. Capture local client version, remote SSH banner and - remote login shell. **If the current form unexpectedly yields `2`, that is positive evidence for - an unmodeled saving layer**, and the next probe captures the command string seen remotely. - -6. The SSH slice needs a discriminating program containing spaces, a pipe, quotes, brackets and a - value containing a single quote, and must reuse the grammar-owned quoting the effect-plan Bash - path already demonstrates rather than retaining `shell_quote`. -7. Reverse ingestion — reading argv back through the same rows — is the terminal second reading - and is **not a prerequisite for the first emitter cut**. -8. **Measured substrate observation — `Optional` at a primitive `T` in record-field - construction position.** `stdin: Optional` with an inline `Present { value: … }` - refuses with `expected 'Coproduct(Optional)', got 'Primitive(String)'`, independent of the - payload expression: a pattern-bound variable, a renamed binding, a shorthand pattern and a bare - `"x"` literal all reproduce it, while `stdin: Absent` resolves. Inline `Present` into a declared - `Optional` field is ordinary elsewhere (`gunbc.witness_row_cost` `basis`, - `gunbc.declared_import_closure_binding` `binding_source`, - `v2.workflow.effect_plan_bash_materialize` `operation`), but every such `T` is a record or - coproduct — so the discriminator is the **primitive type argument**, not the field form or the - explicit-vs-sugar spelling. This lane uses the corpus's own `String?` sugar, the existing - modeled form for an optional field (625 sites), not a workaround. **The observation is - recorded, not diagnosed:** whether sugar and explicit generic genuinely differ at a primitive - argument, or the refusal has another cause this bisect did not separate, is unestablished — a - §5 line-stop is owed before anything in this lane relies on the distinction. - -9. **A live Class B specimen, in this lane's own new code (found by review, 2026-08-19).** - `extdeps.tools.jq` `jq_option_canonical_spelling` calls `cli_long_option_spelling` while the - module's import list named only `CliArgumentSyntax`, `CliSurface` and `serialize_cli_arguments`. - The call resolved — and every Wave 1A assertion passed — because `cli_surface` was already in - the assembled closure via those imports: the accidental coverage DESIGN's import-strip thread - records as **blocking all further `dag/**` import stripping**, binding by pool membership, not - the bare-reference closure. The bystander probe above does *not* catch this — it refuses a - *qualified* import of an unexported name, whereas this is an *unimported bare reference* into a - module present for other reasons; only the second is silent. Fixed by naming the symbol in the - import list. **The general defect is untouched and not this lane's:** nothing refuses the next - such reference, and a green witness is not evidence that a module's imports are complete. - -9-ter. **BLOCKER, PROVEN BY EXECUTION: the argv transport silently destroys argument boundaries - for computed lists (2026-08-19).** A `List` built by folding is CONCATENATED into one - argv word; the identical literal list is SPLICED. Same declared type, elements and handler, two - different processes. - - **Receipt, through the production handler `jq.Process.RunWithStdin`:** - - ``` - arguments = ["--raw-output", "."] literal -> exit 0 (jq ran; two argv words) - arguments = fold_list(... same two ...) folded -> exit 2 - jq: Unknown option --raw-output. - ``` - - And from the first wet run of the real vertical, three lowered arguments arriving as one: - - ``` - $ jq --raw-output--exit-status.missing (exit=2) - ``` - - **Mechanism** — `v1_interpreter.rs` `push_shell_argv_tokens`. `Value::List` splices each item. - `Value::Variant` tries `value_as_host_string` FIRST, which walks a free monoid and concatenates - it into one String, pushing a single argv word; only on `None` does it fall through to - `free_monoid_to_vec` and splice. A folded list is monoid-encoded with all-`Str` elements, so - the concatenating reader wins. `gunbc.WitnessBin.Run` escapes only because its `args` is a - literal at each call site. - - **Independently attacked:** an outside reviewer failed to refute this — the source matches the - reading, and the collapse occurs BEFORE `Command::args`, ruling out a downstream shell or SSH - join. - - **Why neither obvious repair is correct.** Reordering the arms would splice this case but SHRED - a modeled `String`, because a String is itself a monoid here (`value_as_host_string` - reconstructs one from code points): both readings succeed on a monoid of `Str`s, and the runtime - cannot separate *list of arguments* from *string assembled from pieces*. The disambiguator is - the DECLARED TYPE, erased at that seam — the missing application-argument typechecking that - `target_model` `target_text_carrier_scaffold_note` names as the trigger for every type-based - construction guarantee at a call boundary. Hand-authoring a literal argv in the handler defeats - row-derived lowering and leaves the next caller silently joined — the workaround shape §5 treats - as a line-stop. - - **Rung:** a §5 fail-open — it fabricates one plausible argument instead of refusing — and the - LANE'S OWN DEFECT CLASS one layer below the lane: Wave 0's witness asserts fragments concatenate - within an argument and never across, and the transport violates exactly that. Also a live - specimen of the model↔realization fork DESIGN tracks as an open thread. - - **CORRECTION (same day, adversarial review): "blocked on application-argument typechecking" - was WRONG, and too strong.** The diagnosis survived independent verification — the reviewer - traced `build_service_param_env` (clones call arguments with no conformance check), - `dispatch_shell`, and `Command::new(&argv[0]).args(&argv[1..])`, confirming the vector is - malformed before `Command` sees it, so no later join is an alternative explanation. The refusal - to reorder the branches was upheld. The conclusion was not: **a correct construction is - available now.** - - In-tree counter-evidence: `OperationInputValue = InputText | InputTextList` already carries - tagged list intent, and its realization converts that tag into a native `Value::List` - (`free_monoid_to_vec` then `list_value`), so the later flattening splices. Tagged intent → - native list is proven possible *without* global argument typechecking. - - The deeper correction: even a fully typechecked system would not answer the transport's - question. `List` does not say whether a collection becomes several argv words, one JSON - argument, a comma-joined option value, or repeated option/value groups. **That is a transport - ROLE, not a data type**, to be modeled rather than inferred from runtime encoding. - Application-argument typechecking remains independently valuable and is neither necessary nor - sufficient here. - - **The construction:** one explicit nominal carrier — `ProcessArgvExpansion { surface: CliSurface }`, - sealed — with an interpreter branch BEFORE the generic string/list heuristic that requires the - nominal `CliSurface`, iterates its arguments, and pushes exactly one host word per - `CliArgument`, concatenating that argument's fragments into that word. It carries `CliSurface` - rather than `List` so the payload cannot be dynamically ambiguous: a modeled string - cannot impersonate a list of nominal `CliArgument` records, and `value_as_host_string` is used - only on one admitted argument's text, where concatenation IS correct. Wave 0's theorem enforced - at the host edge. - - **Explicitly not to be landed** (each rejected for a stated reason): reversing the two `Variant` - branches (damages modeled strings); `map(identity)` to coerce a native list, rebuilding the - emitted argv as a literal, or join-then-shell-split (runtime-representation workarounds, and the - last reintroduces shell parsing); routing jq through `OperationInputValue` permanently (that - carrier belongs to declaration-owned operation materialization); accepting BOTH a raw - `List` and the explicit carrier (two authorities, future callers free to pick the unsafe - one). The raw-list handler signature is REPLACED, not retained for compatibility. - - **Consequence for this lane:** the cutover is blocked at the execution seam *until that carrier - lands* — a build, not a decision. The semantic spine is built and resolving; no consumer can - move until then, because every migrated call site computes its argv rather than authoring it - literally — precisely what the migration asks callers to do. - -9-bis. **The Class B specimen RECURRED, in code written after it was documented (review 53669, - 2026-08-19).** `jq_invocation_process_plan` used `bind_outcome` and `outcome_accepted` with - neither in the module's `v2.std.diagnostic` import list. Same mechanism as the - `cli_long_option_spelling` case one commit earlier, same lane, same author who had just written - that entry. - - **Two things this settles.** First, the mitigation must run *after every addition*: the - unimported-reference sweep was clean, then a function was added and the sweep not re-run. A - one-time audit does not close a class whose defect is invisible. Second — correcting the review - that found it — the stated consequence, "as written this file will not resolve", is **false**, - and its falsity is the point. The file resolves, and - `stdin_survives_lowering_into_the_process_plan` executes green *through the affected function*, - because `v2.std.diagnostic` is already in the closure via other imports. A reviewer predicting a - resolution failure would be refuted by running it and might dismiss a real finding. The correct - statement: the reference is unimported, it binds by pool membership, and **nothing in the tree - will tell you** — not the resolver, not CI, not a green witness. - -10. **The spelling-seam extraction is right, but its obvious shape is forbidden — corrected - 2026-08-19.** The static-dependency finding stands: at execution grain the jq fold reaches no - compiler machinery, but at dependency grain `cli_surface` imports `v2.std.compilers.target_model`, - whose closure carries target-representation, host-runtime and node-query machinery. The property - worth having is not *nobody calls translate* but *this fold cannot acquire that dependency - accidentally*. - - The proposed repair — extract a shared `spell_concrete_syntax_fragments -> Outcome` - below `target_model`, with the TargetText serializer wrapping its result — **violates an - operator ruling in the file it would edit.** `target_model` `target_text_carrier_scaffold_note` - (operator, 2026-07-15) places `TargetText` beside `bound_tokens_source_text` so that fold is - *carrier-native*: the single `String -> TargetText` introduction is `text_atom` applied to one - grammar-classified token **inside** the fold, the only join is `target_text_seq`, the only exit - is `render_target_text`, and there is deliberately **no** `String -> TargetText` lift of an - already-composed unit — "that direction is the smuggle the wall closes." Wrapping a composed - `String` is that lift. - - **The corrected decomposition, better anyway.** The shared authority is *what is this token's - spelling* — `lex_rules_literal_for_class` and `bound_spelling_from_map`, both token-grain — - **not** *how spellings are concatenated*. Concatenation is carrier-specific: emitted source - composes through `TargetText`, an argv argument into a `CliArgument` — different media. So the - extraction moves the token type and the two token-grain lookups into - `v2.std.compilers.concrete_syntax`, `target_model` imports them back (re-export proven — see - below), and each carrier keeps its own fold. No composed `String` is lifted, `cli_surface` stops - importing `target_model`, and one lookup authority serves both. - - **Re-export is measured, not assumed.** A three-module probe showed a consumer importing a - symbol from a module that merely imported it resolves and executes. Alone that proves nothing, - since this resolver can bind by *pool-membership coincidence* (DESIGN's Class B finding). The - discriminating control: importing the same symbol from a bystander module that never saw it - **refuses**, located — `name 'ProbeToken' not found in module '...bystander'` — even with the - defining module in the pool. Qualified-import resolution is import-list-driven, so - `target_model` importing the seam back keeps its 167 other importers untouched. - - **Not performed.** `target_model` is load-bearing and carries the ruling above; the move is its - own increment under its own review. - -11. **A downstream typed verdict may be the redundant lower rung, not a peer** (raised by - `eager-wren-138`, 2026-08-18). `gunbc.host_effect_realize` `bmcweb_token_extraction_verdict` - refuses blank stdout as its own decode — the wall that closed that fail-open, with `jq -e` only - loudness beside it. If the semantic layer makes *absence-is-a-value* unwritable at the decode, - that verdict is a second representation of one requirement (§2/§3) and must **dissolve into** - the decode. A §4b climb: its discriminating RED stays enrolled against the new decode while the - production check is deleted. Open because it is not established whether the requirement is - exactly one nonempty string at *every* consumer or only at the credential path; the OpenBMC - sensor path models absence as a legitimate third state (`OpenBmcSensorValueAbsent`), so a - blanket absence-is-refusal decode would be the state-space collapse this lane exists to remove. - ---- - -## 11. Corrections receipt - -Claims asserted during this design and refuted by measurement, kept so they are not re-derived: - -- *"The declarative `transport shell` form is fixed-arity and cannot express variadic options."* - **False** — `ssh.Session.ExecArgv` and `ExecPortableWords` splice `List`. -- *"SSH becomes a bound handler over the same tree."* **Incomplete** — RFC 4254 forces a string - at that boundary; structure cannot cross it as structure. -- *"`ProcessInvocation` is the general process carrier."* **False** — it is an emit-harness carrier; - its argument variants are provenance, not CLI roles. -- *"`SensorIntegerValue` has no consumers."* **False** — two live consumers, and its empty-output - branch is a modeled `OpenBmcSensorValueAbsent`, so an `-e` sweep would have collapsed a correct - three-state decode into a refusal. -- *"`ExtractRaw` and `SensorIntegerValue` are both latent traps."* **False** for the second; only - `ExtractRaw` is zero-consumer. -- The plan's census (262 lines / 37 files; 148/51 success/exit_code) is **stale**; measured - 597/129 and 173/69. -- *"`JqProducedNoResult` is a general arm on every jq execution."* **False** — verified against - `jqlang.org/manual/`: default jq exits 0 regardless of output, and only `--exit-status` yields - exit 4 for "no valid result was ever produced". Claiming the arm unconditionally would have - reintroduced the `Absent → Refused` collapse #8454 had just withdrawn. -- *"'Exactly one nonempty string' is a decode the domain can perform."* **Over-claimed** — jq emits - a stream and `-r` permits newlines inside a raw string, so a collapsed stdout `String` is - ambiguous. `--raw-output0` exists precisely because of this. -- *"The class sits at mechanically preventable."* **Too coarse** — that is the corpus-wide claim; - the migrated path can reach structurally guaranteed now via a sealed constructor. -- *"Filters move to `gunbc.bmc_fan_projection`."* **Needed splitting** — only policy-bearing - programs move; OpenBMC wire decoders stay in `extdeps.bmc`. -- *"`JqProducedNoResult` is an arm beside `JqExitZero`."* **False** — the live `SensorIntegerValue` - case holds both at once (exit 0, zero results), so termination and output presence are a - product, not a sum. Written as a sum, the live case is unrepresentable. -- *"Removing `success: Bool` closes the contradiction."* **Insufficient** — an unsealed - `JqObservation` lets `{ exit: JqExitZero, exit_code: 4 }` be authored, the same contradiction - under richer names. The carrier must be `sole_constructor`, minted only by the decoder. -- *"The class sits at mechanically preventable because nothing refuses it."* **Internally - inconsistent** — if nothing refuses it, it is not prevented. Replaced by the subject-grained - matrix. -- *"jq owns how it spells options AND `CliSyntax` owns canonical spelling."* **Two authorities for - one decision**, committed inside a note about exactly that. -- *"The carrier probe will hit a named refusal."* **False** — `emit`/`serialize_target` are fixed - to `Medium` and structured return mismatches fall outside the judged conformance - population, so a compile-only probe **false-greens**. -- *"The smallest complete vertical includes SSH."* **False** — SSH adds a second language target - and three independent failure modes; the unit is local-only. - ---- - -## 12. `JqProgram` is public, and the domain still authors jq source - -Raised by the `shell → dag` session against the `SensorIntegerValue` cut: after the migration, -`extdeps.bmc.openbmc_fan_control` no longer authors argv — but it *does* author - -``` -data openbmc_sensor_integer_program: String = - "if (.data | type) == \"number\" then (.data | round) else empty end" -``` - -so the workflow moved from authoring shell to authoring jq. `JqProgram { source: NonEmptyStr }` -is a public constructor, so every domain module may do the same. - -**The objection is right about the residue.** The cut established something narrower than "the -domain stopped authoring foreign syntax": it stopped authoring *argv*. OpenBMC no longer names -`-r`, `--raw-output`, an option spelling, an argument position, argv[0], or the stdin routing — all -derived from jq's cited rows, the property the two-level derivation buys. The jq *program* is -untouched. - -**The proposed remedy — keep `OpenBmcSensorIntegerProjection { content }` public and have the -handler privately select a sealed program identity — is refused as stated, on §2.** It does not -decompose the program; it relocates the same string behind an indirection and adds a registry. -If the handler is OpenBMC's, the jq source is still in the OpenBMC layer with a wrapper added. If -the handler is jq's, `extdeps.tools.jq` knows what a BMC sensor reading is — a layer inversion. -Neither beats the existing row, and the second is worse. A sealed identity buys something only if -*derived*, which is the real answer: - -**The terminal shape is that the program is not authored at all.** `.data`, "is it a number", -"round it", "otherwise nothing" are a modeled JSON projection — a path, a type guard, a rounding -policy, an absence arm — of which a jq program is ONE realization, as an argv vector is one -serialization of a `CliSurface`. The domain declares the projection, jq's module derives the -program text as it already derives option spellings, and a second realization (a native JSON fold -with no process) comes free. The same move the lane made one layer out, applied one layer in — -and what stops this work generalizing into a thousand bespoke jq programs where it found a -thousand bespoke argv lines. - -**Rung, honestly.** Domain-authored jq source is **mitigatable**: nothing prevents it; the only -things between it and an arbitrary computed program are that the row is `data`, not a function — -so it cannot vary per call — and review. Not a wall. - -**Dissolution trigger:** a modeled JSON projection carrier with jq as a derived realization. Until -it lands, `JqProgram` stays public and this section is the reason. Deliberately NOT in the current -cut: a JSON projection algebra is its own vertical, and bundling it would repeat the Wave 1A -mistake of selecting the richest thing first. - - ---- - -## 13. The observation contract travels with the plan - -Raised as "`JqAdmittedProcessPlan` cannot be decoded" by the `shell → dag` session. Verified -against the code and **worse than raised** — a live latent defect in the cut itself, not a missing -convenience. - -The sensor decoder read `exit_code`, `stdout` and `stderr` directly and mapped every nonzero exit to -a refusal — correct under `JqProgramExit`, silently wrong under `JqLastResultExit`, where jq -documents exit **1** as "the last output was false or null" (a legitimate *value*) and exit **4** -as "no valid result was ever produced" (*absence*). Nothing connected the decoder to the policy -that produced the argv, so changing `exit_policy` would leave the decoder answering the previous -contract. §5's own tell: the declaration is edited while the realization goes on lying, and the -witnesses stay green because they never varied the policy. - -**Repair.** `JqAdmittedProcessPlan` carries `exit_policy`, so the plan is self-describing. One -authority, `jq_classify_observation`, reads an exit code *against* the policy that asked for it and -returns `JqOutputPresent | JqOutputAbsent | JqExecutionRefused`. The domain matches those arms and -decides only what OpenBMC alone can — whether a present projection parses as an integer. No domain -module reads a jq exit code any more. - -**Discriminating evidence.** Five assertions hold one observation fixed and vary only the policy: - -``` -exit 1, stdout "false" under JqProgramExit -> Refused -exit 1, stdout "false" under JqLastResultExit -> Present <- the policy-blind decoder fails here -exit 4, stdout "" under JqLastResultExit -> Absent -exit 2, stderr "usage" under BOTH -> Refused -exit 0, stdout " " under JqProgramExit -> Absent -``` - -The second row discriminates: a policy-blind decoder must answer `Refused` for exit 1 under both -policies, so it cannot make rows one and two true at once. The fourth row keeps the law from -overfitting — the policy changes how *some* codes read, not whether jq can fail. - -**This also closes the point `eager-wren-138` reached from the other direction.** That session -proposed sweeping `-e` across jq sites as hardening; the counter was that `-e` reports on the *last -output*, converting legitimate `false`/`null` into failure. Both facts are now modeled: selecting -`JqLastResultExit` changes the *decode*, not just the flag, and absence stays distinct from refusal -on both arms. - ---- - -## 14. Verification scoped to the consumers I already knew about - -Recorded as the third instance of one failure mode in this lane's own work; the first two were -caught by other people. - -Changing `openbmc_sensor_integer_projection_result`'s signature broke two enrolled witnesses -(`failed_sensor_projection_cannot_become_absent`, `successful_empty_sensor_projection_is_absent`) -that call the decoder directly. I did not find them: I selected witnesses by asking which files -import `openbmc_fan_control.dag` **and then hand-picking three** — the verification denominator was -a list I wrote, not one the tree produced. The break surfaced only when a resolve failed on an -unrelated run. - -The same shape produced the two earlier defects: an unimported-reference sweep run *before* the -last addition, and a grep whose zero result was accepted without a control that must hit. Each -time the mechanism was sound and the **denominator** was authored. - -**A third instance, one level up, caught by CI after the above was written.** The rule below fixed -the witness denominator and was still not enough: I minted `type ExpectedOutcome` in the jq witness, -colliding with `gunbc.output_policy`'s load-bearing carrier of the same name, and the failure landed -in `output_policy_witness_test.dag` — **a file referencing none of my symbols, never touched by this -branch.** Bare-reference resolution is corpus-global, so a new top-level name can red a file no -per-file closure of mine would load. Every local run passed; the whole-corpus strict resolve caught -it. - -Two rules follow, both mechanical because the authored version keeps failing: - -``` -# every top-level name the branch ADDS, from the diff -- not from memory -git diff main...HEAD -- '*.dag' | grep "^+" | grep -oE "^\+\s*(type|fn|data)\s+[A-Za-z_][A-Za-z0-9_]*" | awk '{print $NF}' | sort -u -# each one must have exactly ONE declaration corpus-wide - -# and run what CI runs, before pushing, not after it fails -claim_executor --required-floor --source-root dag --source-root src/v2 -``` - -The second matters most: **a per-file run and the floor are different universes**, and only the -floor is the acceptance path. Reporting a per-file green as the floor is rung inflation against a -declared boundary (§4b) — citing the strongest path while another stays silent, committed against -my own change. - -**Rule adopted for the rest of the lane:** verification enumerates test functions from the FILE, -never from memory — - -``` -grep "^test fn" | sed 's/test fn \([a-z_0-9]*\).*/\1/' -``` - -— and every function in every witness file touching a changed signature runs, not a chosen subset. -§5's oracle principle applied to coverage: a population I author is not an observation of the -population that exists. - -**What the break demonstrates — the argument for the cut.** Both witnesses were re-enrolled -unchanged in *requirement* — a refusal must not decay into absence, an empty successful projection -must not decay into a refusal — while their *carrier* moved from a `success: Bool` triple to the -typed jq outcome. The migration also made a third requirement stateable that the old triple could -not: a present numeric projection is an observation. A replacement migration behaving correctly -(§3): deletion surfaced the load, the load was dispositioned as re-enrolled evidence rather than -restored by reflex, and the new representation is strictly more expressive. - ---- - -## 15. Wave 1B cutover receipt - -Executed on BuildBuddy against the branch tree. Counts are from enumerating `^test fn` in each -file, not from a chosen subset (§14). - -``` -extdeps.bmc.openbmc_fan_control SensorIntegerValue operation DELETED - consumers rewired 2 (openbmc_sensor_value, openbmc_sensor_threshold_result) - both now route through openbmc_sensor_integer_projection (one shared fn, not two call sites) - argv authored by the domain after cutover 0 -- no -r, no --raw-output, no argv position, no argv[0] - residual foreign syntax authored 1 jq program row (S12, mitigatable, trigger recorded) - -test/claim/bmc_typed_operations_witness 29 / 29 pass - of which migrated onto the typed jq outcome 2 (requirement unchanged, carrier replaced) - of which newly stateable and added 1 (present numeric projection is an observation) -test/claim/jq_invocation_lowering_witness 9 / 9 pass (lowering) - 5 / 5 pass (S13 exit-policy classification) -test/claim/bmc_fan_converge_witness duty_curve pass -test/manual/process_argv_expansion_receipt case 4 pass -- real jq, exit 0 reachable only - with two argv words, through the production handler -``` - -**What this receipt does NOT establish** — the table reads stronger than the lane's position: - -- The argv splice defect is repaired at **one seam**, not closed as a class. 21 operations still - splice a declared `List` parameter through the guessing path. -- The other three `openbmc.JsonProjection` operations were unmigrated, and two of them - (`ObjectMapperServiceCount`, `ObjectMapperServiceAt`) needed `--argjson` lowering, which was - unbuilt. Both are now landed — see §17 (Wave 1A cutover receipt). The remaining unmigrated - operation is `ProjectFanConfig`, whose sole consumer is dead code; file-input lowering has since - landed with the remote population (Wave 2, §18), so its migration is unblocked but not yet cut. -- No negative falsifier exists yet proving a REST path reaches none of these carriers. -- **The wet receipt does not execute in CI — a declared gap, not an enrollment.** The hermetic - floor refuses `jq.Process.RunWithStdin` (no `mock_response`) and mocking would defeat the - assertion, so the file is named in `cli_run.rs` `floor_prepared_subject_exclusions`. No wet lane - exists to host it: the falsifier and wet batches died with the floor cut, and - `v2.workflow.required_floor` deliberately defers wet lanes until the question can be "asked - against a live consumer". The claim sits at **UNEXECUTED-IN-CI**, evidenced only by the recorded - run above and reproducible locally with: - - ``` - claim_batch --wet --source-root dag --source-root src/v2 --entry dag/test/manual/process_argv_expansion_receipt_test.dag --functions case4_expansion_carrier_splices - ``` - - **Re-enrollment trigger:** a wet lane with a live consumer exists again. - - The first attempt at this admission added rows to `gunbc.ci_layer_roots` - (`witness_exclusion_frontier` + `bin_witness_wet_entries`) with a commit message asserting they - would take effect. They did not — `run_required_floor` consults the Rust list and nothing else; - the CI receipt was an unchanged `modules_excluded=2`. Both rows were reverted: a roster row with - no live consumer is specification-without-execution, and the enrollment half would have *claimed* - an executing consumer for a witness nothing runs. Instructive mistake — I read a neighbouring - row's shape as the mechanism instead of tracing the mechanism's caller. -- The wet receipt run reports `[expectation-frontier] 1 site(s), 1 dispatch(es) undeclared: - jq.Process.RunWithStdin=1` — the new handler's dispatch is not declared to the expectation - registry. Left standing, not silenced, because it is the shape §5 asks for — typed, located, - counted, visible — but it IS an open item; a `PASS` line does not mean nothing else was reported. -- The 5 classification assertions and the wet receipt ran separately from the 29+9; a single run - of all 43 exceeded the 45-minute remote timeout, because every `gunbc run` pays a whole-corpus - typecheck. A cost-shape observation about the harness, not evidence about the code. - -## 16. The negative falsifier: the REST/CLI boundary becomes a row that reds - -Every prior section asserts, in prose, that the REST path constructs no process invocation. -Prose cannot be contradicted by the tree; this section records the mechanism that can. - -**The question already had an authority, so no second mechanism was minted.** -`v2.lens.realization_vocabulary_containment` answers "does module X reach construction vocabulary -from set V" and has since the TypeScript-only ratchet. The obstacle was that `V` was a literal: -`scan_facts_for_leaks_under` threads the *importer-path* axis as a parameter while -`module_is_target_ast_vocab` was welded into the predicate. A second lens for a second `V` would -be the §3 duplication this lens exists to detect, so the vocabulary axis was opened instead — the -§2 horizontal move, one axis rather than N copies. - -What that cost: - -- `RealizationVocabularySet` (name, modules, module_prefixes) and `module_is_in_vocab`. -- `target_ast_vocabulary()` derives from the existing `target_ast_vocab_modules` / - `target_ast_vocab_module_prefixes` rows rather than replacing them — those rows are consumed - directly by `gunbc.realization_vocab_confinement_census`, which has live claims against them. -- `is_vocab_leak_in` / `scan_facts_for_leaks_in` / `vocab_leak_count_in` / `vocab_leak_count_live_in`, - taking vocabulary and exempt-edge population as arguments. Every pre-existing entry point - delegates to these with the target-AST set, so no behavior moved. -- The exempt population is a **parameter**, not a global roster read, so "this vocabulary has - zero admitted exceptions" is a stated fact, not an accident of the target-AST roster naming no - CLI module. - -**Two sites were left target-AST-only, deliberately.** -`realization_vocab_leak_candidate_paths_from_facts` and -`realization_vocab_live_leak_edges_from_facts` feed the grandfathered-roster staleness check, and -every row in that roster is target-AST debt by construction — `RealizationVocabDebtClass` has no -other inhabitant. A second vocabulary reaches the lens with an empty exempt population, so it has -no roster to be stale against; parameterizing them now would answer staleness about a population -that does not exist. Trigger recorded in-file: the first non-empty exempt roster for a second -vocabulary. - -**The subject is not an empty universe** — the usual way a negative claim turns vacuous. The -scanned population (`dag/extdeps/bmc`, `dag/extdeps/transports`) *contains* a module that -legitimately reaches CLI vocabulary: `extdeps.bmc.openbmc_fan_control`, which this lane routes -through jq, in the same directory as `extdeps.bmc.redfish`, which does not. So the scan -discriminates *within* the population, and the RED control is live corpus data, not a planted -fixture: withdraw the one admitted edge and the count must become 1. Without that assertion the -positive result is indistinguishable from a scan that read nothing — the empty-observation narrow -DESIGN names, ⊥-as-answer conflated with ⊥-as-ignorance. - -The admitted edge is named at exact `(importer_path, vocab_module)` grain, not by category or path -prefix, so a *second* jq-reaching module anywhere in the scanned roots reds instead of being -absorbed by a broad pattern. - -**What executes, and what does not.** The witness is floor-discovered — neither `long/`-homed nor -in `floor_prepared_subject_exclusions` — and its scan is a live read of the two named directories: -corpus data, not a fixture. It is **not** whole-corpus coverage: a module outside those roots -reaching CLI vocabulary is not seen, and no green here says otherwise. The whole-corpus half of -this lens is enrolled on a cadence that does not currently run — a fact about that cadence, not -this witness. Both halves are stated because a scoped green presented as general is the rung -inflation §4b calls worse than sitting low. - -**Where this goes, not scoped here.** The boundary in #8535 is a paragraph today. If this -generalization holds, the successor is that the paragraph becomes a row that reds — changing what -the lens is *for*: not one witness for one lane, but the mechanism by which a named architectural -boundary is enforceable at all. A separate change, deliberately not attempted here. - -## 17. Wave 1A cutover receipt - -Executed on the session tree against `origin/main` @ #10104. Counts are from enumerating `^test fn` -in each file, not from a chosen subset (§14). - -``` -extdeps.tools.jq jq_invocation_lower now lowers a JqJsonBinding instead of refusing it - JqCliOptionArgJson --argjson, cited long form, canonical spelling, lex rule - refusal causes JqBindingNameDuplicate | JqTextBindingUnwired | JqFileInputLoweringUnwired - binding argv --argjson as THREE words, before the program operand - binding spelling keys injective over (name, role): role prefix + name, duplicate names refuse - JqBindingLoweringUnwired DELETED (dissolution on climb) - -extdeps.bmc.openbmc_fan_control ObjectMapperServiceAt / ObjectMapperServiceCount operations DELETED - consumers rewired 2 (openbmc_object_mapper_services_rec, openbmc_sensor_service) - both now route through openbmc_object_mapper_service_at / _count (semantic JqInvocation) - argv authored by the domain after cutover 0 -- no --argjson, no -e, no -r, no argv position - residual foreign syntax authored 2 jq program rows (S12, mitigatable, trigger recorded) - callers match OpenBmcServiceObserved | OpenBmcServiceProjectionRefused (+ count sibling) -``` - -**Witness standing at cutover:** - -``` -test/claim/jq_invocation_lowering_witness 16 / 16 pass (lowering + S13 classification) - of which newly stateable and added 3 (json_binding_lowers_to_exact_argv, duplicate_binding_names_refuse, text_binding_refuses) - of which flipped from refusal to positive 1 (the old unlowered_bindings_refuse became json_binding_lowers_to_exact_argv) -test/claim/bmc_typed_operations_witness object_mapper_zero/one_cardinality + duplicate_matches pass -whole-corpus regen first_generation_equal=true -``` - -**What this receipt does NOT establish** — the table reads stronger than the lane's position: - -- Wave 1A lands the **local** `--argjson` vertical. File-input lowering and the first SSH - migration have since landed — see §18 (Wave 2 cutover receipt). `ProjectFanConfig` still carries - a raw argv (its sole consumer is dead code), and `JqFileInputLoweringUnwired` dissolved with the - file-input lowering. -- `JqTextBinding` deliberately refuses: no live consumer this wave, so it must not emit a plausible - guess (DESIGN §5). It lowers when a caller needs `--arg name value`. -- The ObjectMapper callers were verified against the bmc witness mocks; no live BMC was exercised. - -## 18. Wave 2 cutover receipt — the first SSH migration - -Executed on the session tree against `origin/main` (PR #10148). Counts are from enumerating -`^test fn` in each file, not from a chosen subset (§14). - -``` -extdeps.tools.jq jq_invocation_lower now lowers JqInputFile instead of refusing it - file-input argv the path is ONE bound operand AFTER the program operand - process input JqProcessNoStdin -- the file's bytes never enter a process channel - JqFileInputLoweringUnwired DELETED (dissolution on climb) - -extdeps.bmc.openbmc_password_ssh_transport - FanStepwiseCount operation DELETED at the root (raw sshpass/ssh argv: jq -er {config_path}) - RunCommand operation ADDED -- the RFC 4254 command-string carrier, fixed sshpass/ssh argv - note one command-string row declared; the caller never authors argv - -extdeps.bmc.openbmc_fan_control openbmc_fan_config_stepwise_count (semantic JqInvocation -> remote realization) - remote realization openbmc_remote_jq_command / openbmc_remote_jq_execute - stdin-fed remote jq TYPED refusal (OpenBmcRemoteJqStdinUnwired): sshpass -d 0 holds fd 0 - command string grammar-owned single-quote encoding (remote_exec_command_string), - never append(ssh_prefix, inner.argv) - interpretation PRODUCTION PATH PASSES Unknown AND REFUSES until a live target - confirms a POSIX shell (review 38602; openbmc_dropbear_interpretation - stays the pure emitter's witness-time understanding, not a - fabricated production assumption) - decode openbmc_fan_config_stepwise_count_result reads JqOutcome, - never success/stdout/stderr - -test/claim/jq_invocation_lowering_witness 16 / 16 pass (file-input lowering + refusal-cause split) - of which flipped from refusal to positive 2 (unlowered_file_input_refuses -> - file_input_lowers_to_exact_argv; and the file-cause - test became file_input_reaches_the_plan_as_no_stdin) -test/claim/remote_jq_ssh_migration_witness 12 / 12 pass (decode + words + command string + - stdin refusal + interpretation + review pin + - production-gate refusals) -test/claim/bmc_typed_operations_witness object_mapper/threshold/sensor tests still pass -whole-corpus regen first_generation_equal=true -``` - -**What this receipt does NOT establish** — the table reads stronger than the lane's position: - -- Wave 2 migrates the FIRST remote jq population (`OpenBmcStepwiseCount`). The other remote jq - operations (`FanMinimumDuty` … `FanInputAt`, `FanConfigValidate`, `FanConfigVerify`) still carry - raw sshpass/ssh argv; each is the same mechanical shape now proven — a semantic JqInvocation, - the remote realization, a JqOutcome decode — but the cutover is deliberately one site. -- The REMOTE half of the command-string path is UNEXECUTED against a live target. The hermetic - witnesses prove `emit_remote_shell` matches the cited IEEE 1003.1-2017 §2.2.2 single-quote - encoding exactly; they never prove the far side agrees. That is the declared, unexecuted - `remote_exec_command_live_confirmation` receipt in `gunbc.remote_shell_command` — run it against - a live OpenBMC/Dropbear target before the remaining remote population is cut over. -- **The first migration's production cutover is GATED on that live confirmation** (review 38602, - REQUEST_CHANGES): `openbmc_remote_jq_execute` passes `Unknown` and REFUSES until a live target - confirms a POSIX-shell interpretation — DESIGN §4b/§5, the unobserved assumption is never - fabricated into the accepted path. The migration is structurally complete and hermetic-witnessed, - but a live OpenBMC/Dropbear target must confirm the interpretation (and record it in - `remote_exec_command_live_confirmation`) before `openbmc_fan_config_stepwise_count` can produce - an observation instead of the gate refusal. -- `ProjectFanConfig` still carries a raw argv (its sole consumer `openbmc_fan_config_project_local` - is dead code). It needs file-input lowering (now landed) plus its own consumer migration; it is - not part of this wave's first SSH migration. -- `JqTextBinding` still deliberately refuses: no live consumer, so it must not emit a plausible - guess (DESIGN §5). It lowers when a caller needs `--arg name value`. -- The migrated decode was verified against witness fixtures; no live BMC was exercised. diff --git a/docs/plans/cli-run-reconcile-defork.md b/docs/plans/cli-run-reconcile-defork.md index 0eca50ae07c..044b84969ae 100644 --- a/docs/plans/cli-run-reconcile-defork.md +++ b/docs/plans/cli-run-reconcile-defork.md @@ -25,7 +25,7 @@ Three findings, each a reason to *design* rather than transliterate: 1. **CORRECTED (cursor review caught this):** `decl_facts`/`concept_decl_facts` (`src/v2/std/concept_index.dag`) is the wrong authority to cite — its `ConceptDecl`/`DeclFact` rows carry `{qualified_name, name, kind, node, rel_path}`, no import edges. The real structural surface already exists and is `.dag`-modeled: `cli_run.rs`'s `import_resolution_facts`/`module_declaration_facts` (~line 7012–7068) produce `ImportResolutionFact`/`ModuleDeclarationFact` rows, projected purely in `src/v2/lens/module_graph.dag`. So the redundancy is narrower than "cli_run.rs re-scans text instead of using decl_facts": `resolve_transitively`'s BFS in the *resolve/reconcile* path duplicates the closure `module_graph.dag` already computes over that fact shape (see #2). The `extract_import_paths` regex-scan is a real but *separate* lever — still live inside `import_resolution_facts` (the host builtin backing the modeled facts), not eliminated by this lane, and NOT gated on `decl_facts_project.rs`'s dissolution (unrelated fact shape, no import edges there either). 2. **The closure fold already exists in `.dag` — repoint onto it, don't re-author it.** `src/v2/lens/module_graph.dag`'s `import_closure`/`import_closure_live` already computes the transitive module closure over `ImportResolutionFact`/`ModuleDeclarationFact` (a `FreeMonoid`+fuel-bounded fixpoint, not `std.graph`'s `CallGraph` adjacency — a possible second closure-algorithm fork, worth a separate note to the operator, out of this lane's scope). `resolve_transitively`/`resolve_entry_graph`'s closure computation should call `import_closure_live` — not `std.graph`, not a new bespoke BFS, not a new `compile_module_graph.dag`; the modeled authority for module closure is `v2.lens.module_graph`, in-tree today. -3. **Three independent cache tiers, one undeclared authority.** `parse_cache` (keyed by file path), `typed_module_cache` (keyed by module *name*) and the cross-process `resolved_graph_cache` (keyed by content digest via `subject_digest_for_closure`) are three hand-rolled `HashMap`s with three keying policies for one question: "have I already computed this from this content?" `dsl/std/cache_interface.dag` is already modeled AND has real `CacheInterfaceCatalogFacts` inhabitants — `extdeps/realization/resolved_graph.dag` (the disk-tier `resolved_graph_cache` itself) and `extdeps/realization/parse_table_memo.dag` (an existing `locality: InProcess` sibling row — the exact shape `parse_cache`/`typed_module_cache` should take). So this is a de-fork, not greenfield inhabitance: the in-process tiers need `InProcess`-locality sibling rows under the same authority, and the disk-tier realization (`resolved_graph_cache.rs`) should be checked for a model↔realization fork against its own `resolved_graph.dag` row. Keying `typed_module_cache` by name rather than content is a live §5 cache-impurity instance ("key on declared-input content"); the disk tier already keys by content digest, and two policies for one fact is the §3 violation to close. +3. **Three independent cache tiers, one undeclared authority.** `parse_cache` (keyed by file path), `typed_module_cache` (keyed by module *name*) and the cross-process `resolved_graph_cache` (keyed by content digest via `subject_digest_for_closure`) are three hand-rolled `HashMap`s with three keying policies for one question: "have I already computed this from this content?" `dsl/std/cache_interface.dag` is already modeled AND has real `CacheInterfaceCatalogFacts` inhabitants — `extdeps/realization/resolved_graph.dag` (the disk-tier `resolved_graph_cache` itself) and `extdeps/realization/parse_table_memo.dag` (since deleted with the parse memo carrier, operator ruling R2 2026-10-08; at writing an existing `locality: InProcess` sibling row — the exact shape `parse_cache`/`typed_module_cache` should take). So this is a de-fork, not greenfield inhabitance: the in-process tiers need `InProcess`-locality sibling rows under the same authority, and the disk-tier realization (`resolved_graph_cache.rs`) should be checked for a model↔realization fork against its own `resolved_graph.dag` row. Keying `typed_module_cache` by name rather than content is a live §5 cache-impurity instance ("key on declared-input content"); the disk tier already keys by content digest, and two policies for one fact is the §3 violation to close. ## 2. Proposed phased authoring (do NOT attempt all of this as one PR) diff --git a/docs/plans/concept-index-enumeration-census-ledger.md b/docs/plans/concept-index-enumeration-census-ledger.md deleted file mode 100644 index c3660fac121..00000000000 --- a/docs/plans/concept-index-enumeration-census-ledger.md +++ /dev/null @@ -1,17 +0,0 @@ -# Census ledger: `concept_index_enumeration` (7 reds, one root) - -Parent lane: sunny-bat-82 (out-of-gate v2 silent reds). Pin at dispatch: `9bb74408bd`. Re-derived on current main. - -Classification: **(a) producer encoding vs type-name reader**, not (b) drifted claims, not (c) gone subject. - -| Row | Cause | Disposition | PR | -| --- | --- | --- | --- | -| `witness_fieldref_self_projection` | `member_edge_type_name` required an XL-2 binder; `concept_decl_facts` / `_live` marshal `name → Atom` | Read Atom identity when the binder reader is Absent; claims keep `String` | this PR | -| `witness_qualified_concept_self_projection` | same root (`qualified_name: String`) | same | this PR | -| `witness_local_record_enumerated` | same root (`alpha: String`) | same | this PR | -| `witness_parse_only_extract_reflects_canonical_field_types` | live and parse-only share `marshal_variant_arm_target` | same | this PR | -| `witness_parse_only_extract_reflects_perturbed_field_type` | same root; `beta: Int` was never the failing conjunct | same | this PR | -| `witness_coproduct_arms_live` | payload `{ alpha: String, beta: Bool }` is `conj_authored_payload_type_name` over the same function | same | this PR | -| `witness_coproduct_arms_parse_only` | same as live | same | this PR | - -Not Instantiation/`FreeMonoid`: marshal always stamps an Atom from the authored type spelling (`marshal_type_expr_ref`). Discrimination (`String` vs `Bool` vs `Int` vs `ConceptStruct`, coproduct payload vs nullary) is unchanged. Marshal wrapping through `binder_edge` waits on stage0 HOLD (#13388). `member_edge_type_node` is not relaxed. diff --git a/docs/plans/construct-tag-reference-design.md b/docs/plans/construct-tag-reference-design.md deleted file mode 100644 index 46b436823a1..00000000000 --- a/docs/plans/construct-tag-reference-design.md +++ /dev/null @@ -1,156 +0,0 @@ -# Construct tag as a declaration reference (qualified variant construction and patterns) - -Status: PR1 of two (model and migration census). PR2 is the cut. Owner lane: lively-eagle-657, parent -gentle-koi-724, reviewer neat-boar-16. - -## Symptom and the chain (DESIGN 6b) - -`src/v2/compiler/01_tokenize.dag` (`v2.compiler.tokenize`) is file-refused at native ingest with -`body_lowering_reason_unsupported_form` at `v2.std.diagnostic.Rejected { diagnostics: .. }` in -`lex_walk_artifact`, so none of its declarations enter the index and every importer refuses -`tokenize` as unbound. That is the single root of the native seven. quiet-hawk-702 established this -by execution on D1: the native resolve walk names only `tokenize` for all seven, while explicit -imports of admitted files bind. - -The chain behind it: - -- **Unqualified `R { f: e }`** lowers through `v2.compiler.body_lowering_fold` - `body_lower_try_record_literal` to `v2.std.node_query` `construct_node(tag: Symbol, ..)`, which - builds `Conj { R: Atom(R), f: e }`. A fielded pattern lowers to the same shape. -- **Dotted head `a.b.R { .. }`** never reaches that route. `body_lower_record_literal_tag_optional` - reads only one branded atom. The postfix read classifies the brace suffix as - `PostfixChainSuffixCarried` and declines it. Its own comments name the reason: a construct tag - is a `Symbol`, so a module-qualified tag has no representation. -- **The earliest unjustified boundary is therefore the construct carrier**, not body lowering. - -The same boundary is already wrong on the unqualified path, one stage later. `v2.compiler.resolve` -`resolve_pattern_node_walk` and `resolve_node_walk` resolve the tag atom through `resolve_atom`, -then `resolved_reference_node`, into `declaration_reference_node(path)`. The edge label stays the -leaf `Symbol`. After resolve, the readers that key on "the label equals the atom's identity" answer -Absent for every user constructor: `construct_tag_optional`, `v2.std.node` -`arrow_body_record_construct_conforms`, and `v2.std.compilers.target_model` -`target_value_expr_record_construct_gate`. The construct shape holds only before resolve and in -hand-built fixtures. - -## The model - -The tag edge stops being a nickname and becomes a reference. - -- **Construct** = `Conj { : , field edges.. }`. - - `construct_tag_marker` is a STRUCTURAL CORE MARKER in the typed `EdgeLabel` vocabulary of - #12473 (quiet-koi-814, ruling A; review condition from neat-boar-16). It names the edge's role - (constructor tag); the target is an ordinary authored reference. It is counted once in the - #12473 census. Landing order, agreed with quiet-koi-814: this lane's PR2 lands first, because #12473 is the - model document only and its cut has not started. PR2 therefore declares the marker beside - `declaration_reference_marker` in `v2.std.node`, in the same form. It also records the marker once, in - the Core-marker list of `docs/plans/edge-label-coproduct/README.md` (item 1 under "Who owns the - closed set") if #12473 has landed by then, or in #12701 otherwise. The #12473 cut converts it to - the Core arm `ConstructTag` together with the other markers. It is distinct from - `declaration_reference_marker`. If they were the same, a nullary `Rec {}` - would read as a declaration reference under `declaration_reference_body_marked`. - - The label is never the constructor's spelling. That removes the label-as-second-name (§3). -- **Reference form: one form, a qualified_name spine.** Before resolve, `` is the - authored `qualified_name` spine built by `v2.std.qualified_name` `qualified_name_spine_of_atoms` - from the authored segment atoms, so each segment keeps its token occurrence. An unqualified `R` is - the one-segment case of that spine: `fold_list_node` over one atom is the two-edge head/tail Conj - that `qualified_name_spine_shape_present` accepts. Readers do not branch on atom-or-spine. - - The ambiguity of a bare spine (an integer literal is also a cons list) is discharged by the - marker label, not by the spine's shape. -- **After resolve,** `` is `declaration_reference_node(path)` produced by - `resolved_reference_node`, the same carrier every other resolved reference uses. The path is the - VARIANT's own declaration path, never its owning coproduct (quiet-hawk-702's v1 trap). No new tag - type; the shared identity stays the declaration path (`std.decl_ref DeclarationRef` in v1). -- **Lowering never resolves or trims.** The construct producer receives the whole authored path. - Nothing takes the last segment anywhere on the route. -- **Resolution is one route.** A tag spine resolves in both walks through a single function that - reads the path with `qualified_name_from_node` and dispatches by length: - - two or more segments go to the existing `try_resolve_qualified_name_node`; - - one segment goes to the existing bare door, `resolve_atom` on its only atom. - - Both end in `resolved_reference_node`. The one-segment case is needed because - `try_resolve_qualified_name_node` answers Absent for length 1. -- **Nullary qualified values** (`v2.std.diagnostic.None`) are not constructs. They are dotted - references, which already lower to the spine and resolve through `try_resolve_qualified_name_node`. - PR2 adds a control proving they bind to the variant declaration. It changes that route only if - the control goes red. -- **Patterns** use the same producer and the same reference form for the constructor head. - `resolve_pattern_binders` keeps reading only the field edges. - -## Reader and producer census, with each migration - -Established by greps over `src/` and `dag/` (`*.dag` and `*.rs`) for `construct_node`, -`construct_tag`, `construct_field_edges`, `RecordConstructBody`, `record_construct`, -`TargetRecordConstructShape`, and the label-equals-identity comparison. No Rust code reads the v2 -construct shape. `src/v1/stage0` `coproduct_reflection` emits the unrelated v1 -`record_construction_spelling` edge. - -| Module | Symbol | Role | Migration | -|---|---|---|---| -| v2.std.node_query | `construct_tag_edge`, `construct_node` | producer | Take the reference node (a spine) in place of `tag: Symbol`, and emit `Named{construct_tag_marker}` targeting it. | -| v2.std.node_query | `construct_tag_optional` | reader | Key on first-edge label == marker, and answer the reference node (a pre-resolve spine or a post-resolve marked reference). It no longer answers a Symbol. | -| v2.std.node_query | `construct_field_edges` | reader | Unchanged apart from the gate. | -| v2.std.node | `arrow_body_record_construct_conforms` | well_formed gate | First edge is the marker, targeting a spine or a marked reference, and every other edge is Named. Fixes resolved bodies being rejected today. | -| v2.std.node | `classify_arrow_body_form`, `declaration_reference_body_marked` | classifier | Classify a construct by the marker explicitly, not as "any unmarked Conj". | -| v2.compiler.body_lowering_fold | `body_lower_try_record_literal`, `body_lower_record_literal_tag_optional` | producer | Read the head as a qualified-name spine (bare or dotted) and pass it to `construct_node`. The brace suffix leaves `PostfixChainSuffixCarried`; the declined arm and its advisory for this suffix are deleted. | -| v2.compiler.body_lowering_fold | constructor-pattern lowering (the `construct_node` call in the pattern arm) | producer | Same: the spine from the pattern head. | -| v2.compiler.body_lowering_fold | `body_lower_is_core_substrate` | reader | Through the migrated `construct_tag_optional`. Check the order against the `qualified_name_spine_shape_present` test. | -| v2.compiler.resolve | `resolve_pattern_node_walk`, `resolve_node_walk` Conj arm | rewriter | Resolve the marker edge's spine through the one length-dispatching function above. Field edges walk as today. Check the `resolve_ctx_snoc_position` side effect now that the label is the marker. | -| v2.compiler.resolve | `resolve_pattern_binders` | reader | Unchanged apart from the gate. | -| v2.compiler.infer | Conj gather arms (`infer_gather_*`, `infer_product_facts_from_entries`) | reader | The marker edge must not be read as a product field. Skip it by its TYPED edge role, never by the label's spelling, and type the construct by the referenced declaration where a row exists. Today the tag is `infer_gather_fold_not_derived`. | -| v2.compiler.eval | `eval_callee_body_refusal_reason` | reader | Refusal only; no change. | -| v2.std.compilers.target_model | `target_value_expr_record_construct_gate`, `target_project_record_construct` | reader | Gate on the marker. `type_name` comes from the resolved declaration's path, spelled through the target's binding rows, not from an atom identity. This admits resolved constructs the gate refuses today; that is a behaviour change, and it is named. | -| v2.std.compilers.target_model | `target_value_expr_arrow_has_record_construct_body`, the arm body projection and match-arm slot readers | reader | Follow the gate. | -| v2.std.compilers.target_model | `TargetRecordConstructShape` decode; the `record_construct_form` rows in extdeps.languages rust, typescript, c and dag | token rows | No change. | -| v2.extdeps.languages.dag | `dag_rec_construct_body_node`, `dag_variant_holds_construct_node` and their users | fixture | Rebuild through `construct_node` with a spine. | -| v2.lens.machine_shape | `constructed_tag` | reader | Compare the resolved declaration path of `MachineShape`, not a leaf Symbol. | -| tests: `variant_field_lowering_test`, `wildcard_pattern_form_test`, `arrow_body_form_witness_test` and `arrow_body_form_semantic_helpers`, `record_construct_emit_test`, `rust_record_construct_emit_test`, `rust_record_construct_emit_host_equals_eval_test`, `reference_conservation_test` (occurrence counts), `value_position_whole_read_test`, `declaration_graft_assemble_test` | | test | Rebuild each fixture with the marker, and recount occurrences (segments keep their token occurrences). The unmarked reference-shaped negative must still go red. | -| recurring_failure_mode and rung_drop prose (`postfix_suffix_step_declined_to_the_pre_existing_arms`, `call_expression_erased_at_v2_body_lowering`), `docs/plans/lowering-occurrence-projection-design.md` | | prose | Update the prose. Retire the brace-suffix population of the rung drop by its trigger. | - -Not readers of this shape: `v2.std.data_initializer_identity`, `v2.std.decl_facts_skeleton` and the -v1 `record_construction_census` fixtures all read the v1 reflection edge. The orchestration -`construct_tag:` fields are an unrelated name clash. - -## One decision for a dotted path (ruled by gentle-koi-724, 2026-09-29) - -Whether a dotted path is a qualified name or a field projection on a local is decided in ONE place: -`v2.compiler.resolve` `try_resolve_qualified_name_node`, by `qualified_head_bound_on_chain`, which is -the scope of the first segment. eager-newt-412's lane owns the projection arm there. This lane only -consumes that door. A construct tag whose resolved answer is not a constructor declaration (or a -kernel canonical atom) refuses `resolve_reason_construct_tag_not_a_constructor`. The door's projection arm is gunbc#12506's -`resolve_bound_head_projection`. The LOCAL wins: a bound first segment shadows a whole-path -declaration, so the bound-head check precedes any declaration lookup, and that change is made inside -#12506. Whichever of #12506 and this lane's PR2 lands second adds the control. It is one module with -`artifact.tree` (a match binder), `v2.std.diagnostic.Rejected { .. }` (qualified), and a local -shadowing a module segment. - -## The live defect on main - -Filed as `gunbc.recurring_failure_mode` -`construct_tag_read_by_label_equality_after_resolve_rewrote_the_target`. Confirmed by execution: a -resolved `PcrA { n: true }` is `Conj { PcrA -> v2.test.pc_provider.PcrA, n -> true }`, -and `construct_tag_optional` answers Absent. The well_formed and target_model gate arms are inferred -from their definitions, and the row labels them that way. - -## PR2 (the cut) and its evidence - -All readers migrate in one motion, and no `Symbol`-tag path remains. Controls, fed by production: - -1. `a.b.R { f: e }`, `a.b.C` (nullary) and `a.b.R { f: x } =>` each lower and resolve to the same - node as their unqualified form under an import of the same declaration. This is a content - equality check. -2. Two variants with the same leaf in different modules, constructed qualified, bind to different - declaration paths. -3. Mutations go red: dropping the qualifier binds the other module's variant or refuses as - ambiguous, and dropping the record body changes the node. -4. The variant is bound, not its coproduct. -5. A qualified tag whose last segment names a TYPE rather than a variant, or a variant of a - different type, refuses at a located site and never resolves by spelling (neat-boar-16). -6. The target_model gate is shown by a discriminating pair: a resolved construct is admitted, and - an unresolved or wrong-variant construct still refuses at that gate. -7. The seven's actual form, `v2.std.diagnostic.Rejected { diagnostics: .. }`, is the positive - control on the production route. -8. The live defect below has a control that is RED on main (a resolved user construct read by - `construct_tag_optional`), turns green in PR2, and stays enrolled as the regression control. -9. `01_tokenize.dag` is admitted on the native route. A base-vs-head census (the #12550 recipe) - shows the newly admitted files and no unexplained new refusal. quiet-hawk-702 runs the native - seven against the head. diff --git a/docs/plans/convergence-one-program.md b/docs/plans/convergence-one-program.md deleted file mode 100644 index 10b9f73bada..00000000000 --- a/docs/plans/convergence-one-program.md +++ /dev/null @@ -1,300 +0,0 @@ -# CONVERGENCE-ONE — program record at wind-down - -One convergence protocol and one fleet authority for persistent host effects. A subject is -**selected**, **observed goal-blind**, **assessed** against the goal, **planned**, **admitted -against the observed basis**, **applied**, **independently read back**, and given a **terminal -verdict**. Anything that calls itself convergence without running that lifecycle is a second -convergence algebra — a §3 authority fork — and the program's job was to end it. - -This page is the record at the point the program was wound down (2026-09-20) under system -pressure, so that v1 performance and v2 migration take priority. It exists because the state -was held in a session's pinned plan and in seven lanes, none of which survive. Its roadmap home -is `fleet-closed-loop-converge`; the two remaining cuts that have no home of their own are -`fleet-convergence-census-closure` and `fleet-convergence-legacy-deletion`. - -## What landed - -- **C0** — the census of persistent host effects; dual-home made unwritable. -- **C1** — the canonical protocol bound; `ensure` is a projection of it. -- **C2** — one selected LiveDeploy roster. `deployment_spec_srv1` has exactly one home. -- **C3** — LiveDeploy scope, member basis, plan-side frontier discharge; and, in its remainder, - the request arm binding `DesiredDeployment.selected_identity`. -- **C4** — the apply frontier row, honestly **awaiting** rather than falsely discharged - (see *An undischarged frontier* below), plus a production gate on the protocol being bound. -- **C5** — readiness defork and the canonical readiness algebra. -- **C6** — owned roots folded into the member model. - -## Ready but unmerged when the program stopped - -Six pull requests were CLEAN with every check green, waiting on an operator merge: - -| PR | Cut | What it carries | -|----|-----|-----------------| -| #11678 | C9a.1 | GCP Secret IAM: read the policy back **independently** after the write | -| #11676 | C7 | provider-state root as a `live_deploy` ensured member, read back — **now DIRTY** | -| #11689 | C11 | GitHub reads rebound onto the canonical join — **parked, see below** | -| #11732 | C9b | App-key rotation: exact-version verifier, contract, `DisableOnly` — **MERGED** | -| #11795 | — | `RungDropAmendment` carrier, so a later ruling on a standing drop has a home | -| #11828 | — | `mtcollins1_boot` narrowed to `FleetSshKeyNotConsumed` | - -**#11689 is parked for the same reason, one rung further on.** It is complete and correct at -`2e63f9d53`: the rebind is done, and a scratch regeneration driver that an earlier `git add -A` had -swept in — no consumer, re-minting a generated workflow path as a literal — was found by review and -deleted, so the PR no longer carries a defect that would land. It then went DIRTY again within the -hour on the contended pair. It is left open, not closed. - -**#11732 is parked green, and parking it leaves a real gap open.** At `e0c453f961` it had one -approval, no change requests, and all four checks green by name; its eighth review cleared the -thing most likely to be wrong in such a cut — that the frontier row names the add-version and -disable-prior *capability* rather than letting the verifier's own existence retire it. It was -stopped by queue position on the contended pair, twice. The consequence, stated so it can be -weighed rather than forgotten: **`ci-github-app-private-key` still has no rotation deadline, no -restore test and no rehearsed compromise answer on main, and nothing in the corpus can answer -"does the key in version N actually work."** That key is the root of trust for every org-admin -action CI takes. - -**#11676 (C7) MERGED.** The provider-state root is an ensured member derived from the spec, so the emitted ensure and the read-back member come from one function and cannot diverge; absent, duplicated, wrong-owner and not-a-directory have no constructor, so the first cut's refusal arms were deleted rather than left permanently green. - -**The launcher deletion is the next cut, and its census is already on main** as an annotation on `deployment_provider_state_step`. For `srv1_live` the provision plan's `DashboardEnsureProviderStateRoot` is now REDUNDANT — that is the op the deletion may remove. For `srv1_lab`, `srv2_lab`, `srv2_deploy` and `macbook_local` it remains the ONLY creator: their specs carry the member, but no production caller applies those specs, so the member creates nothing for them yet. The op may be deleted for an instance only when a production caller applies its spec; deleting it sooner is the serve-binary deadlock again. - -**#11828 must not merge on checks alone.** Its acceptance is a wet dispatch of -`fleet-converge mode=mtcollins1_boot`: if that mode does need the fleet key, it fails at the -point of use, and discovering that on main is strictly worse than discovering it before. - -**C9b's verifier has never run against the live surface.** All fifteen witnesses supply an -observation record; none executes the real mint, which needs the fleet-converge runner, the -federated principal and the live App. Three shapes are therefore *readings* of upstream rather than -observations: that the Secret Manager response carries the resolved version in its name field, that -`curl -D` writes a status line whose second word is the code, and that the mint step's refusal arms -leave their record lines before exiting. **The first dispatch of `app_key_version_verify` is the -inhabitance claim for that whole cut**, and it is cheap — read-only, one installation-token mint, no -mutation. If it refuses with an absent mint observation on a version that plainly exists, suspect -the record wire before the mint. Its actuation half (add-version, disable-prior) is blocked on -`gunbc.github_actions_wif` being absent from main: on a runner there is no federated identity for a -Secret Manager write, so the only honest route today is an operator workstation. - -## Two operator acts C9b left open, one of them dated - -**`rotate_by` on main is `2026-12-18`, and it is a placeholder a lane chose rather than policy.** -The verifier reads it on every run and refuses once it passes. So if nobody sets it, the first -thing that happens on that date is that a green verify run turns red with a deadline-passed -refusal. That is the honest behaviour, and it is also a trap for whoever meets it first: **the fix -is a policy decision and a rotation, not a code change.** - -**Nothing in that cut has run against the live surface.** Every witness supplies its own -observation record. The first dispatch of `app_key_version_verify` against the currently enabled -version *is* the inhabitance claim for the whole thing, and it is cheap and safe — read-only, one -installation-token mint. Three shapes are readings of upstream rather than observations, so that is -where a first-dispatch failure will be: that the Secret Manager response carries the resolved -version in its name field, that `curl -D` writes a status line whose second word is the code, and -that the mint's refusal arms leave their record line before exiting. A refusal reporting an absent -mint observation on a version that plainly exists means the record wire, not the mint. - -**The actuation half is blocked on identity, not on design.** Add-version and disable-prior with -independent readback need a federated identity for a Secret Manager *write*, and -`gunbc.github_actions_wif` is still absent from main — so a runner has none, and the only honest -route today is an operator workstation. Reaching for a print-token on a runner fails at runtime -rather than degrading, which is why the lane did not. - -## Still held, and on what - -- **C8 — wet apply.** Held until three proofs close: temporal order (control 6), Baseline vs - PostApply (control 7), and apply-script path disjointness. C8 must **execute** a nonempty - `ReleaseEffectPlan`; until it does, that terminal answers `AwaitingCapability`. C8 also owes - the discharge of C4's frontier: naming its projection-routed apply carrier moves that row off - a prose condition. Note the cost trap recorded by C4's lane — a bound discharge needs an - observed population, and computing it costs ~60s over the live tree, so it must not go on the - plan route blindly. -- **C9 — census closure by effect family.** The denominator is every non-terminal row in the C0 - census, not a keyword search. One effect family per change, each answering the same seven - questions: who selected the desired state, what independently observed reality, what assessed - the difference, what exact plan was admitted, what authority executed it, what independently - read it back, and what old path was deleted or renamed. C9a (GCP IAM) was pulled forward - because it touches production; C9a.2 (the bare-grant authorization closure) is unstarted. -- **C10 — deletion and the recurrence wall.** Deliberately last, and performs no new semantic - migration: it deletes superseded routes and makes recurrence mechanically visible. Its gate - must join against an independently discovered producer population, never a search for - suspicious names. - -## Unowned residue - -- **C5's poll Ready-exit** is recorded unproven. It closes when a fixture can supply `Ready` - into the poll outcome without materializing the live site. -- **The org-admin credential admission surface** was censused and the answer is DELETE, but the - deletion was never written: that lane produced analysis only and has **no PR**. Its findings - exist nowhere else, so they are here. - - Obligations 1–8 are met or dissolved by the live App-token path, and two carry the argument. - **Genealogy is enforced by construction**: the mint is serial and each link exits on failure, - so a token cannot exist with an incomplete genealogy — deleting a rung-2 validation that a - rung-4 construction replaced. **Capability sufficiency is enforced better**: an unconditional - probe that refuses on the real response proves more than a declared capability list, which is - exactly why the dead model carried `CredentialOperationProbeRequired`. - - **The sharpest fact in the census**: `ci_spec`'s own annotation says the interim PAT step *"was - never taken, and it was never necessary."* So the whole `SecretMaterial` surface — custody, - genealogy, rotation contract, continuity due-dates — modelled the lifecycle of a credential - **that was never minted at all**. Not superseded; never inhabited. That turns the deletion from - a cleanup into evidence about how the model got there. - - **The delete set is grep-verified, not refusal-verified.** Under §3 the deletion *is* the - census, so treat the set as a hypothesis and let the compiler refuse. Six declarations are - already dangling on main today with no consumer at all, including the witness. - - **Keep** `org_admin_app_key_unreadable_message`, `org_admin_installation_token_refused_message` - and the two arms they name: `ci_spec`'s prelude renders them and a witness checks it. They are - a declared frontier with a stated trigger, not dead code, and a dead-code sweep would wrongly - take them. Do not touch `extdeps.github.org_admin_auth` — faithful upstream modeling with four - live importers. - - **Open, for the operator**: `docs/plans/org-admin-credential-acquisition.md` is the prose home - of this model. Deleting it orphans two live annotations that cite it; keeping it leaves a plan - document for a credential that was never minted. -- **#11760 would re-fork the authority #11795 just consolidated.** It still carries the postscript - text inline on the drop's `restoration_trigger`. If it lands after #11795, that string must be - deleted and replaced with a row under `dag/gunbc/rung_drop_amendment/`. Its author agreed. This - is the only known change that would undo that consolidation. -- **The amendment witness is enrolled on no lane at all.** Its four claims were executed on the - committed tree, but not through a claim harness: `claim_executor` has no per-module selection, - and a whole-floor run is a CI job rather than a session one. They were run via `gunbc run - --function`, which refuses to map a `Bool` to an exit code and *prints the value in the refusal* - — a real execution of the real function, but not a harness run. Since required CI runs no claims, - nothing today would notice if that file went red. If claims return to the merge path, it belongs - in the first batch. -- **A read-write-re-read fold is unfalsifiable under the current replay harness**, and this is a - frame-level gap rather than a defect in any subject. REST replay keys a fixture by exact - invocation, and the first read and the re-read of such a fold are the *same* invocation — same - operation, target and input digest — so no fixture set can answer 200 to one and 403 to the - other. Sequencing is not expressible at all. Worse, an invocation's `input_digest` is minted - host-side over the bound parameter environment and has no `.dag` constructor, so an authored - fixture cannot reproduce it for any operation that takes inputs; the existing probe matches only - because its operations take none. This is why C9a.1's unreadable-re-read stop is declared - unexecuted for its route. It belongs to `v2.std.witness_evaluation`, and closing it is what - would let that stop be executed rather than argued. -- **The projected `restoration_trigger`** on the namespace-wave-admission drop does not state - that restoration half (1) was built and then deleted; #11795's amendment supplies that context - in the projection, so check the rendered page before re-opening it. - -## Findings worth keeping - -These cost real time to establish and are not derivable from the diffs. - -**An undischarged frontier was unwritable.** `converge_apply_for_host` calls `host_effect_apply` -directly and never the projection, so no honest consumer exists before C8 — but `FrontierStanding` -carried only *honest* and three defect arms. The corpus forced a choice between a citation that -lies and a protocol that can never bind, whose gate would then refuse every healthy run. C4 -resolved it by inhabiting `std.dissolution`: an awaiting arm that can never be read as discharged, -refusing when the capability has arrived and the row was never updated. The closing sentence is -**derived** from the row's standing; authoring it would have been a second authority for -retirement. - -**A successful actuation return cannot mint convergence.** The GCP IAM writer classified the -policy returned *by its own write* and greened. The repair reads the policy back independently. -The load-bearing control is the one that fails when the write response carries the grant and the -re-read does not. - -**Observation must be goal-blind.** `decode_registration_json` took the expected App identity -*into the decoder*, so a valid response describing another App was filed as *unobserved* rather -than observed-then-diverged — a known mismatch laundered through the refusal arm. - -**Check that a check can fail.** Two witnesses were deleted rather than kept: one asserted that a -prose sentence contained certain words while its name claimed a structural impossibility, and one -tested a state that, once the member was derived from the spec, had no constructor at all. A -permanently green check is worse than none, because it gets cited as coverage. - -**A stale-plan fingerprint must carry identity.** The LiveDeploy baseline hashed member *labels*, -so a changed executable digest, tree revision, dirty tree, unit document, running release or root -owner all left it unchanged. Caught by an external review after two approvals, including mine. - -## A first-apply risk C8 must clear before it touches srv1 - -C7 models the provider-state root as an ensured directory, and the emitted command is -`install -d -m 0755 -o -g `. **`install -d` chmods and -chowns a directory that already exists.** The mode is a constant of the ensured-directory arm, -not a fact read from the host, and nothing in the model refuses a widening. - -So if srv1's provider-state root is currently `0700` — which is what a provider CLI creating its -own state directory under a umask would plausibly leave — **the first real apply silently widens a -credential directory to world-readable**, and the same command re-owns it if the live owner -differs. This was never verified against a host, because no wet effects were permitted; it is an -inference from reading both code paths, and its author flagged it rather than letting it pass as -settled. - -**Before C8's first apply: stat the live directory** and compare its mode and owner against what -the ensured step emits. If the observed mode is tighter, the ensured-directory arm needs a mode -carried per subject — the `ManagedDirectory` derivation — **before** anything applies, not after. - -**A typed outcome on the policy read is a fail-open, and the next reader will re-derive it.** The -obvious way to make C9a.1's inaccessible arm a typed value is to declare `RestOutcome` on -`GetSecretIamPolicy`. Do not. The other caller reads the policy in the node-pattern form, which -cannot branch, so a failed read there returns an empty policy with a blank etag that flows -straight into `SetSecretIamPolicy` — an unconditional whole-policy overwrite of a live IAM policy, -driven by a read that never happened. That is strictly worse than the defect C9a.1 repairs. The -ordering is therefore fixed: make that caller an `EffectPlan` step that can refuse **first**, and -only then declare the outcome. - -## Process facts a later program will need - -- **The required CI check builds the compiler and runs no witnesses** (#11742), and the - namespace-wave-admission gate was removed (#11774). A green check means *it compiles*. Lanes - must run what they claim and say what they ran. Three composition breaks reached main in one - day — each PR green against its own base, red only in composition — and the repair commits say - so in their titles. -- **A PR failing the generated-artifact gate self-heals.** Since #11791, `heal.yml` is - dispatch-only and the repair moved into the required run's fleet lane: the branch picks up a - `gunbai-bot` commit within ~10 minutes. Authors must **wait for the publisher**, never - hand-produce the bytes and never push over a sealed candidate — doing so invalidates it and - restarts the cycle. -- **Heal regenerates `.github/workflows` but cannot publish it, and nothing detects that.** It does - push — a heal commit landed `.gitattributes` and `docs/design-rung-drops.md` on one branch — so - the constraint is *workflow paths specifically*, consistent with an App token that cannot write - them. On one head heal reported success, logged writing the 89,904-byte workflow in its own - workspace, and emitted a candidate manifest holding only the two non-workflow paths. Since #11742 - removed the regeneration gate from PR CI, a stale workflow projection is now **invisible**: that - drift survived three pushes and four green heal runs and was caught only by reviewers. Any PR - changing a `ci_spec` entry target inherits this, and must carry its own derived bytes. -- **`fleet_converge_workflow.dag` and its generated `.yml` are the contended pair**: every lane that - adds a dispatch mode touches the same two rows, so PRs there go DIRTY on queue position rather - than on any defect. A conflict region there can split a step, and a keep-both resolution is not - safe by reading — the class and its fourth specimen are rostered at - `gunbc.recurring_failure_mode.merge_region_excludes_shared_tail`. -- **The verification that discriminates, for anyone hand-committing a projection:** push, then read - heal's repair-candidate *manifest* on that head. An empty entry set against two entries on the - prior head is a real signal. A byte count matching heal's own is corroboration only — a different - delta could preserve length. -- **No session can execute a `gunbc run` remotely.** BuildBuddy executors expose no cgroup memory - limit, so `gunbc.host_budget_source` refuses with `HostBudgetUnreadable` before planning; a lane - that bound a limit by hand got SIGKILLed instead. Briefs that offer "CI or one remote dispatch" - as the escape from local memory pressure are offering something that does not exist today. -- **The local regeneration hazard is LOAD, not a ceiling.** The same command made no progress in - 40 minutes at host load ~270 and finished in about 20 at load ~66, writing correct bytes. Each - `gunbc run` loads the whole corpus, so runs must be serial — running claims alongside a regen - makes both thrash. Do not conclude the local arm is dead; check the load first. -- **A workflow run's branch column is not the ref it built.** `heal.yml` checks out an input SHA, - so the branch column names where the dispatch fired. Reading it as the built ref produced a - false "main is red" alarm and a dispatched repair lane against innocent substrate files. -- **`git merge origin/main` in a session worktree is dangerous and looks fine.** The worktree is a - shallow clone, so the merge base is wrong. On one lane it reported two trivially additive - conflicts and produced a commit that reverted 99 files and 6,886 lines of other lanes' work — - including `witnesses.yml` back to a pre-#11742 revision, so CI then ran an old job roster and - went **green on a revision that had reverted main**. The safe recipe: `reset --hard origin/main`, - check out your own files, then assert that every deleted line in `git diff origin/main` is one - you wrote. -- **A session worktree is a shallow clone, and git lies about it confidently.** Two lanes hit this - with different symptoms: one was told it was 231 commits *ahead* of main and not an ancestor - (truth after `git fetch --deepen=200`: 0 ahead, 26 behind, ancestor yes), the other had a merge - refuse with *unrelated histories* while GitHub's compare API reported eleven ahead and four - behind. It also re-shallows, so the false readings come back. Run - `git rev-parse --is-shallow-repository` before believing any ancestor or count answer — a missing - merge base here is a fetch-depth artifact, never a force-pushed main. -- **Do not revert main's generated-artifact drift out of your own PR.** The auto-heal bot pushes - exactly that regeneration back onto your branch, and its commit **resets the approval tally**. The - only real choice is whose commit carries the bytes, and the bot's is better because it is - attributable. -- **`extdeps.time.rfc3339` models no arithmetic and no parse**, only a conditional lexical - comparator. So a period-shaped deadline (`create_time + N days`) is not expressible, which is why - C9b's rotation contract carries an absolute `rotate_by` that a completed rotation must move - forward by hand. A period first owes rfc3339 parse and arithmetic. -- **A one-arm coproduct is not a coproduct.** `type X = OnlyArm` — a name, equals, a single bare - arm, no pipe — parses as a type *alias* to an unknown name and fails with *name not found in - module*, attributed to the importing module rather than to the declaration. It cost a lane a - cycle. C9b's restore cadence became a record naming the consumer that discharges it, which is - better anyway: the intent builds its roster from that record, so the field is read rather than - declared beside the thing it describes. -- **Merge order is not a preference.** Out-of-order merges broke sibling cuts four times, and none - of it was a defect in the changes. diff --git a/docs/plans/corpus-acquisition-design.md b/docs/plans/corpus-acquisition-design.md deleted file mode 100644 index 6fc197a066f..00000000000 --- a/docs/plans/corpus-acquisition-design.md +++ /dev/null @@ -1,533 +0,0 @@ -# Corpus acquisition: the listing root cut, the descent measure, and the symlink rule - -The corpus manifest landed in gunbc#10445 as a carrier with no producer: `store_corpus_manifest` -builds one from entries a caller already holds, and nothing builds one from a working tree. This -document is the design for that producer, settled **before** any of it is written. - -It exists because the survey turned up two modeling questions the SCM brief does not answer, and -answering them unilaterally is how the previous PR acquired five rounds of findings. Both were put -to external review and ruled on; this records the ruling and the facts that forced it. - -## 0. The two questions, and what the survey actually found - -**Q1 — where does enumeration live?** `extdeps.filesystem.filesystem_io` returns a directory listing -whose `entries` member is a raw newline-joined `String`, with membership answered by a substring test -over that blob. It carries no file-versus-directory discriminator, so a corpus walk cannot ask the -one question it must ask at every entry: descend, or read. - -Three facts qualify that, and the first two cut against widening: - -- **No existing consumer enumerates.** Every one of the eight modules that consume the listing feeds - it straight into `filesystem_entry_presence`, which answers *membership* — is this one name in this - directory. Not one iterates the entries; not one needs `EntryKind`. ~~The blob is adequate for every - consumer that exists.~~ **SUPERSEDED — see §10.** The blob is not adequate for its existing - consumers: its producer silently narrows the population it returns, so the membership answers built - on it are unsound on two paths. That existing consumers ask only membership does not save a - population the producer already narrowed. -- ~~**So enumeration is a new capability, not a failing carrier.**~~ **SUPERSEDED — see §10.** It is - both. Enumeration and `EntryKind` are new capability; the listing is *also* a failing carrier, and - the failure is live on main. The repair therefore comes first and does not wait on the capability - that discovered it. -- **But the blob is a workaround whose justification has lapsed.** `filesystem_io` justifies the - substring test by saying newline-wrapping makes the test line-exact "without needing a split the - interpreter does not offer". That is false today: `split` is a builtin string method — declared in - the seed at `std_algebra` and dispatched by the v1 interpreter as `MethodCallSplit`, requiring no - import — and `extdeps.languages.markdown` `md_split_blocks` calls `split(s:, delimiter:)` on a plain - `String` now, with further splits in `extdeps.git.object_store` and `extdeps.systemd`. - -The deciding argument is `filesystem_io`'s own. The comment above `filesystem_listing_names_entry` -pulls the membership predicate down beside the operation because two consumers had spelled it -independently over one wire format, "which is the second representation section 3 forbids: the -encoding is `List`'s fact, so the membership question over it is too." That argument does not weaken -when the question is enumeration instead of membership. Either the structure belongs at the -authority, or that comment is wrong about membership. - -**Q2 — what does the walk descend on?** DESIGN §4 makes execution bounded and forward, with -termination *checked* rather than discovered. A directory tree offers no static bound and, with -symlinks, not even a guaranteed finite one. - -## 1. RULING — root-widen the listing at its authority - -Replace the lossy listing representation where it is produced and migrate its consumers together. -Names-only consumers project names from the richer result; they acquire no interest in ingestion, -MIME types, or semantic source validity. The unrelated read consumers do not migrate merely because -their operations share a module. - -The migration population, corrected after the first census undercounted it: - -| module | how it reaches the listing | -|---|---| -| `gunbc.generated_artifact_observation` | directly | -| `gunbc.deploy_transition` | directly | -| `gunbc.scm.init` | directly | -| `gunbc.roadmap.roadmap_verification_receipt` | directly | -| `gunbc.roadmap.roadmap_publication_helper` | directly | -| `gunbc.fleet.fleet_converge_plan_cli` | directly | -| `gunbc.devboot.build` | directly | -| `gunbc.roadmap.roadmap_belt_actuate` | directly | -| `gunbc.fabric.fabric_allocation_store_substrate` | only `FilesystemEstablishedAbsence` | -| `gunbc.scm.repository_save` | only `FilesystemEstablishedAbsence` | - -The first census reported eight. It keyed on the listing's *type and constructor spellings*, so it -found every module that names the listing and missed the two that reach it through the derived -absence carrier. Those two move only if the widening changes the shape of `FilesystemEstablishedAbsence` -rather than the blob behind it, so they may be zero-diff — but they are in the subject population, and -a filter whose vocabulary is narrower than its subject reporting its own reach as the population is -the partial-observer class this lane keeps finding. It is recorded here rather than quietly fixed. - -The cut is not eight call expressions: the listing folds, the witnesses, the transport realization -and the emitted representation belong to it too. - -### Two properties must survive the replacement - -**Enumeration failure stays distinct from successful exhaustion.** Opening a directory successfully -does not establish that enumerating it succeeded — a host iterator can fail while advancing. A result -accumulated before such an error must not become a complete listing, and so must not establish -absence for an entry it never reached. This is the absorbing-fallback rule (§5) at the listing -boundary: the failure arm refuses, it does not widen into "not there". - -**Entry names and kind observations stay unconflated.** An entry's name can be known while observing -its kind fails. That entry is neither dropped nor called `Other`; the two facts are separately -carried, because the host interface itself separates a name from a fallible type observation. - -And no newline-delimited membership may survive underneath the structured result as an independent -authority. `filesystem_listing_names_entry` is deleted by this cut, not kept beside it. - -### A consequence the cut inherits: two of `FilesystemEntryName`'s refusals lose their reason - -`admit_filesystem_entry_name` refuses a name containing `\n` and separately one containing `\r`, and -both refusals are justified **by the wire format** — the newline is `Filesystem.List`'s delimiter, so -such a name could make the membership test answer about a different spelling. Under a structured -listing that delimiter does not exist, and those two justifications lapse with it. - -That does not automatically mean the refusals go. It means each must be re-decided on its own -grounds: either the structured representation preserves such a name losslessly and the refusal is -deleted, or the refusal stays as an explicit *unsupported spelling* with a new stated reason. What it -may not do is survive carrying a justification the cut has falsified — which would be this same class -a second time, created by the repair rather than found by it. - -The `/`, `\` and NUL refusals are untouched: path traversal and C-string truncation are facts about -the host, not about the listing's encoding. - -## 2. RULING — layering: acquisition, captured inputs, pure construction - -``` -bounded filesystem acquisition - ↓ -explicitly scoped captured inputs - ↓ -pure corpus construction / semantic ingestion -``` - -Build **both** layers. The already-enumerated interface is not eliminated by the root cut — it is -placed correctly. An explicit input list is the right subject for the pure operation, and it is *not* -evidence that anybody enumerated a directory successfully: a caller may intentionally select ten -files, which is a complete selection of those ten and not a complete capture of their parent. - -The failure mode to refuse explicitly: using the lower layer to claim the upper producer has been -delivered. - -## 3. RULING — finite work fuel is the descent measure - -Admit a finite, nonnegative budget supplied by the acquisition policy. The walk carries `R(s)`, -remaining work fuel. Every continuing discovery transition establishes `R(s) > 0` and -`R(s') = R(s) - 1`. - -That is a well-founded arithmetic descent even though discovering one directory adds several pending -children: **the worklist may grow; the rank does not.** - -``` -all acquisition obligations discharged → completed capture -obligations remain, no work fuel remains → located budget-exhaustion refusal -obligations remain, fuel positive → one bounded transition, continue with less fuel -``` - -Prohibited: resetting the budget per sibling, replenishing it when a new directory appears, or -charging only newly discovered inodes while another continuation path runs unbounded. Each loop needs -its own demonstrable descent or must share the globally decreasing measure. - -The refusal says *the admitted budget was exhausted before completion was established*. It does not -claim the corpus contains more than some number of files — that number was never observed. Reaching -the numeric limit after every obligation is discharged is not a reason to refuse. - -**The budget is operational, not part of corpus selection.** Raising it must not change the identity -of a successfully captured unchanged corpus, and exhausting it must not mean "this now denotes the -prefix we happened to read" — that is the absorbing fallback wearing a limit's clothes. - -### Why the three candidates I proposed are not this - -| candidate | honest role | what it does not establish | -|---|---|---| -| remaining depth | can prove recursive descent when each child enumeration is a finite admitted input | any bound on total discovery work, directory width, or I/O | -| visited-object set | detects repeated expansion; supports traversal of a *fixed finite* graph | that the live universe of discoverable objects is fixed and finite | -| no symlink following | removes symlink-induced traversal into aliases and cycles | that remaining discovery terminates or fits a resource bound | - -A visited set becomes a termination proof through a measure like `|U \ V|` only when `U` is an -established finite universe and each continuation removes an element from it. Maintaining `V` — what -has been seen — does not establish `U`. Directory contents can change during enumeration, and POSIX -leaves aspects of observing concurrent additions and removals unspecified. - -A depth budget is legitimate as an *additional* policy limit with its own located refusal, and an -explicit worklist with work fuel also stops the host call stack from being the mechanism that -enforces it. - -One qualification I had wrong: an unknown compile-time size is not the same as having no termination -argument. A materialized finite tree traverses structurally without a compile-time size, and a finite -list folds without a literal length. The problem here is specifically that a filesystem walk -**discovers more input through effects while it runs** — a child pathname is not a compiler-visible -subvalue of an already-held finite tree. `std.termination` already separates `TreeSize`, -`ListLength`, `ArithmeticValue` and `SetCardinality`; these are different arguments, not -interchangeable labels for "making progress". - -### The fuel must reach the actual discovery mechanism - -The boundary most likely to recreate the problem: - -``` -unbounded host enumeration → fully collected list → check the budget -``` - -A budget cannot bound work that already happened. The listing realization must either expose bounded -progression or perform a bounded collection internally, with explicit completion-versus-limit -outcomes. A new streaming framework is not required; a host-side bounded collector that **refuses -rather than truncates** satisfies the contract. - -The same distinction applies to content: a bound on directory transitions does not bound a whole-file -read. Content acquisition is bounded separately, and I/O deadlines or cancellation stay in the effect -contract. An arithmetic rank bounds modeled continuations; it does not prove an arbitrary kernel or -remote-filesystem call returns within a wall clock. - -And the decrement must be established by the compiler's actual enforcing path. `Strict` existing in -`std.termination` is not proof that the compiler derives it for this walk. - -## 4. RULING — never follow a symlink implicitly - -Do not follow file or directory symlinks during authored-corpus acquisition. Following a link -substitutes the referent's contents for the authored link, which changes the subject being captured. -A link to an external file, a link to a directory, and a dangling link must not acquire different -preservation semantics because one happens to resolve today. - -Three facts stay separate: - -``` -the directory entry is a symlink -the symlink stores a particular target spelling -resolving that spelling currently produces some outcome -``` - -`std.filesystem.types.SymlinkTarget` is `TargetFile | TargetDir | Broken`. That records the *third* -fact only. It cannot serve as a persisted link payload, because it does not carry the stored target -spelling needed to reconstruct the link. A link-preserving representation needs the link's actual -stored target, read without substituting the referent's contents, and without accepting a truncated -target. - -### What this producer does, given the manifest it actually has - -`CorpusManifestEntry` is a path and an `AuthoredSourceTarget`, with no link discriminator. It cannot -express "this path denotes a symlink with this target". So for an in-scope symlink this producer -returns a **located unsupported-symlink acquisition refusal**. It must not: - -- omit the link silently and report complete capture; -- store its target text as though it were an ordinary file; -- put `Broken` or a traversal error into the manifest as though it were authored content. - -The refusal belongs to the acquisition outcome. The manifest's existing ability to represent a -*deliberate* partial corpus is not permission to label an *accidentally* incomplete acquisition -complete — that distinction is the whole of §5 at this boundary. - -A regular-source-only producer with an explicit refusal is a valid narrower contract. It is not -full-directory support, and an unrestricted directory-freezing operation over a scope containing -links needs real link-preservation semantics — an explicit representation with its identity, -persistence and reconstruction behaviour — before it can claim that input. - -### One existing helper is the wrong policy to reuse - -`std.filesystem.is_text_readable` treats a symlink resolving to a text file as readable, and -`partition_entries` divides inputs into `readable` and `skipped`. That is a *readability* policy, not -an exhaustive authored-corpus capture policy. Consuming its `readable` population would both admit -link dereferences and discard other entries. Reuse the `EntryKind` vocabulary; do not inherit that -partition as the capture decision. - -## 5. RULING — enforce no-follow at acquisition, not only at observation - -This sequence is insufficient: - -``` -observe "regular file" → another actor replaces the entry with a symlink → read the pathname -``` - -The observation and the read then concern different objects; a prior `EntryKind` value does not -constrain subsequent pathname resolution. - -The acquisition realization must enforce no-follow and root-resolution when it **opens** the object, -and inspect and read the object actually opened. On Linux `openat2` supplies resolution constraints -(`RESOLVE_NO_SYMLINKS`, `RESOLVE_BENEATH`); plain `O_NOFOLLOW` constrains only the final component, -not every component of a pathname. Those are realization facts modeled under their platform -authority, not assumptions attached to a generic string read. - -This changes nothing for the other read consumers. It means the new corpus-acquisition path obtains -the capability it needs instead of assuming the generic `Read` already provides it — and where a -realization cannot supply it, the result is an explicit unsupported-capability refusal, never a -fallback to following the path. - -## 6. RULING — completion must say what was completed - -**"The walk exhausted its selected observations" is not "these files all existed together at one -instant."** A traversal over a mutable directory is not a snapshot; successful enumeration and -content reads do not supply that temporal guarantee, and POSIX's concurrent-enumeration semantics -rule out deriving it from ordinary listing. - -This producer may freeze the exact admitted contents it captured, under an explicit selection and -observation contract. A point-in-time whole-tree claim additionally needs a snapshot or quiescence -mechanism. Snapshot infrastructure is not silently made a prerequisite for a smaller capture — and a -snapshot is not silently claimed either. - -The completed-capture arm requires that every selected enumeration continuation reached successful -exhaustion, every selected entry was accounted for, and every required content acquisition completed. -Budget exhaustion, an inaccessible selected subtree, an unsupported in-scope entry, or an unresolved -acquisition failure must not reach that arm. Partial observations may remain for diagnostics; they do -not authorize publication as the completed capture. - -## 7. The discriminating evidence - -Witnesses target the distinctions above rather than demonstrating one successful walk. - -| property | discriminating specimen | -|---|---| -| listing completeness | a directory yields one entry then fails: no complete listing, and no false absence for an unreached name | -| work descent | a synthetic provider keeps revealing new work: finite fuel refuses, with no replenishment and no successful prefix | -| budget boundary | a small capture completes; the same attempt with pending work at exhaustion refuses | -| symlink policy | file, directory, ancestor-cycle, external and dangling links cause **no referent acquisition** and receive the declared refusal | -| observation-to-open safety | an entry changes from regular file to symlink between observation and open: no implicit dereference | -| occurrence preservation | two selected paths naming the same underlying file: both paths stay represented | -| completion authority | an acquisition fails after earlier files succeeded: no completed-capture result and no publication under that claim | - -Compiler-side evidence is separate: the intended loop is accepted with its real decreasing measure, -and the non-decreasing variant is **rejected by the path claimed to enforce termination** — because a -wall nothing red has ever hit is a decoration (§4b). - -Inode deduplication must not erase path *occurrences*: object identity on Unix is device plus inode, -and several directory entries may name one file. The manifest needs both selected paths even when -captured content shares storage. - -## 8. Sequencing - -1. **The listing root cut.** Widen the listing at its authority, migrate the ten-module population, - delete `filesystem_listing_names_entry`, re-decide the two `FilesystemEntryName` refusals whose - justification the cut falsifies, and file the failure-mode row below. Self-contained, with real - consumers today. -2. **Bounded acquisition.** Work fuel, the bounded collector that refuses rather than truncates, - no-follow at open. -3. **The producer.** Captured inputs → `CorpusManifestObject`, with the completion contract of §6. -4. **`RepositoryCommit.corpus`**, then the verbs. `gunbc.scm.status`'s own header currently declares - that it has no path and no working tree and calls those facts unproducible; the manifest is what - makes a path-bearing status expressible, so that header is revised *with* the change, not around it. - -## 9. The failure-mode row this survey owes - -`a_workaround_outlives_the_constraint_that_justified_it` — a construction justified by a named absent -capability, where the capability now resolves and nothing re-reads the justification. - -The three nearest rostered rows are each a different class. `unmarked_workaround` is a workaround with -*no* trigger; this one has a stated justification acting as an implicit one. -`trigger_satisfied_before_the_row_was_written` is a trigger already true *at filing time*; this is its -temporal sibling — true at filing, lifted later. -`a_live_authority_name_carries_a_superseded_claim` is a name refreshed while its claim died; here -nobody touched the sentence at all and its precondition moved underneath it. - -Recognition rule, decidable: a comment justifying a construction by a named absent capability, where -the capability now resolves. Receipt: `filesystem_io`'s "a split the interpreter does not offer", -against `split` as a seed builtin dispatched as `MethodCallSplit`. - -It lands with the cut in step 1, not as a bare roster append. - -## 10. A live defect found while sizing the cut, and a correction to how I first reported it - -The completeness property §1 requires is not merely unmodeled. It is **violated on main today**, in -the seed, where no `.dag` wall can see it. The file transport's `list` verb: - -```rust -return match std::fs::read_dir(&path) { - Ok(entries) => { - let mut names: Vec = entries - .filter_map(|e| e.ok()) - .filter_map(|e| e.file_name().into_string().ok()) - .collect(); - ... - Ok(FileResult { success: true, content: names.join("\n"), .. }) -``` - -Two silent drops, both landing in a result whose `success` channel says `true`. **They are not -equally harmful, and my first report of them said they were.** - -**The iterator drop is live.** `filter_map(|e| e.ok())` discards a `read_dir` iterator error -mid-enumeration and returns what was collected as a *complete* listing. ~~the prefix collected so -far~~ — **corrected: not a prefix.** The API permits iteration to continue past an error, so what a -discarding filter collects is an arbitrary *subset*, and "prefix" understates it. It omits ordinary, -askable names, so `filesystem_entry_presence` finds no membership for an entry that exists and -constructs a `FilesystemEstablishedAbsence` for it. - -~~A wrong answer under every absence established in the tree.~~ **Corrected:** that was a -shared-dependency claim dressed as an observed population. What is established is a *reachable route* -from a discarded enumeration error to an unsupported absence assertion — not that any particular -absence is wrong, nor that anything destructive followed. The absorbing fallback ("could not -enumerate" widened into "enumerated, nothing more") sits underneath the module built to make absence -honest; how often it has fired is unmeasured. - -**The non-UTF-8 drop is latent.** `into_string().ok()` discards any entry whose name is not valid -UTF-8, again into a `success: true` listing. But `filesystem_entry_presence` takes `name: String`, so -the asked name is always valid UTF-8 by construction and a dropped entry could never have matched it. -~~`admit_filesystem_entry_name` would refuse such a spelling independently.~~ **Corrected:** it would -not. It checks component spellings — separators, NUL, LF, CR — and is not the native-name conversion -boundary; a non-UTF-8 native spelling cannot reach its `String` parameter faithfully in the first -place. It must not be credited with a second UTF-8 admission check. Since **no consumer -enumerates** — the fact §0 establishes — the dropped entry is currently unobservable. - -It is still a success channel claiming a completeness it does not have, and it becomes load-bearing -the instant enumeration exists, which is this cut. But it is not a live wrong answer, and reporting it -as the second half of a live fail-open was the same defect this lane keeps finding, applied to my own -finding: asserting a population without checking whether the distinction reaches a consumer. The -correction is recorded rather than quietly edited, because a design document that silently agrees -with whatever was true last is not evidence of anything. - -### A third defect, which review found and I had missed: fabricated presence - -The two drops narrow the population. There is a third path that *widens* the answer, and it is the -exact dual. - -While the newline wire stands, a returned name containing a line feed makes one entry -indistinguishable from two. These produce identical bytes: - -``` -one entry: "prefix\nrepo.json" -two entries: "prefix", "repo.json" -``` - -`filesystem_listing_names_entry` searches for a newline-delimited occurrence, so it answers -`repo.json` **present** in both cases. That is a fabricated presence, and admitting the *queried* -name does not protect against it — `admit_filesystem_entry_name` constrains what a caller may ask, -and the collision is in the name the directory *returned*. - -So the repair has two directions to close, not one: a listing may not silently shrink, and it may not -silently claim an entry it does not hold. Both belong to 1A, because both are live under the current -encoding. - -Carriage return is deliberately **not** given a matching refusal. It does not collide under the -exact-`\n` join and split, and minting a refusal for it here would be a restriction with no -demonstrated defect behind it — the inverse error of leaving one standing on a lapsed justification. -Its disposition belongs with the LF case to the cut that retires this encoding. - -### What that implies for sequencing - -Step 1 of §8 splits, and the split settled differently from the first proposal here. - -~~The latent defect has no honest home except the cut that activates it.~~ **Superseded.** Refusing -an unrepresentable name in the *current* producer is also a valid repair, and it is the one taken: -the existing `String` output cannot carry such a name faithfully, and silent omission is not faithful -enumeration. That is the operation's own semantics, not a convenience. - -- **1A — the listing refuses rather than narrowing or fabricating.** All three routes, not the - iterator alone: the advancement error stops being discarded, an unrepresentable native name refuses - instead of being dropped, and a returned name colliding with the delimiter refuses instead of - fabricating an entry. Precisely: *a listing cannot report success after discarding an iterator - error, dropping an unrepresentable native name, or serializing a returned name that collides with - the listing delimiter. Supported successful listings retain their existing representation.* The - wire representation is unchanged; **the conditions for returning success change**, which is - observable behaviour and is not concealed under "the contract is untouched". -- **1B — the structured listing.** The JSON representation, exact-Unicode support with explicit - native-name refusal, one completion authority, deletion of `filesystem_listing_names_entry`, and - re-decision of the restrictions the replacement makes unnecessary — **including the two 1A makes - correct for the old encoding.** They must not survive 1B merely because 1A justified them. - -### The substrate constraint that shapes 1B - -The effect boundary carries only flat scalars and lists of **string-alias** elements: every `List` -output in `extdeps` is `List`, `List` or `List`, all from -`stdout_lines`, and no operation anywhere returns a list of records. So `entries` cannot come back as -`List<{name, kind}>` directly. Three candidates: - -| candidate | verdict | -|---|---| -| per-line encoding decoded in `filesystem_io` | viable — a *rigorously specified* record-per-line format could escape names and carry a terminal record | -| two index-correlated parallel lists | **rejected** — an unenforced index correlation is the conflation this module exists to remove | -| a JSON document on the `entries` channel, decoded once beside the operation | **chosen** | - -~~JSON is the only encoding that preserves a newline-bearing name and makes completion -representable.~~ **Corrected:** it is not the only *possible* lossless framed encoding, and a -per-line format does not *necessarily* restrict names. The reason to choose one document is simpler: -it gives this operation a single place to bind entries, their observations and the enumeration -outcome, and there is no reason to invent another line protocol. Decoding it beside the operation is -the move `filesystem_io` already makes for membership — the encoding is `List`'s fact, so the -structure over it is too. - -**And JSON framing is not a name contract.** JSON strings are Unicode; escaping removes the delimiter -collision but does not make arbitrary native filename bytes into Unicode. The two honest contracts -are *exact Unicode names*, refusing the listing on a native name that cannot be represented, and -*exact native names*, which needs an explicit reversible representation carried through every -operation that consumes the name. **1B takes the first.** Supporting arbitrary native names is a -larger, separately declared capability — not something JSON supplies automatically — and the generic -`OsString` internal encoding is explicitly not a portable wire format. - -Completion belongs to the decoded **outcome's shape**, not to an independent flag beside `entries`: -`EnumerationCompleted { entries } | EnumerationRefused { cause }`, with only the completed arm -supplying a population from which non-membership can establish absence. A document parsing -successfully does not establish that enumeration completed, and missing or malformed completion -information must not default to completion. - -## 11. What this cut does not claim - -`filesystem_absence_establishment_adoption_standing` carries a next-rung trigger reading that the raw -`List` and `Read` result projections cease to be reachable outside this module's folds, so a consumer -cannot spell the conflation at all. **This cut does not satisfy that trigger** — the raw operations -stay callable — and must not be reported as retiring it. A trigger is retired by its trigger and by -nothing else. - -## 12. The seed-growth receipt this cut owes - -1A adds hand Rust to `src/v1`, which `gunbc.seed_growth_admission` -`seed_growth_forward_freeze_policy_note` makes a stop-line unless the addition is enumerated. The row -is `gunbc.listing_completeness_seed_growth`, enrolled in the roster's own declaration list. - -**The population is derived, not authored.** `tools.seed_growth_change_population` runs -`observe_rust_item_change_against` over this branch and projects each added item through -`item_declaration_ref`; the row's list is that output. No cardinality appears in the row or in this -section — the same policy note retired authored hand-item and hand-LOC deltas because they are -functions of the diff, and a number here would re-create exactly the second representation it -deleted. Run the instrument to re-derive the list. - -**Inline modules are scopes, not leaves.** `item_declaration_ref_in` folds `enclosing_module` into -each member's `module_path`, and the admission join compares exact references, so naming a test -module does not cover the functions inside it. A first cut of the row named four items on the -opposite reading; the instrument named the nested functions the row was missing. Test items belong -in the roster on the same footing as production ones — there is no production-only exemption. - -**Its trigger binds the declarations, not a format milestone.** Three conditions, all required: -implementation displacement (the production consumers no longer need this hand code — a renamed hand -successor moves the obligation rather than retiring it), evidence continuity (each still-live -behaviour stays discriminated at the boundary where it can fail; a `.dag` witness starting from -`EnumerationRefused` begins *after* the point where the host defect discarded the error), and the -changed representation contract (under a representation supporting LF-bearing names, "LF must -refuse" is re-decided into exact preservation with no fabricated membership, not carried forward). -The narrow check is that **no operational name observation depends on the ambiguous unescaped -delimiter representation** — not a ban on `List` or on joining, since an internal list of -names, or a join over already-escaped fragments, is not the lossy transport. - -**What the instrument is not.** It is the callable **roster-coverage diagnostic**. A required -admission phase may reuse its observation and coverage components, but must retain the full admission -authority's obligations: `seed_growth_admit_change` takes `change`, `live`, `bridges` and `evidence`, -performs `gunbc.capability_origin`'s origin checks, and preserves diagnostics this entry point -deliberately omits — and the policy's next-rung trigger names stopping the line on -`SeedGrowthAdmissionRefused` specifically. **Scheduling this function would not establish that -trigger**, and running it green retires nothing. - -**Its own partial-observer repair.** `item_declaration_ref` has three outcomes and the first cut of -the verdict decided success from two: `projected_declaration_refs` excludes uncitable *and* refused -projections, `projected_refused_keys` collects only refused ones, so an added `RustImplMethod` — -which `std.decl_ref` cannot name at all — fell out of both lists and the run exited success reporting -neither the item nor the limitation. The same defect this branch exists to repair, committed by the -instrument that measures it. The verdict now carries all three causes together rather than ranking -them, is complete only when every one is empty, and keeps them distinct: unaddressable is an -unclaimed source tree, uncitable is an item no `DeclarationRef` can spell (not a row an author can -add), unjustified is a missing roster row. `test.claim.seed_growth_change_population_witness_test` -hands the decision populations no diff on this branch produces, which is the only way the uncitable -arm is reachable. diff --git a/docs/plans/d13-network-audit.md b/docs/plans/d13-network-audit.md deleted file mode 100644 index 56645f9e0f3..00000000000 --- a/docs/plans/d13-network-audit.md +++ /dev/null @@ -1,199 +0,0 @@ -# D13 Network audit — the selection rule and its citations - -Input to D13 step (b) (`gunbc.plans.demand_engine_program`; rung drop `gunbc.rung_drop` `network_requirement_unrepresented_after_uses_cut`). This page holds the reasoning behind the `requires` clauses on `dag/extdeps` operations: the rule, the rulings, and a citation for each program or API class. **It holds no per-operation verdicts.** Each verdict is the operation's own `requires` clause, which is the single authority (DESIGN §3, §6). Read the population from the clauses themselves, e.g. `grep -rn 'requires ' dag/extdeps`. The per-row table used for review was posted on gunbc#12965 rather than committed. - -## Selection rule (ruled by quiet-seal-543, 2026-10-01) - -A `requires` clause states what a sandbox must GRANT, so the fail-closed direction is to over-declare. An operation is **Network** if executing it Wet **may** open a connection to a non-local endpoint. This replaces the first draft's "necessarily". The judgement is made from the operation's own declaration (transport kind + argv/endpoint) against the cited upstream behaviour of the program or API it invokes. - -- **rest**: Network unless the declared endpoint is a unix socket or a fixed loopback address. Docker Engine binds `extdeps.docker.endpoint` `docker_default_endpoint`, a unix socket, so it is NotNetwork. The GCP metadata server (169.254.169.254) is link-local, not loopback, so it is Network. -- **file**: NotNetwork. -- **shell**: decided by program **and** authored argv: - - A fixed network protocol to a host is Network: ssh/scp/sshpass, `ipmitool -I lanplus`, `curl https://…`, `gh`, `gcloud auth`, hosted-model CLIs. - - A url/remote parameter is Network: `curl {url}`, `git fetch|push|ls-remote {remote}`, Playwright `goto {url}`, and `github.OIDC` `GetToken` with its runtime `request_url`. An operation name is not a type, so `http.Client` `GetLocalhostBounded` is Network until its `{url}` is typed loopback. That typing is the trigger that would move it. - - A fetch that depends on cache state is Network: cargo without `--offline`, `npm ci`, `npm cache add`, `npx -y`, `apt-get install`, `gcloud auth print-access-token`. - - `arping` is Network: it uses the network interface. - - A runtime-program parameter is **OpaqueDemand**: `shell.exec` Run*, `systemd-run` with `command_argv`, the sudo probe `Check`, `gunbc.WitnessBin` `Run`, and the node/python/go `RunFile` runners. The argv is runtime data, so step (b) yields DemandUndecided for their callers instead of an empty demand. - - **Runtime program in an argument** (ruling 2026-10-02): an argument that carries a program the invoked tool executes is **OpaqueDemand** -- scripts, expressions, inner argv, executing templates, and a caller-supplied program path in argv[0] (`codex_app_server` `GenerateJsonSchema`). This includes an unrefined string in option position that the tool accepts as a program-executing option (`git grep -O`). A runtime program whose language has no network or exec primitive (a jq filter; Rust source that rustc only compiles) does not raise the demand above its host program's. - - **Remote-selecting option in an unrefined string** (ruling 2026-10-02): if an unrefined string sits where the tool parses options, and the tool has an option that selects a remote host or URL (`systemctl -H/--host=`, `hostnamectl -H`, curl `-K`/`-x`), the operation is **Network**. That holds unless the argv structurally prevents it, e.g. the string follows `--` or is the value of a preceding option. - - Everything else is a local program on local paths: NotNetwork. - - **Live browser page** (ruling 2026-10-02): an interaction with a running browser is **Network**. A click can navigate, page scripts run during any call, and browser startup can fetch. A program whose option surface cannot be verified (`playwright-runner` is not in this repository) is **OpaqueDemand** for every runtime positional it receives; it is never assumed none. - - **Partial clone** (ruling 2026-10-02): a git read that needs blob or tree objects is **Network**, because in a partial clone git lazily fetches missing objects from the promisor remote, and whether a repository is a partial clone belongs to the repository the operation is pointed at, i.e. its input. Reads of commits, refs, the index or config only are not affected. -- **Repository-selected executable** (ruling 2026-10-02): if the invoked command executes a program selected by the input repository or its configuration, the operation is **OpaqueDemand**, unless the operation structurally disables that surface. This covers hooks, clean/smudge/process filters, diff/merge drivers, textconv, the credential helper, the fsmonitor hook, `core.sshCommand`, `remote.*.uploadpack`, a cargo build script or proc macro, `.cargo/config` aliases and runners, the `.npmrc` `git` executable, and agent hooks in a settings argument. Upstream surfaces, all git per git-scm.com/docs: - - githooks(5): `pre-commit`, `prepare-commit-msg` (not suppressed by `--no-verify`), `commit-msg`, `post-commit`, `post-checkout` (checkout, worktree add), `pre-merge-commit`/`post-merge`, `pre-push`, `reference-transaction` (every ref update, including `update-ref`), `post-index-change` (every index write: add, read-tree, update-index, write-tree). - - gitattributes(5): clean on add/status/diff-against-worktree/`hash-object` with a path; smudge on checkout/restore/reset/checkout-index/merge; diff drivers and textconv on patch output; merge drivers on merge and merge-tree. - - git-config(1): `core.fsmonitor` on index refresh (status, add, commit, untracked scans); `credential.helper`, `core.sshCommand`, `remote..uploadpack`/`receivepack` on every transport (fetch, push, ls-remote). - - Cargo: build scripts and proc macros (doc.rust-lang.org/cargo/reference/build-scripts.html), and user aliases that shadow external subcommands such as `fmt` (doc.rust-lang.org/cargo/reference/config.html#alias). - - npm: the `git` config key (docs.npmjs.com/cli/using-npm/config#git) for git dependencies. - - Claude Code: hooks in `--settings` (docs.anthropic.com/claude-code/hooks). - - Agent CLIs (`codex exec`, and `claude -p` with a runtime permission mode) execute model-chosen commands, which makes them runtime programs. - - - Index reads (ruling 2026-10-02, no special case): any git command that reads the index can run the `core.fsmonitor` hook on the first index load (read-cache `tweak_fsmonitor`). That covers plain `ls-files`, and also every unrefined revision argument, because a revision may be spelled `:` or `::`, which reads the index (gitrevisions(7)). So rev-parse/show/cat-file/ls-tree/merge-base/reflog/rev-list/commit-tree/diff with a runtime revision are opaque. - - Toolchain proxies in an input directory (ruling 2026-10-02): rustup selects the toolchain from `rust-toolchain.toml` in the working directory (rust-lang.github.io/rustup/overrides.html). npx reads the project `.npmrc` (e.g. `node-options`). So an operation that runs these in a cwd or repository it takes as input is opaque. The same programs run with an ambient cwd (`cargo --version`, `rustc --version`) fall under the host-environment boundary. - - Git operations that reach none of these surfaces keep their transport verdict. They have fixed revisions and no index read: rev-parse of `HEAD`/`--show-toplevel`, `rev-list --before=… HEAD`, `for-each-ref`, `worktree list`, `branch -r`, `config`, `init`, `hash-object --stdin`, and `log -- {path}` (Network: partial clone). Paging is not reached because stdout is captured, not a TTY. -- **Boundary — host environment is not demand** (ruling 2026-10-02): host-wide configuration (global/system git config, `~/.ssh/config`, rustup toolchain selection from an ambient cwd) and host-wide resolver configuration (NSS, which can route `id`/`whoami`/`stat %U` to LDAP; DNS) is the sandbox's environment, not an operation's demand, and does not raise a clause. - -## Citations by class - -| verdict → clause | citation | modules | -|---|---|---| -| Network → `requires Network` | Cloudflare API v4 (developers.cloudflare.com/api) | `extdeps.cloudflare.account_api_tokens`, `extdeps.cloudflare.r2_buckets` | -| Network → `requires Network` | GCP metadata server (cloud.google.com/compute/docs/metadata/overview): metadata.google.internal = 169.254.169.254, link-local, not loopback | `extdeps.cloud.gcp.sts` | -| Network → `requires Network` | GitHub REST API docs (docs.github.com/rest), base https://api.github.com | `extdeps.github.actions_jit_runner`, `extdeps.github.app`, `extdeps.github.checks`, `extdeps.github.code_search`, `extdeps.github.commits`, `extdeps.github.gists`, `extdeps.github.git_database`, `extdeps.github.issues`, `extdeps.github.org_actions`, `extdeps.github.pulls`, `extdeps.github.repository_contents`, `extdeps.github.rulesets`, `extdeps.github.users`, `extdeps.github.workflow_runs`, `extdeps.github.workflows` | -| Network → `requires Network` | Google Drive API v3 reference (developers.google.com/drive/api/reference/rest/v3) | `extdeps.google.drive` | -| Network → `requires Network` | Google Sheets API v4 reference (developers.google.com/sheets/api/reference/rest) | `extdeps.google.sheets` | -| Network → `requires Network` | PARAM remote: git-fetch(1)/git-push(1)/git-ls-remote(1) use the remote's transport (gitprotocol-v2(5)); a {remote} may be a local path (file transport) or a URL; operations run in a repository are now opaque (repository-selected credential helper/sshCommand/uploadpack/pre-push/reference-transaction); the Network clause remains only where none of those surfaces is reached | `extdeps.git`, `extdeps.git.publication_transport` | -| Network → `requires Network` | PARAM request_url: endpoint is the runtime ACTIONS_ID_TOKEN_REQUEST_URL (docs.github.com/actions/reference/security/oidc); declaration fixes no host | `extdeps.cloud.gcp.sts` | -| Network → `requires Network` | PARAM tarball: npm cache add (docs.npmjs.com/cli/commands/npm-cache) accepts a path or a URL | `extdeps.tools.npm` | -| Network → `requires Network` | PARAM url: Playwright page.goto (playwright.dev/docs/api/class-page#page-goto) navigates to a runtime URL | `extdeps.browser` | -| Network → `requires Network` | PARAM url: curl(1) URL scheme+host both runtime (could be file:// or loopback) | `extdeps.http.client` | -| Network → `requires Network` | RULING link-layer: arping(8) broadcasts ARP on {device} to {target}; L2 traffic, no connection to an endpoint | `extdeps.iputils.arping` | -| Network → `requires Network` | SEC EDGAR APIs (sec.gov/search-filings/edgar-application-programming-interfaces) | `extdeps.sec.edgar_rest` | -| Network → `requires Network` | STATE credential cache: gcloud auth print-access-token (cloud.google.com/sdk/gcloud/reference/auth/print-access-token) refreshes over oauth2.googleapis.com only when the cached token is expired | `extdeps.shell` | -| Network → `requires Network` | STATE package cache: apt-get(8) install downloads .debs from sources.list unless already installed/cached; PARAM package | `extdeps.apt` | -| Network → `requires Network` | STATE package cache: npm ci (docs.npmjs.com/cli/commands/npm-ci) fetches from the registry unless every tarball is cached; superseded for `npm.Ci` by the repository-selected executable rule (`.npmrc` `git`): opaque | `extdeps.tools.npm` | -| Network → `requires Network` | STATE package cache: npx -y -p (docs.npmjs.com/cli/commands/npx) installs the package from the registry unless already cached | `extdeps.typescript` | -| Network → `requires Network` | STATE registry cache: cargo build/check/test (doc.rust-lang.org/cargo/commands/cargo-build.html, --offline/--frozen) fetch the index/crates only when Cargo.lock deps are not already downloaded; argv does not pass --offline; superseded for `cargo.Build` by the repository-selected executable rule (build scripts, proc macros): opaque | `extdeps.cargo_build` | -| Network → `requires Network` | TCGplayer API (docs.tcgplayer.com), base https://api.tcgplayer.com | `extdeps.tcgplayer.catalog`, `extdeps.tcgplayer.pricing`, `extdeps.tcgplayer.store`, `extdeps.tcgplayer.tcgplayer` | -| Network → `requires Network` | curl(1) to fixed https://api.github.com (GitHub Apps REST docs.github.com/rest/apps) | `extdeps.github.app` | -| Network → `requires Network` | curl(1) to https://{bmc_host}: DMTF Redfish DSP0266 / MegaRAC web API over HTTPS — argv fixes the https scheme to a BMC host | `extdeps.bmc.http`, `extdeps.bmc.megarac` | -| Network → `requires Network` | eBay REST APIs (developer.ebay.com/api-docs), base https://api.ebay.com | `extdeps.ebay.browse`, `extdeps.ebay.inventory`, `extdeps.ebay.oauth` | -| Network → `requires Network` | fixed HTTPS endpoint https://api.anthropic.com (upstream API reference for that host) | `extdeps.llm.anthropic_rest` | -| Network → `requires Network` | fixed HTTPS endpoint https://api.openai.com (upstream API reference for that host) | `extdeps.llm.openai_rest` | -| Network → `requires Network` | fixed HTTPS endpoint https://api.tailscale.com (upstream API reference for that host) | `extdeps.tailscale.acl_api` | -| Network → `requires Network` | fixed HTTPS endpoint https://cloudresourcemanager.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.iam` | -| Network → `requires Network` | fixed HTTPS endpoint https://iam.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.iam`, `extdeps.cloud.gcp.iam_admin` | -| Network → `requires Network` | fixed HTTPS endpoint https://iamcredentials.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.iam` | -| Network → `requires Network` | fixed HTTPS endpoint https://jsonplaceholder.typicode.com (upstream API reference for that host) | `extdeps.test.http_pilot` | -| Network → `requires Network` | fixed HTTPS endpoint https://oauth2.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.gcp` | -| Network → `requires Network` | fixed HTTPS endpoint https://secretmanager.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.secret_manager` | -| Network → `requires Network` | fixed HTTPS endpoint https://serviceusage.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.serviceusage` | -| Network → `requires Network` | fixed HTTPS endpoint https://sts.googleapis.com (upstream API reference for that host) | `extdeps.cloud.gcp.sts` | -| Network → `requires Network` | gcloud auth login (cloud.google.com/sdk/gcloud/reference/auth/login): OAuth flow against accounts.google.com | `extdeps.cloud.gcp.gcp` | -| Network → `requires Network` | gh(1) manual (cli.github.com/manual): `gh api`/`gh pr`/`gh run` call api.github.com | `extdeps.github.actions_runs`, `extdeps.github.ci_runner`, `extdeps.github.org_actions`, `extdeps.github.organizations`, `extdeps.github.pulls` | -| Network → `requires Network` | ipmitool(1) INTERFACES: -I lanplus = IPMI v2.0 RMCP+ over UDP/623 to -H {bmc_host} | `extdeps.bmc.ipmi` | -| Network → `requires Network` | ssh(1)/scp(1) (+sshpass(1)): opens TCP/22 session to the host named in argv | `extdeps.bmc.openbmc_password_ssh_transport`, `extdeps.ssh.password_session`, `extdeps.ssh.session` | -| Network → `requires Network` | vendor CLI prompt mode calls the hosted model API (Claude Code docs.anthropic.com/claude-code/cli-reference; Codex CLI `exec` github.com/openai/codex; Gemini CLI github.com/google-gemini/gemini-cli); `claude.Invoke.Run` (hooks in `--settings`), `llm.Anthropic.CliPrompt` (runtime permission mode) and `llm.Codex.Review` (agent exec in `-C {cwd}`) are opaque | `extdeps.llm.anthropic_rest`, `extdeps.llm.cli` | -| NotNetwork → `requires none` | /usr/bin/stat: local coreutils/POSIX utility (man stat(1)); argv names only local paths/values | `extdeps.tools.stat` | -| NotNetwork → `requires none` | Docker Engine API (docs.docker.com/reference/api/engine): default endpoint unix:///var/run/docker.sock (extdeps.docker.endpoint docker_default_endpoint), a unix socket | `extdeps.docker.container_inspect`, `extdeps.docker.container_stats` | -| Network → `requires Network` | RULING live page: Playwright Page/BrowserContext API (playwright.dev/docs/api/class-page); a running page and browser startup may fetch (`Launch`, `CurrentUrl`, `Title`) | `extdeps.browser` | -| OpaqueDemand → `requires opaque` | RULING unverifiable surface: `playwright-runner` is not in this repository, so a runtime positional (selector, text, path, ms, context) may reach an unknown option | `extdeps.browser` | -| OpaqueDemand → `requires opaque` | RULING runtime program: Playwright page.evaluate / locator.evaluate (playwright.dev/docs/evaluating) run runtime JavaScript in the page, which can call fetch | `extdeps.browser` | -| NotNetwork → `requires none` | cargo(1) --version / cargo-fmt: no registry access (doc.rust-lang.org/cargo/commands); `cargo fmt` is now opaque (a workspace alias can shadow the external subcommand); `--version` stays none | `extdeps.cargo_build` | -| NotNetwork → `requires none` | cat: local coreutils/POSIX utility (man cat(1)); argv names only local paths/values | `extdeps.linux.cgroup_v2`, `extdeps.linux.procfs` | -| NotNetwork → `requires none` | chmod: local coreutils/POSIX utility (man chmod(1)); argv names only local paths/values | `extdeps.shell` | -| OpaqueDemand → `requires opaque` | RULING runtime program: argv[0] is the caller-supplied `CodexAppServerExecutable.path`, so the executed program is runtime-selected (codex app-server generate-json-schema itself writes schema files locally, per the github.com/openai/codex app-server README) | `extdeps.llm.codex_app_server` | -| NotNetwork → `requires none` | cp: local coreutils/POSIX utility (man cp(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | crontab(1): local spool | `extdeps.cron` | -| Network → `requires Network` | RULING remote option: curl(1) --unix-socket, but the `{url}` positional is unrefined and not after `--`, so it can carry curl options (`-K`, `-x`) that retarget the connection | `extdeps.http.client` | -| NotNetwork → `requires none` | date: local coreutils/POSIX utility (man date(1)); argv names only local paths/values | `extdeps.clock` | -| NotNetwork → `requires none` | diff: local coreutils/POSIX utility (man diff(1)); argv names only local paths/values | `extdeps.tools.diffutils` | -| NotNetwork → `requires none` | dpkg(1): local status database | `extdeps.dpkg` | -| NotNetwork → `requires none` | find: local coreutils/POSIX utility (man find(1)); argv names only local paths/values | `extdeps.linux.cgroup_v2`, `extdeps.shell` | -| NotNetwork → `requires none` | getconf: local coreutils/POSIX utility (man getconf(1)); argv names only local paths/values | `extdeps.posix.getconf` | -| NotNetwork → `requires none` | git(1); local subcommand per git-scm.com/docs reading only commits, refs, the index or config (no transport; only fetch/push/ls-remote/clone/pull use git transfer protocols, gitprotocol-v2(5)), and running no hook, filter, driver or fsmonitor surface | `extdeps.git`, `extdeps.git.inspect`, `extdeps.git.plumbing`, `extdeps.git.publication_transport` | -| Network → `requires Network` | RULING partial clone: git-partial-clone (git-scm.com/docs/partial-clone) lazily fetches missing blobs/trees from the promisor remote; diff, show, cat-file, ls-tree, log -- path, read-tree, checkout-index, unpack-file, merge, merge-tree, checkout, restore, reset --hard, worktree add; any of these that also runs a repository-selected executable is opaque instead | `extdeps.git`, `extdeps.git.inspect`, `extdeps.git.plumbing` | -| OpaqueDemand → `requires opaque` | RULING runtime program: git-grep(1) `-O` opens matches with a runtime program; `{pattern}` and `{ref}` (GitRef is only non-empty) sit in option position | `extdeps.git.inspect` | -| NotNetwork → `requires none` | grep: local coreutils/POSIX utility (man grep(1)); argv names only local paths/values | `extdeps.tools.grep` | -| NotNetwork → `requires none` | gunbc file transport: local filesystem read/write (POSIX open(2), read(2)) | `extdeps.filesystem.filesystem_io`, `extdeps.linux.procfs` | -| NotNetwork → `requires none` | gzip: local coreutils/POSIX utility (man gzip(1)); argv names only local paths/values | `extdeps.tools.gzip` | -| NotNetwork → `requires none` | hostname: local coreutils/POSIX utility (man hostname(1)); argv names only local paths/values | `extdeps.tools.hostname` | -| Network → `requires Network` | RULING remote option: hostnamectl(1) `-H/--host=` runs over SSH; `Process.Run` forwards an unrestricted ProcessArgvExpansion | `extdeps.tools.hostname` | -| NotNetwork → `requires none` | id: local coreutils/POSIX utility (man id(1)); argv names only local paths/values | `extdeps.shell`, `extdeps.tools.id` | -| NotNetwork → `requires none` | ip-address(8): rtnetlink to the local kernel | `extdeps.iproute2.ip_address` | -| NotNetwork → `requires none` | journalctl(1): reads the local journal | `extdeps.systemd.journalctl` | -| NotNetwork → `requires none` | jq(1) manual: filter over stdin/args | `extdeps.bmc.openbmc_fan_control`, `extdeps.tools.jq` | -| NotNetwork → `requires none` | kill: local coreutils/POSIX utility (man kill(1)); argv names only local paths/values | `extdeps.posix.signal` | -| NotNetwork → `requires none` | ln: local coreutils/POSIX utility (man ln(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | mkdir: local coreutils/POSIX utility (man mkdir(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | mktemp: local coreutils/POSIX utility (man mktemp(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | mv: local coreutils/POSIX utility (man mv(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | node: local coreutils/POSIX utility (man node(1)); argv names only local paths/values | `extdeps.tools.node` | -| NotNetwork → `requires none` | npm --version (docs.npmjs.com/cli/commands/npm) | `extdeps.tools.npm` | -| NotNetwork → `requires none` | npm ci --offline (docs.npmjs.com/cli/using-npm/config#offline): forces cache-only, no network; superseded by the repository-selected executable rule: opaque | `extdeps.tools.npm` | -| NotNetwork → `requires none` | nvidia-smi(1): local NVML | `extdeps.nvidia.system_management_interface` | -| NotNetwork → `requires none` | oomctl(1): local systemd-oomd | `extdeps.systemd.oomd` | -| NotNetwork → `requires none` | openssl-dgst(1): local signing | `extdeps.tools.openssl` | -| NotNetwork → `requires none` | printenv: local coreutils/POSIX utility (man printenv(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | ras-mc-ctl(8): local EDAC sysfs/rasdaemon DB | `extdeps.linux.edac` | -| NotNetwork → `requires none` | realpath: local coreutils/POSIX utility (man realpath(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | rm: local coreutils/POSIX utility (man rm(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | rmdir: local coreutils/POSIX utility (man rmdir(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | rustc --version / local compilation (doc.rust-lang.org/rustc) | `extdeps.rustc` | -| NotNetwork → `requires none` | rustfmt: local coreutils/POSIX utility (man rustfmt(1)); argv names only local paths/values | `extdeps.tools.rustfmt` | -| OpaqueDemand → `requires opaque` | RULING runtime program: GNU sed's `e` command and `s///e` execute shell commands; the operations take a runtime sed program without `--sandbox` | `extdeps.tools.sed` | -| NotNetwork → `requires none` | sh -c script authored in the declaration uses only local programs (mktemp(1), find(1), sort(1), head(1)/tr(1) over /dev/urandom, rustc --emit=metadata, command -v) | `extdeps.entropy`, `extdeps.rustc`, `extdeps.shell` | -| NotNetwork → `requires none` | sha256sum: local coreutils/POSIX utility (man sha256sum(1)); argv names only local paths/values | `extdeps.crypto.hash`, `extdeps.tools.sha256sum` | -| NotNetwork → `requires none` | sha512sum: local coreutils/POSIX utility (man sha512sum(1)); argv names only local paths/values | `extdeps.tools.sha512sum` | -| NotNetwork → `requires none` | sleep: local coreutils/POSIX utility (man sleep(1)); argv names only local paths/values | `extdeps.tools.sleep` | -| NotNetwork → `requires none` | stat: local coreutils/POSIX utility (man stat(1)); argv names only local paths/values | `extdeps.shell`, `extdeps.tools.coreutils_stat` | -| NotNetwork → `requires none` | sudo(8) -l: local policy | `extdeps.sudo.nopasswd_execute_probe_check_op` | -| NotNetwork → `requires none` | systemctl(1): talks to the local systemd manager over D-Bus/private socket; only operations with no unrefined positional string (`DaemonReload`) | `extdeps.systemd.systemctl` | -| Network → `requires Network` | RULING remote option: systemctl(1) `-H/--host=` runs over SSH; the unit/pattern positional is an unrefined NonEmptyStr with no `--` before it | `extdeps.systemd.systemctl` | -| NotNetwork → `requires none` | tailscale CLI `serve status` reads the local tailscaled LocalAPI socket (tailscale.com/kb/1242/tailscale-serve) | `extdeps.tailscale.serve` | -| NotNetwork → `requires none` | test: local coreutils/POSIX utility (man test(1)); argv names only local paths/values | `extdeps.linux.cgroup_v2`, `extdeps.shell` | -| NotNetwork → `requires none` | tmux(1): local server socket (`List`, `Kill`) | `extdeps.tmux` | -| OpaqueDemand → `requires opaque` | RULING runtime program: tmux new-session runs `inner_argv`, a runtime command | `extdeps.tmux` | -| NotNetwork → `requires none` | uname: local coreutils/POSIX utility (man uname(1)); argv names only local paths/values | `extdeps.shell` | -| NotNetwork → `requires none` | wc: local coreutils/POSIX utility (man wc(1)); argv names only local paths/values | `extdeps.tools.wc` | -| NotNetwork → `requires none` | whoami: local coreutils/POSIX utility (man whoami(1)); argv names only local paths/values | `extdeps.access.posix_effective_principal_read_op` | -| NotNetwork → `requires none` | xorriso: local coreutils/POSIX utility (man xorriso(1)); argv names only local paths/values | `extdeps.tools.xorriso` | -| OpaqueDemand → `requires opaque` | PARAM bin_path: program is runtime | `extdeps.gunbc` | -| OpaqueDemand → `requires opaque` | PARAM command_argv: systemd-run(1) itself is local; the transient unit runs a runtime command | `extdeps.systemd.systemd_run` | -| OpaqueDemand → `requires opaque` | PARAM probe.command_path: sudo(8) runs a runtime program | `extdeps.sudo.nopasswd_execute_probe_check_op` | -| OpaqueDemand → `requires opaque` | PARAM program/command body: argv supplied at runtime | `extdeps.shell.exec` | -| OpaqueDemand → `requires opaque` | PARAM script_path: behaviour is the runtime script | `extdeps.go`, `extdeps.node`, `extdeps.python` | - -## Follow-ups (better modeling than the clauses above; not done here) - -- Repository-selected executables in git, by structural disabling. Each item has its citation, and each would move the affected operations back to their transport verdict: - - `-c core.hooksPath=/dev/null` disables every hook, including `prepare-commit-msg` and `reference-transaction` (git-config `core.hooksPath`; githooks(5)). `--no-verify` alone is insufficient. - - `-c core.fsmonitor=false` (git-config `core.fsmonitor`) disables the fsmonitor hook on every index read. `--no-optional-locks` does not: it only skips the opportunistic index write (git(1)). - - Revision arguments: a refined revision type that excludes the `:` index forms, or `--end-of-options` plus a full object id, removes the index read from rev-parse/show/cat-file/ls-tree/merge-base/rev-list. - - Toolchain proxies: pin the toolchain explicitly (`cargo +`, `RUSTUP_TOOLCHAIN`) and run npx with `--userconfig`/an isolated cwd, or the operation stays opaque. - - `--no-textconv --no-ext-diff` on diff (git-diff(1)). - - `hash-object --no-filters` (git-hash-object(1)). - - Overriding the filter and merge-driver surfaces needs per-driver `-c filter..*`/`merge..driver` overrides, which are unknown in advance. A typed attribute-free checkout realization is the structural route; otherwise those operations stay opaque. - - On transports, `-c credential.helper=` (an empty value resets the helper list; gitcredentials(7)), `-c core.sshCommand=ssh`, and dropping `--upload-pack`. -- Cargo: no flag disables build scripts or proc macros, so those operations stay opaque until the workspace's build-time programs are modeled. -- npm ci: `--git=false`-style suppression is not documented. Pin the lockfile to registry-only sources and refuse git dependencies. - -- `systemd.Systemctl` and `hostnamectl.Process`: terminate options before the unit/pattern positional (`--`), or refine the input to a unit-name type. Either move those operations back to `requires none`. -- `sed.Sed`: a typed non-executing sed subset, or `--sandbox` in the argv, would move both operations to `requires none`. -- `tmux.Session.New`: an inner-command carrier whose program demand is declared. -- `browser.Page.Evaluate` / `browser.Element.EvaluateOn`: a structurally non-network expression vocabulary. -- `git.Inspect` grep operations: `-e {pattern}` plus `--` before the revision, or a refined GitRef, would remove the `-O` route. -- git object reads: add `git --no-lazy-fetch` (git 2.44+) to the read operations, then move them to `requires none`. -- `extdeps.browser`: bring the runner's CLI into the repository (or cite its option surface) and terminate options before positionals; the selector operations then fall to the live-page Network rule. -- `http.Client.PostStdinWithinUnixSocket`: put `--` before `{url}` and type the url to the socket's authority. - -## Product-layer operations outside `dag/extdeps` (same rule) - -These 14 services in `dag/gunbc` had no clause; an absent clause is Undecided, so every caller would refuse. Verdicts: - -| verdict → clause | reason | operations | -|---|---|---| -| Network → `requires Network` | rest to `approval_ntfy_endpoint` (`https://ntfy.sh` or the tailnet HTTPS base), not a unix socket or loopback | `gunbc.auth.approval_ntfy_deployment` `ntfy.Publish.PublishMessage` | -| OpaqueDemand → `requires opaque` | runtime program: argv[0] is the caller-supplied `bin_path` | `gunbc.cli_services` `gunbc.Cli.Run`, `claim_executor.Executor.VerifyBuildArtifacts` | -| OpaqueDemand → `requires opaque` | runtime program: the script runs the caller-supplied `{ipmitool}` path (lanplus to `{bmc_host}`) | `gunbc.machine_intake.sol_hold` `ActivateHeld` | -| OpaqueDemand → `requires opaque` | runtime program: launches the caller-supplied `command` argv | `gunbc.owned_process` `launch.LaunchOwned` | -| NotNetwork → `requires none` | local only: reads the pid record, checks `/proc`, `kill`s the recorded pid | `gunbc.machine_intake.sol_hold` `ReleaseHeld` | -| NotNetwork → `requires none` | no transport: a pure fold over its inputs | `gunbc.code_change_workflow` `ClassifyGithubPrTerminalAnchor`, `DecideTransition`; `gunbc.pr_digests` `ExtractAttachedUrls`, `RenderPrSummaryLine`, `JudgeMergeReadiness`, `ClassifyRestFallback`; `gunbc.review_verdict` `Parse`, `Tally` | - -## Deliberately undeclared test fixtures - -These test and fixture operations carry no `requires` clause on purpose. Each exists to exercise a transport, argv or governance mechanism, and no test of theirs reads a demand, so none needs a clause today. A fixture gains one when a test first needs it. The list is the population at this writing; read the current one by scanning for `operation` blocks without `requires` outside `dag/extdeps` and `dag/gunbc`. - -- `dag/test/claim/m4_governed_service_witness.dag`: `GovernedProbe` `Allowed`, `Forbidden` -- `dag/test/claim/reconstruction_door_fixture_probe.dag`: `DoorProbe.Fetch` -- `dag/test/claim/reconstruction_door_rest_probe.dag`: `DoorProbeRest.Fetch` -- `dag/test/claim/shell_spawn_refused_real_execution_witness_test.dag`: `test.ShellSpawnProbe.Observed` -- `dag/test/fixture/argv_executable_position_probe.dag`: `ArgvExecutablePositionProbe` `SmuggleExecutable`, `LiteralExecutable` -- `dag/test/fixture/m4_universal_governed_probe.dag`: `GovernedUniversalProbe` `Allowed`, `Forbidden` -- `dag/test/fixture/rest_exchange_replay_probe.dag`: `test.RestReplay` `Observe`, `RootArray`, `RootFile`, `RootPage`, `WorkflowRunsPage`, `WifProvidersPage`, `WifPool`, `WorkflowRunsPageLegacy`, `RootUncontracted`, `Sibling`, `OptionalObserve`, `Legacy` -- `fixtures/atomic_materialization/subject.dag`: `AtomicFixture.Read` -- `fixtures/bare_service_provider/provider.dag`: `fixture.BareServiceEcho.Say` -- `fixtures/fixture_closure_rustc/argv_word_list_splice_probe.dag`: `fixture.WordListTransport` `InteriorList`, `TrailingList`, `TwoListsOneArgv`, `SingleWordOnly` -- `fixtures/fixture_closure_rustc/shell_multi_field_projection_probe.dag`: `fixture.MultiFieldProjection.TwoFieldExitSuccess` -- `fixtures/fixture_closure_rustc/shell_single_field_projection_probe.dag`: `fixture.SingleFieldProjection.OneFieldExitSuccess` diff --git a/docs/plans/dag-emit-one-grammar-backward.md b/docs/plans/dag-emit-one-grammar-backward.md deleted file mode 100644 index 5bf453025e3..00000000000 --- a/docs/plans/dag-emit-one-grammar-backward.md +++ /dev/null @@ -1,248 +0,0 @@ -# The dag target emits by reading `dag_grammar_root` backward - -Status: **implemented** in gunbc#12878 (`v2.std.grammar` `grammar_emit_parse_tree`, `v2.extdeps.languages.dag` `dag_emit_parse_tree`) (work item adhoc-da997c00-fa0). This page states how emit selects a -production for a Node shape and how each terminal's token class is recovered. It was written as the -model before code and accepted, and the implementation follows it except where a section below says -otherwise. - -## The defect being replaced - -DESIGN §4 says there is one grammar, read in both directions. For the v2 dag target that is -currently false (measured by tidy-lark-233 on main `cc2fefacac`): - -- `v2.extdeps.languages.dag` `dag_translation_rules_node` carries one row, the `fn add` fixture. -- `token_class_emit_transforms` is `target_model_emit_transforms_empty`. -- The `emit_row_*` functions have no consumer outside the module. -- `data m: Int = 1` refuses with `v2.compiler.ingest` `parse_tree_atom_token_class_not_recoverable`. -- The only declaration round trip, - `v2.test.execution.emit_ingest_type_decl_round_trip`, runs on - `dag_type_decl_structural_formal_productions` and its own lexer - (`dag_type_decl_structural_lex_rules`). That is a second, hand-authored grammar for one - language, which is a parallel authority under DESIGN §3. - -`gunbc.dag_grammar_fork_census` rosters the root of the problem: two production tables. One is the -reachable `dag_grammar_root`, which is `GrammarExpr` with choice, repeat, optional and -nonterminals. The other is the unreachable flat `FormalProduction` token spines, and every -relation row is grounded on them. The backward machinery in `v2.std.grammar` -(`grammar_relation_row_backward_selection`, `resolve_emitted_productions`) can only read the -second table. - -## The subject: which Node emit starts from - -Emit's input is the **parse-tree Node that ingest's parse produces**. That is the surface Node whose -production wraps are stamped by `v2.compiler.parse` `parse_wrap_production_captured`. It is not the -core Node that body lowering produces from it. - -The grammar's own `emitted` values are surface atoms (`^dag_surface_fn_decl`, and so on). So -"one grammar read backward" relates source text to *this* Node. Inverting body lowering is a -different relation with a different authority, and it is out of scope here (see the open -questions). The round-trip control is therefore: - - parse(text) = N - emit(N) = text' - parse(text') = N' - -and it asserts `v2.std.node` `content_hash(N) == content_hash(N')`. `content_hash` does not cover -occurrence identity, so source spans can legitimately differ. - -## How emit selects a production: read the stamp, then invert each `GrammarExpr` arm - -**Production selection is a lookup, not a search.** Every production capture in the parse tree is -the Conj `{grammar_production_identity_node_projection: production.emitted, -grammar_production_captured_node_projection: captured}`. Emit reads the identity edge and looks up -the one production in `dag_grammar_root` whose `emitted` is that identity. That lookup is total over -the closed production list. A missing production is the typed refusal below. - -There is no shape-guessing at production grain, and no second index. `emitted` atoms are unique -per production. The implementation checks that once, when it builds the index from the -`GrammarRoot`, and refuses a duplicate there. - -**Within a production, emit walks the `GrammarExpr` and the captured Node together.** Each arm is -the exact inverse of what its parse arm in `v2.compiler.parse` builds: - -| `GrammarExpr` arm | What parse captured | What emit does | -|---|---|---| -| `Sequence { left, right }` | Conj `{sequence_left: cap1, sequence_right: cap2}` (`parse_stamp_derived_conj`) | emit `left` over `cap1`, then `right` over `cap2`, and concatenate the token spines | -| `Repeat { element }` | right-folded Conj spine of `sequence_left`/`sequence_right`, ending in empty Conj (`parse_repeat_captured_node`) | unfold the spine and emit `element` over each item | -| `Optional { element }` | the element's capture, or `grammar_empty_node()` | empty Conj gives no tokens; otherwise emit `element` | -| `Choice { left, right }` | the winning arm's capture, untagged | **ordered:** try `left` backward; if it refuses, try `right`; if both refuse, refuse | -| `Expect { element, reason }` | the element's capture (an Expect is a label, never a cut) | emit `element` | -| `Nonterminal { production }` | a production wrap | select by stamp as described above, and check the stamp names `production` | -| `Terminal` / `LiteralTerminal` | an atom, or a literal payload node | emit one token (next section) | - -Ordered backward choice mirrors parse's ordered forward choice. If the left arm accepts a shape, -its spine re-parses through the left arm first. - -There is one place where a shape alone could mislead: a `StampClass` atom and a `StampLexeme` -atom are both childless atoms. An identifier spelled exactly like a token-class symbol could -therefore satisfy the wrong arm. That is not a case to guess about. The round-trip control is what -falsifies it. The walk resolves the collision one way: a `StampLexeme` position reads a childless -atom as its lexeme first. - -None of this needs a per-production emit row. The rows ARE the `dag_grammar_root` productions. A -new production gets emit for free, and a new target language is still rows, not an edit to the -walk. The walk is generic over `GrammarExpr`, so it belongs in `v2.std.grammar` beside the forward -reader, not in `extdeps/languages/dag.dag`. - -## How each terminal's token class is recovered: from the grammar position, never from the atom - -`parse_tree_atom_token_class_not_recoverable` exists because today's reverse path asks the *atom* -for its class. A `StampLexeme` atom carries the interned lexeme, so its class cannot be read back -from it. Walking the grammar alongside the tree removes the question. At every terminal, emit -already holds `Terminal { token_class, stamp }` or `LiteralTerminal { token_class, lexeme }`, so -the class is a fact of the grammar position. - -The **spelling** of the token then comes from one of two places. - -**Fixed-text classes**, such as keywords and punctuation, are spelled by the class's -`TokenRule { pattern: LiteralPattern }` in `dag_lex_rules`. That is the lexer's own row, so there -is no second spelling table. A `LiteralTerminal` is spelled by its `lexeme`. - -**Value-carrying classes** are spelled by `v2.extdeps.languages.dag` -`dag_emit_class_terminal_spelling`. Each arm is the inverse of the parse-side decoder that #12759 and -its predecessors stamped. These arms do **not** go into `TargetModel` `token_class_emit_transforms`: -that map transforms one spelling into another, whereas these arms start from a decoded value. The -map stays empty for the dag target. - -| class | captured Node (parse side) | emit transform | -|---|---|---| -| `dag_token_ident` (`StampLexeme`, `StampBinding`) | atom whose identity is the interned lexeme | the interned spelling | -| `dag_token_int_literal` | `dag_int_literal_node_from_magnitude` payload | the magnitude in decimal, the inverse of `dag_int_literal_node_from_lexeme` | -| `dag_token_float_literal` | `float_literal_lexeme_field` payload | the stored lexeme, verbatim | -| `dag_token_string_literal` | `string_literal_value_field`, the decoded value (#12759) | quote-frame the value, and **escape it from the same `dag_string_escapes` rows** the decoder reads; a scalar with no printable row is written `\u{H..H}` | -| caret symbol | `dag_token_caret` then an ident (`dag_grammar_caret_symbol_expr`) | not a transform: a fixed token, then the ident transform | -| quoted field key | `dag_grammar_field_init_string_key_expr`'s class-stamped string terminal | not a transform: the string-literal row (the key and the literal are one terminal since #12759) | - -For strings, the encoder and the decoder read one table. Decode composed with encode is the -identity on values. Encode composed with decode is not the identity on spellings, and does not need -to be: the round trip is on the Node. - -**Literal payloads are read through the model's own readers:** -`dag_int_literal_magnitude_int_from_node`, `dag_string_literal_value_optional`, and -`dag_float_literal_lexeme_optional`. Lowering uses the same readers. A literal is an `Atom` that -carries a payload edge, and `v2.std.node_query` `named_child_lookup` searches only `Conj` roots, so -it cannot be used to read one. - -**Token separation.** Emit writes one space between adjacent tokens. This is **not** source-faithful -layout: `module rt.x` comes back as `module rt . x`. The one exception is layout the grammar -itself requires. An `AfterLineBreak` row (gunbc#12773) matches only after a line break, so the walk -yields `GrammarEmitLineBreak` before its element, and the dag spelling writes a newline there. -A `RefuseOnMatch` row derives no tree, so backward it is an arm that never fits. Whitespace is a -`TriviaRule`, so this cannot change the Node. Annotations (DESIGN §4c) are erased from the parse -tree and are not emitted. A target that needs layout is a separate projection. - -## Typed refusals - -Each refusal is located with `node_locus` at the node being emitted. The generic ones come from -`v2.std.grammar`; the spelling one comes from the dag model. - -**Hard refusals.** These are never retried by an enclosing choice: - -- `grammar_emit_production_unknown`: the stamp names no production in the root. This is the - "missing emit row" red. -- `grammar_emit_projection_ambiguous`: a capture carries two projection edges of one name. Parse - mints each projection edge once, so this is a malformed tree, not an arm that does not fit. - -**Arm mismatches.** An enclosing ordered choice tries its next arm; if no arm fits, the last -mismatch is the refusal: - -- `grammar_emit_production_stamp_absent`: a nonterminal position holds a capture with no - production stamp. This is neat-boar-16's condition (3): a production is never guessed. -- `grammar_emit_nonterminal_stamp_mismatch`: the stamp names a different production than the - position expects. -- `grammar_emit_sequence_shape_mismatch`, `grammar_emit_terminal_not_atom`, - `grammar_emit_terminal_class_mismatch`: the capture is not what the arm's parse builds. - -**Index refusals.** These refuse the grammar itself: - -- `grammar_emit_production_emitted_not_atom` -- `grammar_emit_production_emitted_duplicate` - -**Spelling refusal.** `dag_emit_token_class_untransformable`: a class with neither a fixed lexeme nor -a value inverse. It is located at the terminal. - -The atom-collision case named above (a class atom versus a lexeme atom) is not a separate refusal. A -`StampLexeme` position reads a childless atom as its lexeme first, and the round-trip control is the -falsifier. - -There is no fallback arm anywhere. A refusal never widens to "emit the atom's spelling" (DESIGN §5). - -## The cut - -This is one change, per DESIGN §3 on replacement migrations: delete first, then fix forward. - -1. **Add** the generic backward walk to `v2.std.grammar` (`grammar_emit_parse_tree`), and the dag - spelling to `v2.extdeps.languages.dag` (`dag_emit_parse_tree`). The string encoder shares - `dag_string_escapes`. -2. **Not done here; this is the declared frontier.** `dag_translation_rules_node` still enrolls only - the `fn add` row. It serves the core route, which needs the inverse of body lowering before it - can read this walk (see "Rulings and declared frontier"). -3. **Delete, in the same change:** the hand-authored declaration grammar. That means - `dag_type_decl_structural_formal_productions`, - `dag_type_decl_productions_only_translation_rules`, `dag_type_decl_structural_lex_rules`, - `dag_type_decl_structural_lex`, `dag_type_decl_structural_target_model`, and their only consumer, - `v2.test.execution.emit_ingest_type_decl_round_trip`. Its roster entries in - `v2.workflow.floor_grandfathered_roster` and `gunbc.witness.witness_deferral_freeze` go with it. - - **Correction, measured while cutting.** The flat `dag_formal_production_fn_*` table and - `v2.compiler.ingest` `parse_atom_frontier_class` are **not** part of this route, so this change - does not delete them: - - They serve the cross-language forward bridge `parse_tree_to_emitted_node`, which Python and - TypeScript also use. - - They serve `v2.compiler.body_producer_forward`. - - They serve the core-route emit, `compile_dag_source_to_target_text`. - - That is the core -> surface frontier above. They are retired when that item lands, against the - roster in `gunbc.dag_grammar_fork_census`, which keeps rostering them until then. -4. **Replace** `emit_ingest_type_decl_round_trip` with the coverage round trip over the real - grammar. - -**Controls.** Round trip on the parse-tree Node for each of these fixtures: - -- `data m: Int = 1` -- a fn declaration -- a record type -- a coproduct -- `match` -- `let` -- a list -- a map literal -- a quoted key -- string escapes (`\"`, `\\`, `\n`, `\u{e9}`, `\x41`) -- a caret symbol - -Quoted keys add more controls (from gentle-koi-724, for #12740 and #12758): - -- **The key's kind follows production identity, not spelling.** `{"Node": true}` must emit quoted, - through `^dag_surface_field_init_string_key`, and `{Node: true}` must emit bare. Both are - fixtures. -- **An escaped key round-trips.** `"a\"b"` and a key containing a `\u{..}` scalar re-escape to - an equivalent spelling and re-ingest to the identical node. -- **Two mutants must go red:** emitting the raw lexeme, and emitting a quoted key in bare form. -- **The tree covered is the surface tree.** This is the pre-elaboration map literal, not resolve's - elaborated `map_from_entries(..)`. Quoted keys in record literals still refuse at lowering - (#12740's frontier), so they are parse-tree fixtures only. - -There is also one red: a root with one production removed must refuse with -`grammar_emit_production_unknown` at the right locus. Both the reds and the positives run through the -real `parse_module` route, not a supplied tree. This route is the inhabitance claim. - -## Rulings and declared frontier - -**Subject (neat-boar-16's ruling).** Emit's subject is the **parse-tree Node**. The control is -text -> parse tree -> text -> parse tree, and the two trees must be identical. The core Node is not -the subject. - -**A missing stamp refuses.** A tree that lacks the production stamps emit needs is refused, typed -and located, as `grammar_emit_production_stamp_absent`. Emit never guesses a production from the -shape. - -**Declared frontier: core -> surface.** This emitter is a parse-tree emitter. It is **not** full -dag emission. To emit a program that was constructed or lowered rather than parsed, emit needs the -inverse of body lowering: from a core Node back to a production-stamped surface tree, which this -walk then reads. That inverse belongs to the lowering's own authority and is a separate work item. -Its named consumer is gen-2 self-host emission: the built CLI emitting a closure (DESIGN §7). Until -that item lands, nothing on that route may cite this walk as dag emission. - -**Open: 02_parse's prepared grammar.** I still need its owner to confirm that the projection-edge -shapes in the table above are a stable contract. diff --git a/docs/plans/dag-native-scm-design.md b/docs/plans/dag-native-scm-design.md deleted file mode 100644 index 807b59f5a9e..00000000000 --- a/docs/plans/dag-native-scm-design.md +++ /dev/null @@ -1,548 +0,0 @@ -# A `.dag`-native SCM — model, vocabulary, and first executable slice - -DRAFT, design-note-first. **No code lands from this note.** Claims are marked **verified** (read -out of the live tree), **derived** (a consequence of a verified claim plus an authority doc), or -**proposed** (a design position awaiting a discriminating witness or an operator ruling). - -This note supersedes an earlier draft on the same branch that modelled merge as a three-way diff -over parsed trees. §9 records what was rejected and why; the scenario corpus from that draft -survives unchanged in §8. - ---- - -## 1. Vocabulary — git's words, git's constraints deliberately not inherited - -Operator ruling: **anchor on git's terms** — for anti-forking, not familiarity. Coining a new word -for a concept that already has an industry-standard one is the nicknaming DESIGN §3 forbids, and -git's vocabulary is the shared one. New vocabulary must earn its place by naming something git -genuinely fuses or lacks. - -The separating rule: **keep the word for what it *means*; drop what git *does* to implement it.** -"Commit" means *record a state I can point at later* — that survives. Git's mechanism — a whole-tree -snapshot fused with one parent, an author and a timestamp — is one realization, not the meaning: -DESIGN §3's interface/realization split applied to naming. - -| term | meaning here | -|---|---| -| **object** | immutable language content, content-addressed | -| **proposal** | requested obligations — what an author asks the program to contain | -| **branch** | a line of proposals and commits; a grouping label, contributing nothing to identity | -| **role-requirement integration** | combine proposals into one target commit. Named `merge` until 2026-09-04; renamed because two-commit merge is a materially different contract and one spelling cannot carry both | -| **commit** | the exact frozen program | -| **acceptance** | agreement to use a commit — the one new term | -| **deploy** | make an accepted commit active | - -`acceptance` earns its place because git's `commit` fuses three separate questions: *is this program -valid?*, *have we agreed to use it?*, and *is it what is running?* Merging them back into one word -would re-create the fusion this model exists to remove. - -**Git terms and their disposition.** Almost every row is *refused*: it names a *mechanism* rather -than something a person wants, and exists to repair a model that loses information. One row is a -*withdrawal* — a term this note refused on reasoning that turned out to be wrong — carried here and -marked rather than deleted, so the correction stays attached to the claim it corrects. Each term's -standing is stated by its `disposition` cell; membership in this table no longer asserts refusal. - -| term | disposition | why | -|---|---|---| -| `rebase` | refused | "rewrite my work as though it started elsewhere" is a procedure, not a goal; the goal — make my work apply to the current state — happens without it | -| merge base / common ancestor | **withdrawn 2026-09-04** | It read "requires a total order that need not exist", which is false: a commit DAG supplies a PARTIAL ancestry order, common ancestors are the intersection of two ancestor closures, and best common ancestors are the maximal elements of that intersection. No total order is involved. The concept is a prerequisite of two-commit merge, which this note does not yet model | -| `HEAD~3`, `HEAD^` | refused | parent arithmetic assumes a single line | -| force push | refused | acceptances only append; there is nothing to force | -| index / staging area | refused | an artifact of the working-copy model | -| `reset --hard` | refused | mutating a pointer | -| `cherry-pick` | refused | patch transport, which this model does not do | -| conflict markers | refused | a textual representation of an unresolved choice | -| `squash` | refused | role-requirement integration already produces one exact commit; squash is a git-export description | - -## 2. Comments, formatting, and everything that is not the program - -Stated first because it decides what a commit *is*, and because getting it wrong is how a system -silently loses what people wrote. - -**Verified.** `std.source_annotation` already models this: `AnnotationPlacement`, -`UnboundAnnotationCapture`, `SourceAnnotationDebt`, `SourceAnnotationGraph`, -`AnnotationAttachmentRefusal`. Comments are captured *data attached to structural subjects*, not -discarded trivia and not semantic program content. - -DESIGN §4c rules the two facts this design needs. Adding, deleting or moving an annotation **cannot -alter any semantic occurrence identity, semantic graph, resolution result, semantic hash, or -target-program bytes**; and semantic passes receive the annotation-erased projection while *"source -authoring, formatting, **SCM**, and annotation lenses consume and preserve the authored wrapper."* - -**Derived — two identities, deliberately:** - -- **semantic identity** — over the annotation-erased graph. Annotation-invariant *by rule*. -- **authored identity** — the semantic graph together with its annotation graph. What was written. - -Four consequences, all of them features: - -1. **A comment-only change is provably a semantic no-op** — new authored identity, identical - semantic identity, stated as a fact rather than guessed from bytes. -2. **Comments never cause semantic conflicts.** Annotations are keyed by their subject, so two - people annotating different declarations never interact. -3. **An annotation collision is its own lower-stakes conflict**, reportable separately from a - semantic one. -4. **Reformatting is free**, and the authored form still round-trips because captures carry the raw - `lexeme` — delimiter included — normalized on read, never on capture. That distinction is - load-bearing: its own carrier records that a realization stripping the delimiter where another - does not makes parse→render→parse either double it or silently change content. - -**Comments are eternal carried debt** (operator framing; the type is literally -`SourceAnnotationDebt`). DESIGN calls plain annotations modeling debt: prose not yet migrated into -typed carriers, carried forward until someone models it properly. The SCM's obligation is to **carry -them faithfully without enshrining them** as permanently first-class, so the debt stays payable -rather than load-bearing. - -## 3. The model - -**The store is append-only and immutable.** Editing creates another object, never modifies one. -Objects are content-addressed, so identical content is one object. Nothing in the store ever -conflicts. - -**A program is an act of exclusion, so the store alone is not enough.** The store only grows — -adding an object never invalidates anything — but choosing one `foo` means not the other. A -monotonic store cannot produce that non-monotonic fact, so something must *close* a selection and be -nameable. This is A3 (agreement holds only on what stays stable across time) reaching the design -directly. - -**A commit is that closed thing: one root object identity whose every reachable edge names exact -content.** No parent list, author, timestamp, message, or branch — those are facts about proposals -and acceptances, not the program. - -**Selection must not stay live after a commit.** A program re-querying the store at use time would -let later additions retroactively change its meaning. Integration outputs an immutable exact graph, which -is why the unit of agreement is a commit root rather than a choice function. - -Five facts, kept apart: - -``` -store possibility — every object that exists -commit a program — one exact closed graph -certificate validity — does this program hold together -acceptance agreement — we have agreed to use it -deploy now — it is what is running -``` - -**Identity does not need to survive across revisions, and cannot.** *Verified:* -`std.occurrence_identity` `occurrence_identity_scope_law` makes `OccurrenceId` unique only inside -one graph-scoped allocator and forbids filename, span, authored name, structural equality and -content hash as identity inputs. *Derived:* cross-revision identity could therefore only be minted -at authoring time and carried in the source — and DESIGN §3's fleet ruling says inferring it from -content similarity is the heuristic §4 forbids. **It is also unnecessary:** identity's only job in a -line-based merge is transporting a patch to the right place, and combining proposals transports -nothing. - -**A name identifies a binding role in a context, not a thing.** Renaming changes a binding; the -value is untouched. So content equality does not imply one program occurrence — one object may sit -at several positions, sharing storage while independently addressable by path. - -**"Latest" is a query, not an identity.** Already ruled here: `extdeps.pin` `pin_selection_note` — -*execution always resolves an exact identity, because resolving "latest" at execution is -nondeterminism at the substrate boundary; re-resolution is an explicit action producing a reviewable -diff.* Pinning a moving reference is no contradiction: resolve it to an exact identity and record -that it came from a channel. `Pin` is already that shape, subject-generic by operator -ruling, and `Pin` is an instantiation, not a new concept. - -## 4. What role-requirement integration is - -This section is about combining PROPOSALS into one target commit. It is not about two-commit merge, -which this note does not model; see the vocabulary table in section 1. - -Two proposals combine by taking both. A conflict exists only where one binding is required to hold -two different objects — a genuine disagreement about what the code should be, never a collision of -positions. - -Where the substrate says an edge is `Named`, recursion is keyed by that name; where `Positional`, -**order carries meaning** — list elements, match-arm order — and the region is compared whole. -Refusing there is the safety half of the design: combining positional edits by identity would -fabricate a sequence nobody wrote, which is precisely what line-based merge does and calls success. -*Verified:* `v2.std.node` carries `EdgeLabel = Named { name } | Positional` per-edge at every depth, -and named edges reach deep — `match_arm_pattern`, `binding`, `field`, and a function attaching to -its parent as `Named { name: fn_name }`. - -**One grain — the node.** Depth is not a parameter; it falls out of where the substrate says -identity is by name versus by order. - -## 5. What this model does not claim - -**Integrations are not "always safe."** Deep semantic understanding makes conflicts rarer and better -explained, not absent. Two changes can each be valid alone and interact — a new call to `foo` plus a -narrowing of `foo`'s contract. Structural combination and **validity** are separate, and the second -genuinely refuses: a structurally clean rename plus a stale reference is a broken program. The -honest claim is *far fewer conflicts, each a real question in the user's own vocabulary.* - -**The hard part is not removed, only the accidental part.** Two proposals may admit no common -program, or several materially distinct ones — the same compatibility problem that combining -independently authored work has always addressed. Gone is everything around it: mutable files, patch transport, branch topology, global -history, working-tree alignment, rebasing, and cloning. - -**Identity is currently weaker than the model needs.** *Verified:* `v2.std.node` `Hash` is -`Fnv1a64Structural` — 64-bit, non-cryptographic. A 64-bit non-cryptographic digest is a **locator, -not a durable intersubjective identity**, and adding a host builtin is closed because DESIGN freezes -the v1 seed's growth surfaces. **Declared rung:** the first slice uses the available digest and -states this limitation rather than implying cross-party agreement it cannot support. - -**Corrected 2026-09-21 — the dissolve-on condition has been half met, and the half that remains is a -different one.** This paragraph said "no SHA-256 *computation* exists in `.dag` -(`std.content_hash` `sha256_hex_digest` and `extdeps.crypto.hash` `sha256_digest` validate hex; they -do not hash bytes)". That was true when written. `extdeps.crypto.sha2` now computes SHA-256 in the -substrate (`sha256`, `sha256_hex`, FIPS 180-4), witnessed by `test.claim.sha256_fips_witness` — and -the witness discriminates rather than restating the type, because `sha2` routes every 32-bit -operation through `std.bitwise`, whose `word32_add` wraps by comparison against `word32_modulus` so no value transits above 2^32-1, and the -interpreter's unbounded-`Int` evaluation yields the same residues and a wrong wrap changes the -digest. - -What has **not** happened is the migration: `extdeps.crypto.sha2`'s only importers are -`extdeps.crypto.nist_p256` and its own witness. `std.content_hash` does not import it, so -`v2.std.node` `Hash` and `std.fabric_storage`'s object refs still mint through -`content_hash_of_value`. **Revised dissolve-on:** `v2.std.node` `Hash` minted from a cryptographic -family. `std.fabric_storage`'s object refs are being moved onto the pure kernel in gunbc#11996 — -the shell-out alternative was refused there for a structural reason worth recording, since it is the -one this note would otherwise invite: `fabric_object_preimage` produces bytes **in memory**, so a -shell transport over a file path means a temp-file write per object, which manufactures the custody -gap (hash one file, store another) that verification exists to close. `v2.std.node` is the half that -remains, and it is gated on native emission of the `sha2` closure — blocked by -`gunbc.recurring_failure_mode` `bounded_natural_arithmetic_evaluated_as_unbounded_int`, whose -prerequisite is `MachineWidth` reified as a value. Both halves are owned by the lane holding -`#11819`; open question 2 below is narrowed accordingly rather than closed. - -## 6. Confidentiality - -Secrets are the forcing function for redaction, and redaction is where a content-addressed history -either has an answer or admits it does not. - -**Git's answer is a non-answer**, and that is the design input. A secret in git history requires -rewriting every subsequent commit; every hash changes; every clone, fork and open PR diverges. On a -public repository this is *ineffective*, not merely expensive — the objects are already distributed. -Practitioners actually "rotate the credential and assume the bytes are permanently public." We must -not build a better version of that theater. - -**Five independent facts git fuses into one.** Prevention, current-state retraction, -audience-history retraction, credential invalidation, and bounded erasure are separate, and the -distinction between the two retractions is load-bearing: - -- **Prevention** — bytes never reach the public store. *The only operation that preserves - confidentiality.* -- **Current-state retraction** — the head no longer contains the material. **Not enough**: a clone - replaying from the original root still recovers it. -- **Audience-history retraction** — new readers begin from a sanitized anchor and never receive the - material. This is what people mean by redaction, and a different fact from the above. -- **Invalidation** — rotate the credential so leaked bytes are worthless. **Already modeled**: - `gunbc.auth.secret_rotation`, with exactly-once walls, receipt-backed retirement, and no stored - payload digest. -- **Erasure** — bytes unrecoverable. Claimable only inside a closed sanitization scope covering - every key, wrap, backup, cache, derivative, index and recovery path. Never "delete one key." - -**Derived rule:** on a committed secret, SCM performs retraction, the rotation kernel performs -invalidation, and erasure is claimable only in the private realm. **The SCM must never report -retraction as erasure.** Once disclosure escapes a controlled scope, `UncontrolledCopiesMayRemain` is -a **terminal standing** and **no global-erasure constructor exists** — the impossible claim is made -unwritable rather than merely discouraged. - -Two consequences easy to get wrong in opposite directions. Invalidation is **not** established by -the secret manager disabling a version: the service may still accept the leaked token, so -invalidation needs a subject-specific negative authentication probe. And a rewrite lacking a -complete sanitization receipt is `PrivateHistoryReanchored`, **not** erased. - -**Where the wall sits.** Integration-time or CI-time checking is structurally incapable of confidentiality -— the deleted Stage-0 placement gate established exactly this: a required check could refuse `main`, -but pushed objects had already reached public storage. The chronology must be private capture → -private integration and admission → derive an audience-authorized projection → serialize → write public -storage. The public writer accepts only a projection minted by the authoritative private context, -never an arbitrary `Node`, blob or patch. **Corollary:** a public PR branch cannot safely carry -secret bytes on the theory that squash-landing removes them. The surface is wider than file content: -a secret in a commit message, path, diagnostic, workflow log, or projection metadata is the same -disclosure. - -**Retraction without rewriting.** Where a commit is *accepted parent plus sparse transformation*, -retraction can be an ordinary **appended transformation** — no existing identity changes, no -downstream clone is invalidated. **This holds only if public history is an audience projection -rather than a replayable copy of private transitions**; otherwise replay from an early checkpoint -reconstructs the secret on the way to the head, achieving current-state retraction only. The -resolution is an **audience-specific projection epoch with a sanitized anchor**: re-anchor the -*public projection*, never the authoritative history. - -Three constraints on that epoch, each of which an earlier draft got wrong by vagueness. The -sanitized epoch must have **no public predecessor relation** to the contaminated one — a tombstone -naming the old root, path, or incident reason is itself the leak, so the mapping is retained -privately or not at all. The old epoch must be **retired from authoritative service**: a clean head -with the old refs still advertised is not retraction. And **hiding a ref is not retraction** if -direct object retrieval still serves the bytes. *(Proposed — the least settled part of this note.)* - -**No confidentiality receipt carries secret bytes or a secret-derived digest**, following the -rotation kernel's existing rule. Receipts retain opaque incident identity, subject references, -non-secret-derived transition identities, verification verdicts, audience standings, and bounded -sanitization evidence. - -**Diagnostics are an information-flow surface.** If an old-root lookup answers "retracted" where a -nonexistent root answers "unknown", that difference is an existence oracle; where existence is -confidential the unauthorized responses must be indistinguishable. Over-redaction is the paired -failure — an authorized responder must still receive located detail — so both directions need -controls. - -**At the user boundary:** "keep the revoked secret" is never offered as a `ChoiceRequired`. It is a -hard `CouldNotLand`. - -**`.env` does not belong in the graph.** Secret *values* live in a secret store; the graph holds -`SecretRef` nodes, schema, and required binding identities, and `.env` becomes a local -materialization, not source-control authority. A marker inside the file cannot self-authorize: -removing `secret=true` must be an authorized policy change, not an ordinary content edit. - -**Node-grain withholding survives only at a real interface/realization boundary**, consistent with -the existing refusal of per-statement holes. Public signature with encrypted body: allowed. Public -body with one expression silently missing: refused. - -**Erasure is not strictly harder than git's** — git is already a Merkle DAG, and finer grain can -help: a secret subtree can carry its own key instead of requiring a whole file's destruction. But -*accounting* is harder, and two hazards are specific to this design: never publish the private -canonical root hash as the public root, and **never deduplicate secret plaintext across audience -realms — cross-audience content-address equality is an oracle.** - -## 7. `NATIVE-COMMIT-0` — the first executable slice - -**Honest feasibility ruling first.** An executable slice exists without the parked authoring-capture -surface, but it **cannot honestly integrate arbitrary concurrent edits to existing `.dag` files**. -Without capture, nothing can tell from two file endpoints which proposal, deletion, rename or frame -the author intended; claiming otherwise reconstructs patch inference under new vocabulary. So the -first slice integrates **explicitly authored proposals**, and says so. - -Three operations. No base, ancestor, parent, branch pointer, working copy, or patch. **This is the -shipped signature, corrected 2026-08-21 against the operation then at `dag/gunbc/scm/merge.dag` as -merged in #8719, now `gunbc.scm.role_requirement_integration`** — the -draft below it specified a two-proposal call and a `ChoiceRequired` arm, neither of which exists, -and a stale authority describing an operation nobody can invoke is the premise contamination DESIGN -documents against its own CI paragraph: - -``` -integrate_role_requirements(store, target, target_dependencies, proposals) - -> RoleRequirementsIntegrated { store, commit, roles } - | RoleRequirementsContested { role, alternatives } - | RoleRequirementIntegrationRefused { cause } -accept(store, commit, authority, contract) -> appended; never changes the commit -checkout(store, commit) -> CheckedOut { program } | CheckoutRefused { cause } -``` - -**Why the signature grew a target and lost a proposal count** (operator direction, 2026-08-20). The -two-proposal form had nothing to preserve *from*, so all it could do was union bindings — neither -the literal closed-scope reading nor a dependency model — and it ships a program referencing a -deleted node when one proposal adds `k` depending on `f` while the other deletes `f`. The target -supplies every fact outside the implication frontier, and **silence means preserve it**. Proposals -became a list because a contest is a property of the whole population, not a pair. - -**Why the outcome arms are named for less than they conclude.** `RoleRequirementIntegrationRefused` rather than -`CouldNotLand`: the engine cannot establish terminality — an unsupported dependency kind may be -unmodelled implementation, a missing target root an incomplete fetch. `RoleRequirementsContested` rather -than `ChoiceRequired`: this layer establishes that distinct alternatives exist; that a *user* must -choose requires proving they survive the admitted equivalence quotient, that the candidate -population is closed, and that no further machine work resolves them — none of which lives here. -**`RoleRequirementsContested` is complete over the proposal population supplied to that call and proves -nothing about global candidate-space closure.** That boundary is held by this paragraph and the type -name, not structurally; the landing seam is where it becomes structural. - -A proposal is a set of authored requirements — `RequireBinding { role, value }` or -`RequireBindingAbsent { role }` — meaning *the resulting program must contain this exact value at -this named binding*, or *must not bind this role at all*. Absence is authored, not implied, because -silence already means preserve: a delete by omission would read as "leave it alone", the inversion -of the request. Both sides normalize to a requested state (`DesiredRoleValue`) before any contest is -decided, so identical authorings are **agreement**, not a question with identical alternatives. -Distinct bindings combine by construction; two different requested states at one binding produce -`RoleRequirementsContested` carrying **every** distinct alternative — accumulated over the whole -population so arrival order cannot decide which evidence survives, and never latest-wins, -first-wins, or branch-priority-wins. - -`checkout` follows exact object links from the commit root and consults **nothing else** — not -names, branches, proposals, acceptances, or the live store beyond the identities it was given. - -**Storage:** objects keyed by identity, plus append-only acceptances. Canonical encoding excludes -occurrence provenance, sorts named edges, retains positional order, and tags constructors distinctly. -In-memory first; persistence, packing, networking and collection are separate realizations -deliberately kept out of the semantic slice. - -**Witnesses**, each with a deliberately-wrong control so that a passing suite means something: - -| claim | witness | wrong control that must fail | -|---|---|---| -| a later object cannot change an agreed program | commit, accept, add a second object at the same name, re-checkout | a checkout that queries live names | -| a commit rebuilds from its own closure | discard proposals, branches, acceptances and indexes; checkout from the root alone | — | -| a missing object refuses | remove one reachable object, re-checkout | any fallback substitution | -| proposals combine without node identity | remint every occurrence id, re-integrate | an integration requiring matching occurrence ids | -| conflicting bindings ask rather than guess | same name, different objects | any winner rule | -| arrival order is not authority | integrate (a,b) and (b,a) | — | -| integration does not imply agreement | integrate without accept | — | - -**Deliberate non-goals:** no proposal inference from file edits, no capture, no diffing, no -deletion/rename/move, no repository-wide candidate search, no general constraint solving, no -positional-sequence integration, no git import or export, no `GIT-PLUMBING-0` dependency, no refs, no -compare-and-swap, no cross-process persistence, no deploy, no confidentiality, and **no merging of -this repository's live concurrent edits.** - -**Known scope limit, stated because it affects what the slice demonstrates.** Proposals are keyed by -top-level binding, so two proposals touching one binding differently ask a question even when they -changed unrelated sub-nodes. **The headline win — two people editing different functions in one -module combining cleanly — is the *next* slice**, which recurses §4's named-edge rule. This slice -proves the foundation: content-addressed objects, stability under growth, exact reconstruction, and -combination without identity. - -**Dogfood disposition:** a small controlled subject first. Making this repository's concurrent edits -the first consumer would fuse a model proof with unrelated frontend and performance work. - -**Direction after `NATIVE-COMMIT-0`, operator ruling 2026-08-21: a CLI vertical before the depth -recursion**, reversing the "headline win is the next slice" ordering above. The reason is DESIGN -§5's specification-without-execution trap, not a changed view of which slice is more valuable. -Nothing has ever *consumed* this kernel: the claims declared in `test.claim.scm_role_requirement_integration_witness` are -assertions **about** it, authored alongside it, and a witness suite is not a consumer. A CLI is the -first artifact that uses the store, checkout, identity and role-requirement integration together under conditions nobody -authored to make them pass. - -The ordering is safe because **depth changes how `integrate_role_requirements` combines, not how -`add` authors**. A user -adds a module either way; whether integration recurses into it is orthogonal, so the command surface built -now survives the recursion landing later. - -The known cost, designed for rather than discovered: a CLI makes the top-level-binding limitation -*user-visible*, since the first thing anyone tries is editing two functions in one module — which -today asks a question instead of combining. That refusal must say exactly that, in those terms, -rather than reading as a defect. - -## 8. Scenario corpus (retained) - -No role-requirement integration kernel should be built without these. Each is a RED unless marked -otherwise. - -**Structural integration — with measured coverage as of 2026-08-21 (#8719 merged).** The corpus -opens "no integration kernel should be built without these", and a kernel was built, so this states -which rows it -actually answers. **4 of 13 were covered when that join was run on 2026-08-21**; see the -measurement note below the table for why that figure has not been re-derived since. The `witness` -column names the claim, which now resolves in -`test.claim.scm_role_requirement_integration_witness`; the `blocked on` column says what would close each gap, because -"uncovered" collapses three situations — a gap the current grain could close today, a gap needing -the sub-node recursion, and a gap needing vocabulary the model does not yet have. - -| scenario | required result | covered | witness / blocked on | -|---|---|---|---| -| target changes an untouched named node | preserved, no conflict | **yes** | `an_independent_sibling_is_preserved_exactly`, `an_empty_proposal_set_preserves_the_target` — preserved at *identity* grain, so it is object reuse rather than a recomputed equal | -| proposal edits one named child, target edits another | both preserved | no | **sub-node recursion.** This is the headline win; the kernel is top-level-binding keyed and cannot express it | -| proposal deletes a subtree, target edits a descendant | conflict | no | **sub-node recursion.** `dependent_add_and_delete_refuses` is the role-grain analogue — delete `f` while `k` requires it — and is *not* this scenario | -| concurrent same-name add, same kind, different children | conflict | **yes** | `two_distinct_requests_for_one_role_are_contested` — different children give a different content hash, hence a distinct `DesiredRoleValue` | -| one side changes node kind, other edits a child | conflict | no | **sub-node recursion** | -| both sides produce an identical kind-changed subtree | clean | **yes**, degenerately | `the_same_request_authored_twice_is_not_a_contest` — identical results agree at top-level grain; the sub-node path is untested | -| unchanged-body rename | unique relocation candidate | no | **no rename vocabulary.** Neither `Requirement` nor the outcome can express relocation | -| rename versus edit under the old name | conflict; the edit is not lost | no | **no rename vocabulary** | -| two sides move one subtree to different parents | conflict, never duplicated | no | **no move vocabulary** | -| duplicate named siblings in any input | input refusal | no | **closable at the current grain.** Measured 2026-08-21: nothing in `gunbc.scm.*` or `v2.std.node` refuses a node carrying two children with one name. The nearest live behaviour is content-hash canonicalization *sorting* named edges, which orders duplicates rather than refusing them. This is the one structural gap that needs no new grain and no new vocabulary | -| two different positional appends | conflict or explicit order choice | no | **positional-sequence integration**, an explicit §7 non-goal for this slice | -| distinct positional ordinals edited without shape change | both preserved | no | **positional-sequence integration** | -| only one side changed a node | **preserved — the §6 asymmetry regression control** | **yes** | `an_independent_sibling_is_preserved_exactly` | - -**What the partition says about the next slice.** Four gaps are one job — the sub-node recursion of -§4's named-edge rule. Three are a second job needing rename/move vocabulary the model lacks. Two are -the positional-sequence-integration non-goal. **One — duplicate named siblings — is closable now**: the only -structural scenario waiting on neither depth nor new vocabulary, hence the cheapest real coverage -available. - -**Coverage claims about this table must be measured, not recalled.** The `4 of 13` above was -measured on 2026-08-21 by joining, one row at a time, the claims that the witness module declared -ON THAT DATE — when it was `test.claim.scm_merge_witness` and held a different population. An -earlier estimate from reading was "roughly five" — close enough to feel safe, wrong enough to -mis-scope the next slice. - -**That number is a historical result and is NOT a current-completeness claim.** Naming today's -module does not re-run an August join, so this paragraph deliberately does not cite the renamed -module as the measurement's source: a citation that silently re-points at a changed population -would make a dated receipt look re-derived when nothing re-derived it. The 13-row table below is -the current authority on WHICH scenarios exist; what is missing is an executable instrument that -re-joins today's claims against those rows, and until one exists no row here may be described as -covered on the strength of the August figure. - -**Admission and capture** - -| scenario | required result | -|---|---| -| structurally clean rename plus stale reference | semantic admission refuses | -| occurrence ids collide numerically across allocator scopes | result reminted or remapped | -| comment or annotation absent from `Node` capture | opaque residue or capture refusal, never silent loss | - -**Confidentiality.** Every refusal needs a nearby positive control, so that "always refuse" cannot -satisfy the suite. - -| scenario | required result | -|---|---| -| raw payload reaches the public writer before policy evaluation | writer invocation count stays zero; `PublicationPrevented` | -| publication authority unavailable | refuse before write; never classify as clean | -| secret inside opaque/unparsed file residue | capture-fidelity refusal before serialization | -| secret in commit message, path, diagnostic, log, or projection metadata | refuse or redact; no public object carries it | -| bytes already public but lifecycle reports `PublicationPrevented` | impossible state | -| append a retraction, then clone by replaying from the original root | secret recovered — only `CurrentStateRetracted`, never audience retraction | -| sanitized epoch names the contaminated predecessor | commitment/existence leak | -| old ref hidden but direct object retrieval still serves the bytes | retraction incomplete | -| clean head while the old epoch is still advertised | retraction incomplete | -| any receipt stores a payload digest | type uninhabitable | -| one backup, cache, wrap, derivative or recovery path unobserved | `ErasureUnestablished` | -| rewrite succeeds without a sanitization receipt | `PrivateHistoryReanchored`, not erased | -| private realm erased after public exposure | private erasure **plus** `UncontrolledCopiesMayRemain` | -| secret-manager version disabled but the service still accepts the token | not invalidated | -| unauthorized diagnostic names retracted path, secret kind, or incident time | audience-projection refusal | -| authorized responder denied located detail | over-redaction control | -| retracted-vs-nonexistent lookups distinguishable to an unauthorized reader | existence oracle | -| public graph retains a dangling reference to retracted material | public projection refuses | -| proposal from a revoked epoch persisted publicly before inspection | ingress wall | -| proposal independent of revoked material blanket-refused | recovery-expectation failure | -| proposal relying on revoked material lands via ordinary stale-parent retry | admission failure | -| "keep the revoked secret" offered as a choice | user-boundary failure — must be `CouldNotLand` | -| retraction reported to a user as erasure | never | - -## 9. What was rejected, and why - -Kept because the reasoning is the most reusable part of this note. - -**Three-way merge over parsed trees.** The first draft modelled merge as `capture(base, proposal)` -producing a patch keyed by declaration name. It works and beats line-based merge, but it is -diff-and-patch with a semantic key — reconstructing intent from endpoints because the information -was destroyed before it was seen. It also inherits a base, and through the base a timeline. - -**Cross-revision node identity.** Pursued for most of a day as the way to make renames carry. Closed -by `occurrence_identity_scope_law` and, more importantly, unnecessary: identity exists to transport -patches. - -**`std.change` `keyed_three_way_fold` as the integration kernel.** *Verified:* its leaf verdict returns -`KeyedConflict` when only one side changed a value — correct for reconciliation, where `desired` is -authority, and wrong for integration, where the sides are peers. The traversal is shared with the -fleet-reconcile spine; the verdict is not. - -**The seven inherited assumptions.** Text as storage, commit-as-whole-tree-snapshot, one global -timeline, branch-as-mutable-pointer, merge-as-invoked-event, the working copy, and the repository as -a unit. Three are simply wrong here. Four contain a real requirement in a git costume: a state -commitment is load-bearing though the snapshot is not; arbitration is real at a unique effectful -target though compare-and-swap is not fundamental to content; a sparse working *context* is real -though a materialized tree is not; and authority domain, confidentiality realm, retention scope and -trust policy are real boundaries that must not be re-fused into a "repository" type. - -**The existing P1 kernel.** `gunbc.source_integration_proof_kernel` is built around an -`accepted_parent` and a before/after delta — the mutation-and-parent assumptions this model rejects. -*Proposed:* freeze it as quarry rather than extend it. This touches an active roadmap node and is an -operator decision, not this note's. - -## 10. Open questions - -1. **Authoring capture.** The largest fork, and the reason §7 is scoped as it is. Until an authoring - surface records what an author *did*, proposals must be stated explicitly rather than inferred. -2. **Durable identity** (§5) — the declared rung. **Narrowed 2026-09-21:** a computing - cryptographic digest now exists (`extdeps.crypto.sha2`); what is missing is a consumed path from - it to `v2.std.node` `Hash`, which is gated on native emission of that closure. - `std.fabric_storage` is being migrated in gunbc#11996; `v2.std.node` is the remaining half. - See §5. -3. **Recursion into named edges** — the next slice, and the one that demonstrates the differentiator. -4. **Retraction epochs** (§6) — the weakest claim in the note. -5. **Positional append** — whether two appends commute. Special-casing risks re-importing the - line-merge heuristic through the back door. -6. **Alpha-renaming neutrality** — may hold only for non-exported, non-shadowing names, since an - exported name is an external interface. - -## Dissolution trigger (DESIGN §6) - -This note dissolves into the carriers it names when an integration kernel and a confidentiality -lifecycle -land. Until then it is evidence about a model, never authority over one. The §8 corpus outlives it: -those rows re-enroll against whatever kernel lands, per DESIGN §4b's rule that discriminating -evidence survives the machinery that prompted it. diff --git a/docs/plans/declaration-body-resolution-provider.md b/docs/plans/declaration-body-resolution-provider.md deleted file mode 100644 index 53f1038a641..00000000000 --- a/docs/plans/declaration-body-resolution-provider.md +++ /dev/null @@ -1,201 +0,0 @@ -# Declaration-body types: one resolution per ingest, produced on demand - -Work item adhoc-603dcdcb-f4b (royal-stag-371). Plan only; no code lands under this document until -quiet-gull-780 and neat-boar-16 clear it. Rulings it builds to: quiet-gull-780 (demand-driven, one -resolution, not eager) and neat-boar-16 (conditions a, b and c, stated in §5). - -## 1. The defect, re-derived - -The brief assumed field and payload type positions reach resolve as unlowered shells. On main that -is no longer true. `v2.compiler.body_lowering_fold` already lowers record fields -(`body_lower_field_decl_edge_optional`), positional payloads (`body_lower_positional_payload`) and -alias right-hand sides (`body_lower_alias_rhs_optional`) through `body_lower_type_expr_lowered_optional`, -and #12629 adds the where-head. So `gunbc.recurring_failure_mode` -`declaration_body_type_shell_preserved_unresolved` overstates its frontier. - -The earliest unjustified boundary is one link later. `v2.compiler.infer` `infer_projection_receiver` -(on the field-projection lane, #12506) reads the receiver's declaration from -`ResolvedTree.symbol_index`. That index holds declarations AS AUTHORED: it is built by -`v2.compiler.name_resolve` `resolution_context` from normalized roots, before any resolve. So -`artifact.tree` is typed as the bare atom `ParseTree`, while a parameter annotated `ParseTree` -carries the resolved reference. The match-arm mismatch that calm-pike-507 measured on #12641 -follows directly. - -#12629's `ResolvedTree.resolved_declarations` cannot repair it. `resolved_declarations_of(root)` -folds the SUBJECT module's declarations only, and the real seven reads `v2.compiler.parse` -`ParseArtifact.tree` from another module. On the native lane that declaring module often has no -resolve result at all. The seed main (`v1.compiler.emit_rust`, the native driver loop) resolves, -infers and evaluates one universe member at a time, in universe order rather than import order, and -the universe is test-bearing modules only. - -Why a type position needs its DECLARING module: the reference resolves in that module's namespace -(its import bindings and origins). `v2.compiler.name_resolve` `namespace_for_subject_root` builds -that namespace per subject, lazily, every time the subject is resolved. It is carried nowhere. - -## 2. The construction - -Two produced values, both scoped to ONE INGEST and keyed by declaration identity, both on the -existing per-ingest carrier `v2.compiler.name_resolve` `ResolutionContext` (#11401 R1: one context -per native ingest, built in `v2.compiler.compile`). - -**P1. The module namespace provider.** -- Identity: the module's `QualifiedName`, the key `ValidatedModuleRoots.by_name` already uses. -- Value: the admitted `Namespace` that `namespace_for_subject_root` computes today. -- Scope and retention: one ingest, meaning every module the native ingest read into the closure - (not only universe members). Released with the context. -- Production: on first demand, by either consumer (the module's own resolve, or a declaration-body - resolution in P2). Both read the same stored value. -- Refusal: a module outside the ingested closure refuses at the provider with a typed, located - diagnostic. There is no per-subject rebuild and no fallback. -- `namespace_for_subject_root` stops being a builder that callers invoke and becomes the provider's - producer. It has exactly one call site, the provider's miss arm. - -**P2. The resolved declaration-body provider.** -- Identity: `std.decl_ref` `DeclarationRef`, the identity `v2.compiler.resolve` - `resolved_reference_node` already writes into resolved trees. -- Subjects: every declaration whose TYPE POSITIONS a later stage reads -- type declarations - (record fields, variant payloads, alias right-hand sides, where-heads) AND function signatures - (parameter and return types), because infer reads a callee's Arrow across modules too. -- Value: the declaration's body with every type position resolved in its declaring module's P1 - namespace, by the same `resolve_node_walk` (no second resolver). -- Scope and retention: one ingest, beside P1. -- Production: on first demand. A module's own resolve DEMANDS its own type declarations and grafts - the provided bodies instead of walking them, so no type position is ever resolved twice. After - the walk, the module's resolve demands the transitive closure of declarations its resolved tree - references in type positions, following resolved bodies (`ParseArtifact` then `ParseTree`, and so - on). -- Refusal: a reference whose declaring module is outside the ingest refuses at P1, and so does a - declaration P1's namespace does not hold. - -**P2 mechanism: how "one resolution" and "the module grafts" are realized.** -- Subject grain: one top-level declaration, either a type declaration or a function SIGNATURE (the - Arrow's parameter and return types; never a function body, which is not a declaration type). - Identity: `DeclarationRef` (declaring module, declaration name). -- Production: ONE WALK OF THE DECLARING MODULE IN A DECLARATION-TYPES-ONLY SELECTION. The walker is - the existing `resolve_node_walk` over the module's validated normalized root and its P1 namespace, - and `ResolveContext` gains a selection: `WholeModule` or `DeclarationTypesOnly`. Under the second, - value bodies are carried unwalked: an Arrow's named body edges and a `data` declaration's value. - Every type position is walked exactly as the module's own walk would walk it, so each declaration - gets the scope, position and `under_module_root` the full walk would give it BY CONSTRUCTION. The - spine descent is not replicated by hand. Production grain is the module, because one selective walk - covers all its declarations. Storage and snapshot grain is the declaration: entries are cut from - that walk's resolved root, keyed by `DeclarationRef`. A refusal in a declaration type is stored as - that declaration's outcome, and the grafting module surfaces it, located. -- Graft: before walking, a module's own resolve demands every type declaration and signature it - declares from P2. The walk then carries a READ-ONLY map from occurrence id to provided node in - `ResolveContext`, and at such a node it returns the stored node instead of descending. The walk - itself stays pure; only the pre-walk demand threads the context. -- Snapshot: after the walk, the module's resolve demands the transitive closure of declarations its - resolved tree references in type positions, and `ResolvedTree` carries that set as a projection of - the provider's values (C10). - -**Every loop over modules threads the context in PR2.** In PR1 a module's namespace is demanded only -by its own resolve, so two loops drop the returned context at no cost: `v2.compiler.compile` -`native_demand_execute`, the demand-engine drain that replaced the seed main's lane loop in #12401, -and `gunbc.namespace_xl2_rehearsal_census` `xl2_observe_module`. Once PR2 adds cross-module -declaration demands, a dropped context re-produces what another module already produced. So PR2 -carries the context on `NativeDemandRun` and through that census fold. - -**Sequencing (quiet-gull-780 decision A, 2026-09-30).** #12629, #12407, #12506 and smart-newt-725's PR -land first. PR2 then cuts over on main in one commit: every reader switch plus the deletion. P2 is -built meanwhile on a branch off PR1, not opened for merge, and never merges without its production -reader switch in the same change. Before cutting, the reader census is re-taken on main at identity -grain, because each of the four adds readers of per-module `resolved_declarations`. If one stalls -more than a day, quiet-gull-780 decides whether its reader is pulled into PR2. - -**Purity: how "at most once" is realized in a pure substrate.** A provider cannot mutate. The -context is threaded as a value: every resolve returns `(ResolvedTree, ResolutionContext)`, and the -driver loop carries the returned context into the next module. The native driver loop is the seed -main rendered by `v1.compiler.emit_rust`; that template changes so the module loop binds the -context it gets back. That is a v1 change admitted under `gunbc.v1_maintenance_standing` purpose -admission, because it serves the v2 native route. Inserting under a key that is already present -refuses (`resolve_provider_entry_built_twice`), so a double build is a typed error, never a silent -overwrite. - -**Infer reads a snapshot, not the provider.** `ResolvedTree` carries the P2 bodies demanded for this -module (the closure computed above). Infer stays pure and single-argument. A lookup outside that -snapshot refuses as `infer_declaration_outside_resolved_closure`; it never widens to `symbol_index`. - -## 3. The PRs (stacked) - -**PR1: namespace provider (P1), replacing the lazy per-subject build.** CLEARED (quiet-gull-780, 2026-09-30) to land alone as one motion: the provider replaces the lazy build, the lazy build is deleted in the same PR, and no reader can reach both. Condition (a) then governs PR2. The v1 `emit_rust` main-loop change is admitted under v1 purpose admission because it serves the v2 self-host. A root cut in one motion: after -it, "what is module X's namespace" has one producer and every caller reads it. Context threading -lands here, through `native_lane_module_resolution`, `native_test_resolve_module` and its -`_walk`, the seed main template and the stage0 mirror, `v2.compiler.compile`'s single-subject -route, and the test harnesses. Condition (a): whether PR1 may land alone, as a self-contained -replacement with no two structures, or must fold into PR2, is neat-boar-16's call. Folding it in is -mechanical. - -**PR2: declaration-body provider (P2) and the reader cut, together.** P2 lands; resolve grafts P2 -bodies; four infer readers move to the snapshot in the same commit: `infer_projection_receiver` -(field projection), `refinement_declaration` (#12407, per wise-bat-862), and -`infer_declaration_reference_facts`'s callee-Arrow read (#12506, found by calm-pike-507: it reads -`symbol_index_lookup` and gets unresolved atoms for the seven's cross-module -`parse_module_prepared(...)` call; its Arrow feeds `infer_application_callee_arrow_with_facts` and -`infer_arrow_declared_return_type`), and `infer_match_coproduct_of_type` with -`infer_binding_type_in_scope` (#12641, calm-pike-507: variant payload and field types for match -binders; type parameters are still substituted from use-site arguments, over the resolved body). -A fifth consumer moves in the same cut (quiet-gull-780, 2026-09-30): smart-newt-725's -record-construct field typing. It reads #12629's per-module `ResolvedTree.resolved_declarations` for -same-module records, so it must move in the commit that deletes that field; its cross-module -population is what P2 adds. smart-newt-725 names this PR as its trigger. A sixth consumer (quiet-gull-780, 2026-09-30): quick-hawk-799's gunbc#12809, a v2 `String` -literal lowered through the unfold. Its judge sees `declared = Ref(q.String)`, an imported -declaration, so it needs `q.String`'s resolved body, which #12629's root-only index lacks. It -stays draft until this PR lands. What dissolves in this -commit is #12629's `ResolvedTree.resolved_declarations` with `resolved_declarations_of`; no reader -is left on it. -All of these took declaration types off an authored or subject-local node; after PR2 none of them can -reach it. Branch: `session/royal-stag-371-pr2-declaration-bodies`. calm-pike-507 stacks the return-type derivation on this branch. `ResolvedTree.resolved_declarations` and `resolved_declarations_of` (#12629) are deleted -in the same commit. The RFM row is retired, and its red stays enrolled. - -**PR3 (stacked on PR2): call-return derivation** (scope added by quiet-gull-780 when calm-pike-507's -node closed with #12641 merged into #12506's branch). `infer_arrow_declared_return_type` derives a -declared corpus return type beyond kernel and `Bool`, from the resolved callee Arrow PR2 hands -`infer_declaration_reference_facts`. As a result the seven's `parse_module_prepared(...)` is typed as -`Outcome`. Proof: calm-pike-507's seven's-frontier claim in -`v2.test.claim.match_binder.match_binder_typing` -(`infer_match_scrutinee_type_underived` on that call shape) flips to a typed binder (C12). Concretely, -`mbt_the_sevens_call_scrutinee_is_a_counted_frontier_holds` goes red and is replaced by a positive -claim that the binder `artifact` in `mbt_seven_src` is `ParseArtifact` (`mbp_fact_of` with -`mbp_find_atom(^artifact)`, the shape of `mbt_binder_is_typed_parse_artifact_from_accepted_value_holds`). -The coproduct readers (`infer_match_coproduct_of_type`, `infer_binding_type_in_scope`, -`infer_match_field_type_instantiated`) currently leave non-parameter field types that are scoped to -their declaration underived on purpose. In PR2 they read the resolved types from the provider -instead. - -## 4. Controls - -| # | Control | Kind | -|---|---|---| -| C1 | calm-pike-507's one-module projection-vs-param arm (`v2.test.claim.match_binder.match_binder_typing` `mbt_match_is_typed_parse_tree_holds`) | red before PR2, green after | -| C2 | cross-module `ParseArtifact.tree`, where the declaring module is not a universe member | red before PR2, green after | -| C3 | a field whose type names an undeclared type refuses at resolve, located | red | -| C4 | a declaration demanded from a module outside the ingest refuses at P1, typed | red | -| C5 | AT MOST ONCE: two subjects that both demand module X's namespace build it once. The double-insert refusal goes red if it is built twice. | red if violated | -| C6 | IDENTITY: the `DeclarationRef` for a type in the P2 body equals the one the module's own resolved root holds at the same use | positive | -| C7 | wise-bat-862's `body_cast_node` rows 15, 15b, 15c, 15c2 and 15d stay green, and 15d, the miss, still refuses | regression | -| C8 | #12629's where-head controls (`declaration_graft_where_alias_*`), re-pointed at the P2 read | regression | -| C10 | SNAPSHOT IDENTITY: an entry in infer's per-module snapshot and the P2 provider entry it projects are the identical `DeclarationRef` and body; the snapshot is a derived projection, never re-resolved | positive | -| C11 | a cross-module callee's Arrow read by `infer_declaration_reference_facts` carries resolved parameter and return types (calm-pike-507's measurement on the stacked branch) | red before PR2, green after | -| C12 | the seven's `parse_module_prepared(...)` scrutinee types as `Outcome`, so calm-pike-507's `infer_match_scrutinee_type_underived` frontier claim flips to a typed binder | red before PR3, green after | -| C9 | the retired RFM row's discriminating red stays enrolled as a regression control (condition c) | regression | - -## 5. Conditions this plan builds to (neat-boar-16, via quiet-gull-780) - -- (a) There is no moment where two structures answer the same question. The reader switch, the - deletion of per-module `resolved_declarations`, and the deletion of the lazy per-subject namespace - build all land in the transition that makes them redundant. -- (b) The provider has no fallback. Out-of-closure refuses at the provider. "In the ingest" means - every module the native ingest read into the closure. The provider's identity and per-ingest scope - are named the way #11401's `ResolutionContext` is. At-most-once is C5. -- (c) The RFM row retires in PR2, and its red stays enrolled (C9). - -## 6. Cost - -Baseline, taken before PR1 on `origin/main` 077af249458: `gunbc test //v2/test/claim/body_lowering/...` -on the native route, reading the per-module `[native-prepare-split]` `resolve_nanos` lines and the -`[native-cost-partition]` rows. The same pattern is re-run on each PR head. Cite the run by tag; do not -transcribe its numbers. Expected shape, not a claim: P1 moves namespace work from once per resolve to -once per module per ingest. For a declaring module that is not a universe member it is new work, -bounded by demand. Demand-scoping follows bold-bat-516's cost lane (no eager whole-closure -admission). The baseline measures the change; it does not define it. diff --git a/docs/plans/dedicated-coding-harness-plan.md b/docs/plans/dedicated-coding-harness-plan.md deleted file mode 100644 index e1dd062ed57..00000000000 --- a/docs/plans/dedicated-coding-harness-plan.md +++ /dev/null @@ -1,795 +0,0 @@ -# Dedicated Coding Harness — scope - -Project ID: DCH - -Repository anchor: gunb-ai/gunbc - -Baseline: main@de2f5f86346, read 2026-09-01. - -Operator direction (2026-09-01): serve a large open model on the DGX Sparks and drive a minimal -coding harness against it, retiring the Claude, Codex and Cursor provider runtimes. Written from -the ground up in `.dag` and Rust. **DCH and the dedicated harness must not import, vendor, depend -on, or cite `ctrl` as authority.** Stated at that grain deliberately: the wider claim would be false -of the repository today, because `extdeps.ctrl.gunbc_pin` already declares an `ExternalAuthority` -pointing into `gunb-ai/ctrl` and compares the host pin against the ctrl pin. That row is out of this -lane's scope and is not to be touched here. Spark parity is to be handled by fleet convergence -rather than by hand. **Operator direction 2026-09-02 withdrew the RLM-first start barrier**: DCH-0 -through DCH-2 proceed CONCURRENTLY with RLM, and the two lanes meet at DCH-3 rather than queueing. -DCH-3 is a join, not a start barrier. - -## 0. What is measured, and what is assumed - -Every claim below is either a read of this tree at the stated baseline or a live probe taken -2026-09-01. Where a fact came from another session it is attributed, and where it is unverified it -says so. Three earlier readings in this lane's own correspondence were stale or truncated, so the -provenance is part of the scope rather than beside it. - -## 1. Terminal - -Success is the RLM terminal procedure, unchanged, executed with our harness selected as the -provider realization: - -> From a clean revision R on main, clicking Launch on the roadmap canary creates exactly one -> attempt, one worktree, **one harness process of ours — the default and only harness**, and one -> durable attempt record; the -> worker performs only the canary's exact change; verification evaluates the pinned oracle against -> the exact attempt head; publication records a receipt; the child unblocks on acceptance. - -This lane authors **no new acceptance procedure**. RLM already owns a 14-step terminal that is -provider-agnostic in every step but one, and reusing it is what keeps this from being graded on its -own homework. `docs/plans/roadmap-launch-mvp-plan.md` is that procedure. - -No substitute terminal counts. A harness that completes a turn against a Spark in isolation is a -component probe, not this terminal. - -**And this terminal is necessary without being sufficient** — accepted without objection by the -controlling reviewer on 2026-09-02, at this grain: - -> The RLM terminal is the sole DCH integration terminal and remains necessary; it is **not a -> coverage closure** for DCH's operational failure classes. Its bounded canary proves that the -> accepted components compose on one exact task. It does not prove behavior under long context, -> long tools, unreadable observation, process failure, control-plane deployment, serving -> maintenance, cache reuse, or transport truncation. Each such class is discharged only by its own -> named instrument, exact subject, positive control, and discriminating refused case. **A green RLM -> terminal carries no evidence for a class whose distinguishing subject it did not execute.** - -This strengthens the rule already in §4 — treat one completed turn as evidence for nothing but that -— rather than weakening the terminal. - -## 2. Current-state ruling - -### Already usable - -- **The seam exists and is a single-variant coproduct.** `gunbc.provider_interface_binding` - declares `ProviderControlInterface = CodexAppServer {}`. Adding a harness is a variant addition - at a modeled seam. That module's own annotation already separates the two senses of "provider" — - the runtime we install and control, versus the inference backend it sends requests to — and names - `gunbc.spark.serving_desired` `spark_serving_local_endpoint_url` as the seam where the two would - silently fuse. -- **The serving runtime speaks the shape the harness needs, natively.** Probed against - 192.168.1.225 on 2026-09-01: Ollama 0.32.9 answers `POST /v1/messages` with an Anthropic-shaped - response — content blocks including `thinking` and `tool_use`, `stop_reason: "tool_use"`, and a - `usage` object — for a request carrying a tool with a JSON input schema. `/v1/chat/completions` - and `/api/chat` also route. So no translating proxy is required for gunbc to drive it. -- **Some of the wire shape is already modeled.** `extdeps.llm.anthropic` (378 lines) carries tool - result blocks, image sources and model specs; `extdeps.llm.anthropic_rest` names the `/v1/messages` - path and body. -- **The convergence spine reaches spark serving.** `gunbc.fleet.fleet_converge_plan` imports - `gunbc.spark.serving_membership`, `serving_realization` and `serving_execution_schedule`, and - declares `fleet_converge_spark_serving_row_executions` / `_row_effects`. -- **Both Sparks are reachable and serving.** `/api/version` answers 0.32.9 on 192.168.1.225 and - 192.168.1.226; `/v1/models` returns the same seven ids on each. - -### Prevents it today - -- **The Sparks are not enrolled in the fleet.** `fleet_intent_network.endpoints` lists srv1–srv4 - only, and `srv5`/`srv6` appear in neither it nor `gunbc.fleet_intent`'s `ComputeHost` list. In - that module **membership IS enrollment**, so this is not an omission to be patched around: it is - the reason parity is managed by hand. The module's own note records that the standing reason has - already changed from "there is nothing to enroll" to "enrolling is a decision nobody has taken - yet." -- **"The models resident" is not representable.** `spark_serving_desired_manifest` governs exactly - one member; the other six resident models are outside that authority and nothing in gunbc owns - them. The structural tell, from eager-pike-541: the materializer carries a single manifest, digest - and blob closure, so no carrier could hold seven. Nobody can state, check, or drift-detect the - difference between the model we converge and the models the hosts hold. -- **Desired and observed disagree on both hosts.** The deployed user unit still describes - `gpt-oss:20b`, predating the #9859 amendment to 120b, and carried no context-length line at all. - Two independently declared desired values, un-rendered on both hosts. **This survives #9960** - (see §2.1): that PR completed the DESIRED side, and a complete desired value is not a converged - host. -- **There is no generic inference interface.** `extdeps/llm/llm.dag` is 11 lines and - `llm_contracts.dag` is 14 — anchors only. Every wire fact lives in a per-vendor module, and - tool-calling is modeled only inside `openai.dag` and `cursor_stream.dag`. What exists in - `anthropic.dag` fuses **shape** with **vendor**, which DESIGN's external-upstream decomposition - separates: Ollama implementing the Anthropic wire shape is precisely the shared-standard case, so - the shape belongs in its own module with the two implementations citing it. -- **Whether the spine can APPLY a serving row is unproven.** The `converge` CLI verb is unwired - deliberately — rebuilding it would make the interpreter load-bearing for a new capability while - two lanes delete it — and its refusal names the live successor route. But this lane's own wet plan - run came back `PartiallyApplied` with apply refused. That is what RLM-2b is currently measuring. - -### 2.1 Changes since the baseline — the concurrency axis is RESOLVED ON MAIN - -The baseline above is fixed at `main@de2f5f86346` and is not rewritten as main moves; completed work -is dispositioned here instead. Recorded 2026-09-01, after the controlling reviewer identified that -this plan's active claims had been falsified by a merge that landed while it was in review. - -**#9960 (`7810e68b3ea`, ancestor of current main) resolves the concurrency-axis item outright**, and -verified here rather than relayed: - -- `extdeps.ollama.server_env` declares `OllamaNumParallel` → `OLLAMA_NUM_PARALLEL`, and - `spark.serving_unit_render` now emits **four** environment axes, binding - `ollama_num_parallel_env_assignment` to the spec's slot count. The axis is no longer absent. -- `spark_serving_desired_serving_slots_value` carries a desired count of 4, so the axis has a - desired value and is drift-reportable — the specific consequence this plan said was missing. -- The false cost model is corrected at the desired value, in the direction this plan predicted: - the count does **not** divide the window, each slot receives its own full window, and the - per-slot price is recorded as a property of the **realization** rather than a fleet-wide constant - — the per-realization objection this plan raised is answered by a declared §4b drop naming that - exact subject. - -**One defect found while verifying it, and it is not #9960's to fix by this lane.** -`gunbc.spark.serving_desired` still carries an *earlier* annotation block stating in the present -tense that concurrency "is not modeled anywhere in desired state", that the two facts trade "because -the slot count divides the context window", and that the concurrency row "is P1b and is deliberately -not smuggled in here". All three are now false, and they contradict that same module's own value and -annotation roughly 140 lines below. That is a §3 meaning fork inside one authority — a stale -annotation is data the substrate cannot check, so nothing reds. **Reported, not repaired here**, and -out of this lane's scope. - -**What this does NOT resolve.** #9960 made the state representable, declared and renderable. It did -not read either host back. The observed-versus-desired receipt below stays open, and so do the two -outstanding operator hand-edits it must account for. - -### Explicitly off the critical path - -`ctrl`'s harness and router are **prior art, not a dependency and not a citation**. Two of their -findings are worth re-deriving deliberately rather than inheriting, because gunbc must establish -them on its own evidence: that a cold large model can exceed a default HTTP client's header/body -timeout, and that an assistant turn must be echoed back verbatim — thinking blocks included — with -all tool results for one turn batched into a single user message. Treat both as **hypotheses with a -predicted failure**, not as facts on loan. - -Two more joined that list on 2026-09-01 from the transcript §5 reads, on the same terms: that a -streaming request may terminate a response early where an otherwise identical non-streaming request -returns it complete, and that an assistant turn may stop at `end_turn` having announced work it did -not perform. - -**The second is superseded on 2026-09-02, and the correction is worth more than the hypothesis.** -A mechanical cause was found: a serving package carrying `PARAMETER stop ###` terminates generation -on ordinary Markdown H3 output while returning a well-formed stream and `stop_reason: end_turn`. -That is a *package configuration* colliding with content, reproduced by a tiny prompt and removed by -rebuilding the package — not a model deciding to stop, which is what the single surviving -observation appeared to show. The heading-then-stop symptom therefore had a determinate external -cause the whole time, and the hypothesis about model behaviour was an artifact of reading a -transport symptom as a semantic one. It leaves the hypothesis list and becomes DCH-0p's subject. -The streaming hypothesis is untouched and still has **no** observation: its isolating step was never -run. - -## 3. Serial gate chain - -Only one gate mutates at a time. A later gate may author inert types and fixtures but may not -activate production rows before its predecessor's receipt is accepted. - -**A gate does not complete while a §5.2 question that owns one of its dispositions is open.** This -is a precondition of the gate, not a note beside it: §5 exists to answer these classes *in advance*, -and a plan that lets a gate finish while its own identified fail-open is unadjudicated has recorded -the class instead of handling it. The bindings, and each is repeated in the gate itself: - -**All five returned on 2026-09-02** (§5.2). The rule stands for any question opened later; what the -returned rulings changed in the chain is recorded here: - -| Question | Effect on the chain | -|---|---| -| Q3 | **A new gate, DCH-0r, is inserted before DCH-0c.** A §4b drop was REJECTED as the normal shape — reordering removes the need for debt. | -| Q1 | DCH-2's failure-arm partition is specified, not left open. | -| Q2 | DCH-2 owes a typed-obligation adjudication; length-based dispositions are forbidden. | -| Q5 | DCH-2 owes journal-before-effect ordering and a two-axis model. | -| Q4 | Governs how §2's hypothesis list is read; each hypothesis owes its own instrument before DCH-3. | - -The resulting order is: - -``` -DCH-0 endpoint rows -DCH-0r quiescent-maintenance construction -DCH-0c ComputeHost enrolment / apply reachability -DCH-2 turn activation -``` - -DCH-0c may precede DCH-0r **only** if the destructive restart path stays structurally unreachable, -or refuses because the maintenance fence is absent. - -### DCH-0 — Enrol the Sparks, and reconcile the rendered unit against the live hosts - -**Question owned:** what does the fleet know about these two machines, who says so, and does either -host actually carry what desired state now declares? - -The parallel axis and the cost model **left this gate on 2026-09-01**; #9960 performed both, and -§2.1 records the verification. What remains is enrolment and the live reconciliation. - -- Enrol srv5/srv6 in `fleet_intent_network.endpoints` and `fleet_intent`'s `ComputeHost` list — the - act the address correction deliberately did not perform. (The `ComputeHost` half is DCH-0c; see - below.) -- Reconcile the rendered unit on both hosts against desired state, which is now complete on four - axes where it was complete on three. -- Q3 **returned**: the drain is not deferred and not declared as debt. It becomes its own gate, - **DCH-0r**, sequenced immediately after this one. This gate's own receipt is unchanged. - -**Receipt:** an observed-vs-desired comparison of the rendered unit on both hosts, not a return -code. The rendered unit is the only member of the must-move-together set that produces **no failure -signal** when stale — ref, manifest, digest and closure each raise a checksum event, while a stale -unit is silent, which is how both hosts drifted on two axes unreported. Two hand-edits are -outstanding against that receipt: the context length raised today, and `OLLAMA_NUM_PARALLEL=4` set -on both hosts, each under operator instruction. **#9960 does not discharge this and it sharpens -it**: a hand-set value on a host and a declared value in the corpus agreeing by coincidence is -exactly what a silent carrier cannot distinguish from convergence, so the readback is the whole -receipt. - -**Enrolment is inert today, and that is the risk rather than the reassurance.** -eager-pike-541 looked for the executor rather than arguing from principle: no -`ctrl-fleet-converge` timer or service exists on either Spark in either scope, and -`gunbc.fleet_converge_timer` says of itself that it "remains the naming and cadence authority; it is -no longer a renderer" — its installer chain died with `ci.yml` in #8283. So enrolment cannot cause -an apply, because nothing on those hosts is scheduled to run one. The failure mode to guard is -therefore not prematurity but **a row that reads as an outcome**. - -**The consumer census, which was the open item on that reasoning, is done and it sharpens the gate.** -Of 57 non-test modules importing `fleet_intent_network`, the number that read the `endpoints` list -or `fleet_intent_network_topology()` is **zero** — production modules consume individual endpoint -rows *by name* (`bmc_virtual_media`, `host_identity_access` take `srv1_host_lan_endpoint` and -friends). The topology function has exactly one consumer in the tree: -`test.claim.fleet.fleet_intent_network_witness_test`, asserting -`list_length(items: fleet_intent_network.endpoints) == 11`. - -Two consequences. First, **list membership is not the load-bearing act** — authoring the srv5/srv6 -endpoint rows that named consumers can reach is. Second, enrolment will turn that witness red at -11 → 13, and the right response is not to bump the literal: a count copied from the current tree is -the change-detector DESIGN §5 names, and completeness here is an identity join, not a count -equality. Enrolment is the occasion to fix the oracle, and that repair belongs in this gate. - -**ENROLMENT IS TWO ACTS, NOT ONE, AND ONLY ONE OF THEM IS INERT.** The census above covers -`fleet_intent_network.endpoints`. `fleet_intent`'s `fleet_intent_known_hosts` is its opposite — -censused by eager-pike-541 and verified here — with three production consumers: - -- `gunbc.generated_artifact` maps the list to `RunnerHostSudoersArtifact`, so enrolment **mints two - new generated artifacts**; the rows cannot land without a regeneration in the same commit or the - drift gate reds. Mechanical consequence, mechanical receipt. -- `gunbc.runner.runner_host_deploy` `admit_runner_host` returns `RunnerHostUnenrolled` for any host - absent from the list. Its annotation calls itself **the enrollment wall** and is explicit that it - is construction rather than a check: no function in the module takes a bare `RunnerHostDeploy` and - yields a command, so "run the installer on an unenrolled host" is unrepresentable rather than - discouraged. It prices the srv4 host-convergence OOM — converge against an unenrolled host derives - nothing for it, or stamps drop-ins carrying another host's widths — and names the enrolled row as - exactly where `runner_deployment_plan`'s conservation wall gets the host RAM to check slot caps - against. **So enrolling srv5/srv6 removes a fail-closed refusal that currently protects them.** -- `gunbc.fleet_converge_apply` finds hosts by identity in that list, so enrolment is precisely what - makes a Spark reachable by the apply path this lane measured as `PartiallyApplied` / refused. - -The inertness argument had two independent legs — no executor, and no consumer. `ComputeHost` -knocks out the second. **Only the executor leg survives**, and it is the leg that changes the moment -anyone installs the timer. - -**So DCH-0 lands the endpoints half only.** Author `srv5_host_lan_endpoint` / `srv6_host_lan_endpoint` -as rows the named-import consumers can reach, add them to the list as the authority's completeness -statement, repair the count oracle. The `ComputeHost` half is **DCH-0c**, and it is a decision -rather than a row: it needs the host facts the conservation wall consumes (RAM at minimum), the two -sudoers artifacts regenerated in the same commit, and someone stating out loud that the enrollment -wall is coming down for these two machines on purpose. - -Note the row/list split holds on both sides and inverts between them: `srv1_host` and `srv2_host` are -imported by name in at least `os_install_actuator_selection`, `deployed_intent_v1`, -`nbd_proxy_virtual_media_install`, `ci_deploy_access` and `srv3_install_media_fetch`, so authoring -`srv5_host`/`srv6_host` rows is separable from list membership here too — except that here the LIST -is the load-bearing half and the row is the quiet one. - -**Owner: eager-pike-541**, agreed 2026-09-01. - -### DCH-0r — Quiescent serving maintenance - -**Question owned:** can a converge restart the serving process without destroying work in flight, -and can it prove it? - -Created by the Q3 ruling. A §4b drop was **rejected** as the normal shape: a drop reports a lower -guarantee, it does not turn an unsafe arm green, and DESIGN is explicit that a dissolution trigger -does not authorize creating the debt. Reordering the chain removes the need for debt entirely. - -**The mechanical blocker, verified in this tree rather than relayed.** `spark.serving_realization` -realizes `EnableSystemUnit` as three commands in one effect — `systemctl enable`, `systemctl start` -and `systemctl restart` — so **restart has no independently matchable effect identity** and no fence -can structurally guard it. Two refinements found while verifying, both sharper than the finding as -received: - -- The restart is **unconditional**: it is emitted every time the effect is realized, not only when - the unit definition changed. So converging this unit is destructive to in-flight work even when it - changes nothing. -- **The user-unit path is the opposite shape**, and the Sparks are on it. Its closure is - `[… EnableUserUnit, StartUserUnit …]` — enable and start already separated — and it carries **no - restart effect at all**. So on the very hosts this lane targets, a changed unit definition has no - modeled route to take effect. That is a candidate cause for the observed-versus-desired drift - DCH-0 must reconcile, and it is stated as a candidate because nothing here has read a host back. - -So the first required change is that restart becomes its own typed effect, distinct from -enablement and from initial start, on both paths. - -**The admitted restart sequence.** A bare `active == 0` reading is insufficient, because a turn may -enter immediately after the read: **the admission fence and the drain are one protocol.** - -**THREE IDENTITIES, NOT ONE.** The 2026-09-02 ruling corrected an earlier conflation in this -protocol, and the correction is the load-bearing part of the gate: - -| Identity | Stability | Answers | -|---|---|---| -| **Service locus** | stable across restarts | which service is fenced — host plus unit slot | -| **Service invocation identity** | changes on every activation | is this still the exact process generation observed before? | -| **Running-definition identity** | stable for equal restart-relevant specs | which definition produced this invocation? | - -The `/proc//environ` stamp answers the **third**. It is invocation-BOUND evidence but it is -not an invocation IDENTITY: two successive restarts from one definition carry the same stamp, so it -cannot prove the old process was replaced. DCH-0r still needs the second, read from observation -rather than desired state, and it must change whenever the process is replaced, stay stable within -one observation transaction, never repeat merely because a PID was reused, and bind to the process -whose environment and health were read. `MainPID` alone satisfies none of the last two. - -**The fence is keyed by the LOCUS, not by the incarnation.** An earlier wording said draining refuses -leases "for that incarnation"; that is too narrow, and it fails exactly when it matters — if the -backend restarts unexpectedly mid-drain, an incarnation-scoped fence stops blocking admissions at the -moment an unvalidated new incarnation appears. The pre-restart invocation is a compare-and-swap -condition and an evidence anchor, never the scope of the refusal. Each running lease separately binds -the invocation it uses, which is what lets an unexpected replacement produce a typed interruption -against the right turns. - -1. Observe one transactional pre-state: locus, invocation `J0`, running definition `D0`, desired `D*`. -2. Atomically move `Open` to `Draining`, keyed by locus and maintenance, with `J0` as the expected - pre-invocation. -3. Refuse **every** new turn admission at the locus, whatever invocation is present. -4. Drain the complete active lease population at the locus to zero; each lease identifies its - invocation. -5. While the same fence is held, re-observe zero leases **and** that the invocation is still `J0`. - If it changed, the drain did not succeed: settle the affected turns as interrupted, reconcile the - pre-state, and restart the protocol. -6. Execute the independently typed restart. -7. Read one transactionally joined post-state and require ALL of: `J1 != J0`; `D1 == D*`; the - registered definition equals the render of `D*`; readiness and health belong to `J1`; and an - empty replan. -8. Reopen admission bound to the pair `(J1, D1)`, and have every later admission recheck that the - observed pair still equals the admitted one — so an out-of-band restart refuses immediately - rather than silently granting a turn against an unvalidated backend. - -`std.temporal_effect` already carries the general direction — held leases, refusal on foreign -ownership, `DrainThenStart` as a distinct plan — but the service-level population and admission -fence that make the drain exact do not exist yet. - -**Guarantee grain, stated rather than assumed.** A census of DCH turn leases proves quiescence for -DCH-managed clients only. To claim the *service* is quiescent, one of these must hold: every -admitted client routes through the same lease authority; DCH holds exclusive access for the window; -or the service exposes a complete active-request observation including external clients. Otherwise -external occupancy is **outside the modeled guarantee** — and then an automated destructive restart -must refuse, or the guarantee is stated narrowly as DCH-managed-turn continuity. It may not be -stated broadly and held narrowly. - -**THE UNSTAMPED READING, NARROWED.** A complete successful environment read that finds no stamp is -positive evidence and is not "unobservable" — but its honest conclusion is that **this invocation -cannot be established as having been started from the current desired definition**, whose render -stamps unconditionally. It is NOT that the process temporally predates that definition: a foreign or -manually started process could have been launched later and would carry no stamp either. It still -requires reactivation; chronology is simply not what was established. The arm holds only while four -conditions do: the environment read completed, absence is distinguished from permission, parse, -truncation and observation failure, the desired unit specification stamps unconditionally, and the -stamp identity covers every process-start input whose change requires reactivation. - -**THE SUBORDER.** DCH-0r-a deletes the fused unconditional restart, separates enable from start, and -defines the reactivation vocabulary with its refusing selector. DCH-0r-b observes the -running-definition identity AND the service-invocation identity, and proves the process/environment -read is transactionally joined. DCH-0r-c builds the locus admission fence, the exact lease drain, the -guarded production reactivation, the two-identity readback and the validated reopening. Joining the -reactivation arm into the production planned-effect sum belongs with DCH-0r-c's guarded producer, not -with DCH-0r-a — no production consumer can use the arm before then. - -**DCH-0r-c's CONSTRUCTION HALF HAS LANDED, AND WHAT IT DOES NOT ESTABLISH IS THE LOAD-BEARING HALF OF -THIS SENTENCE.** `gunbc.spark.serving_admission_fence` carries the locus, the three identities kept -apart, the locus-keyed admission fence, the compare-and-swap onto `Draining`, the exact drain -adjudicated as one reading with its revalidation, the occupancy grain, `BoundReactivationPlan`, the -guarded producer and the two-identity readback with its validated reopening; the fence and the -drained evidence are `sole_constructor`, so a plan cannot be assembled from a count taken outside a -held fence. The production consumer is `gunbc.spark.serving_converge_slice_wet` -`spark_serving_reactivation_standing_for`, which computes the remedy verdict from the host's own -observation transaction and REFUSES on every host, because no admission-state authority and no -active-turn population exist. The executable sum was NOT widened: the exclusion in -`SparkServingExecutablePlannedEffect` and its witness still stand, and the plan carries its steps as -a derivation rather than the `{ plan, steps }` field pair sketched above — storing them beside the -plan would rebuild the authored-label-beside-unrelated-commands defect that exclusion holds shut. - -**What remains, and it is the receipt below rather than wiring:** a lease authority that admits every -turn at a serving locus and can enumerate the population held there, a persisted admission state the -fence can swap, and SSH to srv5/srv6 under an operator credential decision. Until those exist the -Planned arm has never been reached from a production call site, and neither DCH-0r-a's nor DCH-0r-b's -standing is retired. - -**Receipt:** a restart executed against a held fence, with the drain observed to zero under that -same fence, and a post-restart incarnation readback. An exit status of zero from the invoking -program establishes only that the program reported completion — service convergence still requires -independent observation and an empty replan. - -### DCH-0b — Make residency representable - -**Question owned:** what is a resident-model fact, and who owns the six nobody converged? - -Split from DCH-0 at eager-pike-541's request, and the reason is the right one: enrolment and the -parallel axis both write a value into a carrier that already exists and has a shape, whereas the -resident set has **no carrier at all**. Someone must first decide whether a resident-model fact is -desired state or observation, whether it is per host or per fleet, what its identity is when the -same weights appear under two refs, and what a drift between the converged member and the resident -set *means* when nothing put six of those models there through gunbc. That is design, and folding -it into a gate of row-writes would make the row-writes wait on it. - -The structural tell that no carrier exists: the materializer carries a single manifest, digest and -blob closure, so nothing in it could hold seven. - -**AND THE SUBJECT IS A BEHAVIOUR-BEARING PACKAGE, NOT RESIDENT WEIGHTS** (ruling, 2026-09-02). The -discriminating pair is: same weight blob, same template, same runtime, same host, **different -effective stop set** — materially different observable behaviour. So a weights digest, a GGUF -manifest or a model tag is not the model identity this lane needs. The carrier is a -`ServingModelPackageIdentity` over the target model artifact, quantization, prompt-template identity, -parser/renderer identity, effective parameter identity, **effective stop-sequence set**, runtime -release and decoding realization. A tag is an allocated handle to that package and must never be -treated as the semantic identity — a package rebuilt from the same weights without a stop string is -a **different serving package**, and treating it as identical makes the repair invisible to -convergence. The gate's question becomes *which exact behaviour-bearing package is resident and -active on each router-eligible host*, not which weights occupy disk. - -### DCH-0p — Effective package qualification - -**Question owned:** what is this endpoint actually serving, and can a stop string in its package -silently truncate our output? - -Created by the 2026-09-02 ruling. It runs after DCH-0b makes package identity representable and -after DCH-0r makes a required restart safe, and it must complete before DCH-2's production -qualification. - -**The defect it exists to prevent, which is not hypothetical.** A serving package carrying -`PARAMETER stop ###` terminates generation on ordinary Markdown H3 output, while the serving surface -returns a well-formed stream and `stop_reason: end_turn`. A tiny prompt reproduces it; removing the -package stop restores the answer. The client can neither identify the cause from the response nor -remove a server-side stop through an additive request option. **This is a mechanical cause for the -heading-then-stop symptom §5 records as a hypothesis** — see the reclassification there. - -**Read the effective configuration, never the intended one.** Before admitting a backend, read what -the running endpoint actually serves (Ollama exposes template, serialized parameters and model -information on `/api/show`). A source Modelfile, a tag name, or the command used to build the package -are all statements of intent, not observations. The receipt binds host, service invocation, model -handle, target artifact, runtime release, template, the complete effective parameter set, the -canonicalized stop-sequence population, parser and decode realizations, and the observation -transaction. Where a behaviour-bearing field is not fully exposed, the answer is -`PackageConfigurationIncomplete` — never an inferred value. - -**Stop-sequence admission is the wall.** Every effective stop carries a typed role: either a -protocol-boundary sentinel binding uniquely to a declared boundary in that exact package's -template/parser contract, or an unbound content stop. A copied allowlist of strings is not a role. -`###` has no protocol-boundary role and is ordinary output content, so a package carrying it refuses -**before** DCH starts a turn. Sentinel-looking values inherited from a prior lane are admitted only -once DCH independently establishes their relationship to the exact conversation protocol — "the -other lane called them legitimate" is not our evidence. - -**Controls this gate does not exit without:** stop sets differing only by `###` produce different -package identities; a package carrying unbound `###` refuses; a fixed package completes a request -requiring H3 output with the H3 bytes present; the original package discriminates by terminating -before those bytes; an unreadable, incomplete, or handle-inconsistent `/api/show` refuses; one tag -resolving to different package identities on two router-eligible hosts refuses; a router endpoint -missing the selected package becomes ineligible rather than silently retained; and a package -configuration changing under an active session changes the session generation, so old evidence -cannot carry. - -The runtime H3 pair is evidence that the endpoint realizes the modeled rule. It is not a substitute -for reading the effective configuration. - -### DCH-1 — Hoist the wire shape off the vendor - -**Question owned:** what is the interface, and which module owns it? - -- One module for the messages/tool-use shape, cited to its specification; `extdeps.llm.anthropic` - and the Ollama serving surface become two implementations that reference it. -- Tool-calling gets a shape here rather than a third copy inside a vendor module. -- This is a replacement migration at the root, not a new fork: the vendor module's shape rows move, - they are not duplicated. - -The authority is `extdeps.llm.anthropic_messages_api`, its own module rather than a row -in `extdeps.llm.llm` or `extdeps.llm.llm_contracts`: those two are agnostic anchors, and the -specification is a named, versioned upstream subject with a citation and a version axis of its own, -which is the `extdeps.whatwg.html_navigation` position rather than the generic-hub one. It keeps the -publisher's names because coining a neutral second name for a concept that already has one is the -nicknaming violation, and it names no implementation: `MessagesApiImplementation` is the carrier -each implementation declares its own row in. - -The shape rows MOVE, they are not copied. `extdeps.llm.anthropic` retains only what is true of the -API product — its model roster — plus its own conformance row; the wire contracts move with the -shape into the `_contracts` sidecar the emitter merges into the specification module, so no contract -row is left pointing at the vendor module. `extdeps.ollama.api` gains the `/v1/messages` endpoint -row and `ollama_messages_api_conformance`, and re-models none of the shape. This section records the -design the gate implements; git records what merged, and nothing here claims a merge state. - -The version binding is the axis worth having, and it lives on the carrier rather than in this -document. Upstream documents that Ollama ACCEPTS the `anthropic-version` header and does not use it, -so an accepted request is not evidence of the revision served — an accepted-and-ignored parameter is -worse than a refused one, because it looks like a control and is not. The two implementations -therefore declare different arms of `MessagesApiVersionBinding` against the same spec version. -`test.claim.messages_api_conformance_witness_test` carries the discriminating controls: it goes red -if the axis collapses, if either arm goes uninhabited, if a conformance row stops pointing at a -declaration in its own module, or if a wire contract is left behind on the vendor module. Ollama's -`/v1/messages` behaviour is grounded in upstream's own `docs/api/anthropic-compatibility.mdx`, not in -a probe transcribed into prose. - -### DCH-2 — The harness, in `.dag` and Rust - -**Question owned:** can we drive a turn to a terminal disposition and report it? - -- The loop: post a request, and while the response stops on tool use, execute the tools and post the - results back. Four tools is the starting surface: run a command, read, write, edit. -- **The tool surface is where §5.1 row 7 lands, so it is specified here rather than left to the - implementer.** Every quirk in the relayed transcript was a real semantic of this surface that - nothing announced, so the rule for all of them is one rule: **a tool's contract is carried in the - tool's declared shape, and any limit it enforces is reported in its result rather than inferred - from a truncated or missing one.** Concretely, six dispositions this gate owes: - - **Working directory.** Either a command's working directory is part of the call's declared - input, or the surface states that each call is independent — the failure to avoid is a caller - that happens to comply, which is what theirs did, by luck and unprompted. - - **Duration.** A limit that kills a build is a real refusal and must arrive as one: a typed - timeout disposition naming the limit, distinguishable from the command's own failure. A default - chosen for a chat turn is the wrong default for a compile. - - **Input.** No stdin means anything that prompts hangs until the limit kills it, and the operator - sees a timeout rather than a prompt. Either the surface supplies input, or a prompt-shaped hang - is typed as its own disposition. - - **Truncation.** Keep their shape: a cap that **announces** it, so a large result is - distinguishable from a complete one. This is already right and is adopted deliberately. - - **Edit matching.** Keep their shape: refuse on zero or multiple matches rather than patching the - first. Construction over validation, and already right. - - **Context exhaustion.** A full window must be a typed refusal, not a turn that begins to fail. - Compaction is a later capability; the disposition is owed now, because without it exhaustion - presents as the model getting worse. -- Preserve what the codex realization learned expensively, because these are provider-independent - and were paid for once: the three separated layers (durable goal, execution lease, controller), - the exactly-once command identity, and the trap that an idle thread never authorizes a start. -- A harness that **reports** its own status retires the observation layer that dominates every - existing provider module — guessing which transcript belongs to a session, inferring state from a - foreign schema. That is where the simplification is, and it should be measured, not asserted. - -**The exit bar, set by the Q1/Q2/Q5 rulings.** This gate does not exit on "it performs the four -tools and reaches one terminal turn". Four qualification groups, each owing a named instrument with -a positive control and a discriminating refused case: - -- **Supervision and observation.** A harness throw yields a durable failed-or-interrupted attempt - **while the controller stays observable**. A transcript-resolution failure stays distinct from - idle and from provider silence — the specific conflation that made a working session read as a - frozen one. -- **Execution provenance and lifecycle.** Tests execute the production transport rather than a - test-owned twin; a control-plane restart does not terminate the turn worker; a backend restart - produces a typed interruption. -- **Turn and transport semantics.** The stream/non-stream differential; `EndTurn` with outstanding - typed obligations; performance separated by cold, warm and cache-reused state. - - **The harness may never state that `end_turn` means the model naturally finished.** On this - transport at least two upstream causes produce that same observed label — natural completion, - and a configured server-side stop colliding with content. So the raw provider observation and - the semantic disposition stay different axes: a provider terminal observation carries the wire - stop reason and, honestly, that the native cause is *not exposed*; the DCH disposition is - decided by obligations, and external pinned verification — never the model's stop reason — - decides success. This is the second of two independent walls: DCH-0p prevents known - content-colliding package stops, and DCH-2 still refuses to equate a provider label with task - completion, because the client cannot prove the native cause from a surface that erased it. - - **Throughput is a construction, not a threshold.** "No four-digit rate is reachable" is a - threshold over a measured value — a view, which #9946's ruling forbids as a correctness wall, - and which faster hardware would falsify while a smaller fabricated value passed. The durable - shape is a cache lineage (backend host, service invocation, package identity, conversation - prefix identity, cache generation) joined to per-turn accounting that separates total input - tokens from **newly evaluated** and **reused** ones. A prefill rate may be constructed only over - newly evaluated tokens against the prefill duration; anything else divides mismatched subjects - and produces a valid arithmetic operation that is not a hardware measurement. -- **Tool-contract honesty.** The six dispositions above, at this grain: explicit working directory - on every invocation or a separately leased persistent shell; typed stdin or a typed refusal; a - per-operation budget rather than one universal kill; termination of the **owned process - population** rather than one PID; and compaction or an explicit typed context-exhaustion refusal. - -### DCH-3 — Bind it at the seam, make it the default, and run RLM's terminal - -**Operator direction, 2026-09-02: the harness is to be the only and default harness; Claude and -Codex are to be ignored.** That is adopted, and it changes this gate's shape in a way worth stating -rather than absorbing. - -**Default is a selection; retirement is a deletion. They are separated deliberately.** "Ignore the -other providers" is discharged by making ours the default selection at the seam and stopping -investment in the others — it does **not** require deleting them, and DCH-4's deletion still waits -on this gate's receipt. Deleting them to satisfy "only" before that receipt would leave the -repository with zero working harnesses, which is the failure mode the replacement-migration doctrine -calls erasing a correctness distinction rather than completing the replacement. The other variants -stay **frozen** in the doctrine's sense — no new investment, no new rows on their growth surfaces — -until DCH-4. - -**"Only" raises this gate's bar; it does not lower it.** With three providers, a weak DCH-3 is -tolerable because a fallback exists. With one, there is none — every DCH-2 qualification class -becomes load-bearing on the day it becomes default, and the relayed transcript in §5 is precisely a -record of what a sole harness with no fallback feels like when it breaks: three sessions dead, an -exit status of zero, and an operator who cannot tell working from dead. So the combined admission -below is not ceremony to be traded away for focus — it is the thing that makes "only" survivable. - -- The `ProviderControlInterface` variant, its receipt types, and the belt binding. -- Make it the **default selection** at that seam. -- Re-run the RLM terminal with the variant selected. -- **Admission requires BOTH**, and neither substitutes for the other: every DCH-2 qualification - receipt accepted, **and** the unchanged RLM 14-step terminal with the DCH variant selected. The - §2 hypotheses each owe their own DCH-run instrument before this gate, per Q4. - -### DCH-4 — Retire the three provider runtimes - -Delete-first at the root, per the replacement-migration doctrine, once DCH-3's receipt is accepted -— which now means the **combined** admission above, not the canary alone. Retiring a working runtime -on the strength of an easy-path green is how a replacement erases a correctness distinction instead -of completing. -The population, to be counted rather than estimated at the time: in `extdeps/llm` the anthropic, -openai, cursor and codex families; in `gunbc` the codex supervised-turn and runtime modules, the -cursor SDK modules, provider account and standing, and the Claude setup-token enrolment. The auth -surface goes with them — it exists to solve a problem that a self-hosted endpoint does not have. - -## 4. What this lane must not do - -- Import, vendor, or cite `ctrl`. -- Become the default production harness before DCH-2's qualification receipts and the RLM terminal - are BOTH ready for the DCH-3 join. Concurrent authoring and qualification are authorized; an early - cutover is not, and "only" raises that bar rather than licensing it. -- Land a change to an authority already inside another lane's frozen approved delta without - serializing it. Concurrency is authorized for authoring, testing and review; it is not authorized - for landing on a shared file, because a clean textual composition still yields a blob nobody - approved. `dag/gunbc/fleet/fleet_converge_plan.dag` is the live instance, shared with #9832. -- Enrol the Sparks as a side effect of harness work — DCH-0 is a separate gate with a separate owner. -- Bump `fleet_intent_network_witness_test`'s endpoint-count literal to absorb enrolment. Repair the - oracle or leave it red; a count copied from the tree it measures is not one. -- Treat "it completed one turn" as evidence for anything but that. -- Delete or break the existing provider runtimes to satisfy "ours is the only harness" before - DCH-3's receipt. Default first, retire after — otherwise the count of working harnesses passes - through zero. - -## 5. Adversarial review — the failure classes this lane must answer in advance - -**Provenance, stated once.** On 2026-09-01 the operator relayed a transcript of the `ctrl` -mini-agent lane debugging its own harness against these same Sparks, and directed that DCH be made -to answer it before it starts. Everything in this section is **second-hand and unreproduced here**. -It is admitted on exactly the terms §2 sets for the rest of that lane's output: each row is a -**hypothesis about our design with a predicted failure**, never a fact on loan, and none of it is -citable as evidence. What their session buys us is not findings — it is a cheap enumeration of -where a harness of this shape breaks, produced by someone who paid for it. - -The value is concentrated in one property: **every class below was operator-visible as something -other than itself.** A harness throw read as a clean exit. A dead session read as a working one. A -control-plane restart read as a Spark fault. An inflated rate read as fast hardware. A model that -stopped early read as a truncated message. That is one class, not five, and it is the class DESIGN -§5 exists for — so the obligation this section places on DCH is not "handle these bugs" but -**produce a disposition that cannot be mistaken for a different one.** - -### 5.1 Classes, and the gate that owns each - -| # | Class | Observed as | Owner | -|---|---|---|---| -| 1 | A harness fault reaches the operator as success | process exited 0 for an hour after a first-write throw | DCH-2, and Q5 below | -| 2 | One throw destroys unrelated work | any error in the loop took down the whole container | DCH-2, and Q1 below | -| 3 | Self-reported status that no observer can resolve | frozen at `idle` while the harness emitted `working` throughout | DCH-2 | -| 4 | A backend restart kills every in-flight turn | control plane and inference router shared a process | **DCH-0**, and Q3 below | -| 5 | A turn ends without doing the work and nothing types it | `end_turn`, one heading, after 50k input | DCH-2, and Q2 below | -| 6 | An instrument that fails toward a flattering number | 687 tok/s the hardware never reached, from KV reuse | DCH-2 | -| 7 | Tool-surface semantics that are real and unannounced | `cd` not persisting; a 120s SIGKILL; no stdin; no compaction | DCH-2 | -| 8 | A repair whose blast radius exceeds the bug's | a failed `chown` under `set -e` blocked every spawn on the node | this lane's own discipline | -| 9 | Evidence filtered by survivorship | one completed final turn existed; the rest died of other bugs | Q4 below | - -Row 3 is the one that most directly touches a claim this plan already makes. DCH-2 says a harness -that reports its own status retires the observation layer. Their harness **did** report its own -status, correctly, the entire time — and the operator still saw a frozen session, because the -report's *transport* failed independently of the report. So self-reporting is necessary and is not -the simplification on its own; the plan's existing instruction to **measure** that claim rather -than assert it is upheld, and this is what it will be measured against. - -Row 6 is the same defect this plan already corrects in DCH-0 for a different subject: a plausible -number that nobody could re-derive. DESIGN §6 governs both — name the instrument, never transcribe -its output. A harness of ours may not report a rate it cannot ground, and where a quantity is not -measurable the honest render is a refusal to render, not a zero. - -Row 7 was a single line in DCH-2 ("four tools: run a command, read, write, edit"), and that line is -where every operator-visible quirk in their lane lived. **It is now specified in DCH-2 as six owed -dispositions** — working directory, duration, input, truncation, edit matching, context exhaustion — -rather than deferred by a forward reference, since a promise to expand a brief elsewhere is the same -unreachable route this plan refuses in others. Two of their choices are adopted deliberately because -they are already the right shape: an output cap that **announces** its truncation, so a large result -is distinguishable from a complete one; and an edit that **refuses on zero or multiple matches** -rather than patching the first, which is construction over validation. - -Row 8 is not about the harness. It is a note to this lane's own reviewers: their repair for a -mini-agent-only failure aborted bootstrap for every session on the node. A fix that widens the -failure population is a regression regardless of the class it closes. - -### 5.2 The five dispositions, returned 2026-09-02 - -Sent 2026-09-01 and **all five returned**. Recorded as rulings rather than positions; where my -proposal was accepted with refinement, the refinement is what binds. - -**Q1 — the stop-the-line boundary. Accepted with refinement.** "Whose failure?" is evidence; it does -not decide. The controlling discriminator is **which continuation preconditions remain established, -and what is the smallest closed causal scope containing the uncertainty**. Stop-the-line means no -ordinary next effect is admissible — it does **not** mean the supervisor must die: a supervisor -alive in a typed stopped state substitutes no answer and admits no ordinary work, so it is not an -absorbing fallback, and destroying the mechanism that reports the failure was never §5's bar. -Consequences DCH-2 owes: - -- A tool timeout is first an observation that completion was **not seen by the deadline** — not - proof the command failed or changed nothing. It splits: timeout **plus** a settled owned process - population **plus** known effect state may be returned to the model as a typed result and the turn - continues; timeout with a surviving process or unknown effect state enters reconciliation, where - mutating tools and automatic retry are refused. So the honest surface is two dispositions, - `TimedOutQuiesced` and `TimedOutEffectStateUnknown`, plus termination of the owned process - population rather than one PID, and captured output through the last observed byte. -- An unexpected model stop reason is **turn-stopping**, not tool-level. A recognized-but-state- - invalid reason is a typed turn-protocol refusal; an undecodable wire value is a typed - unreadable-stop-reason observation carrying its raw evidence. None may throw through the - supervisor boundary, and none may collapse into one generic failure — terminal disposition and - native cause are separate axes, and an unreadable stream is neither "nothing happened" nor "the - provider failed". - -**Q2 — a degenerate turn. Accepted at typed-obligation grain, rejected at prose or character grain.** -No length, token count or heading shape may produce a disposition; those may drive an operator -attention view and nothing more. The adjudication is over **outstanding typed obligations**, whose -authority is the task and execution contract — the requested change, required validation, tool calls -awaiting results, mutations requiring readback — not the assistant's prose. The model may add -structured obligations; it may not erase task-owned ones or self-declare an empty set to obtain -completion. Three arms: `EndTurn` with obligations outstanding is **ended-incomplete**; with -obligations closed it is **awaiting verification**, which is deliberately *not* acceptance, because -the model's own `EndTurn` cannot grade the model's work; with obligation standing unobserved it is -**unadjudicated**. A heading-only reply becomes incomplete without any length rule, whenever the -task obligation is still unsatisfied. This also fixes the UI defect at its root: the surface renders -a typed state and never infers idle from a small text buffer. - -**Q3 — convergence restarting the serving process. (a) required, a constrained (b) required, (c) -rejected.** The drain is the construction and it becomes its own gate, **DCH-0r**; a §4b drop is not -the planned answer, because a drop reports a lower guarantee rather than making an unsafe arm green. -The prior-art incident is also **two** defects, answered differently: a control-plane restart killing -the worker is a lifecycle-separation problem (the harness worker must be supervised independently of -the dashboard), while a serving restart killing a request is a quiescence problem. The constrained -(b): the harness must survive backend death as a typed `BackendStreamInterrupted` carrying the old -incarnation, the observed termination and the retained stream prefix — and it must **not** resend. -A partially streamed response may already have executed tool calls and shown the operator prose; -exactly-once command identity covers commands that reached that boundary, not the replayability of a -model turn. Restart survival means **survive to report interruption**, never transparent -continuation. - -**Q4 — n=1 under survivorship.** It licenses exactly one existential proposition — that at least one -surviving execution showed the symptom — and not frequency, typicality, threshold, cause, or any -claim about long-context turns in general. Because it is prior art rather than DCH-owned evidence, -it does not establish even that existential proposition **for our harness**. Both hypotheses join -§2's list now and stay **separate**, because they are different subjects: streaming terminal -integrity is transport and decoder; incomplete-stop is turn semantics. Sharing a fixture does not -merge their verdicts. The streaming instrument is a paired differential holding model artifact, -host, service incarnation, request bytes, sampling parameters, context and cache condition fixed and -**changing only the stream mode**, judged on content-block sequence, tool-call sequence, usage and -terminal completeness rather than final rendered text. The incomplete-stop instrument constructs a -typed task with a known outstanding obligation and must not mention character count. And any -throughput receipt binds its cache condition as an **input identity**, not a footnote. - -**Q5 — the process exit status. Neither of my options exactly; closest to the first, corrected.** -An exit status carries **how that exact observed process terminated** — never whether the attempt -completed. An exhaustive integer partition does not fix the incident, because no mapping can say -whether the integer belongs to the watchdog, the wrapper, the container or the harness, whether the -harness ever started, whether a child outlived its wrapper, or whether the attempt had already -failed before something else exited zero. Two axes: a process-termination observation -(exited / signaled / unobserved, which this tree already models in `std.process_termination`, -including its refusal to fabricate a code for signal death) is **evidence carried into** an attempt -disposition, and never substitutes for it. The ordering, which is a real constraint on DCH-2: -**journal before effect** — attempt, task, base and worktree identity durably written before the -harness spawns or mutates anything; the process bound to that record; the semantic receipt written -before voluntary exit; and if the harness disappears without one, an independent supervisor records -interrupted, carrying the exact termination observation. A supervisor may move a journaled record -from running to interrupted and **may never move it to accepted**. So a watchdog exiting zero over a -thrown harness resolves to interrupted-with-receipt-missing, and no integer is read as "done". - -### 5.3 Standing rules this section adds to §4 - -- Do not report a measurement the harness cannot ground; render a refusal where a quantity is not - measurable, never a zero and never a back-computed figure. -- Do not treat a green canary as evidence for any class in §5.1. -- Do not land a repair whose failure population is larger than the one it closes. -- Do not produce a turn or attempt disposition from a length, token count or output shape. -- Do not resend a request whose response was already partially streamed. -- Do not read any process exit status as evidence that an attempt completed. diff --git a/docs/plans/demand-engine-program.md b/docs/plans/demand-engine-program.md index 1aa669cd470..83fb223241a 100644 --- a/docs/plans/demand-engine-program.md +++ b/docs/plans/demand-engine-program.md @@ -277,7 +277,7 @@ Every decision row is Ruled or Deferred with a trigger. Do not transcribe a coun 8. **D8 — First authority cut.** The v2 native fold's driver (M3a). Recommendation: confirm. **Deferred:** trigger is M3a's detailed plan confirming that driver as the first production scheduling cut; the consumer is not changed here. 9. **D9 — Crate partition of the emitted closure; generator promotion is a separate fact.** Two facts joined through generation identity, not one decision (side-chat ruling, 2026-09-15). GENERATOR PROMOTION -- seed (v1 emitting v2) versus self-hosted (v2 emitting itself) -- is an admission root cut governed by DESIGN §7 and D1: the seed remains the production generator until the self-hosted route satisfies the complete judgment and refusal contract, then the root cuts over; cost never keeps the seed as a semantic fallback, and it is not a `std.decision` candidate. PARTITION SELECTION is a realization selection over the closure the ACTIVE generator produced: hard constraints first (every module owned exactly once; every source and reference edge represented; complete `CompilationUnit` interface closure; Rust R4 validity; no illegal private access or impl ownership; the stable facade; complete semantic and refusal equivalence), then `std.decision` `select_realization` over a bounded declared candidate field -- one crate (the baseline, which remains a candidate), runtime + remainder, a few derived target-count cuts via `PartitionPolicy`, the policy-derived cut, SCC-per-unit as the diagnostic extreme -- funded on clean emitted-closure build wall, warm incremental wall over a declared edit population (a leaf, a high-fan-in foundation module, a large ordinary compiler module), total CPU, peak memory and process count, link/metadata/process overhead, artifact and binary size, and emission/workspace-construction wall; scoped by closure identity, producer compiler, toolchain, build profile, machine shape, candidate-field identity, evidence snapshot and policy; result `SelectedWithin`, never 'optimal'. At self-host cutover the same authority and decision re-run over the new closure; the prior selection is evidence, not an inherited answer. MEASUREMENT: the reading taken so far is a route wall partition -- seed workspace build, closure emission and one-crate emitted build as three walls read from the native route's build-log timestamps (`build.sh` in the srv1 A/B harness is the producer) -- not a rustc phase partition; `[native-prepare-split]` measures native-driver prepare, not rustc, and is not this instrument. The trigger is therefore the direct candidate A/B: each admitted candidate built from the same emitted closure, toolchain, profile, features and warm shared dependency target, recording per-crate start/end wall, ready/running crate concurrency, total wall and CPU, peak RSS and process count, link wall and artifact size; then the three edit classes with the crate DAG's PREDICTED rebuilt set pre-registered against cargo's observed rebuilt set. Pre-registered structural consequences if a derived cut is right: more than one admitted crate is ready during a nontrivial part of the build; cargo overlaps their compilation; observed rebuilt crates equal the predicted downstream closure after each edit; semantic outputs and refusals identical; the selected candidate wins under the declared field. Line share is not a bound on front-half time and is not a prediction. SEQUENCE: this is a standalone native-route realization cut with a current production consumer (the emitted native closure build) and a current displaced route (the one-crate writer), depending on no engine, fabric supply, cross-run store or width deletion -- it does NOT wait for M5. Its serial position is after M1.a (the JudgmentContract cut) and before M1.b (the keying-relation cut) and M1.c (the cross-run hit cut): it precedes M1.c because the selected `RustCratePartitionIdentity` is part of the compiler artifact's build configuration and therefore of its evaluation identity from the first persistent implementation; if the candidate field selects the one-crate baseline, D9 records that `SelectedWithin` result, performs no physical cut, and the sequence continues; if the runner envelope cannot honestly bound multi-crate cargo concurrency, the authority and A/B still complete and execution defers to M5 -- no `-j` setting belongs here. **Deferred:** trigger = the edge-provenance classification of the emitted reference graph and the candidate A/B above; until both exist this row admits no implementation. **Declared frontier waiting on this row (DESIGN §3c): cross-run materialization of the per-file pool heads reading.** Every entry-subject run re-derives the heads reading of every pool file (`v1_compiler.cli_run.pool_acquire` `heads_reading_for`; the `[pre-entry]` rows `pool_source_tokenize`, `pool_source_newline_index` and `pool_heads_parse`) because the declared-name index it feeds is a tree-subject fact each new process demands for the same tree: recurrence across processes, not authored duplication. Its request is a sibling variant of `std.materialization_provider` `ParseModuleRequest` with the same key fields (`source_digest`, `compiler`) and its own kind and output roster, the persisted value spelling-free with spans rebound on load, stored in the bounded local store under a per-family budget sized so the whole pool fits. It is not built, because its `compiler` input must be the identity of the tokenizer and heads parser alone: the running-binary hash re-keys on every commit, and a lane-local crate digest would be a second crate-identity mechanism beside this row. TRIGGER: D9's crate partition identity exists AND the front-end unit (`v1_compiler_tokenize` and `v1_compiler_parse`, below `v1_infer`) is a partition crate, so that crate's identity is the `compiler` input. CASE ON FILE, a one-off probe of 2026-10-03 and not an instrument (uncommitted test, release build, BuildBuddy runner, the live `dag` plus `src/v2` pool of 7417 files): recomputing every heads reading took 22.5 s (16.2 s under a timer that then covered lexing AND the newline index together, heads parse 6.3 s); reloading them from per-file `serde_json` took 3.3 s (read 0.12 s from a warm page cache, deserialize 3.17 s); 7417 of 7417 reloaded readings compared equal; the JSON was 777 MB for 98 MB of source. WHAT THE PROBE DID NOT TEST: it persisted and reloaded the reading AS IS, with the file spelling still in every span, so its equality count and its reload time are evidence about that form only. The proposed form (spelling-free, spans rebound on load) was not built, not compared and not timed; no pool file had a heads refusal, so a persisted refusal was never exercised. Owed before any build, and unmeasured: an executing comparison, over the live pool, of a fresh reading against the spelling-free persisted value after rebinding, covering the module node, diagnostic and refusal spans, and the file-local intern strings and occurrence-allocator end; the measured cost of that rebinding; the size of the spelling-free form (if it stays near that figure, a compact encoding through `std.materialization_object`'s versioned encoding rather than JSON), one cold-disk read, and the family budget declared from that measurement. The tree name census is a second candidate keyed over the heads digests of a source root's closure; it waits on this one, and `UniverseArtifactRequest` is checked as its family before any new variant. 10. **D10 — Native production admission: when the v2-compiled route becomes the production semantic route.** Operator direction (2026-09-15): anchor on the native route now and root-cause disagreements over time; v1 is not the semantic oracle -- the authority is the modeled `JudgmentContract`, and a v1/v2 disagreement is evidence demanding investigation, with neither side winning by age. Two cuts stay separate: native v2 EXECUTION becoming the production semantic route (this row) and the self-hosted GENERATOR replacing the seed (D9 / §7, which still requires the complete generation judgment and refusal contract; a native evaluator may be production while the seed still generates it). Admission is a typed whole-route verdict, `NativeProductionAdmission = NativeProductionAdmitted { contract_identity, required_subject_denominator, result_contract_coverage, refusal_census, divergence_dispositions } | NativeProductionRefused { causes }`, requiring: a named denominator of the judgments and subjects the route promises to serve; total outcomes -- every demanded subject yields an admitted result or an attributed typed refusal; zero unattributed refusals; no contract-violating acceptance; no contract-violating refusal inside the required denominator; every v1/v2 divergence classified against the independent contract or source authority; and no interpreter fallback after the cut. A v2 refusal where v1 accepted is worked after cutover only when the refusal is correct under the contract or the subject lies outside the declared denominator with that boundary explicit -- deferring a false refusal of a valid required subject is not a complete production handler. After cutover, a red parity or admission result latches `NativeProductionRevertRequired` or stops new admissions and awaits an explicit operator disposition; it never silently re-executes the judgment through v1 (the current routing consumer's fall-back-to-interpreted arm is exactly the escape hatch this row deletes). Recommendation: adopt, and run the pre-anchor census of §1 as its first instrument. **Deferred:** trigger is that census reaching zero unattributed refusals with every divergence dispositioned, at which point the admission verdict is computed for the declared denominator; the interpreter-remains-production sentence in M3a is retired by this row's `NativeProductionAdmitted` and by nothing else. -11. **D11 — Effects on the native route: the minimal set, and callability. Ruled 2026-09-16.** Scope (operator): Filesystem plus the Gap-B boundary of section 2.9 (argv, stdout/stderr, and the existing elapsed and peak-resident measurement seams) are realized in the emitted closure through bound realizations returning values; Clock, Network, Entropy and AuthContext are not realized by this program -- emitted interfaces refused by `NativeEffectRealizationAdmission`, outside D10's denominator; a scope ruling, not a drop. Callability (side-chat ruling, refined option (a)): resource operations are admissible in ORDINARY functions only through an explicit effect/resource requirement -- an effect envelope or capability parameter on the function -- and the entry realization supplies the matching handler; no ordinary function acquires an ambient global capability merely because a native handler exists. Option (b), entry folds as workflow functions, is refused: it makes 'workflow function' the price of any effect and pulls the workflow realization into every native entry, conflating a semantic effect requirement with one orchestration realization -- against this program's terminal goal of ordinary sequential programs receiving lawful realization for free. E1 falsifies the emitter's implementation of this ruling; it does not reopen it. The fork that option (b) would have deepened -- `uses` read as workflow identity -- is resolved by D12 below: the workflow-function concept is DELETED, not separated. There is no marker and no function-kind authority; executable-entry identity is the artifact-relative `compiler_pipeline_entry` (an executable has exactly one, a library none, and more than one or an unresolved driver refuses), never a property of a function; `uses` keeps its one meaning as an effect requirement; and sync/async derives from effects plus the bound handler, which the emitter was measured to key on `has_services` and `uses`, never on any entry role. **D11 terminal** (a condition of D10 and of this plan's dissolution): ordinary-function effect callability is implemented through explicit contracts; every in-denominator host effect has exactly one admitted native handler; unsupported effects produce typed admission refusals; the direct per-driver implementations those handlers displace are deleted; and the no-interpreter controls execute on the production route. A terminal D10 may not be reached by choosing a denominator that sidesteps this obligation. +11. **D11 — Effects on the native route: the minimal set, and callability. Ruled 2026-09-16.** Scope (operator): Filesystem plus the Gap-B boundary of section 2.9 (argv, stdout/stderr, and the existing elapsed and peak-resident measurement seams) are realized in the emitted closure through bound realizations returning values; Clock, Network, Entropy and AuthContext are not realized by this program -- emitted interfaces refused by `NativeEffectRealizationAdmission`, outside D10's denominator; a scope ruling, not a drop. Callability (side-chat ruling, refined option (a)): resource operations are admissible in ORDINARY functions only through an explicit effect/resource requirement -- an effect envelope or capability parameter on the function -- and the entry realization supplies the matching handler; no ordinary function acquires an ambient global capability merely because a native handler exists. Option (b), entry folds as workflow functions, is refused: it makes 'workflow function' the price of any effect and pulls the workflow realization into every native entry, conflating a semantic effect requirement with one orchestration realization -- against this program's terminal goal of ordinary sequential programs receiving lawful realization for free. E1 falsifies the emitter's implementation of this ruling; it does not reopen it. The fork that option (b) would have deepened -- `uses` read as workflow identity -- is resolved by D12 below: the workflow-function concept is DELETED, not separated. There is no marker and no function-kind authority; executable-entry identity is the artifact-relative `compiler_pipeline_entry` (an executable has exactly one, a library none, and more than one or an unresolved driver refuses), never a property of a function; `uses` keeps its one meaning as an effect requirement; and sync/async derives from effects plus the bound handler, which the emitter was measured to key on `has_services` and `uses`, never on any entry role. **D11 terminal** (a condition of D10 and of this plan's dissolution): ordinary-function effect callability is implemented through explicit contracts; every in-denominator host effect has exactly one admitted native handler; unsupported effects produce typed admission refusals; the direct per-driver implementations those handlers displace are deleted; and the no-interpreter controls execute on the production route. A terminal D10 may not be reached by choosing a denominator that sidesteps this obligation. **Declared extension for native SERVICE entries (gentle-dove-36, 2026-10-04, extending this operator ruling of 2026-09-16):** a `NativeServeDriver` entry (first consumer the approval broker) is a different denominator from the compiler route, and in it Clock and Network are admitted; Entropy and AuthContext stay out, and the compiler route keeps this ruling whole. No transport is added: the broker's clock is the `extdeps.clock` `Clock` service on the existing shell transport and its outgoing HTTP is a `transport rest` service on the existing REST lowering, so each is admitted as the one handler `bind_operation_transport` already binds. The carrier, both denominators and the emission refusal are `v1.compiler.emit` `NativeEffectRealizationAdmission`, computed by `v1.compiler.emit_rust` `native_entry_effects`, and both native mains bind exactly the handlers it admits. 12. **D12 -- Executable composition root and legacy workflow deletion. Ruled 2026-09-17 (operator + side-chat A').** Workflow was never a semantic kind: it was an emitter heuristic (`v1.compiler.emit_rust` `is_workflow_item`, true for `uses` present, a service or resource name in the registry, or a registry miss) whose sole product was the clap subcommands `emit_main_rs` rendered. The census that grounds the ruling: every built binary is single-entry through `compiler_pipeline_entry` and pulls no workflow function into its main; a closure with no entry already renders the typed exit-2 `emit_entry_point_absent_main_rs`; `v1.compiler.infer` gates no resource operation on workflow, since admissibility is the `uses` clause; and the `is_async` signature field had no reader. So deletion breaks no production binary. Entry identity is an artifact-relative RELATION carried by `compiler_pipeline_entry`; a multi-command surface, when one is demanded, is explicit dispatch data as `gunbc.cli_dispatch_surface` models it, not a function kind. The cut deletes `is_workflow_item`, `collect_workflow_funcs`, `WorkflowFunc`, `to_workflow_func`, `validate_workflow_param_defaults`, the `emit_main_rs` workflow arm, the imported-data-reference default folding that only that arm consumed, and the dead `is_async` field; it keeps `compiler_pipeline_entry` and the entry-absent refusal, and leaves `uses` as an effect requirement -- the legacy workflow population is not marked. Enrolled discriminators (`test.claim.entry_authority_witness_test`): adding a `uses` requirement or a service call does not change the entry population, so reintroducing `uses`, a service name or a registry miss as entry identity turns a regression control red; one declared entry is the sole root beside an effect; two declarations and an unmodeled driver each refuse. The witnesses whose subject was folding an imported data reference into a CLI default are deleted with the path, because that folding had no other consumer. This SUPERSEDES the D12-marker / FunctionRole direction and its migration of the legacy workflow population. D13 below refines the remaining `uses`-as-carrier sentence: for transparent bodies the requirement is derived, not authored. 13. **D13 -- Derived dependency demand and provider binding. Ruled 2026-09-18 (operator).** This SUPERSEDES the E1b 'add uses to every call chain' migration -- that migration is not run. For a transparent (visible-body) function, dependency demand is DERIVED from resolved dependency and operation references, not authored in the header. Direct demand is produced once by resolution; transitive demand is derived over resolved call relations. The qualified carrier is DependencyDemand (the R1/#11401 derive-once-carry pattern): produced once by resolution, not re-read per backend. Dependency identity includes the logical SUBJECT (`source_tree` is not `output_tree`), not only the interface type. Opaque declarations (no body) carry explicit dependency contracts. Policy (may-not-use-Network, upper bounds) constrains the derived set; it is not an asserted use-list. Composition roots bind demanded subjects to admitted providers. Targets may closure-convert derived demands into explicit params or a typed environment. No source operation reaches an ambient host by name; absent or ambiguous provider is a typed refusal. Existing `uses` rows are classified before migration (USES-0); restating rows are deleted because they are derived, and binder / opaque / policy rows move to their owning carriers. A local `std::fs` impl is admitted as ONE Filesystem provider, not a second source-level operation surface. D13 refines D11's uses-as-carrier: the requirement is DERIVED for transparent bodies, authored only for opaque declarations, distinct subjects, and policy envelopes. USES-0 is the identity-grain join of authored `uses` rows against resolved direct-plus-transitive demand -- not a count equality -- and it decides the split; no `uses` row is added to a call chain and no `uses` row is deleted until that census is dispositioned. The analysis that holds the join is `docs/plans/uses-occurrence-census.md`. E2 shape under this ruling: CLI code references `source_tree.list` / `source_tree.try_read`; resolution binds the `source_tree` SUBJECT; the compiler derives direct and transitive DependencyDemand; the NativeCliDriver root binds LocalFilesystem (impl Filesystem: try_read -> `std::fs::read`, list -> `read_dir`); realization threads it; `std::fs` leaves main; tests bind InMemoryFilesystem. No bare `filesystem_try_read` / `filesystem_list` globals. `emit_file_call` stays renderer machinery reused internally, not the dependency model and not a handler. 14. **D13 follow-up population.** Cut (c) measured its population, then main added more `uses` rows while it was in flight; those were carried unmeasured until a final-base census over 5ff323b6d29..4446f2e990f measured every authored row in the tree and, under the same criterion, deleted each one whose derived demand was nonempty EXCEPT the identities explicitly retained under EditedWitnessHasNoHermeticRoute. None had empty demand, so no function flipped from effectful to pure. What remains is two kinds of row, each named at the row identity USES-0 defined and each carrying the trigger that is true for IT rather than for its module. Rows the criterion cannot type: ordinary fns whose module does not resolve, one refusal probe that never will, and pattern declarations the criterion never ranged over. And rows MEASURED DELETABLE whose deletion is not yet landable: wet witnesses whose edit the floor would plan onto a hermetic route that has no arm for their operation. The retained rows, each with its trigger: test.probe.census_app_acquisition_forged_probe::mint_a_receipt_from_authored_standings (uses net: std.resources.Network; trigger: the probe declaration is deleted from the corpus -- its module refuses by design, so resolution will never type the row, and its identities leave with the probe); test.probe.census_app_acquisition_forged_probe::mint_a_receipt_from_authored_standings (uses fs: std.resources.Filesystem; trigger: the probe declaration is deleted from the corpus -- its module refuses by design, so resolution will never type the row, and its identities leave with the probe); test.manual.runner_microvm_lifecycle_wet_receipt::start_payload (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::clear_slice (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::slice_is_quiet (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_sigterm_ignoring_child_is_stopped_by_force (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_process_in_a_child_cgroup_is_found_and_the_flat_read_is_empty (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_missing_cgroup_events_is_unobservable_and_never_empty (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::show_property_reads_a_units_active_state (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::show_property_reads_a_units_environment (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::unit_property_text (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::the_unit_reports_the_attempt_invocation_id_it_was_started_with (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_unit_with_no_attempt_assignment_reads_as_unread (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::start_with_invocation (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::stop_incarnation_unit (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_successful_job_can_leave_the_cell_subtree_populated (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::the_cell_wide_force_ends_a_survivor_in_a_sibling_cgroup (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::the_cell_wide_force_ends_a_survivor_in_a_sibling_cgroup (uses fs: std.resources.Filesystem; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); test.manual.runner_microvm_lifecycle_wet_receipt::a_clean_slice_reads_empty_and_absent_paths_read_absent (uses net: std.resources.Network; trigger: extdeps.linux.cgroup_v2 ReadInterfaceFile gains a mock_response, so an edited witness in this module reaches a verdict on the floor's hermetic route instead of a route gap); gunbc.auth.credentials::gcp_secret_credential (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.auth.credentials::gcp_oauth_access_token_via_adc_refresh (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.auth.credentials::gcp_adc_token_from_credentials (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.auth.credentials::gcp_oauth_access_token_adc_for_path (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.tailscale_acl_phase2_credential::tailscale_acl_operator_ephemeral_credential (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.tailscale_acl_phase2_credential::tailscale_acl_credential_with_operator_token (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations); gunbc.assimilate.bmc_bootstrap_provision::bmc_bootstrap_provision_srv3 (uses net: std.resources.Network; trigger: derived demand ranges over pattern declarations). diff --git a/docs/plans/dependency-acquisition-gap-analysis.md b/docs/plans/dependency-acquisition-gap-analysis.md deleted file mode 100644 index 67346ef029a..00000000000 --- a/docs/plans/dependency-acquisition-gap-analysis.md +++ /dev/null @@ -1,172 +0,0 @@ -# Dependency acquisition: why every new dependency is a modeling project - -**Status:** gap analysis, 2026-09-07. Reached from a concrete blocker — the approval loop needs an -ntfy server on srv1, and "install ntfy" turned out to be a slice rather than a data row. The -question this answers is the operator's: *the convergence is supposed to handle this; what is the -gap from here to there?* - -**Framing correction, operator, 2026-09-07:** fleet convergence "was never supposed to be host keys -— I think we started making a world convergence but got halfway through like all things." That is -the right frame and this document adopts it. What follows is not "convergence lacks a feature"; it -is an inventory of which half got built. - ---- - -## 1. The finding - -**There is no `Dependency` concept.** There is no type whose inhabitants are "a piece of software -this fleet depends on", and consequently no single place that answers *where does it come from, how -is it verified, and how do we know it is present*. - -What exists instead is `gunbc.host_effect` `HostEffect`, a closed coproduct of **34 arms**. It is -genuinely the world-convergence spine the operator describes — it has arms for host OS, BMC, -deploys, runners, compile pools, and OS install media. But its vocabulary is **one arm per -instance, not one arm per concept**, and at least eight of the 34 arms mean the same thing: - -`OsInstallActuatorToolchainEnsure` · `EnsureBuildCacheInstance` · `ProvisionCodexRuntime` · -`EnsureCompilePoolSlice` · `Srv3SeededInstallMediaToolchainEnsure` · `LiveDeployEnsureDependency` · -`EnsureActionsRunnerRelease` · `EnsureRunnerSlotDirectory` - -Every one of those is *ensure some software is present and configured on a host*. A world -convergence with one arm per thing in the world is an **enumeration wearing a convergence's name**, -and its cost per new dependency is constant — it never amortizes. That is DESIGN §2 exactly: the -same work, duplicated once per dependency, priced as if each were new. - -## 2. The census — six dependencies, five mechanisms, five homes - -| dependency | acquisition mechanism | authority home | actuated through | -|---|---|---|---| -| tailscale, tmux | apt package, `dpkg -s` guard then `apt-get install` | `live_deploy.spec` `EnsuredDependencyKind` | `LiveDeployEnsureDependency` | -| docker-ce | third-party apt repo + **pinned GPG fingerprint** | `extdeps.container.docker_ce` | `runner_host_docker_provision_script` | -| sccache | pinned GitHub release tarball + **per-arch sha256** | `extdeps.cache.sccache` | `EnsureBuildCacheInstance` | -| actions-runner | GitHub release artifact | `extdeps.github.actions_runner` | `EnsureActionsRunnerRelease` | -| codex | npm registry + lockfile integrity | `gunbc.package_delivery` (3,391 lines) | `ProvisionCodexRuntime` | -| **npm itself** | **a prose string** | `gunbc.host_cli_dependency` | **nothing** | - -The verification posture is good and was clearly reasoned each time — docker's keyring fingerprint -is pinned against a published fact, sccache's tarball is sha256-verified per architecture. **The -defect is not rigor; it is that the rigor was re-derived from scratch six times**, and lives in -five modules that share no vocabulary. - -### The last row is the class the operator named - -`gunbc.host_cli_dependency` carries the fallback for a dependency with no bespoke arm. - -The roster is `gunbc.host_cli_dependency` `codex_wet_materialization_host_cli_requirements`, walked -by `first_absent_host_cli_dependency` and observed through `observe_host_cli_dependency`. Read those -declarations rather than a copy of them here — §6, name the instrument rather than transcribing its -output, and §3, cite the symbol rather than a positional or duplicated copy. - -**Updated by #11061, because that change falsified how this section used to make its point.** It -transcribed the row as it then stood and observed that its `provision` field was a `NonEmptyStr` — -"an instruction to a human, typed as a string". That field is gone: the requirement wrapper carried -a tool NAME resolved back to a `CliTool` by a hand-rolled if-chain, and carrying the `CliTool` -itself made the unresolvable row unconstructible, which deleted the resolver, its unresolved arm, -and the `provision` prose that only fed that arm. The roster is now a plain `List`, and -the install sentence a refusal carries is derived from `CliTool.installable_via` through -`install_hint` rather than authored beside it. - -**That narrows the defect below without dissolving it.** The hint is no longer a free-text sentence -that can disagree with the tool it describes; it is derived from the tool's own declared install -sources. But `installable_via` is still a description of how a human would install the thing, not a -route anything executes — `SourceApt { package }` names a package, and no modeled effect consumes it -to converge a host. So the paragraph that follows was written about `provision` and remains true of -`installable_via`: what changed is that the sentence is now derived rather than authored, not that -anything acquires the dependency. - -It is worth being precise about what is and is not wrong here, because the obvious reading is the -wrong one. The module's *refusal* behaviour is correct and deliberately so: an absent tool stops -with a typed `HostDependencyAbsent` carrying tool and hint, and the module's own annotation says -"absent npm stops as HostDependencyAbsent, **never auto-install inside the witness**". That is -§5 fail-closed, and auto-installing inside a witness would be the absorbing fallback. - -**The defect is that the refusal has no route to a fix.** It hands a person a sentence — derived -from the tool's declared install sources since #11061, but still only a sentence — and there is -no modeled thing that sentence refers to — so the convergence cannot act on it, no census can count -what is unprovisioned, and nothing goes red when the hint rots. An unmodeled dependency is invisible -to every instrument that would otherwise rank it for work. - -## 3. The gap is already named by the corpus - -This is not an unrecognized problem. `extdeps.cache.sccache` states it in its own dissolution -trigger: - -> It dissolves when `host_effect_apply` binds a **verified-download-plus-install effect -> (FetchAndVerify then InstallBinary)** without the shell leaf, at which point -> `sccache_install_script` is deleted rather than edited. - -Neither `FetchAndVerify` nor `InstallBinary` exists anywhere in the corpus — grep returns exactly -that one comment. The missing concept was identified, its dissolution trigger was written, and the -capability was never built. `extdeps.container.docker_ce` carries a parallel trigger for the same -reason. - -So `sccache_install_script` and `docker_ce_repo_install_script` remain **shell leaves**: install -sequences assembled as strings, typed as `String`, with the modeled row supplying only version and -digest. The shell-leaf residue and the missing `Dependency` concept are **one gap seen twice**. - -## 4. What "install ntfy" costs today - -Concretely, why the blocker is a slice and not a row. Adding ntfy through the existing shape -requires: - -1. a new `extdeps` module for the release, its version and per-arch digests (sccache's shape); -2. a **35th `HostEffect` arm**, plus its realize handler and its plan handler; -3. an actuation site — `live_deploy` (scheduled for deletion) or `runner_host_deploy` (runner-scoped, and ntfy is not a runner concern); -4. a shell-leaf install script that lands already carrying a dissolution trigger it cannot discharge; -5. a systemd unit, which has no general home either — units are emitted per service family (`RunnerUnitStanding`, `LaunchUnitStanding`) rather than by one authority. - -Five pieces of bespoke modeling to install one binary — and every one of them is work the *next* -dependency will do again from scratch. **That is the cost the operator is feeling, and it is a real -cost rather than an impression.** - -## 5. The route - -The shape of the repair follows from §2: model the concept once, derive every use. - -**Phase 1 — `Dependency` in `extdeps`.** Identity, version, and an acquisition source as a -coproduct over the mechanisms already in evidence: - -``` -AptPackage { package } -AptRepoPackage { package, repo_url, keyring_url, keyring_path, keyring_fingerprint } -PinnedRelease { url_template, digest_by_arch } -NpmPackage { name, lock_integrity } -``` - -Every arm is a shape the corpus already implements correctly somewhere — this is decompression and -reduction of existing rows, not invention. Verification stops being per-dependency diligence and -becomes a property of the arm: a `PinnedRelease` **cannot be constructed without a digest**, which -moves supply-chain verification from §4b rung 1 (each author remembered) to rung 4 (unwritable -otherwise). That is the safety payoff, and it is larger than the DRY payoff. - -**Phase 2 — one arm.** `EnsureDependency { dep: Dependency }` replaces the bespoke arms; the six -existing dependencies become **data rows**. This is a §3 replacement migration and it must cut at -the root: build the arm, move all six at once, delete the old arms and their shell leaves in the -same motion. Six is a tractable atomic cut, and a surviving `EnsuredDependencyKind` would be an -attractor — every later dependency would be answered in its vocabulary. - -**Phase 3 — the prose route closes.** Stated against `provision: NonEmptyStr` when written; #11061 -deleted that field, so the subject is now the install sentence `host_cli_dependency` derives from -`CliTool.installable_via` through `install_hint`. The phase is unchanged in substance: that -derivation becomes a reference to a `Dependency`, so the refusal *names the acquisition that would -satisfy it* and an absent tool is a routable obligation rather than a sentence — derived prose is -still prose. Unprovisioned dependencies become countable only at that point. - -**Phase 4 — ntfy is a data row**, and so is the next one. - -**Ordering note.** Phase 1 is worth landing on its own even if Phase 2 waits: it is additive, it -gives the digest-by-construction win immediately, and the six existing rows can migrate one at a -time *into* it. Phase 2's cut is what must be atomic — not the whole program. - -## 6. What this does not settle - -- **Units.** `EnsureDependency` gets the binary onto the host; it does not run it. Unit emission has - no general authority either (`live_deploy` for apps, per-family standings elsewhere), and that is - a **separate half-built convergence** deserving its own analysis. The ntfy work needs both. -- **`live_deploy`'s disposition.** It holds app deployment members and the only working apt-ensure - path, and is scheduled for deletion with no declared cut. Phase 2 would take the apt path out of - it, which makes the cut smaller — but does not make it. -- **ntfy's actual packaging.** Whether ntfy ships an apt repo, a `.deb`, or only a release tarball - was **not verified while writing this** (no apt available in the session container). It changes - which arm the row uses and nothing structural, but it should be checked rather than assumed - before the row is written. diff --git a/docs/plans/deploy-convergence-observed-side.md b/docs/plans/deploy-convergence-observed-side.md index 753fcc68608..eebf2b8a754 100644 --- a/docs/plans/deploy-convergence-observed-side.md +++ b/docs/plans/deploy-convergence-observed-side.md @@ -85,7 +85,7 @@ And it is this record's own class once more: repository revision equality is a s Three placements, and the question was which one -- the answer is that they are three different boundaries and all three are required. **At the OPERATION boundary**, a publication plan must be unconstructible without a guard proving release-consuming actuators are quiesced: the guard is a required field, so an unguarded publication has no constructor rather than being refused by a check. **At the PARENT TRANSACTION boundary**, that same frozen plan must acquire the quiescence, publish, verify, and restore actuation automatically -- because a guard supplied from outside would leave the fleet permanently undeployable (the belt is normally enabled), so someone disables it, retries, and re-enables by hand, which is out-of-band convergence wearing a safety label. **In the STEADY-STATE model**, belt admission must consume a coherent release identity rather than a repository revision. -**The objection that made this look like a new ownership claim was wrong, and it was mine.** `BeltTimerUnit` is already a live-deploy OWNED artifact kind -- its upsert installs and enables the unit, its teardown disables and removes it -- so deployment already owns that lifecycle. What changes is not who owns it but that the dependency becomes honest instead of relying on apply-all moving everything fast enough. +**The objection that made this look like a new ownership claim was wrong, and it was mine.** `BeltEventPathUnit` is already a live-deploy OWNED artifact kind -- its upsert installs and enables the unit, its teardown disables and removes it -- so deployment already owns that lifecycle. What changes is not who owns it but that the dependency becomes honest instead of relying on apply-all moving everything fast enough. **The fourth requirement sits underneath all three: the quiescence authority must span admission THROUGH actuation.** A check performed before a dispatch, followed by an unrelated mutation, is another time-of-check/time-of-use gap -- a tick that passed admission a moment before deploy acquired quiescence still executes during publication. So two mechanisms are needed, not one: a PERSISTENT inhibition that survives process failure, so a deploy that dies after publishing the tree and before restarting the service leaves the belt still refusing and the next converge resumes or rolls back with no operator re-enabling anything; and an EXECUTION LEASE that both sides hold, deploy exclusively from its final pre-mutation observation through read-back, every release-consuming belt path shared from its standing read through operation commitment. A lock that serializes only two deployment runs does not protect against the third actuator. diff --git a/docs/plans/deployment-risk-conformance.md b/docs/plans/deployment-risk-conformance.md deleted file mode 100644 index 0aaf1789486..00000000000 --- a/docs/plans/deployment-risk-conformance.md +++ /dev/null @@ -1,41 +0,0 @@ -# Deployment-risk conformance - -Operator direction 2026-10-03; vocabulary fixed the same day by a side-chat ruling relayed through silent-lark-156. Home: `gunbc.deployment_risk`. - -## Terms (approved; use these names) - -| Term | Meaning | Is NOT | -|---|---|---| -| `DeploymentId` | the stable identity of one Deployment whose desired state, mutable state, and external attachments move together; an instance realizes that Deployment and carries this identity | a host, machine, process, release stage, dashboard `instance_id` | -| `DeploymentRiskClass = TestRisk \| ProdRisk` | coarse harm class, DERIVED from `ProdRoleSelection` only | a number, machine qualification, stage, branch, host | -| `DeploymentHarmMagnitudeBet` | Fermi order of magnitude of harm conditional on doing the wrong thing, typed as a §4d bet | a probability, a fact, the class, an SLA | -| `ProdRoleSelection = NoProdRole \| ProdRoleHeldBy { deployment }` | the sole switch (`prod_role_selection`) | inferred from srv1, "live", main, a host | -| `DeploymentExternalBindingSet` | the external attachments of exactly one class; absent set refuses | a fallback, a shared default, a borrow | -| `DeploymentRiskInterface` | what each deployment exposes (identity, harm bet) and consumes (class, bindings) | a `DeploymentSpec`, an actuator, a stage model | -| `DeploymentReleaseStage` | independent rollout axis | out of scope; next candidate | - -`DesiredDeployment` (selection row) and `DeploymentSpec` (desired realization) stay distinct from `DeploymentId`. `MachineOperatingEnvironment` is replaced by `MachineQualificationPolicy { admits: DeploymentRiskClass }`: a machine admits a class, never holds one. A `ProdRoleHeldBy` assignment refuses unless its host admits `ProdRisk`. - -## Deliverables - -1. **Model** (this PR): `gunbc.deployment_risk`; `HostDashboardInstance.deployment` (required, all six constructors); `dashboard_instance_risk_class`; the failure-mode row `a_test_deployment_acts_on_the_prod_deployments_state`; RED `test.claim.deployment_risk_witness_test`. - - Declared frontier (§3c): `resolve_deployment_bindings` and `dashboard_instance_risk_class` gain production consumers in deliverables 2 and 3.1. - - Deferred to deliverable 2 (review 74937): `DeploymentRiskInterface` and `DeploymentHarmMagnitudeBet` land with their first executing consumer, the operator-disposition gate (which actions on a deployment need an operator), and the bet's magnitude is grounded as an order of magnitude over `std.measure` `Duration`, not a bare `Int`. -1b. **Machine qualification migration**: one motion over machine_intake (re-census on main first; coordinate warm-crane-577's mtcollins1 untangle lane). eager-gull-22 handed it to this lane. -2. **Repoint the prod pins** through `prod_role_selection` (broker owner, fabric storage placement, approval client/store/trust, serve/dispatch entry points, apply's prod peer → `DashboardProtectedSibling`, `fleet_reach_endpoint`). RED: a fixture selection moves the broker owner and storage placement. -3. **Split shared bindings**, one PR each: ntfy → Codex account/home → admitted ref → publication identity/GitHub App → R2/GCP → OIDC. Each adds a field to `DeploymentExternalBindingSet` and either a TestRisk resource (operator approval first) or a standing refusal. -4. **One spec per deployment**; retire the `live_deploy.desired` rung drop by its own trigger. Renames: `srv1_production_desired_deployment → srv1_primary_desired_deployment`, `roadmap_belt_production_spawn_mode → roadmap_belt_operational_spawn_mode`. -5. **Conformance row** `deployment-risk conformance` in `gunbc.design_argument conformance_domains`, once the home is consumed. - -Widened scope (side chat): classify every convergence subject as deployment-bound (Spark serving/experiments/seat grants, fabric storage, approval broker, R2 keyed by class + `BucketPurpose`, IAM targets, DNS/routes/certs, runner workloads, mutable caches), host-shared (Spark Wi-Fi/SSH/admin grants, KVM, base networking, toolchains), or fleet-control (root IAM writer, account admin). Pure computation identity and content-addressed outputs gain no deployment key. Manual convergence requests take `DeploymentConvergence { deployment } | HostResourceConvergence { host, resource } | FleetControlConvergence { operation }`. - -## Shared-binding table (as of deliverable 1) - -| Binding | State | Reason | -|---|---|---| -| ntfy topic | modeled in the binding set; ProdRisk only; TestRisk **refuses** at the resolver | broker path still reads `approval_ntfy_topic` directly until 3.1 | -| GitHub App (publication) | modeled; ProdRisk only; TestRisk **refuses** at the resolver | publishers still name `gunbai_*_declared` until 3.4 | -| Codex account and home | still shared | 3.2 | -| admitted ref | still shared (labs on `refs/fleet/desired`) | 3.3 | -| R2 / GCP / WIF | still shared | 3.5; coordinate royal-moth-86 (#13035) | -| OIDC client | still shared, unmodeled | 3.6 | diff --git a/docs/plans/derived-node-identity-design.md b/docs/plans/derived-node-identity-design.md deleted file mode 100644 index 49fcf128cc2..00000000000 --- a/docs/plans/derived-node-identity-design.md +++ /dev/null @@ -1,184 +0,0 @@ -# Derived-node identity in v1 infer - -Status: step 1 landed (#12913, #12914); step 2 scheduled 2026-10-04, starting with §7's instrument. -Owner: jolly-pike-330 (staffed by gentle-dove-36); scheduled by gentle-dove-36 on neat-boar-16's -handoff, 2026-10-04. Step 1 owner: clever-lynx-801. Ruling: quiet-gull-780, 2026-10-01. -Class row: `gunbc.recurring_failure_mode` `generic_identity_decided_by_spelling`. - -## 1. The question - -When `v1.compiler.infer` **builds** a type node after resolution, what identity does that node -carry, and who says so? - -"Builds" covers: -- substituting a generic's binding into a signature, -- instantiating a generic record's fields or an alias chain, -- forming a call plan's formals, -- minting a kernel container type for a value. - -Today the answer is: whatever spelling the node happens to have. Every consumer that needs to know -whether a node *is* a generic parameter, rather than merely spelled like one, has nothing else to -read. This note models the identity those nodes must carry. - -## 2. What was measured (not inferred) - -These facts locate the problem. Their magnitudes are deliberately not transcribed (DESIGN §6); -§7 names the instrument that re-derives them. - -- **The spelling collides by construction.** The kernel container type names its element child - after its own template parameter. Every `List` therefore carries a child spelled `T` and - resolved to `M`, and it collides with any generic parameter also spelled `T`. -- **Three sites decide generic identity by spelling:** - - `unify_generics` binds a bare formal by its label; - - `substitute_generics_apply` replaces a bare node by its label; - - that function's self-binding guard compares labels. -- **Two identity facts already exist after resolution.** Neither reaches the nodes infer builds: - - `TypeVariable` (inferred) is bound to each declared parameter by - `v1.compiler.infer_env` `env_with_type_variable_bindings`. Its id is the bare parameter name. - - `Node.declaration = (owner, TypeParameter { name })` is written by `v1.compiler.resolve` - `binder_marked_type` (#12690) on authored signature positions: parameter types, returns, type - arguments, declaration fields and arrow positions. -- **Coverage, measured over the whole-corpus compile.** A large share of generic bind and - substitute events act on nodes that carry neither fact in a usable form. They come from about a - dozen constructors, listed on the class row, led by `infer_call_arguments_generic_pass` and - `build_call_application_plan`. No magnitudes are given here: per DESIGN §6 they belong to the - instrument (§7), not to prose. -- **What happened when each fact was read alone.** Reading the `TypeVariable` mark made the - compiler's next generation refuse its own sources: generic record fields arrive unmarked. Reading - the declaration mark would stop substituting at every event the constructors above produce. - -## 3. Hypotheses step 2 must settle first (labelled as bets, DESIGN §4d) - -1. **Read before marking, not lost in copying.** The unmarked call-plan formals are read from a - signature representation built before `binder_marked_type` runs, not lost while copying. - `v1.compiler.infer` `bind_local_func_conformance` builds `ResolvedFormal.declared_type` from - `param_node_type_expr` of the func-env signature. If that signature predates marking, the repair - at those two sites is to read the marked signature, not to add a carrier. The test is the same - instrument, tagged by whether the node is reachable from a marked signature. -2. **`substitute_generics_apply` keeps its input's declaration.** It already copies - `n.declaration` into the node it rebuilds, so it is not a loss site. Its *output* node, the - binding, carries the binding's identity, which is correct. -3. **The kernel element child needs no parameter spelling.** Its identity is "element position - of this container". The open question is whether any reader depends on the child being spelled - `T`. The instrument answers it by deleting the spelling and listing what breaks; quiet-gull-780 - asked for that reader to be named. - -## 4. The model - -A derived type node carries exactly one **origin**, named by the constructor that builds it, never -reconstructed from the node's shape (bold-fox-455's constraint): - -```dag -type DerivedTypeOrigin - = CarriedDeclaration { declaration: DeclarationRef } // a copy keeps the identity it was given - | ParameterBinding { parameter: DeclarationRef } // the node a generic parameter was bound to - | KernelElementPosition { container: KernelContainer, position: Int } - | DerivationRefused { constructor: NonEmptyStr, cause: NonEmptyStr } -``` - -- **`CarriedDeclaration`** is the default for every copy. A constructor that rebuilds a node from - another node carries the input's declaration unchanged. This is the copy law, and most of the - inventory falls under it. -- **`ParameterBinding`** names the generic parameter a substituted node stands in for. It is - needed only where a consumer asks "which parameter did this come from" (cross-owner keying, §6). -- **`KernelElementPosition`** replaces the spelled `T`. A container's element child is identified - by its position in a kernel container and is never mistaken for a `TypeParameter`. -- **`DerivationRefused`** is the fail-closed arm. A constructor that cannot name an origin refuses, - located at the constructor. It never falls back to the spelling. - -Generic identity at the three sites becomes: a node is generic parameter `p` exactly when its -origin is `CarriedDeclaration` with `field: TypeParameter`, or a `ParameterBinding` to `p` whose -binding is `p` itself. A self-binding is identity equality. Spelling never enters the decision. - -**Laws** (the step-1 carrier states these and its control executes them): -- **L1, copy:** a copied node's origin equals its input's origin. -- **L2, no shape inference:** a constructor that builds a node names its origin, and two nodes - that are structurally equal but have different origins stay distinct. -- **L3, kernel distinctness:** a `KernelElementPosition` origin is never a `TypeParameter` identity, - whatever its spelling. -- **L4, refusal:** `DerivationRefused` is never read as a generic parameter or as a concrete type; - a consumer reaching it refuses. - -## 5. Its own relation, or an extension of an existing one (DESIGN §3b) - -This is **not a new keying relation**, and **not an extension of `std.occurrence_identity`**. - -- **Not #12790.** Occurrence identity answers "which authored occurrence". The nodes in question - are built by infer and have no authored occurrence, so allocating one would infer their cause - from their shape. That is option A, which 3e already rejected for the same reason. -- **Not the facts site key (3e).** That key answers "which site of one inferred tree a fact is - about". It is positional inside one tree and says nothing about what a node denotes. -- **The same relation as `std.decl_ref`, carried further.** Its relation is "this node denotes this - declaration-level entity". The model extends where that identity is *carried*, through infer's - constructors, and adds the two arms `DeclarationRef` cannot express: a kernel element position - and a refusal. - -**Reconciliation with A′** (`keying-relation-design.md` §3e, declared model item): A′ is "identity -for every node at infer's input, with each builder naming its cause". This note is A′'s -type-level interior: the same constraint, that builders name their cause, applied to the type -nodes infer builds *inside* the stage. They are one model item, not two. If A′ lands, its -input-side origins and these interior origins are arms of one carrier. This note's carrier is -named and placed so A′ extends it rather than forking it. - -**Scope:** type nodes built by `v1.compiler.infer` after resolution, covering the dozen -constructors inventoried in §2. The following are outside this note: -- value-expression nodes; -- other stages; -- v2 infer, which has its own carrier work (#12763 and its line). - -## 6. Sequencing - -1. **Step 1, in two PRs, with no infer edits:** - - **1a (#12913):** this note and the class row. - - **1b (#12914):** the std carrier `std.derived_type_origin`, which declares `DerivedTypeOrigin` - and `GenericIdentityVerdict`, and its control `test.claim.derived_type_origin_witness_test`, - which executes L1 to L4 over supplied origins. Until step 2 lands, that control is the - carrier's only consumer, a declared frontier whose trigger is step 2 (DESIGN §3c). -2. **Step 2:** first settle §3's three bets with the instrument. Then carry origins through the - inventoried constructors, switch the three sites to identity, and delete their name branches. - The evidence is: - - the compiler's next generation builds and compiles its own sources (the control that caught - the first attempt); - - a whole-corpus census in both directions at identity grain; - - the srv3 witness, the `00_compile` native-lane site, and `head_of(xs: [1], d: 2)` into a - String parameter. - - **Step 2 does not wait on the site-keyed facts store** (struck 2026-10-04, gentle-dove-36). This - note used to sequence step 2 after that work "because both rewrite infer's copying paths". They - are two modules both named infer, and they share no symbol: - - step 2 edits `v1.compiler.infer` (`unify_generics`, `substitute_generics_apply`, - `infer_call_arguments_generic_pass`, `build_call_application_plan`, - `bind_local_func_conformance`, `infer_record_lit_structural`) and `v1.compiler.infer_env` - `env_with_type_variable_bindings`, which copy type nodes; - - the facts store edits `v2.compiler.infer` (`inferred_facts_map_enter`, - `facts_map_from_entries`, `inferred_facts_witness_for_node`) and the readers of - `InferredTree.facts` in v2 eval, emit, translate and the lenses, which key a facts table. - - `v1.compiler.infer` imports nothing from v2 and holds no facts table, and §5 already scopes v2 - infer out of this note. The facts store was never built past its model - (`keying-relation-design.md` §3e) and has no owner. The one indirect link: step 2's - next-generation control compiles `src/v2`, so a facts conversion landing during step 2 changes - that control's corpus, not step 2's code. - **Residue admitted to step 2** (quiet-gull-780, 2026-10-01, via clever-newt-773): a generic - variant literal with no expected type, such as `Cons { head, tail }` at - `gunbc.spark.host_commitment`, is typed as its parent's *unapplied* declaration in - `v1.compiler.infer` `infer_record_lit_structural` (`raw_resolved`). Instantiating it from its - field values needs an applied-type constructor that carries identity, which is this model's - `ParameterBinding` origin. It is left to step 2 rather than added as another spelling-keyed - instantiation site. -3. **Then:** re-key the substitution map by the `TypeParameter` declaration - `(owner, declaration, name)`, so two owners' parameters spelled alike cannot share a key. This - is a separate PR, and the first production reader of #12690's field for this population. - -## 7. Instrument - -- **What it measures.** For each event in which `unify_generics` or `substitute_generics_apply` - binds or substitutes a generic, whether the node carries a `TypeParameter` declaration, and its - two nearest infer callers. It runs over the whole-corpus compile, `gunbc compile --source-root dag - --source-root src/v2 --target dag --repository gunbc --measured-root-demands - tools/whole_corpus_compile_measured_root_demands.json`. -- **Not yet an entry point.** The one measurement taken so far was a local, uncommitted probe on - main `b793732902`. It is a one-off, so its numbers are not quoted in this note or on the row. - Step 2's first task is to make it an instrument: a `gunbc test` label over the same compile, - reporting by constructor. The step-2 decisions rest on its output, not on any figure written - here. diff --git a/docs/plans/dogfood-route-manual-interventions.md b/docs/plans/dogfood-route-manual-interventions.md deleted file mode 100644 index 490651cee3e..00000000000 --- a/docs/plans/dogfood-route-manual-interventions.md +++ /dev/null @@ -1,175 +0,0 @@ -# Dogfood route: the manual interventions, dispositioned - -Status: plan, operator-agreed 2026-10-03. Each functional row below lands under the home it names. `factory-dogfood-route` (gunbc#13077) consumes their receipts, or carries dependency edges where they gate the qualifying route. Its lane coordinates route integration; it does not own every underlying capability. - -## Why this exists - -The first end-to-end attempt on srv2 (attempt `01feb0b65dee1377`, node `shell-dag-live-deploy-restart-tailscale`) did its work: 40 steps on a group-b seat, ending at its planned alignment checkpoint (exit 3). It never reached a PR, because every belt tick after about 06:50 UTC hit its start timeout and was killed. Getting it that far took six kinds of hand intervention. The operator's go/no-go on external customers is their own confidence from dogfooding, and a route that needs a person to keep it moving is not yet that evidence. - -This document records each intervention, what it exposed, what must become automatic, what stays with the operator, and the test that must go red first. - -## Three properties, not one - -Each intervention was first read as "make it idempotent / retryable". That conflates three properties: - -1. **Safe to retry**: repeating an operation cannot duplicate or corrupt its effect. Required everywhere. -2. **Convergent**: desired state and observed state decide Noop, Apply or Refuse. Required everywhere. -3. **Automatically repaired**: the controller chooses and applies the repair with no operator. A policy choice, and today the answer is **no**. - -`gunbc.ensure` `EnsurePolicy` already carries the distinction. `EnsureObserveOnly` refuses a mismatch; `EnsureMayApply` applies an available plan. Convergence therefore does not imply self-healing. It can converge to a safe, visible state and stop there. - -### Operator ruling (2026-10-03): manual resume, for now - -A crash or failure is observed by a person, root-caused, then resumed by hand, until there is confidence in the automated remedy. This is DESIGN §5's factory model: the line stops, and the stop is analysed before restart. What the system owes is that the stop is loud and located, and that the operator's resume is a single retry-safe action. - -### The incident shape every intervention below uses - -```text -normal desired state - -> fault observed -safe incident state - evidence captured - route or work withdrawn where necessary - AwaitingOperatorDisposition - -> operator records a disposition, keyed by incident identity -ResumeRequested | AbandonRequested | ClearRequested | QuarantineRequested - -> ensure -Noop | Apply | Refuse -``` - -Every disposition effect is keyed by its incident identity, so running it twice is a no-op. A crash after the effect but before its receipt re-observes the subject and finds the effect already done. - -The same rule applies to every outward effect on the route: "open the PR" means "ensure logical work item X has exactly one PR". An attempt or candidate revision updates that PR; it never acquires a second PR identity. After uncertain remote success, observe before create. The attempt identity may key the publication operation and its receipt, but not the PR itself. Pushes, comments and R2 writes follow the same rule. - -## The six interventions - -### 1. Stale units cleared by hand, three times - -**Exposed:** exited worker and supervisor units blocked redispatch until a teardown pass ran. With the timer off, they had to be cleared by hand, and the clearing left no record. #13066 records the same follow-up. - -**Automation owed:** -- observe the terminal or lease-expired unit; -- bind it to one incident identity; -- preserve its status, journal, attempt, lease and process evidence; -- stop counting it as live; -- withhold any replacement attempt until a disposition is recorded. - -A lease deadline (`std.temporal_effect` `HeldLease`) establishes `TerminalOrLeaseExpired`. It never by itself kills, deletes or restarts. - -**Stays manual:** the root cause, and the choice of Resume, Abandon, Clear or Quarantine. - -**Home:** the factory attempt lifecycle, consumed by `factory-dogfood-route`. - -**RED first:** kill a worker mid-turn. The unit is reported stale under one incident identity, with evidence preserved, and no replacement spawns. Recording `ClearIncident` twice changes nothing the second time. - -### 2. srv10 Wi-Fi restored by hand - -**Operator ruling:** the Sparks will serve on Wi-Fi for a while, and that is a valid medium. Why the link dropped still needs debugging. - -**Exposed:** a host lost its network and stayed routable. - -**Automation owed:** -- a missing or stale health receipt withdraws the host from routing; -- link evidence is captured before reconnection obscures it: association state, addressing and routing state, relevant service and driver evidence, and timestamps. - -A fresh health reading may say the host is technically available again, while a separate unresolved incident keeps it operator-held. - -**Stays manual:** the diagnosis, and returning the host to service. - -**Home:** fabric host health (`gunbc.fleet_health`) and network incident handling, with an edge into the route. The existing roadmap row "A host serves only on a fresh satisfied health receipt" is the admission half. - -**RED first:** drop Wi-Fi while the serving process stays alive. The host leaves the routable population, the incident evidence survives reconnection, and the host does not return until the operator's disposition permits it. - -### 3. Untracked senders on group-b, paused or killed by hand - -**Exposed:** a second door. Seat admission (`gunbc.serving.turn_admission` `serving_group_admission`) is not the only path onto group-b. - -**Operator ruling:** move the other senders onto seat admission, and add authentication to the Spark deployments through a real auth process, so every caller is forced onto the right path. - -**Authentication alone does not close the door.** A caller with a valid general-purpose credential could still bypass the seat ledger. The forwarding boundary needs both: -- an authenticated caller identity; **and** -- a current serving grant naming the admitted serving launch, the granted route or group, the seat entitlement, the caller or work identity, and an expiry or revocation boundary. - -A practical realization is a short-lived signed request permit minted by successful seat admission. A Spark-side front door verifies it before forwarding to vLLM, and refuses before the engine sees the request. - -**Stays manual:** migrating the existing senders, and deciding which identities may request grants. - -**Home:** managed identity owns the credentials (`credential-lifecycle-revocation`, gunbc#13068); serving and capacity own request admission and grant validation. - -**RED first:** a request with a valid caller credential and no seat grant is refused at the front door, and the refusal is counted. - -### 4. Belt timer started by hand - -**Operator ruling:** event-driven wherever possible, not timer-driven. - -**Automation owed:** -- events invoke the smallest affected obligation directly; -- deadline-bearing obligations schedule a one-shot wake-up; -- a bounded anti-entropy sweep discovers missed events and **queues** them. It never performs the work itself. - -**Stays manual:** only typed refusals and incident dispositions. - -**Home:** factory controller and event delivery. The timer is not a product requirement and does not become a desired-state row. - -**RED first:** with no timer installed, a worker exit causes its capture to run. - -### 5. #13066 deployed to srv2 by hand - -**Exposed:** srv2 runs `cc5bbdcc` plus the #13066 commit, placed by hand. The model exists: `gunbc.live_deploy.desired` declares srv1-live, srv1-lab and srv2-deploy (`srv2_deploy_desired_deployment`) beside `repository_convergence`. What was missing is converging an admitted revision onto the host. - -**Correction to the first reading:** a branch revision is valid when it has been explicitly admitted to the deployment that runs it. "Not main" is not inherently drift. - -**Operator context:** srv1 and srv2 are both used for development today. That use is separate from deployment risk: the srv1 daily-workspace deployment holds ProdRisk because `ProdRoleSelection` selects it, and the srv2 deployment derives TestRisk. - -**Automation owed:** each deployment's explicitly admitted revision is observed and converged into the running deployment, with readiness and member-identity readback. - -**Stays manual:** selecting and admitting the revision for that deployment. - -**Home:** generic live-deploy convergence (`gunbc.live_deploy`). - -**Operator ruling (2026-10-03): converges are triggered manually today, and automated later.** Until they are automated, the manual trigger is one retry-safe action, the converge entry point, never a hand-run sequence of steps. Running it twice against a converged host is a Noop. The deployment hand-placed on srv2 for #13066 is the case this rules out: the admitted revision goes onto the host through the converge, not around it. - -**Which deployment is prod: decided 2026-10-03.** The operator approved the deployment-risk conformance vocabulary. `DeploymentRiskClass = TestRisk | ProdRisk` is derived from one switchable row, `ProdRoleSelection = NoProdRole | ProdRoleHeldBy { deployment }`, and is never inferred from srv1, "live", main or a host name. The row initially holds the srv1 daily-workspace dashboard. Turning prod off is setting that row to `NoProdRole`. A TestRisk deployment that lacks its own `DeploymentExternalBindingSet` refuses rather than borrowing prod's. The migration that names it is a separate lane; this item only consumes it. The belt-tick near miss (a tick ran `git worktree add` into srv1's production tree) is one of the wrong-instance incidents that migration files as a recurring failure mode. - -**RED first:** admit a revision for srv2. Convergence brings the running deployment to it and reads it back. A running revision that differs from the admitted one is reported as drift. - -### 6. Every belt tick times out (unsolved) - -**Exposed:** `gunbc.roadmap_belt_actuate` `belt_run_once` is one monolithic pass that runs these stages in sequence: -1. spawn; -2. teardown; -3. launch; -4. commit; -5. verify; -6. review; -7. publish; -8. write the served observation. - -Any slow stage blocks every stage behind it. The module's own note on `belt_observe_once_cli` says that when the monolithic pass is too slow to reach its observation write, the page freezes at the last completed tick's bytes. After about 06:50 UTC, no tick completed, so capture, verify and publish never ran for the attempt. - -**Not the fix:** stage checkpoints that make the monolithic tick resumable. That preserves the structure that should go. - -**Automation owed:** expensive work leaves the tick. Each durable obligation gets its own event-driven ensure and its own effect identity: -- a worker-terminal event triggers capture and submission classification; -- a captured `SubmissionIsCandidate` triggers authoritative verification; -- a passing verification receipt triggers review and goal audit, independently; -- an approved review plus an approved goal audit admits publication; -- a yielded, blocked or needs-context capture stays visible but does not enter verification. - -The anti-entropy pass only discovers and queues a bounded number of outstanding obligations. - -**Stays manual:** intervention only on a typed refusal. - -**Home:** belt demotion and the factory lifecycle. - -**RED first:** a verify that runs long does not delay publish of an unrelated attempt, or the served observation. - -## Order - -The two that matter most for the dogfood clock: -- **6 (belt):** it is why the first attempt has no PR. -- **3 (second door):** untracked traffic blocks admission. - -Next, 1 (incident record for stale units) and 5 (admitted-revision convergence) turn the remaining hand steps into single recorded actions. Items 2 and 4 follow. - -None of this counts toward the dogfood window by itself. The window starts with the first qualifying run under `factory-dogfood-route` that also carries the joined evidence receipt from `service-evidence-capture` (gunbc#13068). diff --git a/docs/plans/dsv41-cut-d-redesign.md b/docs/plans/dsv41-cut-d-redesign.md deleted file mode 100644 index 76f468529d9..00000000000 --- a/docs/plans/dsv41-cut-d-redesign.md +++ /dev/null @@ -1,721 +0,0 @@ -# DSV41 Cut D — redesign against the observed Group A, not the assumed one - -Cut D of DSV41-ENGRAM-RUNTIME-0 was specified as a bounded replacement transaction against a -live V4 Flash incumbent: drain its offer, stop it, launch V4.1 TP4, probe, then **promote or -restore V4 Flash**, with promotion gated on *rollback demonstrated* — an incumbent request -served again after the experiment. - -**No incumbent was observed**, twice, 14 hours apart. Every terminal in the original cut that -names V4 Flash therefore has no subject *on those readings*, so the cut cannot be executed as -written, and executing a lightly-edited version would silently drop safety properties the -original bought. - -**That absence is a dated observation, not a property of Group A** — D0 re-establishes it under -the transaction's own claim, because something can become live between a reading and a stop. -This document records what was observed, what follows, and the cut that replaces the original. - -> ## ⛔ THIS CUT CANNOT BE EXECUTED TODAY -> -> It describes a transaction that depends on **suspending Group A's pair-serving authority** — -> the realization disabled while all four hosts stay held for the named successor, build and -> probe admission still fenced, under a bounded lease with cleanup. -> -> **No carrier provides that.** The model can express *withdrawal* only, as -> `spark_pair_serving_groups` shrinking — and for Group A, which is that roster's sole member, -> shrinking means **emptying**, which reclassifies the hosts the cut needs and hands two of them -> to any lane that asks. Emptying the roster is not a suspension substitute. -> -> So this is a **design with a named prerequisite**, not an instruction sheet. Modelling -> `PairServingGroupAuthority` (§2) is the work that has to land before D0 can take its first -> step. Everything below assumes that operation exists. - -**HOW TO READ THIS DOCUMENT.** It went through nine review rounds and several of its own -premises were rejected along the way. Rejected premises are marked **⚠ REJECTED PREMISE** and -kept only so they are not re-derived; everything else is operative. Nothing here should require -inferring that a later section supersedes an earlier one. - -## What was observed - -Read directly from the four Group A ranks (`spark-a3ee`, `spark-3bd5`, `spark-c2b1`, -`spark-ac79`) over SSH, twice, ~14 hours apart: - -| fact | reading | -|---|---| -| serving process | none — no `vllm`/serving systemd unit running | -| OpenAI endpoint on the serving port | no answer | -| occupant | one `gunbc-spark-pair` container, image `gunbc-spark-pair-runtime:ray-2.58.0`, created `2026-09-14T00:15:35Z`, up >2 days | -| memory held | 107–109 GiB of 121 GiB per rank; `nvidia-smi` attributes ~100.7 GiB to `ray::RayWorkerProc.run` | -| top process RSS | 2.6 GiB | -| free disk | 2.6–3.1 TB of 3.7 TB per rank | - -Two details make that table read correctly rather than look contradictory. `nvidia-smi ---query-gpu=memory.total` returns `[N/A]` on a GB10 because there is no separate accelerator -pool — the figure it attributes to the Ray worker is a claim on the same unified RAM `free` -reports. And the top process RSS being 2.6 GiB while 108 GiB is used is not an inconsistency: -CUDA device-side allocations on a unified pool do not appear in RSS. A process listing -therefore makes these nodes look idle while they are 89% full. - -The operator's statement that Group A "is not being used by anyone" is correct about -**traffic** and not about **occupancy**. - -### These readings do not contradict the fold's input, and the document must say so - -`gunbc.spark.pair_serving_observed` `group_rank_free_memory_observed` — the reading the fit -fold consumes — is qualified **"with the engine idle"**. The table above is a reading of an -**occupied** rank. They are answers to different questions and neither supersedes the other: - -| question | condition | who answers it | -|---|---|---| -| would V4.1 at TP4 fit if these hosts were cleared? | engine **idle** | `group_rank_free_memory_observed`, consumed by `candidate_component_budget` | -| are these hosts clear right now? | **as found** | the table above, and D0 | - -So the occupancy reading is not evidence that the fold's input is stale, and the fit verdict -stands on its own reading. **Presenting both without reconciling them would have been the -defect** — a reader comparing 116.9 GiB free against 107–109 GiB used would conclude one of -them is wrong, when what actually differs is whether anything was running at the time. - -### Why these readings are prose here, stated as a divergence rather than left silent - -§6 says name the instrument, never transcribe its output — and `pair_serving_observed` is the -modeled home, carrying `PairServingObservationProvenance` with `observed_on` / `observed_by` / -`sources`, and free memory as a `std.measure` interval with an `instrument` and a -`precision_caveat` rather than a hand-typed figure. A `FabricGroupA` occupancy row there is the -conforming move, and these figures are not in it. - -**This is a §3b divergence and the reason is that D0 is the step that produces that row.** The -cut below exists precisely to take the claim, reconcile the occupancy against the declared -realization, and emit an entry-state receipt. Hand-authoring the row now would author by hand -the artifact the cut produces by execution — and it would do so from a one-off SSH procedure -rather than through the instrument the model would name, which is the weaker evidence of the -two. - -So these figures are carried here as **what motivated the redesign**, not as the corpus's -record of Group A's occupancy. The corpus's record is `EntryStateReceipt`, D0 produces it, and -if this program stalls before D0 runs then the corpus correctly continues to have no modeled -observation of Group A being occupied — which is honest, because nothing executing has made -one. - -## What follows, stated as consequences rather than edits - -### 1. On the incumbent-absent branch there is no drain, and calling a reclaim a drain would claim a property we do not have - -**Everything in this section is scoped to `IncumbentAbsent`.** If D0 finds a live incumbent, the -original drain-and-preserve transaction applies instead and none of what follows does. - -The original sequence was *withdraw the incumbent offer → stop the incumbent*. A drain exists -to preserve continuity for live traffic. There is no live traffic and no offer being served, -so what the transaction actually performs is a **reclaim** of an occupying container. - -The distinction is not cosmetic. A drain's safety property is "no in-flight request was lost"; -a reclaim has no such property and needs none. Keeping the word would assert a guarantee the -step does not provide — §4b rung honesty applied to a transaction step. - -**But dropping the drain obligation does not drop the QUIESCENCE obligation, and an earlier -draft read as though it did.** No answering front door means no demonstrated serving traffic. -It does not mean the occupant is safe to stop: those Ray workers hold ~100 GiB each, and what -they are holding it for is unread. Before the reclaim, D0 must establish or revoke — never -assume — Ray jobs, actors and placement groups; serving offers and new-admission eligibility; -acquired or stranded seats; host claims, leases and ownership; and any actuator that could -recreate the occupant after it is stopped. So the operation is: - -``` -fence new acquisition -→ establish or revoke active work, claims and seats -→ stop the occupying realization -→ verify resource release -``` - -A reclaim with no in-flight-request obligation still has a fencing one. And note that -enrollment says only that Group A is a route that *may be asked* — it does not say the route -is live, and availability is separately three-valued (Available / Unavailable / **Unread**). -Unread is where Group A sits, and unread is not empty. - -### 2. The occupant is DECLARED, not residue — and that changes the rollback question entirely - -An earlier draft of this document rejected "restore the occupant" on the grounds that *we hold -no declaration that produces that Ray container, so restoring it would mean reconstructing from -an observation*. **That sentence is false**, and the correction matters more than the error. - -The corpus declares exactly what was observed: - -- `extdeps.docker.images.sparkrun_vllm_ds4_gb10` `gunbc_spark_pair_runtime_image_tag` is - `gunbc-spark-pair-runtime:ray-2.58.0` — the observed image, with its pinned base and its - exact `ray[default]==2.58.0` requirement; -- `gunbc.spark.pair_serving_realization` `spark_pair_container_name` is `gunbc-spark-pair` — - the observed container, with head and worker units and a one-container-per-host realization; -- `gunbc.spark.pair_serving_desired` `spark_pair_serving_groups` still contains - **`FabricGroupA`**, and `spark_pair_realization` maps that desired set into realizations. - -So the ranks are not carrying residue. They are carrying an instance with **declared lineage** — -image tag, container name, desired roster, realization and apply path all join. - -**But "converged" is stronger than the evidence, and an earlier draft claimed it.** This -document's own observation table says no vLLM process, no serving unit running, no answer on -the enrolled endpoint — while the declared realization includes a head unit that starts Ray and -then vLLM, worker units under supervision, a head engine with `Restart=always`, and an apply -that treats an inactive unit as grounds for restart and finally requires `systemctl is-active`. -An instance matching that declaration would be answering. What the evidence supports is: - -``` -declared producer and authority ESTABLISHED -full realization presently converged NOT ESTABLISHED -observed instance apparently DRIFTED -``` - -So on the facts written here, the expected D0 answer is **`DeclaredOccupantDrifted`**, not -`DeclaredOccupantObserved`, unless D0 reads the rendered units, container specs, rank membership -and serving state and proves otherwise. That does not weaken this section's conclusion — the old -authority can still recreate its desired realization, which is the whole hazard — it only stops -container lineage being used as proof that the realization currently converges. - -**The consequence is a design break, not a wording fix.** If Group A remains in the desired set, -an existing apply path can recreate exactly what D1 would call residue. "Return to a released -empty state" is then *not a converged state*: a converger reasserting the desired realization -would undo D1's terminal, and nothing in the transaction would notice. A terminal that another -authority can silently reverse is not a terminal. - -So the question is not *may we restore the occupant* — it is **under whose authority do the -hosts sit while V4.1 is on them**, and the plan must answer it explicitly: - -- **suspend Group A's pair-serving desired authority**, and record that suspension as part of - the authorization, so the hosts are never in a position where two authorities both believe - they own them. *Suspend*, not *withdraw*: different operations, and only withdrawal is - expressible today. - -**THE SUSPENSION'S LIFETIME IS ONE RULE, because an earlier draft gave three and they were not -interchangeable** — "for the experiment's duration", "record that withdrawal", and an -undifferentiated "claims released" each permitted a different implementation: - -``` -atomic transfer into suspension -→ the suspension AND the exact four-host reservation persist through - QuiescentReservedBaseline — they do NOT end when D1 ends -→ they end only when one of: - D2 promotion assumes the authority - the prior authority is explicitly restored - FleetReleasedBaseline is explicitly selected - -D1 cleanup RELEASES rank and process claims, offers, seats, live allocations -D1 cleanup RETAINS the four-host reservation, under QuiescentReservedBaseline -``` - -"Claims released" is not one category: the execution claims must end and the host reservation -must not, and a cut that says only "claims released" leaves an implementer to guess which. - -That is a real decision with its own consequences — it means Group A is deliberately not -pair-serving while this runs — and it belongs in the authorization, not in a footnote. It also -**cannot be performed today**: the operation it names does not exist, which is the gap this -section ends on. - -**THE CORPUS HAS FACED THIS HAZARD BEFORE, THOUGH IT ANSWERED A DIFFERENT QUESTION.** -`gunbc.serving.serving_enrollment` records that one list used to answer two questions that move -for different reasons — *whose hosts this repository claims and converges*, and *which endpoints -a turn may be offered* — and that they stopped coinciding on 2026-09-08. The reason given is the -hazard this section is about: Group B's head is a host another authority also names, so leaving -B in the convergence roster left **two authorities claiming one machine, and the first -pair-serving convergence to run would have acted on a host it believed idle.** The operator -authorised removing the claim, and the module was split so the two questions could move apart. - -That split is real and is observable in the current rosters — it is why a group can be enrolled -for serving without being claimed for convergence: - -``` -serving_enrolled_groups = [FabricGroupA, FabricGroupB] ← who may be ASKED -spark_pair_serving_groups = [FabricGroupA] ← whose hosts we CLAIM and converge -``` - -**⚠ REJECTED PREMISE, kept only so it is not re-derived.** An earlier draft concluded from this -that Group B is "precisely the state this cut needs for Group A", that withdrawal therefore -needs "no new mechanism", and that the precedent could simply be applied a second time. **That -is wrong and the next subsection says why.** Group B's withdrawal was a removal from a roster -that still had another member; Group A's would empty it, and Group A is the row that speaks for -srv7 and srv8 whereas Group B's hosts were claimed by another authority that wanted them. The -precedent establishes that the enrolled/claimed split exists and that a claim can be withdrawn — -it does **not** establish that this withdrawal is safe. - -**Does the withdrawal also remove the host admissibility or launch authority D1 and D2 need?** -**Yes — and an earlier version of this section answered that wrongly.** It traced admissibility -to `gunbc.spark.host_commitment` `spark_serving_admissible_hosts`, found that removing a claim -*loosens* commitment, and concluded the withdrawal "revokes a competing claim, which is the -objective." That conclusion is false, because it asked who is let in and never asked who is let -out. - -`host_commitment` derives every `MemberOfClaimedServingGroup` cause directly from -`spark_pair_serving_groups`, and uses those causes to decide **which serving subject owns each -host**. The cell-role roster assigns `SparkServingCell` to **srv5 and srv6 only** — the Group A -claim is what speaks for srv7 and srv8. So a literal withdrawal produces: - -| hosts | after withdrawal | -|---|---| -| srv5, srv6 | cell role remains, Group A ownership cause gone → serving role held by **no serving subject** | -| srv7, srv8 | no cell-role row, no ownership cause → **uncommitted**, admissible to any serving subject, and eligible as "unplaced" effect hosts | - -And V4.1's own candidate field derives its host population through `spark_serving_admissible_hosts` -for the **Group A pair-unit subject** — `PairServingUnitOn { FabricGroupA }`, deliberately -unchanged across the V4→V4.1 transition. **So the TP4 subject this program exists to run loses -two of its four hosts**, while srv7 and srv8 become available to unrelated lanes: the vLLM -source-build and runtime-image-probe roots both authorize effects through `admit_unplaced_host`, -and the latter pulls an image and starts a container. - -The remedy would have destroyed the authority relation it exists to protect, and the plan's -prose-level "exclusive claim" would have disagreed with the production authority that build and -probe lanes actually consult. Loosening is not neutral when your own exclusivity is what is -being loosened. - -**WITHDRAW AND SUSPEND ARE NOT INTERCHANGEABLE, and this document may not use them as though -they were.** Suspension is the right operation here. **No such modeled operation exists yet** — -that is the gap this cut must close before it can run, and it is a modeling obligation rather -than a sequencing detail. It needs a typed authority state whose projections differ per -consumer: - -``` -PairServingGroupAuthority - = PairServingActive { group, exact_realization } - | SuspensionPendingReconciliation { group, transaction, lease } // both actuators disabled - | SuspendedForAuthorizedSuccessor { group, authorization, - exact_candidate_realization, - lease, cleanup } - | FencedRefusal { group, cause, disposition_authority } // foreign / unread - | ReleasedToFleet { group, release_receipt } -``` - -**Every state the rest of this document requires is in that list.** -`SuspensionPendingReconciliation` and `FencedRefusal` are authority states, not diagnostic -labels — D0 holds the fleet in the first while both actuators are disabled, and the second is -the long-lived terminal for foreign or unread occupancy. A coproduct without them cannot express -the state machine D0 declares total. - -And the realization must survive in the genealogy: `PairServingActive { group }` alone cannot -distinguish V4 from V4.1, because both deliberately share `PairServingUnitOn { FabricGroupA }`. -A successor is discriminated by its exact authorization and candidate realization, never by -subject identity. - -**THE LEASE NEEDS AN EXPIRY TRANSITION, and the lifetime rule above does not give it one.** The -three ways it ends — D2 assumes the authority, the prior authority is restored, -`FleetReleasedBaseline` is selected — are all deliberate acts. A bounded lease also ends by -*running out*, and each silent answer is unsafe: expiry releasing the reservation lets another -lane take a host with no cleanup or readback; expiry restoring the old authority may restart -convergence over drifted, foreign or unread state; expiry changing nothing makes "bounded" a -word rather than a property. So: - -``` -authorization expires -→ launch, offer and seat authority are GONE -→ cleanup and fencing authority REMAIN -→ release or restoration only after the required receipt -``` - -Expiry removes the right to *start* things and keeps the right to *finish* them, which is the -only arrangement where a lease can expire mid-transaction without stranding the fleet. - -with each consumer told explicitly what it sees: - -| consumer | Active | Suspended | -|---|---|---| -| pair realization / apply | acts | **does not act** | -| host commitment | all four hosts owned | all four hosts **still owned**, by the named successor | -| build / probe host admission | placed | **still placed**, never unclaimed | -| serving enrollment | unchanged | unchanged | -| capacity standing | ordinary result | a **typed suspended** result, never an empty population | - -**One positive result from the trace:** serving enrollment is genuinely independent of pair -desired state. Group A stays enrolled as a route and the harness still probes it before -offering work, so suspending the old realization does not delete the front door. - -### The withdrawal EMPTIES the roster, and that is not a local toggle - -`spark_pair_serving_groups` is `[FabricGroupA]`. **Group A is its only member**, so withdrawing -it does not remove one entry — it leaves the list empty. Every fold over that roster then runs -over an empty domain, and the repository already carries this class by name: -`gunbc.recurring_failure_mode.predicate_vacuously_true_on_an_empty_domain`. - -An `all(...)` over an empty list is **true**. So a check of the form *every claimed serving -group satisfies X* stops being evidence the moment the roster empties — it passes, loudly and -greenly, because there is nothing left to fail it. That is the opposite of what a safety check -should do when its subject disappears, and it would arrive exactly when the fleet is least -ordinary. - -**The obligation lives on the roster, and this document deliberately does not restate it.** §6 -says the mark on the carrier is the authority; an earlier draft then restated the obligation -here in its own words, which drifted from the carrier's as the carrier was corrected — the two -disagreed on whether it covers *folds* or *every direct and transitive consumer*, and on whether -the row is *ownership* or a *commitment input*. Restating an authority is how you end up -maintaining two of them. - -So the obligation is the annotation above `spark_pair_serving_groups` in -`gunbc.spark.pair_serving_desired`, and it is authoritative over anything this document says -about it. Read it there. - -The roster also reaches **build and probe admission** and **capacity**, not only convergence — -which is precisely why the accepted design is suspension and **not** a roster edit: a suspension -that emptied this row would reclassify the hosts it is trying to hold. The authorization names -the authority it suspends and the reservation it retains; it empties nothing. - -**THREE SUBJECTS, THREE NAMES.** An earlier draft used one word — "baseline" — for the state -D0 records and the state D1 returns to. Those are mutually exclusive, and the ambiguity sat in -the one sentence that tells a rollout worker where to leave the hosts. - -| subject | what it is | who produces it | is it a return target | -|---|---|---|---| -| **`EntryStateReceipt`** | the ranks as found — occupant present, ~108 GiB held | D0 | **no** | -| **`ReleasedBaselineSpec`** | the *desired* post-experiment state: no ranks, no engine, no serving offer, no seat, no occupying container, no live device allocation. **Whether a host reservation remains is the choice made below** — for the ordinary D1→D2 path it does | declared before D1 runs | it is the target | -| **`ReleasedBaselineReceipt`** | a readback establishing that the spec holds | D1's terminal | — | - -A spec and its readback are two facts, and collapsing them is how "we returned to baseline" -becomes an assertion instead of a reading. - -**RESERVED OR FREE — the spec must choose, and an earlier draft claimed the guarantees of -both.** It defined the released state as having *no host claim* while also suspending the old -authority only "for the experiment's duration". That leaves three readings, and two of them are -broken: if suspension ends when D1 ends, the old realization can be reapplied immediately and -the released terminal is invalidated; if suspension persists with no reservation, srv7 and srv8 -become effect targets and srv5 and srv6 are held by nobody. Only the third is coherent: - -``` -QuiescentReservedBaseline ← the target when D2 is expected to follow - no rank processes, no engine, no offer, no seat, no live device allocation - immutable verified artifacts may remain - the exact four-host reservation REMAINS -``` - -That holds no runtime live across the authorization boundary — only the right to use the four -hosts, so an unrelated build or probe cannot occupy them before D2. If the intent is instead to -give the hosts back to the fleet, that is a **different** terminal and must be named as one: -`FleetReleasedBaseline`, where D2 must reacquire the hosts and may not assume the same four -remain available. - -**What may remain.** The released state releases *processes, seats and live memory* — not bytes -at rest, and under `QuiescentReservedBaseline` not the reservation either. The exact runtime image and the content-addressed row stores may stay on -local storage. Rematerialising hundreds of gigabytes to satisfy a definition would be redundant -work, and immutable content-addressed artifacts carry no live runtime across the authorization -boundary. The spec says so explicitly rather than leaving a later reader to decide whether a -staged row store violates "released". - -The **released state** remains the return target rather than the entry state, but now for a -stated reason: the entry state is an instance of a desired authority we are suspending, so returning to it means *restoring that authority*, which is a separate decision -from ending the experiment. D1 ends the experiment; restoring pair-serving authority is its own -step with its own evidence. - -### 3. On the same branch the risk profile inverts, and that makes P1 Cut 3 more important rather than less - -**Also scoped to `IncumbentAbsent`.** The original cut's danger was **breaking a live service**. -On this branch that danger is absent — not abolished, and it returns the moment D0 answers -`IncumbentLive`. The remaining -danger is **not being able to give the hosts back** — a partial or stalled launch leaving -ranks up, seats held, and 121 GiB per node unavailable, with no incumbent whose restoration -would incidentally clear it. - -The original brief said P1 Cut 3 "should block" ordinary production promotion. Against an -incumbent, that hedge was defensible because restoring the incumbent was itself a cleanup path. -Without one it is not: **promotion must refuse.** But the condition is CONJUNCTIVE, and an -earlier draft joined two different problems with `or`: - -``` -(P1 Cut 3, or an exact seat-lifecycle equivalent) -AND -(a demonstrated rank / process / offer / claim cleanup path to ReleasedBaselineSpec) -``` - -P1 Cut 2 detects an exporter still live over an engine that has stopped advancing and -explicitly leaves held-seat invalidation to Cut 3. That is Cut 3's subject: **seat and -incarnation invalidation.** It is not the same fact as killing partial ranks, withdrawing -offers, releasing host claims, stopping containers and returning unified memory — those are -host and runtime cleanup. A single realization may discharge both, but only if its evidence -actually proves both, and writing `or` invites treating either one as sufficient. - -### 4. The serving route is declared and unfulfilled, and the experiment must not paper over it - -`gunbc.serving.serving_enrollment` enrolls `FabricGroupA` — `serving_enrolled_groups` carries -it, with an endpoint, a port and a turn ceiling. The model says Group A is a serving route. -Nothing answers there. - -So *one bounded request through the normal route* is, today, unachievable for **any** model -rather than specifically for V4.1. A cut that lists it as a terminal without saying so would be -specifying an outcome nobody can currently produce, and the first lane to attempt it would -discover the gap under a live transaction. Establishing that the enrolled route actually -carries a request is its own precondition, and it is stated as one below. - -## The replacement cut - -### D0 — take the claim, reconcile the occupancy, then record the ENTRY state - -**D0 performs an ATOMIC AUTHORITY TRANSFER, not an acquire-then-withdraw sequence.** An earlier -draft said D0 takes the exclusive claim first *and* that the hosts must never be owned by two -authorities. **Those cannot both hold without a transfer operation**, and writing both was a -contradiction rather than a plan: - -``` -acquire experiment claim, then withdraw old → OVERLAP: two authorities own the hosts -withdraw old, then acquire experiment claim → GAP: build/probe lanes may take them -atomic transfer → one active owner throughout -``` - -The gap arm is not theoretical: srv7 and srv8 become `admit_unplaced_host` targets the moment -their ownership cause disappears. So the transfer is the operation, and it is part of the -suspension gap named in §2 — there is no modeled transfer today. - -**D0 still brackets its readings with the claim it holds.** "No incumbent was -observed" is a dated observation, not a property of Group A — the two SSH readings above are -14 hours apart and say nothing about the interval between the last one and the stop. Without a -claim held across that interval, something can become live between observing and reclaiming, -and the transaction would proceed on a stale premise. That is the same defect this whole -document exists to correct, one level down. - -**THE QUESTIONS ARE ORDERED, because an earlier draft's branches overlapped on the load-bearing -state.** It asked occupancy reconciliation and incumbent presence as if independent — but this -document establishes that a *converged* declared realization would be answering, so -`DeclaredOccupantObserved { converged }` and "a live serving incumbent" are the **same state**, -sent by one branch into the reclaim redesign and by the other into the original transaction. The -incumbent question is asked **first**: - -``` -under SuspensionPendingReconciliation, both actuators disabled: - - incumbent live - → hand off to the original drain-and-preserve transaction, - or a typed fenced refusal - incumbent absent - → reconcile the remaining occupancy (below) -``` - -**Then** the reconciliation question. Not *what is here*, but: - -``` -does the observed container / image / unit population reconcile -with the declared pair-serving realization? -``` - -with four answers, each leading somewhere different: - -| answer | meaning | disposition | -|---|---|---| -| `DeclaredOccupantObserved` | declared, and shown converged **without answering** — a narrow case, since a converged realization would ordinarily answer | `SuspensionPendingReconciliation → SuspendedForAuthorizedSuccessor` | -| `DeclaredOccupantDrifted` | declared lineage, convergence not established — **the expected answer on the readings above** | `SuspensionPendingReconciliation → SuspendedForAuthorizedSuccessor`, drift recorded; the drifted state is not a target | -| `ForeignOccupantObserved` | something we do not declare | `→ FencedRefusal` — not ours to reclaim; needs operator disposition | -| `OccupancyUnread` | the question could not be answered | `→ FencedRefusal` — unread is not empty | - -**EVERY ARM NEEDS AN AUTHORITY-STATE TERMINAL, including the refusing ones.** The transfer -happens before the readings — that is what closes the overlap and gap arms — so a refusal fires -*after* the authority has already moved, and an earlier draft said nothing about what becomes of -it. Neither silent answer is acceptable: restoring `PairServingActive` may reactivate -convergence over a foreign or unread occupant, and leaving `SuspendedForAuthorizedSuccessor` -standing means a **refused** D0 has permanently changed a long-lived authority. So the transfer -lands in an intermediate state that inspection happens inside: - -``` -PairServingActive -→ SuspensionPendingReconciliation { transaction, lease, - old actuator disabled, - successor actuator disabled } -→ read and reconcile, then: - SuspendedForAuthorizedSuccessor declared eligible branch - | PairServingActive only when restoration is shown safe - | FencedRefusal foreign or unread — needs operator disposition -``` - -A versioned compare-and-swap protocol with the same dispositions would serve equally. The -requirement is that **no D0 answer leaves the authority state undefined**, and that both -actuators are disabled while the answer is being determined. - -And it branches on the incumbent rather than assuming its absence: - -``` -live serving incumbent observed → the original drain/preserve path, or refuse this one -no incumbent + quiescent declared occupant → the reclaim path below -foreign, active, or unread occupancy → refuse -``` - -Only then does it record the entry state per rank: occupant identity and creation time, memory -used and free, absence of a serving process, absence of an answer on the enrolled endpoint, and -free disk. That receipt is evidence of **what changed** and forensic record. It is not the -rollback target. - -**D0'S TERMINAL IS TWO SHAPES, NOT ONE, because an earlier draft required a complete receipt on -every arm — including the arm where the instrument answered "unread".** If occupancy could not -be read, D0 cannot produce the entry-state facts an `EntryStateReceipt` is defined to contain, -and demanding one anyway would force either a fabricated receipt or an unreachable terminal: - -``` -D0Eligible { - entry_state_receipt : EntryStateReceipt, // complete, all four ranks - authority : SuspendedForAuthorizedSuccessor, -} - -D0Refused { - partial_readings : whatever was read, - cause : foreign | unread | incumbent-live, - authority : FencedRefusal, -} -``` - -Equivalently, `EntryStateReceipt` could itself be total, with per-rank unread arms carrying their -causes. What must not happen is a complete positive receipt being required after the instrument -has already said it could not read. - -Terminal: the authority is in exactly one of `SuspendedForAuthorizedSuccessor` or -`FencedRefusal`, the corresponding receipt exists, and no host state was altered. - -### D1 — reclaim, bounded experiment, mandatory return to the RELEASED state - -Authorization is exact and time-bounded over: candidate key, the four host identities, the -**produced** runtime image digest, model source identity, the four row-store identities, the -TP4 profile, the selected Engram route, the D0 entry-state receipt, and the cleanup path of §3. -Candidate identity is not authorization. - -``` -claim the four ranks -→ reclaim the occupant, read back that its memory is released -→ re-verify the staged artifacts against their declared identities -→ launch V4.1 TP4 -→ establish all-rank agreement -→ complete model load -→ complete graph/runtime initialisation -→ answer semantic and differential probes -→ ONE NORMAL ROUTED REQUEST, while the candidate is still live -→ RETURN TO THE BASELINE THE AUTHORIZATION NAMED, unconditionally - (QuiescentReservedBaseline on the ordinary D1→D2 path) -→ read back that baseline's receipt -``` - -D1 **always** ends at the released state — not at the entry state, which it deliberately does -not restore. It does not promote. - -**AND THE IDEMPOTENCE CLAIM AN EARLIER DRAFT MADE HERE WAS FALSE.** It said a rerun of D1 -mutates nothing. A rerun of D1 cannot mutate nothing: it reclaims hosts, launches V4.1, -initialises it, routes a request and tears it down. **The experiment is REPEATABLE; the cleanup -converger is IDEMPOTENT**, and those are different properties that one sentence was conflating. -What is actually checkable: - -``` -run the D1 experiment -→ converge the baseline (QuiescentReservedBaseline or FleetReleasedBaseline) -→ read the baseline receipt -→ run THAT CONVERGER again -→ require no mutation -``` - -The no-mutation property belongs to the converger and is asserted against it. Attaching it to -the experiment made the receipt unfalsifiable, because no honest rerun could have satisfied it. - -Weight loading is not success. Acceptance begins after runtime initialisation and one valid -semantic completion. An HTTP 200 discharges nothing. - -**The normal routed request happens INSIDE this window, and an earlier draft had it outside.** -The end-to-end join — router request, selected offer, authorization, four agreeing ranks, -produced completion — binds the exact realization D1 created. Deferring it past the teardown -would mean relaunching V4.1 to perform it, which is a second fleet-mutating experiment, or -performing it against a different model, which establishes nothing about this candidate's -offer and rank wiring. The original Cut D placed it correctly and this redesign keeps that -ordering. - -**The cleanup closure D2 requires is needed HERE too**, not only at promotion: once D1 -acquires a seat to serve that request, any failure after acquisition can strand exactly the -seat P1 Cut 3 exists to invalidate. - -Every stage capable of preventing a later observation carries a terminal disposition: ranks -stopped, rank and process claims released, offers and seats released, live allocations released, -**the host reservation retained or released exactly as the named baseline requires**, that -baseline read back — or a typed refusal naming the missing cleanup evidence. A failure arm that cannot reach it is the one outcome that must -stop the line loudly, because it is the risk this cut actually carries. - -### D1a — the route reaches a process, which is the only model-independent half - -An earlier draft claimed the routed request was separable from V4.1 and then wrote a terminal -requiring *four agreeing ranks and a produced completion* — which no model-independent step can -produce. **That terminal was not separable and the claim of separability was wrong.** The two -halves come apart like this: - -**Model-independent, but NOT free, and an earlier draft called it cheap.** Whether the enrolled -route reaches a listening process is a fact about routing rather than about V4.1, and it can be -established against any responder. But **binding a responder to the enrolled endpoint means -starting a process on Group A's declared address and port** — that is fleet mutation, on the -hosts this entire cut exists to fence. It cannot happen before the authority transfer, and it -owes a teardown like any other launch: - -``` -after D0 reaches SuspendedForAuthorizedSuccessor -→ launch a bounded endpoint responder -→ issue the enrolled-route reachability request -→ stop the responder -→ re-establish QuiescentReservedBaseline -``` - -Its output is narrow and should be named so nobody reads it as more: -**`EnrolledEndpointReachabilityReceipt`** — it establishes router-to-endpoint transport and -listener reachability, and **nothing else**. Not offer selection, not seat acquisition, not rank -agreement, not model advertisement, not that any candidate serves. D1 and D2 still owe every one -of those joins. - -**Model-dependent, and therefore NOT separable.** The end-to-end join — router request, to -selected offer, to authorization, to four agreeing ranks, to a produced completion — cannot -exist without a TP4 model actually loaded. It belongs inside D1 (as the experiment's semantic -terminal) and inside D2 (as the promotion terminal), and it is stated in both. Extracting it -into a precondition would be specifying a step nobody can perform alone. - -### D2 — production promotion, separately authorized - -A new authorization over the already-proven realization. Promotion **refuses** unless: - -- the candidate is realized, not merely keyed — produced image digest, applied patch - population, four row-store identities; -- row-store faithfulness is established over the **published** population, not a fixture; -- D1 completed and its `ReleasedBaselineReceipt` was read back; -- D1a established that the enrolled route reaches a process, and D1 produced the end-to-end join while the candidate was live; -- **P1 Cut 3 (or an exact seat-lifecycle equivalent) AND a demonstrated cleanup path to `ReleasedBaselineSpec`** — two facts, not one; -- **and the no-mutation rerun is a TERMINAL, not a precondition.** An earlier draft listed - "a second convergence run mutates nothing" among the conditions promotion refuses without — - but a no-mutation *promotion* rerun cannot precede the first promotion. The order is: - -``` -D2 preconditions satisfied -→ apply the production promotion -→ one exact normal routed request succeeds -→ apply the production desired state AGAIN -→ require no mutation -``` - -Capacity calibration is a follow-up. Record steady resident bytes, cache retention, local read -bytes, lookup hit/miss population, prefill and decode rates, TTFT and ITL — and do not turn any -of them into policy before correctness converges. - -## What this redesign does not change - -The identity discipline is untouched: checkpoint identity is not runtime identity, route B -versus D is not a different binary, TP4 is an execution-profile fact, patch order is semantic, -and build evidence does not transfer across a patch, tokenizer, checkpoint, row-store, topology -or route change. The published runtime's scoped `NoFeasibleRealization` stays true about the -published runtime; a file-backed runtime is a new candidate subject and does not weaken it. - -## Why the program is still worth running - -**Named, not transcribed.** An earlier draft re-derived the fit in prose and inverted it. The -correction is not a better number — it is that this document should not carry the number at -all. - -The authority is `gunbc.spark.serving_deployment_selection` `candidate_component_budget`. The -reading it consumes is `gunbc.spark.pair_serving_observed` `group_rank_free_memory_observed`. -Its per-rank verdict at 8 and at 4 ranks, the component breakdown behind the budget, and the -corroborating figure from the rollout lane's independent instrument are all carried in the -annotation beside `test.claim.spark.serving_deployment_selection_witness_test`'s selecting -witness. Read them there; they move when the fold's inputs move, and a copy here would not. - -What this document needs to state is the **shape** of that result, because the cut depends on -it: **route A at TP4 is excluded by a measured shortfall that exceeds the reading's own -precision** — an established exclusion, not a near miss. - -Three things invert that answer if taken from outside the fold, and all three are mistakes this -document made before it was corrected: - -- **the denominator is the observed idle-free reading, not the device total.** - `candidate_component_budget` says so in as many words, and refuses rather than substituting - the total when a reading is missing; -- **the budget is what remains after the artifact's other three components, not one.** The - vision tower with its aligner and the DSpark draft model are the difference between a - two-component sum showing headroom and the fold showing a shortfall; -- **the shortfall must clear the reading's precision.** The runtime prints one decimal of GiB, - so the free figure is an interval, and a verdict drawn against it is established only if it - exceeds that width. The fold's does. - -**That is why Cut B exists.** Not because the published runtime nearly fits — it does not fit, -by a margin the measurement can carry — but because moving Engram off the resident budget is -the only change that alters the term the fold is short on. diff --git a/docs/plans/edge-label-coproduct/README.md b/docs/plans/edge-label-coproduct/README.md deleted file mode 100644 index b05f9f5fd4f..00000000000 --- a/docs/plans/edge-label-coproduct/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Typed EdgeLabel coproduct — model proposal (step 1 of 2) - -Status: model approved and landed (gunbc#12473). Step 2, the cut, is the follow-up PR from quiet-koi-814; its scope was ruled by gentle-koi-724 (see "Step 2 as landed" below). -Climb on record: `gunbc.recurring_failure_mode` `behavior_named_edge_label_validated_not_constructed` (the next-rung trigger names "edge labels ... a closed coproduct owned by `v2.std.node`"). No parallel RFM row is filed. The text-matching readers below are a second instance of that class, and the step-2 PR adds their evidence to that row. - -## Defect - -`v2.std.node` `EdgeLabel = Named { name: Symbol } | Positional`. Grammar-structural labels (for example `^dag_surface_module_header`, `^arrow_body_edge`, ``) and AUTHORED names (a field, a named actual, a declaration name) share one `Symbol` identity space. A reader that asks "is this the module-header edge?" can only compare text, and an authored name spelled the same way inhabits the answer. The invalid state is constructible, and today only `v2.std.type_binder` `type_binder_labels_conform` refuses it, for behavior nodes (rung 3), while four readers are not guarded at all: - -| reader | module | reads | -|---|---|---| -| `dag_surface_module_header_metadata_edge` | `v2.extdeps.languages.dag` | `Symbol ==` two header symbols; its callers bind `Named { name }` first | -| `dag_node_is_module_root_conj` (fallback arm) | `v2.extdeps.languages.dag` | `sym == ^dag_surface_module_header` | -| `d1_edge_names_where_clause` | `v2.test.claim.parse.d1_declaration_grammar_parse_test` | `n == ^dag_surface_where_refinement_clause` | -| `site_is_import_syntax_mention` | `v2.lens.module_graph` | `contains(site.position, ^dag_surface_import_*)`; the path is a Symbol list that already mixes production names with authored names (`v2.compiler.reference_site_collector` `reference_sites_in_edge`) | - -## Proposed shape - -``` -// v2.std.node -type EdgeLabel - = Structural { label: StructuralEdgeLabel } // constructed only by a grammar / lowering authority - | Authored { name: Symbol } // carries the source name - | Positional -``` - -`Named { name: Symbol }` does not survive beside these arms (DESIGN §3 replacement migration). Step 2 deletes it first and fixes forward. - -### Who owns the closed set (ruled: A) - -Candidate structural labels come from two layers: - -1. **Core substrate markers**, owned by `v2.std.node` and `v2.std.type_binder`: arrow body and signature order, loop bound and carrier, loop domain (`^loop_domain_edge`, #12548) and the MQ-5 Loop head-reference edge `^loop_realized_declaration_edge` (Core arm `LoopRealizedDeclaration`; at most one, never on for/while; deep-raven-602 S3), match arm pattern and body, cast target, type annotation, type params, type body, type alias, the `v2.std.node_query` projection markers, and `declaration_reference_marker`. This set is language-independent and closed today, so it can be a closed coproduct in `v2.std.node`: `CoreEdgeLabel = ArrowBody | ArrowSignatureOrder | LoopBound | ...`. -2. **Grammar production edges**, per language: `dag_surface_*`, `rust_*`, `ts_*`, `target_model_edge_*`, and the per-language `*_named_edge` helpers that build them. These belong to the language rows in `extdeps/languages/`. Enumerating them in `v2.std.node` would be a layer inversion (DESIGN §3: each upstream has its own authority, and a generic hub may not enumerate products). Adding a language would also widen a core enum. - -Recommended (A): -``` -type StructuralEdgeLabel - = Core { marker: CoreEdgeLabel } // closed, v2.std.node - | Production { grammar: GrammarRef, edge: Symbol } // minted only by the grammar fold from a declared row -``` -Here `Production` is closed per grammar by construction. Its only constructor is the grammar's own projection (`v2.std.grammar` / `v2.compiler.02_parse` `parse_tree_projection_edge`, already a closed roster, is the precedent). Readers compare `Production` values minted from the same row, not `^text`. -Alternative (B): one flat closed enum of every candidate in `v2.std.node`. Rejected for the layer inversion above. -**Ruled (neat-boar-16, 2026-09-28): A.** Core markers live in `v2.std.node`; `Production { grammar, edge }` can be minted only by the grammar fold. Rung 4 is claimed only with a discriminating RED enrolled on the real acceptance path. Otherwise the claim is rung 3. -Open for the cut: whether `Production.edge` can be a row reference rather than a Symbol. That depends on `v2.std.grammar` exposing a row identity type. If it cannot, `Production` is rung 3 per grammar (it is checked when the grammar is admitted), not rung 4. - -### Other judgment calls flagged for review - -- **Hand-built type-node literals** (`^magma_field_op` in `v2.std.algebra`, `effects`, `testgen`, `target_model`, and others) model the AUTHORED field names of modeled records but are spelled like markers. **Ruled: `Authored`.** If those nodes are meant to equal what ingest produces, their names are already wrong (`magma_field_op` ≠ `op`). That is a separate finding. -- **`ReferenceSite.position`** must carry `EdgeLabel` segments, or only Authored segments plus a separate structural context. This is a model change to `v2.compiler.reference_site_collector`, not a rename. **Ruled: it lands in the cut**, because `site_is_import_syntax_mention` is a consumer that needs it. -- **Pending question (adhoc-db43a2ff-e50):** whether the namespace-spine mark belongs in this coproduct as a `Core` marker. The rule is the same as for the other markers: it joins `CoreEdgeLabel` only if it is language-independent and minted by the substrate, not by a grammar row. -- **Consistency with the Loop edge roster (#12548 and MQ-5 S3, deep-raven-602):** the loop domain edge and the Loop head-reference edge are `Core` markers, added ONCE, so the Loop edge roster and this census agree. Whichever PR lands second adds the label to the other roster. If this model lands first, S3 constructs the label through the `Core` arm and never as an authored Symbol. `v2.std.node` `loop_edge_role` (#12548) is the one reader of Loop labels, so it is the single site the cut converts. -- **Symbol-keyed query APIs** (`v2.std.node_query` `find_named_child`, `named_child_lookup`, `named_edge_target_lookup`, and `v2.std.node` `name_occurrences`) split into a structural lookup that takes a `StructuralEdgeLabel` and an authored lookup that takes a `Symbol`. A single Symbol-keyed lookup must not survive. -- **named-args PR-B #12382** (vivid-ram-65): the Named edges for named actuals under Transform are `Authored`. `PositionalPlusOneNamedEdges` for Transform then counts `Structural { Core { CastTarget } }` separately from the authored actuals. - -## Consumers of each new declaration (DESIGN §3c) - -Every declaration below lands in the step-2 cut together with its consumers. None of them lands in this model PR. - -| declaration | consumer, executing route | -|---|---| -| `EdgeLabel.Structural` / `Authored` (replacing `Named`) | every current `Edge.label` reader and constructor. The fail-closed deletion refuses each one until it is dispositioned. `v2.std.node` `content_hash` hashes both arms. | -| `StructuralEdgeLabel.Core` / `CoreEdgeLabel` | `v2.std.node` edge discipline (`kind_edge_discipline`, `arrow_signature_order_label`, the loop bound/carrier checks) and `v2.std.type_binder` `type_binder_labels_conform`, whose behavior-label arm dissolves | -| `StructuralEdgeLabel.Production` | the grammar fold (`v2.compiler.02_parse` `parse_tree_projection_edge`, `v2.extdeps.languages.dag` productions) mints it. `dag_surface_module_header_metadata_edge`, `dag_node_is_module_root_conj` and `d1_edge_names_where_clause` read it. | -| `ReferenceSite.position` segments carrying `EdgeLabel` | `v2.lens.module_graph` `site_is_import_syntax_mention` | -| split structural/authored lookups | callers of `v2.std.node_query` `find_named_child` and its siblings | - -## Census - -[census.md](census.md) keeps only the findings the ruling was made over: the four readers and the non-EdgeLabel `Named` types that are excluded (`std.algebra` `ContainerSource`, `extdeps.formats.spice`). No counts are transcribed (DESIGN §6). The authoritative consumer population is the step-2 deletion itself (DESIGN §3, "the deletion is the census"): removing `Named` makes every dependent refuse under self-host and neat-boar-16's srv1 per-file native census, and each refusal gets one disposition: Structural/Core, Structural/Production, Authored, or arm-only. The largest dependent populations are the per-language `*_named_edge` helpers and the Symbol-keyed lookups in `v2.std.node_query`. Rust `src/v1/stage0` has no hand-written EdgeLabel sites; the generated reflection regenerates. - -## Identity impact (a deliberate change) - -`v2.std.node` `canonical_hash_of_edge_label` gains distinct tags for Structural/Core, Structural/Production and Authored. `Positional` keeps its tag. `label_sort_key` (canonical order) follows. Churn: - -- **Pinned digests:** in `node_hash_protocol_witness_test`, every vector with a named edge changes. The positional and no-edge vectors stay stable, which is the control that only the label arm moved. -- **SCM:** every `object_store` object id derived through `content_hash_of_children` changes. The JSON wire `encode_label` / `decode_named_label` splits its `"named"` tag. No committed fixture carries a `"named"` label. -- **Hermetic fixtures keyed by a content-hash locator** change. Their on-disk store is still to be located before step 2. -- **Invalidate only, nothing pinned:** the test-claim cache digest (`05_eval`), the parse memo grammar digest (`02_parse`), and the bootstrap closure hash. - -## Step-2 controls (the cut) - -1. For each of the four readers, an authored name spelled like its structural label (for example an authored `dag_surface_module_header`) reaches none of them. -2. The real structural labels still route (positive control over the real ingest path). -3. A mutation that re-keys a reader on text makes the control red. -4. Gating: base-vs-head over shape consumers, neat-boar-16's srv1 per-file native census, and self-host. - -## Step 2 as landed - -Arm names differ from the sketch above because `.dag` names are corpus-global and `Structural`, `Production` and `Core`-prefixed spellings already name other types: the arms are `Authored { name }`, `StructuralLabel { label: StructuralEdgeLabel }` and `Positional`, with `StructuralEdgeLabel = CoreMarker { marker: CoreEdgeLabel } | ProductionEdge { language, edge }`. - -- **Root first.** `Named { name: Symbol }` was deleted and every site renamed to `Authored` in the same change; the compiler's exhaustiveness refusals were the census of every reader that had to decide what a structural edge means at its site. -- **Converted, every minter and reader:** the core markers (arrow body and signature order; loop bound, carrier and domain; match arm pattern and body; cast target, type annotation, type params, type body, type alias; declaration reference; field projection base and field) and the dag surface productions the four readers need (module header and its qualified name, where-refinement clause, import decl, import block, import decl block, import decl qualified name). `v2.std.node` `Path` steps and `ReferenceSite.position` carry whole labels. -- **Deviation, stated:** the positional-payload field `"0"` stays Authored. It is consumed as `DeclaredField.name` like every authored field, so making it a marker would fork the field-name authority. -- **Frontier:** grammar-emitted labels no converted reader matches stay on the Authored arm. The population (by minting declaration) and the capability trigger are recorded on `gunbc.recurring_failure_mode` `behavior_named_edge_label_validated_not_constructed`. -- **Identity:** an authored label keeps the tag every Named edge had, so only nodes holding a structural label change identity. The two pinned vectors in `test.claim.node_hash_protocol_witness` that carry converted labels were re-derived by an independent reference implementation that first reproduced their previous values. -- **Coordination:** the construct tag (#12714) landed first, so this cut converted it to `ConstructTagEdge`. `^loop_realized_declaration_edge` (#12550) is not on main; whichever lands second adds its arm. diff --git a/docs/plans/edge-label-coproduct/census.md b/docs/plans/edge-label-coproduct/census.md deleted file mode 100644 index 771acdd1e7a..00000000000 --- a/docs/plans/edge-label-coproduct/census.md +++ /dev/null @@ -1,24 +0,0 @@ -# EdgeLabel `Named` census — scoping findings, not an instrument - -This file keeps only the FINDINGS that need a ruling. The one-off lexical scan that located them is not committed, and neither are its counts: the repo admits no `*.py`, and DESIGN §6 forbids transcribing an instrument's output. The step-2 census is the fail-closed one DESIGN §3 names. Deleting `Named { name: Symbol }` first makes every real dependent refuse loudly, under `gunbc test //gunbc/instruments:self-host` and neat-boar-16's srv1 per-file native census. The dependents that refuse are the population the cut is scoped over. Symbols are cited by module and name. - -## Premise correction: other types with a `Named` arm - -Only three types in the corpus declare a `Named` variant: -- `v2.std.node` `EdgeLabel` (the subject) -- `std.algebra` `ContainerSource = SameAsReceiver | Named { name: String }`: string-method rows in `dag/std/algebra.dag`. Excluded. -- `extdeps.formats.spice` `Named { node: NamedNode }`: spice.dag and its fixtures and tests. Excluded. - -`target_model.dag`, `testgen.dag`, `effects.dag`, `algebra.dag` (src/v2/std), and `body_lowering_fold.dag` have NO other `Named` type. All of their `Named {` hits are EdgeLabel (they import `Named` from `v2.std.node`). - -## The four named readers - -| reader (actual symbol) | file | how it reads | disposition | -|---|---|---|---| -| `dag_surface_module_header_metadata_edge(name: Symbol)` (asked-for "header_metadata_edge") | src/v2/extdeps/languages/dag.dag | Symbol equality against `^dag_surface_module_header`, `^dag_surface_module_header_qualified_name`; callers in symbol_index_fill, namespace_graft, 03_name_resolve, reference_site_collector `reference_sites_in_edge`, 03_resolve and zero_metadata_test — every caller first binds `Named { name: sym }` | becomes Structural arm match: take `StructuralEdgeLabel`, match the two arms; callers match `Structural { label }` instead of binding a Symbol | -| `dag_node_is_module_root_conj` | src/v2/extdeps/languages/dag.dag | fallback arm folds children: `Named { name: sym } => sym == ^dag_surface_module_header` | becomes Structural arm `dag_surface_module_header` (Authored arm → false) | -| `d1_edge_names_where_clause` (asked-for "edge_names_where_clause"; it lives in **src/v2/test/claim/parse/d1_declaration_grammar_parse_test.dag**, not module_graph.dag) | test | `Named { name: n } => n == ^dag_surface_where_refinement_clause` | becomes Structural arm `dag_surface_where_refinement_clause`; production minter is `v2.compiler.body_lowering_fold` (`label: Named { name: ^dag_surface_where_refinement_clause }`) | -| `site_is_import_syntax_mention(site: ReferenceSite)` | src/v2/lens/module_graph.dag | `contains(site.position, ^dag_surface_import_decl / ^dag_surface_import_block / ^dag_surface_import_decl_qualified_name)` — no Named match at all; `ReferenceSite.position` is a Symbol path mixing production and authored segments (`v2.compiler.reference_site_collector` `reference_sites_in_edge`) | requires `ReferenceSite.position` to carry label arms; then becomes a Structural-arm membership test. Not a mechanical rename. | - -Related closed roster already present: `v2.compiler.02_parse` `parse_tree_projection_edge`. It is the precedent for `StructuralEdgeLabel` being a closed coproduct. - diff --git a/docs/plans/enrolment-margin-eval-step-denomination.md b/docs/plans/enrolment-margin-eval-step-denomination.md deleted file mode 100644 index bf0dbaa5d2d..00000000000 --- a/docs/plans/enrolment-margin-eval-step-denomination.md +++ /dev/null @@ -1,194 +0,0 @@ -# Denominating the enrolment margin in eval steps - -Governed by DESIGN §5 (fail-closed, oracles, construction over validation) and §4b -(the guarantee ladder). This plan discharges clause (iv) of the §4b(3) drop -`gunbc.rung_drop` `floor_cost_cpu_regression_at_constant_eval_steps`, which is the -only clause of that row gunbc#11195 left standing. - -## The defect this closes, stated as the drop already states it - -gunbc#11195 moved the required floor's **claim ceiling** off CPU and onto eval steps, -and made CPU observed-only for every claim. It did not move every per-subject budget. -`v2.workflow.floor_enrolment_margin` still derives a 302ms budget from -`required_floor_per_subject_cpu_line_ms` and compares an **observed CPU reading** -against it, so a newly enrolled witness can still be refused on a figure that is a -property of the machine rather than of the claim. - -That is exactly what the drop's restoration trigger forbids, in its own words: - -> NO PER-SUBJECT BUDGET MAY CONSUME A RUN-LEVEL READING - -The trigger is stated at **budget grain and not at denomination grain** deliberately, -and gunbc#11195 records why: an earlier revision quantified over the *primitives* a -cost-fidelity report must cover and said nothing about *which budgets* must stop being -vulnerable, so it could have been discharged by denominating one ceiling while another -per-subject budget kept charging a run-level cost to an arbitrary claim. Denominating -the claim ceiling was one step toward the trigger and is not the trigger. - -## Why the first phase is not the enrolment margin - -`floor_enrolment_margin` consumes `gunbc.floor_cost_distribution` `ClaimCostReading` -directly — `EnrolmentCostReading = EnrolmentCostMeasured { reading: ClaimCostReading } | …`. -Every arm of that coproduct is CPU-shaped: - -``` -type ClaimCostReading - = ObservedCpuReading { cpu_ms } - | RightCensoredCpuReading { cpu_lower_bound_ms, censoring_ceiling_ms } - | CpuLowerBoundWithoutCeiling { cpu_lower_bound_ms } -``` - -So the gate **cannot see a step count at all** until the carrier carries one. The step -count exists today only as `FloorCostRow.eval_steps: Int`, a bare scalar beside the -coproduct, and a consumer reaching for it inherits a defect the corpus has already -filed against itself. - -## The filed stall this phase discharges - -`gunbc.guarantee_stall.eval_steps_outside_the_reading_coproduct_stall` — `current: -OutsideTheLadder`, `ceiling: StructurallyImpossible`, `blocker: ClimbableButUnbuilt`, -population `["gunbc.floor_cost_distribution"]`. Its subject, in its own words: the bare -`Int` means a right-censored row's observed-steps-**before-censor** and a completed -row's **performed** steps share one type and one constructor, so a fold may compare -them for equality and manufacture an equal-work cohort out of two observations that -measured different things — a **half-applied construction**, because the same file -already models the CPU half correctly and one field did not follow the decision its -sibling already made. - -Its `next_rung_trigger` is this phase, verbatim: *eval_steps moves INSIDE the -`ClaimCostReading` arms, so an observed step count and an observed-so-far-before-censor -count are reached through different constructors and no fold can compare them.* - -This plan therefore **does not invent its own first phase**. It executes a trigger the -corpus declared before this lane existed. - -## Phase 1 — the carrier (the model) - -``` -type ClaimCostReading - = ObservedCpuReading { - cpu_ms: Millisecond - eval_steps: EvalStepCount - } - | RightCensoredCpuReading { - cpu_lower_bound_ms: Millisecond - censoring_ceiling_ms: Millisecond - eval_steps_before_censor: EvalStepCount - } - | CpuLowerBoundWithoutCeiling { - cpu_lower_bound_ms: Millisecond - eval_steps_before_censor: EvalStepCount - } -``` - -with two accessors mirroring the CPU pair the file already carries, and named for the -same distinction: - -- `observed_eval_steps(reading) -> EvalStepCount?` — `Present` on the observed arm - only. The point question. A bound cannot reach a site that treats it as performed - work without the caller writing an `Absent` arm. -- `eval_steps_at_least(reading) -> EvalStepCount` — total over all three arms. The one - question a censored row can answer soundly: *this claim performed at least N steps*. - -`FloorCostRow.eval_steps: Int` is **deleted**, not deprecated. DESIGN §3's replacement -migrations cut over at the root, and a surviving bare `Int` is the attractor that rule -exists to prevent: while it stands, every nearby question is answered in its vocabulary. - -### Why inside the arms rather than a sibling coproduct - -A sibling `EvalStepReading` beside `cost: ClaimCostReading` would satisfy the trigger's -*letter* — two constructors, no bare `Int` — while leaving the incoherent state -constructible: a row claiming an **observed** CPU reading beside a -**before-censor** step count. Whether the claim reached its end is ONE fact about ONE -occurrence, and it discriminates both readings. Carrying the step count inside the arm -makes disagreement unconstructible, which is the stall's declared ceiling -(`StructurallyImpossible`) rather than one rung below it. - -This is not the clock fusion `std.measure` `measure_clock_basis_note` forbids, and the -distinction is worth stating because gunbc#11195 was reworked twice over exactly it. -Fusion is *comparing* magnitudes read from different clocks. This is *co-locating* -three observations of one occurrence under the discriminator they genuinely share. No -arm compares a step count to a millisecond. - -### Phase 1 blast radius, measured rather than estimated - -Measured on `origin/main` at `3a0f2d4b585`: - -| surface | sites | -| --- | --- | -| `ObservedCpuReading {` | 41 | -| `RightCensoredCpuReading {` | 21 | -| `CpuLowerBoundWithoutCeiling {` | 11 | -| `FloorCostRow {` | 41 | - -across 7 `.dag` files (`floor_cost_distribution` and its witness test and instrument, -`floor_enrolment_margin` and its test, `floor_cost_debt_admission` and its test) plus -the seed mirror in `required_floor_runner.rs`, `cli_run.rs` and `v1_interpreter.rs`. - -The type change admits **no partial state** — every constructor site moves in the same -commit or the corpus does not compile. That is why this is its own phase and why this -PR opens with the plan rather than a half-migration. - -**The step value at each site is authored, not mechanical.** A fixture's step count is -part of what the fixture asserts, so these are not a `sed`. Fixtures whose subject is -the CPU half take a step count that is declared as incidental beside the reading they -actually exercise. - -### Phase 1 evidence - -- **RED** — a fold that compares a performed step count to a before-censor one must - become unwritable. The discriminating probe is the *source* handed to the compiler by - a fixture, per §4b: the invalid program is authorable there even once it is - unwritable in the accepted corpus, and declining the fixture would be - specification-without-execution. -- **CONTROL** — `observed_eval_steps` answers `Absent` on both bound arms and `Present` - on the observed one, executing, so the accessor is not a decoration. - -## Phase 2 — the enrolment margin consumes it - -`floor_enrolment_margin_budget_ms` becomes an `EvalStepCount` derived through the **same** -pinned calibration the claim ceiling uses (`v2.workflow.floor_eval_step_calibration` -`eval_step_budget_for`), from a declared policy in milliseconds of work. It is not a -second calibration and not a literal: §5's oracle rule admits the budget only because it -is grounded in a controlled fixture, and a second rate would be a §3 fork of that -authority. - -`EnrolmentMarginStanding` keeps its arms and changes their denomination: -`EnrolmentWithinMargin` / `EnrolmentOverMargin` carry `EvalStepCount`, and the CPU -reading is **carried beside the verdict as an observation** rather than deciding it — -the same shape gunbc#11195 gave the claim ceiling. - -`EnrolmentBoundWithoutCeiling` survives unchanged in meaning: a preempted row's steps -are a lower bound with no ceiling, exactly as its CPU is. - -### Phase 2 evidence - -- **RED** — a claim whose eval steps exceed the enrolment budget refuses. -- **CONTROL** — a claim far over the 302ms CPU line with in-budget steps is admitted, - with the CPU observation recorded. This is the same pair gunbc#11195 enrolled for the - claim ceiling, and it must be re-authored here rather than cited: a different budget - is a different subject. - -## Phase 3 — retire the CPU line, or say why it stays - -`required_floor_per_subject_cpu_line_ms` exists only because per-subject decisions still -read a CPU clock. Since gunbc#11700 `v2.workflow.floor_cost_debt_admission` compares nothing -(a typed admission is identity and reason); the consumers are both in -`v2.workflow.floor_enrolment_margin`: the margin budget, which must sit below the line, and -the live Roster ground (`enrolment_declared_measured_standing`), which admits only a reading -over it. When Phase 2 denominates the margin, the Roster ground is the remaining consumer, and -Phase 3 decides that one: either it is denominated too and the symbol is deleted with its own -dissolution condition discharged, or the symbol survives with a population of exactly one, -stated. - -**Only when no per-subject budget reads a run-level figure does the drop's clause (iv) -retire**, and the drop is retired by its trigger and by nothing else. - -## What this plan deliberately does not do - -- It does not re-arm a CPU ceiling anywhere. gunbc#11195's evidence stands: three - prose-only heads measured the same floor at 36.7s / 40.8s / 44.7s of summed CPU, and - one identity at an identical 2884 eval steps read 34ms, 455ms and 511ms. A CPU line - refuses on the environment, not on the claim. -- It does not normalise per runner, and does not enrol slow identities as cost-debt - rows. Both are refused designs of record, for the reasons the floor authority states. diff --git a/docs/plans/execution-cell-contract.md b/docs/plans/execution-cell-contract.md deleted file mode 100644 index 119236fde94..00000000000 --- a/docs/plans/execution-cell-contract.md +++ /dev/null @@ -1,92 +0,0 @@ -# The execution-cell contract (owner directive 2026-09-25) - -Supersedes the "crash-era residue" framing of the Group B start failure. - -## The ruling, verbatim - -Treat the Group B failure as a missing execution-generation dependency, not as -incidental residue. The new unit's cleanup hooks are downstream of systemd start -admission, so they cannot repair start-rate state or orphan resources that -prevent the start. Move prior-generation inventory, quiescence, disposal, -manager-state settlement, and readback ahead of preparation. - -Prepare every rank before activating any. A four-rank arm is one cohort: -preparation failure starts none; partial activation rolls back all started -members; success requires one exact generation across every rank. - -Extract this as a realization-neutral execution-cell contract. Keep -systemd/Docker as the current Group B realization and adopt Firecracker next for -disposable harness workers. Do not claim that microVMs provide idempotency by -themselves — the host controller must still own operation identity, CAS, -resource conservation, disposal, and readback. - -Preserve the current wet run as measurement evidence only. Even if it succeeds -after manual cleanup, prove the repaired convergence route against planted -failed-unit and orphan-container residue before restoring persistent service. - -## What this means structurally - -### The phase order is law - -``` -inventory(prior generation) - → quiescence(prior generation) - → disposal(prior generation) - → manager-state settlement (rate counters, failed state, unit-table) - → readback(the prior generation is actually gone) - → preparation(new generation, every member) - → activation(cohort) -``` - -Anything in the new generation's cleanup hooks is unreachable for failures of -start admission; cleanup of the PRIOR generation must precede preparation of -the new one. The Sep 25 failure is the canonical witness: start-rate counters -and an orphan container blocked admission before the unit's own ExecStartPre -could run. - -### Cohort semantics - -An N-rank arm is ONE execution cell, not N units: - -- preparation is per-member but all-or-nothing: any member's preparation - failure activates none; -- activation is sequenced with rollback: a member that fails after others - started disposes the started members; -- success is a single exact generation (identity of the rendered intent) - observed across every member — never "three ranks on generation G, one on - G-1". - -### Realization neutrality - -The contract names phases, not tools. systemd/Docker is the current Group B -realization; Firecracker is the next realization for disposable harness -workers. A realization supplies the mechanisms for inventory / quiescence / -disposal / settlement / readback / prepare / activate. Idempotency is NEVER a -property of the substrate: the host controller owns operation identity, -compare-and-set on admission, resource conservation (nothing leaks, nothing is -double-consumed), disposal, and readback, for every realization. - -### Evidence discipline - -The 2026-09-25 wet run (manual pre-cleanup by the operator session) is -measurement evidence only — its memory-profile receipts feed the fit proof, -but it does NOT discharge the convergence-route proof. Before persistent -service is restored, the repaired route must pass against PLANTED failure -state: a failed-unit (tripped start-rate counter) and an orphan container, -planted per host, with the route required to inventory, settle, and read back -their absence before activation. That planted-residue witness joins -`fleet-slot-retirement-wet-proof` as a reopening precondition. - -## Work queued from this ruling - -1. Execution-cell contract module (realization-neutral): the phase vocabulary, - cohort type, operation identity + CAS, conservation laws. -2. systemd/Docker realization of the contract, replacing the current apply - bracket's prepare-then-start shape (shared by the experiment bracket and - the persistent serving apply). -3. Planted-residue witness: failed-unit + orphan container, per host, repaired - by the route itself. -4. Conjunctive reopening gate amendment: + execution-cell route proven against - planted residue. -5. Firecracker realization for disposable harness workers (the microVM node), - owning identity/CAS/conservation/disposal/readback in the host controller. diff --git a/docs/plans/extdeps-disputed-row-classification.md b/docs/plans/extdeps-disputed-row-classification.md deleted file mode 100644 index 60f47c242b6..00000000000 --- a/docs/plans/extdeps-disputed-row-classification.md +++ /dev/null @@ -1,265 +0,0 @@ -# Disputed extdeps rows: a two-axis classification template, tested on the adjudicable subset - -Operator ruling (2026-10-05): placement batches pause because about half of -placements were rejected on review — the classification rule is not yet ready to -be used as a template. This document records **two judgments per case**, because -the disputes of the last several batches were two different questions being -answered with one label: - -- **Semantic standing** — who owns the semantic information the declaration - carries: *exact upstream fact* (a versioned upstream artifact states the - surface and the declaration transcribes it field-for-field), *shared formal - semantics* (recorded somewhere nameable but not one product's artifact), - *product policy* (the product's own choice: dispatch algebras, serializer - policies, normalizing tables, partial grammars, transports), or *unsupported* - (nothing states it and no product purpose holds it up). -- **Consumption standing** — per DESIGN 3c's three honest states: *executing* - (consumed by execution in this tree), *declared frontier with trigger* (a - named consumer lands in a named later change, trigger stated beside it), or - *dangling* (nothing consumes it — red regardless of how well modeled it is). - -The **disposition follows from both axes together**: scope (fact + executing), -re-home or re-label (policy + executing), delete or name-a-consumer (fact or -unsupported + dangling), repair (a mixed module whose subjects must be separated -before either axis can be judged honestly). No single axis decides. - -## The evidence must fit the surface - -A standing is only as good as its evidence, and the evidence has to be of the -kind the surface admits: - -- **Data shapes are judged by field/constructor diff** against the cited - artifact. If the declaration adds, drops, reshapes or renames anything the - artifact does not state, the standing is not exact-upstream-fact — the diff - is how the judgment is made, not a formality after it. -- **Decision procedures are judged by behavioural conformance** to the - artifact's stated rule. A precedence rule, an ordering, a matching policy: the - question is whether the implementation orders/matches the way the artifact - says on its cases — not whether its fields resemble the artifact's. A field - diff cannot establish conformance, and a passing field diff cannot excuse a - failing behaviour. -- **Consumption is established only by an executing route** — a fold, an entry - point, a realization, an emitted artifact that reads the declaration. A grep - hit names a candidate site; it cannot establish that the site executes. A - name-keyed pattern establishes nothing: a predicate that reads no module - content is a classification, not a consumer. A mirror's definition of a - declaration is not a consumer of it. A witness that exercises a call inside a - test is not a production route. -- **Both consumption labels are positive claims at this grade.** `executing` - asserts a route exists; `dangling` asserts no route exists across the decoded - surfaces. Grep-grade evidence supports neither — it supports **unadjudicated: - grep-grade only**, and that is what this document records wherever its own - evidence is grep-grade. The one exception in the sample is `semver_scheme`, - whose `dangling` label below is a recorded ruling of the side chat (review - 5424597802), kept with its evidence grade attached and marked as a ruling, - not an adjudication. - -**Misfile shapes (the recurring review failure).** A citation can be live and -real and still not own the declaration. Observed, each a real pulled row: a -tool's CLI citing the standard the tool implements; a repo-normalized table -citing the spec it normalizes; a dispatch algebra citing one tool it drives; a -repo extension inside an otherwise transcribed enumeration silently breaking the -predicate; a repo plan citing fragments of a CLI grammar it does not fully -reproduce. - -## The recorded cases - -Seven cases with both axes recorded. Semantic standings were decided against -the cited artifacts (the instruments those surfaces admit); every consumption -standing below is grep-grade, so each carries its evidence kind in place — -the four `unadjudicated` labels are this document's own grade, and the three -`dangling` labels are recorded rulings of the side chat (reviews 5423944315 and -5424597802), kept with their grade attached. Dispositions that would change -with the consumption walk are marked provisional and name what would settle -them. - -**1. `docker/cli.dag` — `DockerCreateSpec` and its flag types.** -Semantic standing: **product policy.** The type is a repo-normalized plan: its -flag grammars are partial (Docker's CLI reference owns `--restart` fully — -`on-failure:N` cannot be expressed by `DockerRestartPolicy` as written, cli.dag -lines 32-39 — and `DockerNamespaceMode` covers only host/private, line 43), so -it borrows CLI grammar fragments rather than transcribing the cited surface. -Consumption standing: **unadjudicated: grep-grade only** — candidate consumer -sites (grep hits): the `dag/gunbc/spark/*` serving modules and -`docker/container_inspect.dag` name `DockerCreateSpec`; no execution-grade walk -has run. -Disposition: **repair** (consumption-independent). Either complete the grammars -field-for-field against Docker's CLI reference — after which the (a) predicate -is judged again on the diff — or re-label the plan as repo vocabulary in the -product layer. What the walk decides is the module's longer-term standing -(kept-and-consumed vs delete-or-declare); that waits. - -**2. `bmc/openbmc_operation.dag` — `OpenBmcOperation`.** -Semantic standing: **product policy.** Owner: this repo — the dispatch algebra -over file ops, systemctl, busctl, jq, mkdir/rm/cp, sleep is our vocabulary of -what a BMC operation is; the busctl man page owns only busctl's argv semantics. -Consumption standing: **unadjudicated: grep-grade only** — candidate consumer -sites (grep hits): `openbmc_fan_control` and `openbmc_password_ssh_transport` -name it. -Disposition: provisional **re-home** to the product layer. The algebra is -product vocabulary regardless of the walk — that part is consumption- -independent; what the walk decides is whether the row leaves as re-homed -(kept-and-consumed) or as deleted. The internal consumers, if the walk confirms -them, keep depending on it — they should, it is the product's own vocabulary. - -**3. `languages/json/grammar.dag` — `json_production_rows`.** -Semantic standing: **product policy.** Owner: this repo — the columns -(`rfc_section`, `lhs`, `rhs`, `value_variant`) are a repo-normalized grammar -whose `lhs` names a local `literal` nonterminal and whose `value_variant` maps -to repo AST names; the raw RFC 8259 production strings embedded in the rows are -upstream fragments, not the surface the table models. -Consumption standing: **dangling, as ruled** (side chat, review 5423944315; -grep-grade: no production consumer found; the parse path is `json/parse.dag`, -scoped separately). -Disposition: **delete or re-home** — the table is a documentation artifact in -code; both dispositions are live and the call belongs to the operator. - -**4. `languages/markdown.dag` — `commonmark_gfm_spellings`.** -Semantic standing: **product policy.** Owner: this repo's serializer policy — -choosing which dialect's spellings to emit over CommonMark 0.31.2 and GFM -extensions (tables, task lists) is a product decision; no single upstream owns -"both dialects at once." -Consumption standing: **unadjudicated: grep-grade only** — candidate consumer -sites (grep hits): `truth_table_projection` and the markdown tests name the -spellings. -Disposition: **re-label** as product policy (consumption-independent); the row -stops requesting a citation that cannot exist. What the walk decides is kept- -vs-deleted; the serializer behavior itself is untouched. - -**5. `tools/curl.dag` — `curl_cli_tool` and its policy rows.** -Semantic standing: **mixed — repair required.** The exit-code table (6/7/28/35/ -52/56) is an exact upstream fact owned by curl's own man page, which the anchor -cites correctly. The pinned minimum version (>= 7.68), the localhost timeout -rows, and the apt install realization are product policy — choices, not curl -facts. -Consumption standing: **unadjudicated: grep-grade only** — candidate consumer -sites (grep hits) name the tool rows. -Disposition: **repair — split** the policy rows out from under the -external-authority anchor (consumption-independent); the CLI-fact side is then -re-judged field-for-field against the man page on its own diff. Kept-vs-deleted -waits on the walk. - -**6. `extdeps/ebay/mock_corpus.dag` — dangling repo data; the deletion-safety -check.** -Semantic standing: **unsupported.** The module is repo-authored -`PublishedMockCase` hermetic-replay data; no upstream states it. -Consumption standing: **dangling, as ruled** (side chat, review 5424597802). -The census (2026-08-22, re-derived 08-26) -classifies it STILL-UNCONSUMED, and the v1 mirror's -`ends_with(".mock_corpus")` predicate (in -`external_authority_is_clean_tree_roster_excluded_for_module_path`, -`src/v1/stage0/src/cli_run/external_authority.rs` line 211) does not change -that: the predicate reads no module content, so it is a classification, not a -consumer. -Disposition: **delete vs declared future consumer** — operator's, between -deletion and re-homing to a fixture namespace with a named trigger. The -convention scan survives only as a **deletion-safety check** (before deleting, -confirm no compiled logic keys on the module's name in a way that reads its -content), not as consumption evidence — and it is the check the census's -mention scan does not collect. - -**7. `dag/extdeps/docker/container_stats.dag` — dangling and not -field-for-field.** -Semantic standing: **product policy over upstream fragments.** It adds a -repo-authored `cpu_percent` field and reshapes `networks`, so it is not the -Engine API's stats payload; owner of the real surface: Docker's Engine API -stats reference. -Consumption standing: **dangling, as ruled** (side chat, review 5423944315; -grep-grade: no production caller; the #13304 review confirmed it -independently). -Disposition: **delete or name a consumer** (DESIGN 3c). Repairing the -transcription — dropping `cpu_percent`, restoring the API shapes, re-diffing — -is worthwhile only once a consumer or a declared frontier with a trigger -exists; on today's tree the honest move is deletion or a named trigger. - -## Set aside — not adjudicated at execution grade - -Five cases from the sample could not be adjudicated on both axes now. They are -recorded so the work is not lost, with their consumption standings marked -**unadjudicated: grep-grade only** — grep hits name candidate sites and cannot -establish that the site executes (the one recorded ruling in this section, -`semver_scheme`'s dangling, is attributed above). - -- **`cloud/gcp/errors.dag` — `GcpRpcCode`**: semantic standing **non-exact**. - The module adds `GcpRpcCodeOther { raw: NonEmptyStr }` (errors.dag line 32) - beside the 17 transcribed `google.rpc.Code` values, so there is no - exact-upstream subject until the enumeration is split from the repo variant - and the enumeration alone is diffed against - `cloud.google.com/apis/design/errors#http_mapping`. Consumption: - unadjudicated (grep-grade: only its grounding witness test references the - module). -- **`crypto/hash.dag` — `HashAlgorithm`/`Digest`**: **FIPS-derived fragments - plus local carriers.** `Sha256`/`Sha512` is not FIPS 180-4's family naming — - the carriers are repo-local and sit beside `sha256sum(1)` machinery (coreutils - manual owns the tool). There is no exact-upstream subject until the FIPS - facts are split out and diffed against FIPS 180-4, and the verifier machinery - is judged against the coreutils manual. Consumption: unadjudicated - (grep-grade: the sccache pin and live-deploy paths name the verifier). -- **`ssh/session.dag` — `service ssh.Session`**: **mixed subjects.** The module - quotes ssh(1)'s exit-255 contract (an exact upstream fact owned by the - OpenSSH client manual — not RFC 4254, which owns channel messages) but also - carries repo transports, session records and mocks. Until the subjects are - separated (one module per authority), neither side's semantic standing can be - judged. Consumption: unadjudicated (grep-grade: repo consumers name the - machinery subjects). -- **`version/semver.dag` — `semver_scheme`**: an **upstream-owned decision - procedure** (semver.org section 11 precedence) whose fitting evidence is - **behavioural conformance** — does `compare` order identifiers the way §11 - says — not a field diff. That conformance has not been established; the - dispatch was lexical (`semver_identity_compare`), which is the recurring - failure mode filed as - `a_citation_attests_a_decision_rule_the_code_does_not_implement` (#13235). - Consumption standing: **dangling, as ruled** — the side chat's ruling - (review 5424597802): only the witness test calls `semver_scheme.compare` - (`extdeps_version_semver_witness_test.dag`), and the v1 stage0 mirror defines - `semver_scheme()` — a definition is not a consumer. Recorded with its - evidence grade attached: under this document's own rule the witness-only - grep supports `unadjudicated`, so the ruled label is kept as a ruling, and - any delete-or-scope decision on this module should not fire until an - execution-grade walk confirms it. Whether the type family passes a field - diff against semver.org's grammar is likewise unverified and not asserted - here. -- **The `github/*` family**: **dropped from this document.** The per-row rule - (each surface judged against its specific REST page, field-for-field; each - row's consumption its own) is carried as a rule, but no row of the 19 was - adjudicated here — none has had a field-for-field diff or an execution-grade - consumer walk run for it in this work. - -## What this template changes for bulk work - -1. **Two judgments, two instruments — and the instrument must fit the - surface.** Field/constructor diffs for data shapes; behavioural conformance - for decision procedures; an executing route for consumption. An anchor alone - decides nothing: an anchor is exactly the thing product-policy rows can - borrow, and a grep hit, a mirror definition or a name-keyed pattern is - exactly what a dangling row can borrow. -2. **Dispositions are derivable, not argued** — from admissible evidence only: - fact + executing → scope; fact + dangling → delete or declare a trigger; - policy + executing → re-home or re-label; unsupported + dangling → delete - (safety-checked); mixed → repair by splitting subjects, then re-judge each - side. Where the evidence grade is below the axis, the axis is recorded - unadjudicated — and a disposition that would depend on it is recorded as - provisional, naming what would settle it (the execution-grade walk or the - fitting diff), rather than fired on the lower-grade evidence. -3. **Shared formal semantics stays a discipline.** Among the cases recorded - here, none finally landed there. That is a statement about this sample, not - about the frontier. The procedure stands: when a row looks shared, hunt for - the recording source; if none exists, the row is product policy or - unsupported, never settled as shared by default. -4. **Unconsumed is a delete-or-declare decision, not a scope** (DESIGN 3c) — - and "unconsumed" itself is only decided at the evidence grade above: a row - whose consumption is unadjudicated is not thereby dangling, but it is not - scopeable either; the execution-grade walk has to run first. - -## Verification status of this document - -Every consumption claim above is **grep-grade**: no executing route has been -established at identity grade for any case in this document, so the four -`unadjudicated` labels are the grade's honest reading and the three `dangling` -labels are recorded rulings of the side chat with that grade attached — none -of them is presented as execution-established. The semantic standings of the -recorded cases were decided against the cited artifacts (the docker/cli line -numbers are quoted from the module); standings in the set-aside section are -recorded with their missing evidence named. The census numbers are its own (2026-08-22 / re-derived 2026-08-26) and -carry that clock. No code, no tsv, and no roster is changed by this PR; it is -a classification template, and bulk work waits on the operator's ruling on it. diff --git a/docs/plans/fabric-switch-100g-convergence-gap-analysis.md b/docs/plans/fabric-switch-100g-convergence-gap-analysis.md deleted file mode 100644 index 77d3ff8d71b..00000000000 --- a/docs/plans/fabric-switch-100g-convergence-gap-analysis.md +++ /dev/null @@ -1,237 +0,0 @@ -# Fabric switch 100G convergence — gap analysis - -Subject: bringing the eight DGX Spark fabric legs on the MikroTik CRS812-8DS-2DQ-2DDQ-RM from -50GBASE-CR2 to 100GBASE-CR2, end to end, by execution rather than by hand. - -Authorities read: `extdeps.mikrotik.crs812`, `extdeps.ethernet.link_mode`, -`gunbc.spark.fabric_switch_desired`, `gunbc.spark.fabric_switch_observed`, -`gunbc.spark.fabric_switch_assessment`, `gunbc.spark.managed_access_bootstrap`, -`gunbc.spark.grant_privileged_operation`, `gunbc.fleet_ssh_locus`, -`product.breakout_leg_programming`. - -## 0. The standing observation, and what changed under it - -`gunbc.spark.fabric_switch_observed` records the legs as QSFP28-coded — extended compliance byte 192 -= `0x0B`, byte 116 = `00h` — so nothing attests 100GBASE-CR2, and the ConnectX-7 derives -`50G_2X` and refuses `100G_2X` (mstlink opcode 16). - -**The operator re-flashed the legs with an FS BOX V4 on 2026-09-17.** That invalidates the EEPROM -half of the observation above; it does not replace it, because nothing has re-read the bytes. - -Read live on 2026-09-17 against `spark-a3ee` (192.168.1.226) as `gunbc-automation`: both fabric -netdevs (`enp1s0f1np1`, `enP2p1s0f1np1`) are **up, autoneg on, 50000Mb/s**. - -That reading is **not** evidence the re-flash failed, and must not be recorded as such. Two causes -are live and unseparated: - -1. the coding did not change, or -2. the coding is correct and **nothing has forced the mode on either end** — - `crs812_passive_dac_requires_forced_mode` is `true`, and the host is currently on autoneg. - -The discriminator is the EEPROM bytes plus the NIC's supported-speed set. Neither is readable today -(§1). Establishing which cause holds is the **first** work item; everything downstream is premised -on it. - -> **SUPERSEDED 2026-09-17 (see G12).** This §0 was written before the read grant landed and the -> bytes were read. They have since been read: byte 192 = `0x40` (attests 100GBASE-CR2) and the NIC -> reports `Supported Cable Speed: 100G_2X`. So cause (1) is ruled out — **the re-flash took** — and -> "the first work item" is done. The current standing fact lives in **G12** and the actuation log; -> the two paragraphs above are retained as the original framing, not a current open question. The -> live blocker is now the switch-side QSFP-DD end, not the host coding. - -## 1. Gap: the host end cannot be read or set - -`sudo -n -l` on spark-a3ee, 2026-09-17, shows `gunbc-automation` holds exactly: - -``` -(root) NOPASSWD: /usr/bin/loginctl enable-linger gunbc-automation -(root) NOPASSWD: /usr/bin/systemctl reboot -``` - -So `ethtool -m` (EEPROM read), `ethtool -s` (forced speed) and `mstlink` (supported-speed set, -`100G_2X` enable) are all unreachable by the principal the fleet runs as. - -Missing, in dependency order: - -| # | Gap | Home | -|---|---|---| -| 1.1 | No `extdeps` authority for the `ethtool` binary — name, path, verbs | new `extdeps/ethtool/` | -| 1.2 | No `extdeps` authority for `mstlink` / the MFT tools | new `extdeps/mellanox/` | -| 1.3 | `SparkManagedGrant` has no arm for a module read or a link-mode set | `gunbc.spark.managed_access_bootstrap` | -| 1.4 | No disclosure standing for what `ethtool -m` returns | same, `spark_managed_grant_disclosure_standing` | - -On 1.4: the existing arms are `GrantReturnsNoHostData` (reboot) or `GrantDisclosureUnestablished` -(container inspect, which is why that grant is **desired but not installable**). `ethtool -m` returns -module vendor / part / serial and the EEPROM page — host data, but with no analogue of `Config.Env`. -It needs its own classification; it should not borrow `GrantReturnsNoHostData`, and it is not the -hard case `InspectServingContainer` is. - -**A read grant and a set grant are two grants, not one.** Reading the EEPROM is what settles §0; -forcing the mode is an actuation on a live fabric. They should be installable independently so the -discriminating read does not require authorizing the mutation. - -## 2. Gap: the switch has never been read - -Better than expected — the REST surface is already modeled in `extdeps.mikrotik.crs812`: -`routeros_rest_authority`, and paths for `/rest/interface/ethernet`, `/rest/interface/ethernet/monitor`, -`/rest/ip/neighbor`, `/rest/system/identity`, plus `crs812_factory_address` (192.168.88.1/24) and -`crs812_factory_credential_location`. - -And the assessment vertical is genuinely complete: `gunbc.spark.fabric_switch_assessment` carries -intent, observed, per-lane divergence with causes, typed refusals, expectation-driven unknowns, and a -`GoalInspection`. `gunbc.spark.fabric_switch_desired` correctly derives the desired lane speed from -the cable plant rather than declaring it, and refuses rather than degrades when the plant does not -converge. - -**The gap is the transport, and it is total.** `fabric_switch_subject()` passes `readings: []`. -`observe_fabric_switch_attempt` takes readings as a **supplied** argument. Nothing in the corpus has -ever performed an HTTP request against the switch, so today every lane resolves to -`Crs812LaneUnknown` and the assessment is structurally incapable of returning anything else. - -| # | Gap | Home | -|---|---|---| -| 2.1 | No RouterOS REST **transport** — nothing turns a modeled path into a request | new realization handler | -| 2.2 | No producer of `List` from a live response | `gunbc.spark.fabric_switch_observed` or a new observer | -| 2.3 | No management address for the switch (only `crs812_factory_address`) | `gunbc.fleet.fleet_intent_network` | -| 2.4 | No credential standing for the switch | new, mirroring `gunbc.spark.bootstrap_credential` | -| 2.5 | No apply/actuation path — assessment can diverge but nothing converges | new, after 2.1–2.4 | - -Note the ordering constraint from DESIGN §3d: selection precedes convergence, and the assessment -fold must not grow a realization. 2.1 is a **bound handler**, peripheral to the interface — not an -edit to `assess_fabric_switch`. - -## 3. Gap: the switch credential - -`crs812_factory_credential_location` already records that the credential is on the underside label, -user `admin`. What is missing is a **carried** `SecretRef`. - -Per the operator ruling of 2026-08-07 recorded in `gunbc.spark.grant_privileged_operation`, secret -names are **carried, never derived** — a computed name makes the secret namespace a function of how a -host is spelled today. And a bare `SecretRef` asserts a secret exists, which is why -`SparkCredentialStanding` models `CredentialMaterialized` against `CredentialMaterializationPending`. - -Minted now so the operator can upload against an exact name, in project `gunbai-secrets` -(`fleet_secrets_gcp_project`), following the established kebab convention -(`bmc-srv3-admin`, `spark-administrator-password`, `fleet-automation-ssh-key`): - -- **`fabric-switch-factory-admin`** — the factory label credential, treated as **one-time - authorization material** exactly as `OperatorBootstrapCredentialRequired` treats the Spark - bootstrap admin. Not a serving credential, not retained. -- **`fabric-switch-admin`** — the rotated credential the fleet actually runs under, generated during - bootstrap. - -Bootstrap is therefore **not** "log in with the sticker password". It is: authorize once with -`fabric-switch-factory-admin`, mint and store `fabric-switch-admin`, and leave the factory credential -dead. Leaving the device on its factory password would be a standing exposure the model would be -silent about. - -These rows land **with their first consumer**, not before — a `SecretRef` nothing reads is the -dangling declaration DESIGN §3c makes red. - -## 4. What is NOT a gap - -Worth stating so the work does not re-invent it: - -- the desired-speed derivation (cable-plant-driven, refuses rather than degrades) — done; -- the divergence / unknown / refusal algebra over lanes — done; -- lane interface naming, lane-speed and FEC wire encodings, the forced-mode fact — done; -- the SSH credential locus and known-hosts anchor for the host end — done, and verified working; -- the human cost of the re-flash, and the part that removes it (`extdeps.transceiver.fs_145681`) — done. - -## 5. Sequence - -**A. Settle §0 before building anything in §2.** Land 1.1 + 1.3 + 1.4 (read grant only), install it, -read bytes 116 / 192 / 222 and the mstlink supported set off a re-flashed leg. One read decides -whether the fabric work proceeds or returns to the cable plant. - -**B. If the coding attests the mode:** the switch address and `fabric-switch-factory-admin` upload, -then 2.1–2.4 — read the switch for the first time, which turns eight `Crs812LaneUnknown`s into real -readings and makes the existing assessment say something. - -**C. Then converge,** both ends together: 1.2 + the set grant on the host, 2.5 on the switch, forced -100GBASE-CR2 with matched FEC. - -**D. Re-read and confirm** `100G_2X` actually enabled and the link at 100000Mb/s, then update -`gunbc.spark.fabric_switch_observed`. Per that module's own standing, this is an **experiment**: only -the post-change read establishes anything about the copper. If the legs are correctly recoded, both -ends are forced, and the link still will not come up at 100G, `extdeps.ethernet.link_mode` is explicit -that operating at 50GBASE-CR2 never certified 100GBASE-CR2 — that outcome is a real possible answer, -not a failure of the procedure. - ---- - -## 6. Modeling gaps & incompatibilities found during actuation (2026-09-17) - -Discovered by actually driving the switch and hosts (see fabric-switch-actuation-log.md). -Each is a place the `.dag` model does not yet express something the real path required. - -### Switch model (extdeps.mikrotik.crs812 / gunbc.spark.fabric_switch_*) - -- **G1 — no autonegotiation dimension.** The lane model carries speed and FEC but NOT - auto-negotiation on/off. Copper (BASE-CR) bring-up is entirely governed by autoneg - state; the whole experiment turned on it. `Crs812LaneIntent` needs an autoneg field. -- **G2 — the FEC enum names FEC by RouterOS label, not by codeword, and the correct 100G - codeword is UNWITNESSED.** `Crs812FecMode = FecAuto|FecOff|Fec74|Fec91` carries RouterOS's - strings, not the RS codeword (528,514 vs 544,514) each implies — a modeling nicety. Two - §4d errors to NOT repeat: (a) an earlier head asserted 100GBASE-CR2 needs RS(544,514) from - the IEEE 50G-PAM4 lane FEC and concluded the CRS812 might not expose it — an inference - stated as a fact about this switch, retracted; (b) a later head asserted "MikroTik - documents fec91 for 100G-baseCR2" (from review, no cited vendor document) and on that - strength deleted the converge's FEC-capability refusal arm — the same move in the other - direction, also retracted. NEITHER codeword claim is grounded. What IS grounded: the eight - fabric legs are OBSERVED running fec-mode fec91 today (at 50G). So `fec91` is the desired-FEC - CANDIDATE (the FEC the plant already runs), the converge KEEPS its refusal arm rather than - assuming the candidate trains 100G, and grounding the codeword needs a cited MikroTik doc, - not an annotation. -- **G3 — the live link OUTCOME is unmodeled.** The assessment models intent-vs-observed - lane divergence, but the observed reading needs: negotiated speed, link state - (up / polling / down / auto-init-failed), and FEC-locked, read from BOTH ends. RouterOS - `status` (link-ok/auto-init-failed) and `eeprom-checksum` are unmodeled. -- **G4 — a link is two-ended; the model treats the switch lane as the subject.** The - switch reporting `link-ok/100Gbps` is a LOCAL claim; the host was in `Polling`. Link-up - is a bilateral fact requiring both ends to agree. The observed model joins both ends for - the CABLE receipt but not for the live LINK state. -- **G5 — QSFP-DD (switch-side) module is unmodeled.** The switch reads the QSFP-DD end, - which is CMIS/SFF-8024, NOT the SFF-8636 the host QSFP56 end uses. `sff8636_*` decoders - don't cover it. The `eeprom-checksum: bad` on the Generic-coded DD end has no home. -- **G6 — no RouterOS REST transport / reader / apply.** Still the whole of step B: nothing - turns crs812_rest_* paths into requests; `fabric_switch_subject()` passes `readings: []`. -- **G7 — no switch credential standing landed.** `fabric-switch-factory-admin` exists in - Secret Manager but no SecretRef rows; factory->rotated bootstrap unmodeled. -- **G8 — the fabric legs are on qsfp56-dd cages, not qsfp56.** Confirm desired/assessment - target the qsfp56-dd interfaces (the 50G breakout legs), per the observed roster. - -### Host model (gunbc.spark.*) - -- **G9 — host link control is via mstlink (MFT), which has NO extdeps authority.** The - NVIDIA firmware tool surface (mstlink, mstconfig, mstflint) is entirely unmodeled. Host - link force/FEC/lane-count lives there, not in ethtool. -- **G10 — ethtool cannot express 2-lane (CR2) forcing on ConnectX-7.** `ethtool -s speed - 100000` is lane-count-ambiguous and selects CR4; only mstlink can pin 100G_2X. A modeled - host-force MUST use mstlink, and mstlink's flags `-s`, `--link_mode_force`, `-k` are - mutually exclusive per invocation (separate calls required). -- **G11 — no host-side SET/force grant.** This PR modeled only the EEPROM READ grant. - Forcing the host link needs a mstlink grant (root), unmodeled and uninstalled. -- **G12 — fabric_switch_observed is now STALE on main.** It states legs are 0x0B-coded and - the NIC refuses 100G_2X. Post-recode: byte 192 = 0x40, NIC Supported Cable Speed = - 100G_2X. The observed authority must be updated with the new reading (and should model - the recode as an event, not overwrite silently). - -### Operational / workflow - -- **G13 — spark_grants dispatch cannot fan out.** Dispatching 8 fleet-converge runs 3s - apart CANCELLED 4 via the workflow concurrency group. Multi-target grant install must be - serialized (one run completes before the next) or the workflow needs a fan-out mode. -- **G14 — spark_grants installs ALL missing grants, not a scoped one.** No dispatch-level - way to install only the EEPROM grant vs also InspectServingContainer. -- **G15 — dropin filename drift.** Host holds `/etc/sudoers.d/gunbc-gunbc-automation` for - the linger grant while the model derives `gunbc-enable-linger`. The installed filename - and the modeled `spark_managed_grant_dropin_name` disagree; re-install would write a - second file. (Noticed earlier, unrelated to this PR, still real.) - -### Credential containment (process, not model) - -- **G16 — actuation ran on ad-hoc curl with pasted GCP tokens.** The token expired - mid-experiment and left srv5 down with no recovery path in-session. The modeled path - (WIF on a runner, credential materialized and removed in-step) exists for grant install - and must be the ONLY actuation path; ad-hoc curl from a session is the scaffold to kill. diff --git a/docs/plans/fabric-switch-actuation-log.md b/docs/plans/fabric-switch-actuation-log.md deleted file mode 100644 index b855a680fcb..00000000000 --- a/docs/plans/fabric-switch-actuation-log.md +++ /dev/null @@ -1,118 +0,0 @@ -# Fabric switch 100G — out-of-band actuation log - -Operator-approved workaround (2026-09-17): the switch and host link modes were driven -directly over the RouterOS REST API and the host's `mstlink`, from a session on the LAN, -ahead of the modeled convergence path (docs/plans/fabric-switch-convergence-build-plan.md). - -**This file is an INPUT to the modeled apply, and its dissolution trigger** -- it is NOT an -exact transcription of a successful configuration, because no bilateral 100G configuration -worked. It records the facts established, the NORMALIZED operation shapes attempted (with -placeholders like ``/``/``, and RouterOS CLI equivalents for REST -mutations), and the verdict. A future receipt may carry the exact REST method, resource -identity, request body, real interface/PCI identifiers, timestamps and pre/post -observations; that is safely deferred. The -scaffold is retired when that modeled path lands and reproduces this end state. Round-by- -round exploration (wrong turns, corrected framings) is intentionally NOT preserved here; -git history carries it, and this file states each conclusion once, at witnessed confidence. - -## Facts established - -- **Switch:** MikroTik CRS812-8DS-2DQ-2DDQ, RouterOS 7.20.8, management 192.168.1.240, - identity `gunbc-fabric-switch`. REST API live (401 unauth). Credential in Secret - Manager: `fabric-switch-factory-admin`. -- **Host recode confirmed** (read on spark-a3ee, 2026-09-17): EEPROM byte 192 = `0x40` - (was `0x0B`), attesting 100GBASE-CR2; `mstlink` reports `Supported Cable Speed: 100G_2X` - (was `50G_2X` only); the former "Cable speed not enabled" refusal is gone. All 8 - ConnectX-7 NICs advertise `100000baseCR2/Full`. This is carried on the modeled carrier: - gunbc.spark.fabric_switch_observed `fabric_leg_eeprom_observations` (spark-a3ee row). -- **The fabric legs are on the switch's `qsfp56-dd` (400G breakout) cages**, comment - "gunbc fabric leg": qsfp56-dd-{1,2}-{1,3,5,7}. Each was forced to `50G-baseCR2`, - autoneg off, `fec-mode fec91`. `crs812_passive_dac_requires_forced_mode = true`. -- Lane roster (cage:lane -> host): 1:1 srv5, 1:3 srv8, 1:5 srv7, 1:7 srv6, 2:1 srv10, - 2:3 srv12, 2:5 srv9, 2:7 srv11. - -## Normalized operation shapes attempted (an input to the modeled actuator) - -Switch, per lane, over `/rest/interface/ethernet/` (RouterOS CLI form): -``` -/interface/ethernet/set speed=100G-baseCR2 auto-negotiation=no fec-mode=fec91 -/interface/ethernet/disable ; /interface/ethernet/enable # bounce to retrain -``` -Host (ConnectX-7, PCI 0000:01:00.1), forced 2-lane 100G via mstlink (ethtool -s cannot -pin CR2 — it ambiguously selects CR4; the flags below are mutually exclusive per call): -``` -mstlink -d --link_mode_force -s 100G_2X --yes -mstlink -d -k RS --fec_speed 100G_2X --yes -``` -Restore to known-good 50G: switch `speed=50G-baseCR2 auto-negotiation=no fec-mode=fec91` -+ bounce; host `mstlink -d -s 100G_2X,50G_2X,50G_1X,25G,10G,1G --yes` (re-enables -AN) then `ip link set down; ip link set up` (the host needs its own bounce to -retrain after the mstlink change). - -## Verdict (witnessed confidence) - -The QSFP56 host recode succeeded and is recognized: the ConnectX-7 advertises 100GBASE-CR2 -and no longer refuses the cable speed. On the CRS812 breakout legs, **no bilateral 100G -link was established** under either tested production configuration — automatic (both-ends -autoneg) or forced (both-ends forced, via mstlink for the host), across every switch FEC -mode. The host sits in `Polling`; the switch reports its own side `link-ok/100Gbps` -locally while the host never trains. On the autoneg path, RouterOS reports -`auto-init-failed` and `eeprom-checksum: bad` for the still-Generic-coded QSFP-DD end. - -This makes **switch-side QSFP-DD coding/compatibility the leading hypothesis, NOT a proven -cause**: the current tests do not isolate it from signal integrity, lane grouping, -firmware, or endpoint interoperability. The tested srv5 leg was the mutated specimen and -was restored and verified at 50G; all eight legs were verified at their known-good 50G state -after the experiment. - -## FS support escalation (2026-09-18) - -FS support asked us to follow the CRS812 breakout documentation: force the port speed with -auto-negotiation off (`auto-negotiation=no speed=100G-baseCR2` on the odd sub-ports). That -is the configuration already tested above. They also asked whether the ConnectX-7 was -actually up when the switch reported link-ok. To answer, one operator-approved retest ran on -the srv5 leg only (`qsfp56-dd-1-1`, 12:55:07-12:56:00 UTC). Both credentials were loaded -before the change and the restore was armed to run on exit, which closes the expired-token -failure above. - -- **FS's exact configuration was applied** on the switch with `fec91` and a bounce. The - host was forced to `100G_2X` with RS-FEC and bounced. -- **Both ends were read at +10s and +30s, with the same result each time:** - - CRS812: `link-ok`, `rate=100Gbps`. - - ConnectX-7: `mstlink State: Polling`, speed N/A. - - Linux: `NO-CARRIER`, carrier 0. `ethtool`: `Link detected: no (Autoneg, No partner - detected)`, active FEC None. - - Ping across the fabric link: 100% loss. -- **Answer to FS: no, the ConnectX-7 was not operationally up.** The switch's 100G reading - describes only its own side. -- **RouterOS reports `eeprom-checksum: bad` for the QSFP-DD end at the healthy 50G state - too.** On its own, the flag therefore does not explain the 100G failure. -- **The restore was verified from both ends:** - - Switch: 50G-baseCR2, fec91, link-ok. - - Host: Active, 50G, 2 lanes, RS(528,514), carrier 1. - - Ping to .14 and .12: 0% loss. -- **Operator sent the evidence to FS the same day** (after redacting it): the CRS812 system - information and hide-sensitive export, the port and module information, the ConnectX-7 - mstlink/ethtool/module EEPROM baselines, and simultaneous captures from both ends for the - forced test and the restore. Awaiting FS engineering's review. Cable: FS - QDD-400G-4QPC015, host-end serial S2630771509-2. Switch: RouterOS 7.20.8. NIC firmware: - 28.45.4028. - -The verdict above stands unchanged. The leading hypothesis is still unproven, and the next -step is still the substitution control below, unless FS identifies a configuration error. - -## Next step: a substitution control, not more configuration - -The single experiment that isolates cable/coding from switch/NIC/signal-integrity is a -hardware substitution: a known-good dual-compatibility cable (FS SKU 145681 / 101806) in -the same ports, OR the same cable with a known-good 100GBASE-CR2 endpoint pair. Until that -runs, contacting FS is reasonable but "cause proved" is not. Further out-of-band actuation -is paused pending credential containment. - -## Cross-references - -- Credential/restore incident (token expired mid-run, srv5 left down): filed as - gunbc.recurring_failure_mode - `a_rollback_depends_on_a_credential_that_can_expire_mid_actuation`. -- Modeling gaps found while driving the hardware: gap-analysis §6 (G1–G16). -- The modeled path this scaffold dissolves into: fabric-switch-convergence-build-plan.md. diff --git a/docs/plans/fabric-switch-convergence-build-plan.md b/docs/plans/fabric-switch-convergence-build-plan.md deleted file mode 100644 index b151cb7d779..00000000000 --- a/docs/plans/fabric-switch-convergence-build-plan.md +++ /dev/null @@ -1,90 +0,0 @@ -# Fabric switch convergence — build plan (the normal path) - -Goal: when the correctly-coded QSFP-DD end arrives, converge the 8 CRS812 fabric lanes to -100GBASE-CR2 through a MODELED, repeatable path — no ad-hoc curl. This defines that path -and sequences it into reviewable PRs. It dissolves the actuation scaffold in -fabric-switch-actuation-log.md and closes gaps G1-G16 in -fabric-switch-100g-convergence-gap-analysis.md. - -## Layering (DESIGN §3: interface / realization / policy are three facts) - -- INTERFACE — `extdeps.mikrotik.crs812` already owns the REST paths, lane speed and FEC - wire values, and the forced-mode fact. Extended here with the autoneg axis and a live - link-outcome shape (G1, G3). This is what RouterOS's API IS, not what we do with it. -- REALIZATION — RouterOS REST operations declared as `transport rest` operation rows - (method × path × basic-auth), realized by the shared HTTP-client machinery and - fixture-tested via `RestExchangeFixture`. Precedent: `extdeps.bmc.http` Redfish ops. -- POLICY — `gunbc.spark.fabric_switch_*` (workflow layer): which lanes, forced 100G-CR2 + - RS-FEC, the credential, the converge order. This is our business decision, not a fact - about MikroTik. - -## Credential (G7) - -Mirror `gunbc.spark.bootstrap_credential` / `grant_privileged_operation`: -- `fabric-switch-factory-admin` (exists in Secret Manager) as one-time bootstrap material. -- `fabric-switch-admin` (rotated) as the operating credential. -- A `FabricSwitchCredentialStanding = CredentialMaterialized{ref} | CredentialMaterializationPending`. -- Fetched runner-side over Secret Manager REST (as spark_grants does), never in argv. -- Bootstrap = rotate factory -> admin; the factory credential is not a serving credential. - -## PR sequence - -- **PR-1 (model only, safe, this session's target): domain + credential.** - - Add `auto_negotiation` to the lane intent/reading (G1). - - Add `Crs812LinkOutcome` (negotiated speed, link state: - LinkUp|Polling|AutoInitFailed|Down, fec-locked) as a two-ended live fact (G3, G4). - - Carry the FEC codeword the RouterOS label maps to (G2), a modeling nicety. The desired - FEC is `fec91` as a CANDIDATE -- the FEC the eight legs are observed running today -- not - a deduced fact: neither the earlier RS(544,514)-is-needed claim (IEEE inference) nor the - later fec91-per-MikroTik claim (uncited) is grounded, so the converge intent KEEPS a - FEC-capability refusal/observe arm rather than assuming `fec91` trains 100G (§4d). - - `FabricSwitchCredentialStanding` + the two SecretRef rows. - - Update `gunbc.spark.fabric_switch_observed` (G12): the 2026-09-17 recode is an EVENT - (byte 192 0x0B->0x40, NIC Supported Cable Speed 50G_2X->100G_2X) plus the live 50G - link reading; do not silently overwrite the prior observation. - - Witnesses for each new type/derivation; fixtures from the real REST/mstlink output - already captured in the actuation log. - -- **PR-2: the read path.** RouterOS REST read operations (GetEthernetInterfaces, - MonitorLane) as `transport rest` rows, a reader producing `List` from - the JSON, fixture-tested against captured responses. Wire `fabric_switch_subject()` to a - live read so `assess_fabric_switch` stops being vacuous (readings: []). Read-only. - -- **PR-3: the converge path, as ONE two-ended transaction.** The mutating actuator is a - single transaction over BOTH endpoints, never a switch-only mutation. Its safety shape is - fixed now even though the successful parameter tuple and the implementation defer: - 1. obtain and validate ALL forward AND recovery authorization before the first effect; - 2. capture host and switch pre-state; - 3. apply the complete candidate to both endpoints; - 4. accept success only on bilateral agreement on the required link state (switch monitor - AND host both report the negotiated speed, never one end's local "link-ok"); - 5. on every other terminal -- refusal, timeout, interruption, `Polling`, producer - disagreement -- restore both captured pre-states WITHOUT fetching new authorization; - 6. read both endpoints back and make VERIFIED restoration part of the terminal result; - 7. remove credentials only after confirmed success OR confirmed rollback. - It mirrors `gunbc.spark.managed_access_apply` for the per-step typed-outcome/receipt - shape, run via `fleet-converge.yml`. The desired FEC is the `fec91` candidate (G2); the - intent KEEPS a FEC-capability refusal/observe arm -- whether `fec91` trains 100G is - unwitnessed, so the actuator observes and refuses rather than assuming it works. - **Sequencing rule (the design commits to this now):** because forcing the HOST end needs - PR-4's mstlink set grant, the mutating switch effect and the host effect are components of - ONE actuator -- PR-3 and PR-4 land together, or the switch converge stays READ-ONLY until - both exist. The design does NOT authorize "mutate the switch now, add host actuation and - rollback later": that terminates with the endpoints intentionally divergent, which step 5 - forbids. Reading negotiated speed is unprivileged (`ethtool`), so OBSERVING both ends is - available before PR-4; only the two-ended MUTATION requires it. Forced-both is the - MikroTik-documented production candidate, not an established working config (the one - mstlink run of it did NOT train -- host stayed in Polling -- fabric-switch-actuation-log.md), - so PR-3+PR-4 let us RE-TEST it once the QSFP-DD coding is fixed rather than achieve - convergence by themselves. - -- **PR-4 (host side): mstlink authority + set grant.** `extdeps.mellanox` for mstlink - (G9), the 2-lane-force fact (G10: ethtool cannot pin CR2), and the host-side set grant - (G11) so the host end is forced through the modeled grant path, not sudo-over-SSH. - -## What none of this can do until the hardware is right - -The model + fixtures do not need a live 100G link to be correct and tested. But end-to-end -CONVERGENCE to 100G cannot SUCCEED until the QSFP-DD end links at 100G (pending FS / -substitution). PR-3's converge will correctly REFUSE or report Polling until then — which -is the honest behavior, not a failure of the model. diff --git a/docs/plans/fleet-revision-relation-fixture.md b/docs/plans/fleet-revision-relation-fixture.md deleted file mode 100644 index 1f9e7b9dc9d..00000000000 --- a/docs/plans/fleet-revision-relation-fixture.md +++ /dev/null @@ -1,118 +0,0 @@ -# The fleet-revision-relation fixture repository - -Rebuild recipe for the controlled git repository behind -`dag/test/claim/fleet_revision_relation_wet_matrix_test.dag`. - -## Why a fixture repository at all - -The pure merge-base fold is already witnessed by authored exit codes, and -`git.Inspect.MergeBase` itself is witnessed by record/replay. Neither covers the -*composition* — `observe_fleet_revision_relation_in` → `MergeBase` → -`fleet_revision_relation_from_merge_base` — where a swapped operand, a dropped `trim`, or a -mis-decoded stdout would live. A fold fed authored inputs cannot catch those: authoring the -inputs is the step under test. - -## The graph - -``` - B main - / - A ──┤ base-a - \ - C sibling - - D orphan an independent root, no common ancestor with anything above -``` - -Every commit carries a branch **on purpose**. `C` was originally created on a detached -HEAD and so unreachable; `merge-base` still answered only because the object had not been -collected yet. A fixture whose correctness depends on gc not having run is not a fixture. - -## Recipe - -Identities and timestamps are fixed because the object ids participate in the operation -input hash: a machine-dependent id makes the recorded fixtures unportable. - -```sh -FX=target/test-fixtures/fleet-revision-relation -rm -rf "$FX"; mkdir -p "$FX" -export GIT_AUTHOR_NAME=fixture GIT_AUTHOR_EMAIL=fixture@invalid -export GIT_COMMITTER_NAME=fixture GIT_COMMITTER_EMAIL=fixture@invalid -export GIT_AUTHOR_DATE="2020-01-01T00:00:00+0000" -export GIT_COMMITTER_DATE="2020-01-01T00:00:00+0000" -git -C "$FX" init -q -b main -echo a > "$FX/f"; git -C "$FX" add -A; git -C "$FX" commit -q -m A -git -C "$FX" branch base-a -echo b > "$FX/f"; git -C "$FX" add -A; git -C "$FX" commit -q -m B -git -C "$FX" checkout -q -b sibling base-a -echo c > "$FX/f"; git -C "$FX" add -A; git -C "$FX" commit -q -m C -git -C "$FX" checkout -q --orphan orphan -echo d > "$FX/f"; git -C "$FX" add -A; git -C "$FX" commit -q -m D -git -C "$FX" checkout -q main - -mkdir -p target/test-fixtures/not-a-repository # case 7 needs a real non-repository path -``` - -Verified reproducible: two independent builds produced identical `show-ref` output. - -## Object roster - -| ref | commit | oid | -|---|---|---| -| `base-a` | A | `3728e5635c811256194971bcaed367914a6040e6` | -| `main` | B | `3ad80d79bd1f64a7f75e121d7323aed053a81f0b` | -| `sibling` | C | `d0d3a7f6e323428bd771a32b1a19554481e1ae7c` | -| `orphan` | D | `8286843fce4511d0157573ba33b48ba46e48958a` | -| — | absent, decodable | `1111111111111111111111111111111111111111` | - -The absent id must be **decodable**, or the probe is never dispatched and the case proves -nothing about exit 128. - -## The seven cases, and what each one is for - -| # | current, accepted | exit | relation | why it is in the matrix | -|---|---|---|---|---| -| 1 | B, B | — | `SameRevision` | short-circuits before any dispatch | -| 1b | B, B (non-repository path) | — | `SameRevision` | the short-circuit does not depend on the repository existing | -| 2 | A, B | 0, base = A | `DeployedIsAncestor` | ordinary forward advance; base **is** current | -| 3 | B, A | 0, base = A | `CandidateIsAncestor` | superseded; base **is** accepted | -| 4 | B, C | 0, base = A | `UnrelatedHistories` | diverged **with a shared ancestor** — the case whose receipt used to claim a disjoint history | -| 5 | B, D | 1 | `UnrelatedHistories` | no common ancestor; exit 1 is an **answer** | -| 6 | B, absent | 128 | `RelationUnverifiable` | could not look — must not collapse into 5 | -| 7 | B, C (non-repository path) | 128 | `RelationUnverifiable` | a second route to 128; if it ever answered a relation, the observation is reading some other repository | - -Cases 5 and 6 are additionally asserted as one row -(`no_merge_base_and_could_not_look_stay_distinguishable`) so the distinction cannot be -half-satisfied. A consumer reading the operation's `success: Bool` instead of its exit code -cannot tell them apart at all — the live residual recorded on `MergeBaseOutcome`. - -## Record, replay, and the controls - -Commands are in the exclusion row's dissolution description -(`gunbc.ci_layer_roots`, pattern `fleet_revision_relation_wet_matrix_test.dag`) so the recipe -sits beside the reason a reader looks it up. - -Measured results: - -- **record (wet)** — 9/9 PASS, 6 distinct fixtures written for `git.Inspect.MergeBase` - (six, not seven, because case 5 and case 6 each appear twice across the nine rows). -- **replay (hermetic, exact store)** — 9/9 PASS. -- **control: empty store** — the two short-circuit rows still PASS; the other seven FAIL - with `missing recorded fixture`, each naming a **distinct** input hash. This proves the - passing replay consumed recorded observations rather than reaching live git, and separately - that the short-circuit is real. -- **control: wrong input hash** — one fixture renamed to a bogus hash, the rest left in - place. Its case refuses with its own exact hash while a sibling case still passes from - the same store. The store is an exact observation map, not a nearest-match catalog. - -The recorded `inputs` array includes `repository_path`, which is the direct evidence that -the repository participates in the key rather than being ambient. - -## What this does not cover - -The file does not run in the required floor: `git.Inspect.MergeBase` publishes no -`mock_response`, and recorded shell observations cannot reach a floor run — -`WitnessEvaluationFrame` carries only `rest_fixtures`, and `--fixture-store` is a -`claim_batch` flag with no `claim_executor` equivalent. The exclusion row carries the -substantiated reason and the dissolution trigger — a shell/service fixture arm on -`WitnessEvaluationFrame`, after which these rows enroll unchanged. diff --git a/docs/plans/floor-aggregate-cost-authority.md b/docs/plans/floor-aggregate-cost-authority.md deleted file mode 100644 index 82759f3305c..00000000000 --- a/docs/plans/floor-aggregate-cost-authority.md +++ /dev/null @@ -1,121 +0,0 @@ -# The floor's aggregate cost has no authority - -Status: proposal. No code yet. - -Raised from reading two required-floor runs (35365418267 merge_group, -35366842458 pull_request, 2026-09-18). Those IDs say WHERE the reading was -taken and are deliberately not doing the work of a producer: nothing in this -document quotes a duration, a population or a ratio as a standing quantity. -The producer for a floor run is the `required-witnesses-floor` job of -`gunbc.witness_floor_workflow`, and this proposal's whole point is that the -quantity it is about has no carrier yet -- so where a figure would go, it names -what would emit it instead (DESIGN §6: name the instrument, never transcribe -its output). - -## The gap - -The floor has a rich **per-item** cost model and no **population** model. - -Per item, today: `std.evaluation_budget` bounds one evaluation on two clocks; -`v2.workflow.floor_enrolment_margin` requires a newly enrolled witness to reach -a verdict with derived headroom; `v2.workflow.floor_cost_debt` withholds -identities whose marginal cost exceeds -`v2.workflow.required_floor` `required_floor_per_subject_cpu_line_ms`; -`gunbc.floor_cost_distribution` `implied_clean_run_budget` derives that line -from measured run-to-run inflation as an order statistic. - -Every one of those is denominated in ONE witness. None is a statement about the -run. So the floor's total is a quantity no declaration carries, no gate reads, -no refusal can cite, and no change is charged against. - -Each added witness is individually justified, individually inside its margin, -individually admitted -- and the aggregate is nobody's finding. That is §5's -externalization at authoring time: the cost is real, it is caused by a specific -change, and it is paid by every session that later waits on CI. - -## Why a per-item budget cannot be tightened into a population budget - -Tightening `required_floor_per_subject_cpu_line_ms` prices the TAIL, not the -SUM. A per-subject line low enough to bound the sum would withhold a large -population of individually cheap, individually correct witnesses -- a §4d -over-prohibition, forbidding more than the evidence supports. Sum and maximum -are two facts; one authority cannot carry both. - -## What is missing, stated as a modeling obligation - -Per §3b, a domain whose home does not exist is a modeling obligation, not yet a -conformance row. The obligation is an observation carrier and a disposition. - -**The observation must be attributed, not a single wall figure.** A lump total -would blame whichever witness's window happened to contain a shared cost -- -the same attribution defect `floor_cost_debt` already discovered per-claim. -The grains are: fixed preparation demand; shared-fill demand; marginal claim -work; orchestration; and an explicit **unattributed remainder**. The remainder -is a declared field rather than a silent residue, because on the measured runs -on the run this proposal was raised from it was the largest single component -- -a ONE-OFF reading, taken by summing the `done in` stage lines against the -`[floor-phase]` seam markers in one required run's log, with no entry point that -re-derives it. It is named as a one-off rather than quoted as a quantity, and -the carrier's `unattributed_work` field is what makes the figure a produced -output instead of a remembered one and an authority that cannot say how much it cannot explain is -reporting a number it has not earned. `measurement_completeness` travels with -the observation so a disposition derived from a partial reading is refusable -rather than quietly authoritative. - -**The policy carries three facts, not one budget.** - -- **desired target** -- 20 minutes. -- **hard steady-state ceiling** -- 30 minutes. -- **declared migration debt** -- NOT a figure carried here. It is read from the - `required-witnesses-floor` job of `gunbc.witness_floor_workflow` at the revision - the authority lands, by the producer the observation names. A number transcribed - into this proposal would be the baseline the whole policy is denominated in, - decaying without anyone touching either end (DESIGN §6). - -The three are separate because the floor is ALREADY over the ceiling. A policy -that refused everything above 30 today would deadlock the repository, and a -policy that silently adopted the observed figure as the budget would let the -temporary baseline replace the target. So during migration the ordinary obligation is -**non-worsening against the declared debt**, while the performance lanes lower -the debt; once the floor is under 30 the ceiling ratchets down and becomes an -ordinary refusal boundary. The target stays visible throughout. - -**An increase is a separately authorized act.** An envelope any cost-adding -change may raise is accounting, not governance -- the change cannot grant -itself the room. An amendment names the added capability, its measured cost, -the authorizing operator, and a retirement or offset condition, and it raises -the ceiling without raising the target. - -## What this does NOT license - -It does not license deleting evidence. §4b(4) requires a repaired class's -discriminating RED and positive control to **remain enrolled**. What it does -not require is one permanent authored file and one independently paid compile -per specimen: evidence may be consolidated into a data-driven matrix, carried -by one stronger discriminating construction, evaluated over a shared prepared -closure, or subsumed by a stronger wall that retains the required RED and -positive control. Consolidation of the physical representation is therefore a -legitimate lane for lowering the debt; retirement of the obligation is not. - -If an envelope ever forces the choice, the ladder wins and the envelope is -amended -- which is the whole reason amendment exists. - -## An open quantity this proposal deliberately does not assert - -Whether, and how steeply, floor runtime grows with authored witnesses. The authored -population is re-derived by `git grep -cE '^\s*test fn ' -- '*.dag'` over -any two revisions, and it grows steeply. But the topology filters hard: the -floor's own `required-floor: declared=N offered=N routed=N ...` census line -(`v1_compiler.bin.claim_executor`) is the producer for what survives to run, and -it reports a small fraction of the declared population. Declaration count is -therefore NOT the demand unit, and a -claim that a speedup would be consumed by growth needs a measured slope from -added identities to routed closures, compile groups and critical-path time. -That slope is the first instrument this authority needs and is not yet -measured. Until it is, growth is a motive for building the authority and not -evidence for any particular envelope. - -## Relation to the performance lane - -An aggregate authority does not make the floor faster. It makes the floor's -speed a governed quantity instead of a side effect. diff --git a/docs/plans/floor-cost-distribution-near-the-ceiling.md b/docs/plans/floor-cost-distribution-near-the-ceiling.md deleted file mode 100644 index 42592b5e0ad..00000000000 --- a/docs/plans/floor-cost-distribution-near-the-ceiling.md +++ /dev/null @@ -1,437 +0,0 @@ -# The floor cost distribution near the 500ms ceiling - -*Measurement, not a repair. Commissioned by tidy-lynx-804 after gunbc#10133 to decide whether -further per-producer work is a treadmill. The question is whether the population near the ceiling -is dense, whether the rows now crossing share a root, and what an honest margin policy would be.* - -## The instrument - -**Every figure below is produced by `tools.floor_cost_distribution_instrument` -`floor_cost_distribution_report`, and the derivations it composes are -`gunbc.floor_cost_distribution`.** Re-derive rather than trust this page: - -``` -# one download per run id in `floor_cost_sampled_runs`, into `floor_cost_artifact_root`: -gh run download -n required-floor-claim-cost -D /tmp/floor-cost/ - -gunbc run --source-root dag --source-root src/v2 \ - --entry dag/gunbc/instruments/floor_cost_distribution_instrument.dag \ - --function floor_cost_distribution_report -``` - -The first version of this memo named `gunbc.witness_floor_workflow` as its instrument. That -workflow produces the raw rows and performs none of the analysis — the ten-run intersection, the -band histogram, the counterfactual replay and the inflation percentiles existed only as prose plus -numbers, so a reader could check the arithmetic of nothing. Review 58983 refused it on exactly that -ground. The figures that remain below are illustrative of what the instrument returned on the runs -it names; the instrument is the authority, and where the two disagree the instrument is right. - -**The input needs no floor run.** `required-floor-claim-cost` is uploaded by every required-floor -run and carries the COMPLETE executed population — the `[over-cost]` lines in the job log are a -25-row preview of the same data and say so. The runs are named as data in -`floor_cost_sampled_runs`, because a measurement over "whatever happened to be downloaded" is not -re-derivable: a second reader would get different numbers with no way to tell a real change from a -different sample. - -**THE TEN SAMPLED RUNS REFUSE AS OF THE DISJOINT-COLUMN ARTIFACT CHANGE, AND THIS INSTRUMENT WAS -ITSELF THE SPECIMEN THAT MOTIVATED IT.** Every run named in `floor_cost_sampled_runs` predates that -change: their `required-floor-claim-cost` carries the old shared `cpu_ms` column, in which a -completed cost and the lower bound of a claim a safety deadline preempted sit in one field with -only an adjacent `verdict_reached` to separate them. This analysis resolved `cpu_ms` by name, -refused correctly on a *missing* column, and never asked for that discriminator — so every band, -every worst-cost fold and every inflation ratio on this page consumed bounds as costs. The bound's -magnitude is approximately the ceiling that stopped the row, so the "population dense near the -ceiling" that a histogram over those rows shows is partly manufactured by the ceiling itself. - -`claim_cost_columns` no longer resolves the old header, so those runs load as `RunRefused` — **not -`RunEmpty`**, which is a distinction the reader must not lose: an artifact whose measurements cannot -be read is not a run that measured nothing. The cause is typed and named -`UnsupportedSharedCpuMsVintage`, and `floor_cost_distribution_check` refuses the whole sample. - -That is the correct fail-closed answer rather than a regression, but the honest reason is narrower -than the one this page used to give. It said the two populations "are not separable in those bytes -at all". **That is overstated.** The old artifact carries `outcome` and `verdict_reached`, and this -very instrument treats `outcome=budget_interrupted` as an interruption signal — so an identifying -signal exists. The accurate statement is that the vintage is *known and unsupported*: one `cpu_ms` -column held both populations, and this version declines to reconstruct the split rather than being -unable to detect that one is needed. Nothing here obligates a legacy migration; it only forbids -claiming the bytes are silent when they are not. **The figures this page describes as illustrative should be read as carrying -that defect, not merely as stale.** Re-pointing `floor_cost_sampled_runs` at runs from the new -artifact vintage restores the instrument; the derivations are unchanged and their witness runs on a -hand-built fixture, so nothing but the live figures is waiting on the re-sample. - -**An incomplete sample refuses.** `floor_cost_distribution_check` exits non-zero naming every run -that failed to load. One artifact in an eleventh run (`33667468330`) downloaded empty during the -original analysis and silently emptied a ten-way identity intersection before it was caught — a run -that reads as zero rows is indistinguishable from a run in which nothing crossed, which is the -absorbing fallback DESIGN §5 forbids. Both refusal arms are witnessed in -`test.claim.floor_cost_distribution_witness`, along with a discriminating red for each derivation -below. - -## 1. The band is dense, and the tail is a plateau - -Post-repair run `33691893294`, CPU basis, 3,498 rows: median 2ms, mean 34ms, p90 86ms, p99 397ms. - -| CPU band | rows | cumulative from top | -| --- | --- | --- | -| ≥500 | 5 | 5 | -| 400–500 | 27 | 32 | -| 300–400 | 55 | 87 | -| 200–300 | 89 | 176 | -| 100–200 | 148 | 324 | -| <100 | 3,174 | 3,498 | - -**There is no gap below the ceiling.** The decision-relevant form of that is what happens if modules -are repaired top-down. Taking the worst row remaining across all ten runs, removing modules in -max-cost order after the three gunbc#10133 already fixed: - -| modules repaired | worst row remaining | runs refused /10 | -| --- | --- | --- | -| (the 3 from #10133) | 533 | 1 | -| +`rust_produced_decl_emit` | 525 | 1 | -| +`emit_host_field_access_equals_eval` | 511 | 1 | -| +`emit_host_fold_closure_equals_eval` | 490 | 0 | -| +`semantic_decl_serialize_parity` | 487 | 0 | -| +`emit_host_meet_join_equals_eval` | 486 | 0 | -| +5 more modules | 429 | 0 | - -The first three modules buy 43ms. The next seven buy 61ms — roughly **10ms per module**. That is the -treadmill, quantified: below the top of the band each repair returns about one fiftieth of the -ceiling. - -## 2. The new crossers do NOT share a root the way the #10133 three did - -`eval_steps` is a cheap structural fingerprint of shared work, and it separates the two cases -cleanly. - -The three modules gunbc#10133 repaired reported **612,662–658,336** eval steps — a 1.07x spread -across seventeen rows in three modules with unrelated subjects. That uniformity *is* the signature of -one shared dominant producer, and it was: `compile_phase_frontier_standing`. - -The families now near the ceiling report **63,196–197,227** eval steps — a 3.1x spread. They are -three distinct shapes rather than one: - -| family | rows ≥200ms | eval_steps | wall/cpu | -| --- | --- | --- | --- | -| `v2.test.execution.emit_host_*_equals_eval` | 33 | 63k–174k | **2.05** | -| `v2.test.emit.semantic_decl_*` | 27 | 78k–141k | 1.01 | -| `v2.test.emit.rust_*` / `produced_decl_*` | 48 | 77k–197k | 1.00 | - -The `emit_host_*` family spends half its wall clock off-CPU, which is the region -`v2.workflow.required_floor` already carries a §4b rung drop for (`OpaqueHostCallUnbounded`). - -**There is one shared-producer lead, and it is not a plan.** `required-floor-cross-claim-demand` -ranks `bind_outcome` (`v2.std.diagnostic`, unkeyed, 679 claims / 108,085 evals / 183 modules) at the -top by a factor of 13, and its module samples reach both the emit and the execution families. But -that artifact's own header states `cost_columns=inclusive_of_callees_do_not_sum`, and its column -totals 1,736,654ms against the run's actual `claim_cpu_total_ms=120,177` — a factor of 14. It ranks -candidates; it cannot quantify a saving, and it must not be cited as one. - -## 3. The margin, measured - -Run-to-run variation on the **same identity** across the sample, restricted to rows with enough -signal that integer milliseconds are not noise: - -- median **1.16x**, p90 **1.35x**, p95 ~1.43x, max ~**2.0x**. - -Two mechanism hypotheses were tested and both are refuted, which is what makes a single global margin -defensible: - -- **Expensive rows do not inflate more.** By baseline-cost bucket the median inflation is 1.18, 1.13, - 1.19, 1.17, 1.07, 0.92 for the 20–50 … 400–500ms buckets. -- **Host-call-heavy rows do not inflate more.** By wall/cpu class the median inflation is 1.16 (pure - CPU), 1.13, 1.16 (host-heavy ≥1.8). - -Run-level contention alone spans only 0.92x–1.15x across the ten runs, so most of the spread is -per-row rather than per-run. And run `33688140761` **refused with a below-median run factor of -0.96**, which is why a refusal cannot be read as evidence that the fleet was busy. - -**PR runs are harsher than `main` runs.** All four modules reported crossing on PR runs -(#10109/#10122/#10123) have `main`-run medians of 314–391ms and `main`-run maxima of 417–487ms, so -the PR path reaches roughly 1.6x on a median row. - -At 1.6x a row must sit under **312ms** on a clean run to be safe; at the observed 2.0x, under -**250ms**. **87 rows exceed 300ms today.** - -## 4. What this corrects - -The framing that prompted this measurement was that gunbc#10133 promoted the next rows down the -ranking into the margin. **The promotion mechanism is refuted, and the repair was disproportionate -rather than one step of a treadmill.** - -*Refuted:* rank does not cause a crossing, absolute cost does, and removing rows above a row does not -make it slower. Measured directly, the five rows that crossed on the post-repair run went **above -their own pre-repair maxima** — pre-repair median 343ms and max 472ms over 45 observations, against -504–533ms post-repair. They did not inherit a margin; they got slower, on the most contended run in -the sample (run factor 1.15, the highest of the ten). - -*Disproportionate:* replaying all ten runs with the three repaired modules excluded — - -| | runs refused | -| --- | --- | -| as observed | **5 / 10** | -| with the #10133 modules excluded | **1 / 10** | - -Across the nine pre-repair runs, **no row outside those three modules ever crossed 500ms.** Those -three modules were 17 of the 22 distinct identities that crossed anywhere in the sample. They were an -outlier — one shared 612k-step producer — not a sample of the plateau. - -## 5. What follows - -The plateau is real and per-producer repair is the wrong unit for 87 rows returning ~10ms each. Two -remedies are sanctioned by `v2.workflow.required_floor`: reduce what the witness reaches for, or -enroll it in a lane declaring its own dated ceiling **and** naming the row as an executing consumer. - -**Raising the ceiling is not a third option and should not be re-proposed.** That module records the -value being raised 500→1552→5000 and both raises being repudiated: 1552 was measured-max plus a -scheduler quantum on the same current tree, which is the oracle DESIGN §5 forbids by name, and 5000 -rode in as a passenger on a ruling about something else. The population that accumulated under that -drift is frozen in `v2.workflow.floor_cost_debt` precisely so it is not granted a higher ceiling. - -Which of the two remedies fits the emit and execution families — and whether a `bind_outcome` repair -buys real margin or only a rank change — is a decision this measurement informs and does not make. - -## 6. The premise is measured false: there is no fixed above-ceiling identity set, and the budget is not mis-set - -*Added after the §5 question was worked. It is recorded here rather than in the thread that produced -it because the belief it refutes is the one a reader re-forms from the same symptom — a red floor -lane naming a handful of emit rows — and re-deriving it costs a night.* - -**The claim being retired: "three (later seven) self-host witness modules sit above the 500ms hard -CPU ceiling." Measured, no fixed above-ceiling identity set exists.** Completed green runs place this -family's band below 500ms; run variance can carry its top across, and four completed-over-cost rows -were observed at 506–515ms. What is retired is that the family is *intrinsically* above the ceiling — -a row does not stably "sit" on either side of an attempt-safety boundary — not the crossings -themselves, which are real and recur. Both remedies §5 names are -refused for this family on evidence, and the ceiling is correctly denominated. What remains is a -different subject, stated at the end. - -### How to re-derive this, because there is no instrument module for it - -The §5 instrument reads one artifact per run. The join below needs a second axis — the same -identities measured off-CI — and **no authority owns it today**, which is a gap and not a style -choice. The recipe, at identity grain: - -``` -# CI side: per-claim identity/outcome/cpu_ms for a completed run -gh run download -R gunb-ai/gunbc -n required-floor-claim-cost -D /tmp/ci/ - -# local side: ONE function per process, which matches the floor's fresh-frame-per-claim -claim_batch --source-root dag --source-root src/v2 \ - --eval-budget-ms 300000 --entry --functions -``` - -Join on identity. Use runs whose rows **completed**; prefer green runs for the baseline factor. - -### Three readings that are wrong, and the control that exposes each - -**An `interrupted_before_verdict` figure is the budget, not the row.** The deadline preempted the -measurement, so the cost is unbounded above 500. Every interrupted row reports approximately whatever -ceiling stopped it, so a set of them looks like a tight cluster "just over" no matter what the true -costs are. Only `completed_over_cost_requirement` rows carry costs. Raising `--eval-budget-ms` on a -probe is how you measure one; that is instrumenting, and is not the ceiling-raise §5 repudiates. - -**The crossing set is not the expensive set.** Measured with the budget raised, the row in -`v2.test.emit.rust_body_add_emit` that *passed* on CI is the most expensive of its four, and the -three that were interrupted are all cheaper — a single-digit-ms spread separates survivor from -casualty. Repairing "the rows that crossed" therefore fits noise. **Always measure the non-crossing -siblings**; they are what reveals the band. - -**A one-anchor local→CI factor does not transfer.** A factor taken from one identity predicted -another row comfortably under the ceiling that CI had in fact interrupted. Take the factor from -many identities across several runs, or not at all. - -### What the join shows - -Joined over the emit and execution rows of this family across three main runs, the local→CI cpu -factor is **stable across green runs and materially higher on a failed one** — the same tree and the -same rows, with every row inflated together. At the green factor the family's whole band lands well -inside the ceiling; at the failed run's factor its top crosses. That is CI run-to-run variance, and -it independently corroborates the inflation distribution §3 measures from the other direction — two -instruments built for different purposes agreeing on one phenomenon. - -`v2.test.execution.emit_host_fold_closure_equals_eval` is the worked example: interrupted at -`cpu_at_least=522ms` on one job, it completes under budget on every green run in the join. - -**The host-independent form of this, which is stronger and needs no factor at all.** `eval_steps` is -carried in the same artifact and does not depend on the machine. Across the three runs, **every row -of this family that completed has a byte-identical step count** — identical work — while its -`cpu_ms` swings by up to the full green-to-failed ratio. Milliseconds move; the work does not. That -settles the question without any local→CI conversion, and it is the axis to use. - -The single row whose step count differs is the one that was **interrupted** on the failed run: its -count is truncated where the deadline stopped it. So the one apparent exception is a second -demonstration of the first misreading above — an interrupted row is not measured, in steps or in -milliseconds. *(The `eval_steps`-as-control technique is `gunbc#10158`'s; see the caveat below.)* - -### Why reducing the cost is the only lever, and what the cost is - -Both §5 remedies are refused for this family: - -- **Reduce what the witness reaches for** — refused by measurement. Decomposing `rust_target_model` - component-by-component (each in its own process, against a baseline replicated across four - independently written probe modules) shows every component except the bundle costs nothing, and - the bundle is what the emission reads. There is no oversized subject to trim, the test fixture is - free, and both cost-shape defects were looked for and are absent — the module fold is linear, and - the two `list_snoc_item`-in-a-fold sites operate over ~8 and 3 elements. -- **Enroll it in a lane declaring its own dated ceiling** — no such lane exists. Every candidate - either withholds rows from execution (`v2.workflow.floor_cost_debt`, whose own header states - membership deletes coverage, and which is shrink-only and closed to new crossers) or names a - cadence with no workflow (`FalsifierSubstrateLongLane`; no workflow in the tree carries a - `schedule:` trigger). Taking that arm would mean *building* a lane. - -The per-claim cost is a **fixed floor plus a small per-declaration term**, not per-declaration work: -the second declaration emitted in a frame costs a fraction of the first, and the module fold adds -nothing over separate calls. The fixed part splits into `rust_target_model` construction, the -arrow-body projection, and two smaller serialize steps. The projection term is **measured at the -`v2.std.compilers.target_model` `target_project_arrow_body_to_value_expression` frame and attributed -by elimination** to the primitive-apply step beneath it — the neighbouring candidates -(`dag_value_expression_projection`, `dag_surface_operator_canonicalization_member`, the -translation-rules lookup, the inner-arrow construction) each measure zero, the last two tested with -inputs that would have exposed a table build even on a miss. - -**One live lead, left undetermined on purpose.** The first projection in a frame costs; subsequent -ones do not, *regardless of which declaration body is projected*. That is equally consistent with a -memo keyed above the body and with a one-time warm of a shared structure that any first caller pays. -Those want different providers, so the mechanism must be separated before any provider is proposed — -that ambiguity, not the cost, is the open question. Two prior sharing attempts on this surface are -already refused: a `data`-row promotion cannot reach the floor at all (`required_floor_runner` builds -a fresh evaluation frame per claim, so `data_cache` never spans claims), and `rust_target_model` was -enrolled in `v2.workflow.floor_pure_producer_share` and withdrawn after measuring this exact cluster -*worse* — read that roster's header before re-proposing either. - -**One caveat on that withdrawal, added because `gunbc#10158` landed while this section was being -written.** The roster's rust-row measurements were normalised against *rows in no consumer module of -any enrolled key*, and #10158 shows that control is **biased by composition** — it demonstrated the -point by splitting a subject's own rows on `eval_steps` into those the serve reached and those doing -byte-identical work, and finding the supposedly untouched group moved almost as much. Rows elsewhere -are an assumption about the corpus; rows doing byte-identical work are an observation about the row. -That does not overturn the withdrawal, and the roster's own next trigger still governs — but the -strength of "measured worse" now rests on a control known to be biased, so a re-proposal should -re-measure against the step-split control rather than treat the question as closed. - -A value check was run before treating any of this as a caching opportunity: the projections of -`x + y` and `y + x` are compared and **differ**, carrying both a positive control that the equality -works and a by-construction-false row proving the harness reports failures at all. A cheaper-looking -result that lost operand order would have been a correctness defect, not a saving. - -### The subject, restated - -The floor is not flipping because a few rows are too expensive. **The emit surface sits at roughly -two-thirds to four-fifths of the per-claim budget on a normal run, and CI run-to-run variance is -large enough to carry its top across the line** — so which rows cross is a property of the run, not -of the rows. Reducing the shared fixed cost raises the margin against that variance. Nothing else -currently on the table does, and per-row repair aimed at whichever identities crossed last is -repairing the sample rather than the population. - -## 7. The margin of §3 is a two-run point estimate; the between-run envelope is wider - -*Commissioned after `v2.test.emit.rust_produced_decl_emit.rust_produced_decl_name_discriminates` -passed at 406ms against the 500ms ceiling on green `main` and refused at `cpu_at_least=516ms` on an -unchanged tree — a required-floor refusal with `claims_failed=0`.* - -*This section and §8 are the quantitative form of §6's conclusion. §6 establishes from a local→CI -join that no fixed above-ceiling identity set exists; these two measure how wide the variance that -makes membership unstable actually is, from the CI artifact alone.* - -**§3's figures index over rows inside one pair of runs, not over runs — and its prose says -otherwise.** §3 reads "run-to-run variation on the same identity across the sample", but the -producer behind it, `identity_inflation_permille`, takes a `base` and an `other` and is driven with -exactly two named runs (`floor_cost_baseline_run`, `floor_cost_contended_run`). Its percentiles are -therefore computed across ROWS inside one pair, and describe how unevenly one host-pair ratio lands -over different rows. Two runs are one sample of the between-run quantity and one sample has no -spread, so the pairwise median is a point estimate wearing a distribution's clothes. The instrument -was right and the sentence over it was not, which is the failure the "name the producer" rule exists -to make detectable. If the admitted execution envelope set is wider than the -pair sampled, it is a **lower bound**, and a budget set from it moves the cliff rather than closing -it. §3 is not withdrawn — it measures what it measures — but it may not be used as a budget input. - -**The producer for this section is `gunbc.floor_cost_distribution` `identity_envelopes` / -`complete_envelopes` / `work_invariant_envelopes` / `worst_envelope_permille` / -`run_extreme_census`, driven by `tools.floor_cost_distribution_instrument` -`floor_cost_envelope_report`.** The sample is twelve green `main` runs of `witnesses.yml` on twelve -distinct runner registrations across three hosts, named with their runners in -`floor_cost_envelope_sampled_runs` — the runner is not in the artifact and is acquired from the -run's `required-witnesses-floor` job. Re-derive rather than trust this page; where the two disagree -the instrument is right. - -`floor_cost_envelope_check` is the exit-code actuator and refuses an incomplete sample by name; -`floor_cost_envelope_report` returns the lines, and `gunbc run` renders them through its -entry-point refusal because the host requires a `ProcessExit` from an entry function — the sibling -report in §1 has the same shape. - -One row's envelope is its max over the twelve runs against its min over the same twelve, computed -only over identities present in every run with a verdict and a baseline at or above 50ms, and -restricted to rows whose `eval_steps` were identical in all twelve so the remaining movement is -inflation rather than a tree change. - -**Between-run envelope, work-invariant population (n=398 of 400 complete rows, 3,628 identities -observed across the twelve runs):** median **1.367x**, p90 **1.653x**, p95 **1.819x**, p99 -**2.046x**, worst observed **2.280x** — the instrument's own order statistics, which select an -observed member and never interpolate. §3's pairwise median of 1.16x understates the median by a -fifth and the worst case by more than half. - -**That step equality is a filter, not a finding, and it must not be read as evidence of -invariance.** Selecting rows whose counts agree across all twelve runs removes tree movement from -the sample so the residual is inflation; it says nothing about whether `eval_steps` is invariant in -general. It is not: the rung-drop row's missing item (b) has since been measured properly on one -identical tree (gunbc#10228) and **refutes** — `eval_steps` disagreed on 18 of 3,519 joined rows -against `cpu_ms`'s 1,394. Far more stable, and not invariant. - -**No cost trend.** Restricting the population by baseline leaves the envelope essentially flat — -p50 1.37 / p90 1.65 at ≥50ms, 1.35 / 1.58 at ≥100ms, 1.36 / 1.64 at ≥200ms, 1.47 / 1.51 at ≥300ms -— which confirms §3's refutation of "expensive rows inflate more" over runs rather than over one -pair, and is what makes a single global margin defensible. - -## 8. §6's "property of the run" conclusion, quantified — and it reaches host grain - -§6 concludes from a local→CI join that "which rows cross is a property of the run, not of the rows". -This section reaches the same conclusion from the other direction — the CI artifact alone, no -local→CI conversion — and carries it one grain further, to the machine. - -A median run factor cannot answer this: it is robust exactly where the envelope is driven. The -discriminator is `run_extreme_census`, which counts how often each run holds a row's maximum and its -minimum. Under per-row jitter each of twelve runs is the extreme for about a twelfth of the -population. - -It is not close to that. Over the 398 work-invariant rows, run `33766436293` (**srv4-09**) holds the -minimum for **367**; run `33775106554` (**srv1-19**) holds the maximum for **160**. At host grain -srv1 holds the maximum for **279 of 398 rows from 3 of 12 runs**, against 83 for srv3 (5 runs) and 36 -for srv4 (4 runs). The extremes are concentrated on particular machines, so a row near the line is -adjudicated by which host dequeued the job. - -**An earlier reading of this measurement said the opposite and is corrected here rather than -deleted.** Per-run *median* factors span only 0.878–1.118 and per-host medians are nearly identical -(srv1 1.069, srv3 0.985, srv4 0.992), which reads as "the host does not matter". It is the wrong -statistic for the question: a ceiling is crossed by the extreme, not by the median, and the extreme -census shows the concentration the median averages away. - -## 9. What this bounds, and what it does not - -**The subject row.** `rust_produced_decl_name_discriminates` measured **313–444ms across the twelve -runs** (envelope 1.42x, `eval_steps` 169,297 in every one — the work never changed). Its minimum, -313ms, is above the clean-run budget the measured p90 implies (500 / 1.653 = **302ms**) and well above -the worst-case budget (500 / 2.280 = **219ms**). The row is inside the variance cliff by measurement, -not by anecdote, and the 516ms refusal is an ordinary member of this distribution. - -**The bound is a floor, not a bound.** Twelve runs on three hosts are a subset of the admitted -execution envelopes. A wider sample can only find a larger worst case, so 2.280x may only rise and the -implied budget may only fall. - -**Raising the ceiling remains not an option** for the reasons §5 gives. What this measurement -supplies is a re-derivation the authority already asked for: `docs/design-rung-drops.md`, *Per-claim -cost qualification is unavailable at the subject grain the gate consumes*, sizes its attention -constant as the ceiling over the largest inflation floor observed to date and states that the -constant **must be re-derived the moment a larger floor is measured**. The floor it was sized against -was 1.777 from a single 501→282 pair. This sample measures **2.280**, so the constant falls from 280ms -to **219ms**, and the row is updated accordingly. Restricting the derivation to rows at or above -200ms — where whole-millisecond quantisation cannot dominate — gives 1.874 and a constant of 266ms, -so the direction of the re-derivation does not depend on the small-baseline tail. - -**Which remedy the subject family takes is not decided here, and §6 has already narrowed it.** §6 -refuses both of §5's sanctioned arms for this family on evidence — there is no oversized subject to -trim, and no lane with its own dated ceiling exists to rehome into — leaving the shared fixed cost as -the only lever. What this section adds is the size of the gap that lever has to close: at the p90 -envelope the family needs to come down to 302ms on a clean run, and at the worst observed to 219ms. -The subject's cheapest of twelve observations is 313ms, so the required reduction is real and -quantified rather than "some". diff --git a/docs/plans/floor-memory-envelope-2026-09-12.md b/docs/plans/floor-memory-envelope-2026-09-12.md deleted file mode 100644 index 1226f59b5f6..00000000000 --- a/docs/plans/floor-memory-envelope-2026-09-12.md +++ /dev/null @@ -1,310 +0,0 @@ -# The floor cannot account for the phase memory it refuses on - -The floor refuses on memory, but its cost receipts publish evaluation time and -no resident-memory reading for the terminal-ledger phase. The refusing run -publishes neither cost artifact. **The exact terminal-ledger typecheck peak and -its run median are UNOBSERVED.** This is an instrumentation gap: the available -cgroup readings establish throttle contact but cannot account for the phase's -memory demand. No cap change is justified by this finding. - -The strongest available evidence is the natural experiment: the assignment -reports #11115 refused twice before a genuine floor SUCCESS at 42m23s, while -#11139 refused three times and never reached a verdict. The inspected jobs below -independently confirm a same-merge-subject refusal/success pair for #11115 and all -three refusals for #11139. The workload is marginal, not universally unable to -complete. These selected outcomes are not an estimate of the fleet's pass rate. - -This is a stopped-line measurement report under DESIGN §§2, 4d and 5, not a repair -or a claim that the requested phase measurement has been completed. No refusal, -merge-admission wiring, OOM classification, or runner allocation changes here. - -## Throttled slot envelopes: producer and subject - -The observations below come from `floor_cgroup_envelope` in -`src/v1/stage0/src/cli_run/required_floor_runner.rs`: its `[floor-cgroup]` rows -read `memory.high`, `memory.max`, `memory.current`, `memory.peak`, and event -counters from the named cgroup and its ancestors. Only the **runner service's** -rows enter this table, never the aggregate parent slice's peaks. - -| PR / attempt within run | CI job (raw log producer) | runner | largest printed service `memory.peak`, bytes | terminal result / refusal site | -| --- | --- | --- | ---: | --- | -| #11115 / 1 | [103493724795](https://github.com/gunb-ai/gunbc/actions/runs/34670663495/job/103493724795) | srv3-16 | 16106778624 | refused / `extdeps.git.object_store` | -| #11115 / 2 | [103498744149](https://github.com/gunb-ai/gunbc/actions/runs/34670663495/job/103498744149) | srv1-13 | 16114794496 | success | -| #11139 / 1 | [103491056928](https://github.com/gunb-ai/gunbc/actions/runs/34670582182/job/103491056928) | srv4-19 | 16106856448 | refused / `v2.workflow.floor2_prepared_subject` | -| #11139 / 2 | [103496272864](https://github.com/gunb-ai/gunbc/actions/runs/34670582182/job/103496272864) | srv4-21 | 16106946560 | refused / `v2.workflow.floor2_prepared_subject` | -| #11139 / 3 | [103505983106](https://github.com/gunb-ai/gunbc/actions/runs/34670582182/job/103505983106) | srv4-10 | 16107003904 | refused / `std.primitives` | - -All five read `memory.high=16106127360` (15 GiB) and -`memory.max=17179869184` (16 GiB). Their service-local high events grow; -the printed service OOM and OOM-kill counters remain zero. The refusal is -`MemoryStallRefusedPageThrash`, not evidence of an OOM kill. - -The checked subjects are the merge commits, verified from each job's checkout: - -- #11115: `fb82d4c10374e61a135f4038ef4f7b414bbcaa7d`, head - `53fc8e668b7812bd5f64f3d86feab18c6bb8b52b`. -- #11139: `45a4956c998e9b4ce11ca0c41e9dbb9907fdfbca`, head - `c54441d2a45d0e67c2162be5f01f7c45d276bde0`. -- Both merge subjects use base `6cdebf53c4b1648e282de2f5ed1896b434b15b34`. - -This is a five-job convenience sample, not a census of every attempt mentioned in -the assignment. In particular the selected #11115 run contains one refusal and -one success; this report does not claim to have found its other reported refusal. - -The median of the five **largest printed service high-water readings** is -16106946560 bytes, or 15.000763 GiB: 0.78125 MiB **above** `memory.high`. -The range is 15.000607–15.008072 GiB. These are observations of a reclaim-throttled -slot, not a distribution of uncensored working-set demand. `memory.high` is a -throttle and can be crossed. Calling this median “the typecheck fits within -0.8 MiB” would invert the evidence. - -There is no reset at the terminal-ledger boundary, no terminal-boundary cgroup -read in these logs, and no process RSS peak specific to this phase. The last -printed peak can even include work after ledger publication. Therefore neither -subtracting the threshold from this peak nor subtracting two cumulative peaks -answers the requested phase-memory question. Even the successful job is not an -uncensored measurement of demand on an unconstrained host. - -## What the cost receipts can and cannot decide - -For successful run `34670663495`, downloaded artifacts: - -- `required-floor-claim-cost` (artifact `10292490653`): - `required_floor_claim_cost.tsv`, summary `executed=3771`, columns for identity, - outcome, observed/censored wall and CPU time, eval steps, and safety limits. -- `required-floor-cross-claim-demand` (artifact `10292285976`): - `required_floor_cross_claim_demand.tsv`, summary `claims_absorbed=3771`, - `claim_cpu_observed_total_ms=42837`, `claim_cpu_censored_rows_excluded=0`. - Its header explicitly says `cost_columns=inclusive_of_callees_do_not_sum`. - -Neither schema has an RSS column, and neither file contains -`terminal_ledger` or `floor2_prepared_subject`. They observe claim evaluation, -not the later ledger resolve. The artifact list for refusing run `34670582182` -contains measurement and outcome receipts but neither of these cost artifacts. -An absent artifact is unavailable evidence, not zero cost. No cost-partition -instrument was run; no `OverAttributed` result is asserted here. - -The passing job does carry a phase-local **time** producer: -`publish_terminal_ledger` in `cli_run/terminal_ledger_publish.rs` prints -`phase=terminal-ledger-publish state=completed resolve_ms=31100 total_ms=32485 -rows=3771` at `2026-09-12T05:07:51.2992978Z`. -That measures one resolve, not 3,771 repeated resolves. - -## Why it costs what it costs: established facts and remaining discriminator - -`build_ledger_wire_ctx` uses `process_shared_index(source_roots)` and -`resolve_entry_with_index_for_discovery_corpus` for the exact entry -`src/v2/workflow/floor_terminal_ledger_wire.dag`. `run_required_floor` invokes -publication once after its claim fold. Its prepared graph is still live; -`prepared.subject_digest` also has later consumers. The shared parse/typecheck -index survives. This is **a fresh context beside retained floor state**, not a -fresh empty process. The source explicitly chose rebuilding over retaining an -additional context across the fold; whether that tradeoff still saves memory -needs measurement, not reliance on that comment. - -The named module is where the governor detects the stall. The four refusals -name three different modules, including `std.primitives`; this rules out treating -`floor2_prepared_subject` as a demonstrated unique allocation culprit. It does -not rule out a costly common dependency or retained state elsewhere. - -The specific recurrence repaired by [#10992](https://github.com/gunb-ai/gunbc/pull/10992) -and [#11032](https://github.com/gunb-ai/gunbc/pull/11032) was fixture preparation -re-derived across isolated claim frames. That evidence does not transfer to this -once-per-publication resolve. Re-typechecking an already prepared fact is a -separate possible recurrence; the index's cache hits, misses and retained graph -ownership must distinguish it from new work. No allocation profile here proves -either “quadratic cost shape” or “genuinely irreducible large closure.” - -There is an existing, more specific cost-shape lead: -`gunbc.recurring_failure_mode.entry_scoped_typecheck_rss_tracks_the_name_census`. -It records sleek-swift-789's 2026-09-11 arm64 `gunbc compile --entry -src/v2/workflow/floor2_prepared_subject.dag --target dag` observation: 16 resolved -sources, 5385 census-indexed modules, peak 9964140 KiB from `/proc//status` -`VmHWM`, and exit 0. **That is prior evidence from its named producer, not a new -measurement of this entry or an amd64 requirement.** The row does not pin an exact -source SHA for its binary; this report does not promote its provenance. Its -isolated peak cannot be added to an independently observed floor RSS: shared -pages, caches and lifetimes overlap. - -Current source confirms `tree_bare_census_for_root` builds the whole-root compile -closure's symbol census and caches it per root in `MultiEntryIndex`. The existing -`GUNBC_EDGE_INDEX_CENSUS_TRACE=1` miss trace identifies both root and index. Joining -those identities across preparation and ledger resolve is a concrete discriminator -for repeated census construction. The old isolated observation and current source -make this a stronger lead than assuming the small named module is intrinsically -large, but do not establish the allocation owner in these five runs. - -The entry is **already scoped**. The wire imports identity, disposition and safety -vocabulary; `v2.workflow.required_floor` also imports roster/policy modules. -Narrowing further means separating those concepts at their existing authority -boundaries, not adding an `--entry` flag that is already present or omitting -required witnesses. A smaller import closure may still pay the whole-root census; -source size or an import grep is not a measured byte saving. - -## Ranked repair experiments and costs - -These are ranked by boundedness and expected diagnostic value. Savings are -unmeasured unless explicitly labeled otherwise; implementation costs are estimates. - -| Rank | Candidate | Required discriminating observation | Cost / risk | -| --- | --- | --- | --- | -| 1 | Remove unnecessary co-residence at the terminal boundary | Read current RSS, cgroup charge and live graph/cache ownership immediately before resolve; compare the same subject after releasing only objects with no remaining consumers | Small ownership audit; potentially substantial memory benefit, presently unquantified. Do not clear reusable caches indiscriminately or retain another whole context across the fold. | -| 2 | Investigate the existing whole-root name-census cost-shape lead | Join census misses by root and index; attribute allocations and retained maps across preparation and this resolve | Small diagnostic exercise first; a compiler repair could be medium-to-large. Prior scoped evidence identifies a lead, not a measured saving on this floor. The earlier per-claim fixes are not its evidence. | -| 3 | Narrow the wire's dependency closure through existing concept boundaries | Exact compiler-produced closure census plus matched before/after phase peak and identical rendered ledger/refusals; establish that census cost also falls or is already shared | Small-to-medium model move with import consumers to update. Could reduce the measured 31.1s resolve and its memory increment; neither saving is established. | -| 4 | Size/place the runner for measured co-resident demand | Completed amd64 phase peaks and cgroup anonymous/file/kernel charge under an externally enforced, characterized envelope, including repeat variance | Operational allocation and reduced fleet concurrency; no supported GiB request yet. Legitimate if demand remains after ranks 1–3 are ruled out, not a cap-only green. | -| 5 | Split execution at the terminal boundary | Show that serial release plus a separately executed ledger producer preserves subject binding, all terminal identities and all refusal arms | Largest complexity: process/artifact boundary, startup and parse costs, ownership and failure propagation. Premature without proving retained co-residence is unavoidable. | - -**Plain answer on sizing:** the current slot demonstrably reclaims at 15 GiB on -both heads. We cannot yet say how large a box this phase requires. There is no -evidence here for declaring 16, 18 or 20 GiB sufficient. - -## Did #11078 measurably increase cost? - -**No consistent wall-time increase is visible in this small before/after sample; -the census's isolated CPU and typecheck increment remain unobserved.** This is -not a finding that its incremental cost is zero. - -[#11078](https://github.com/gunb-ai/gunbc/pull/11078) merged at -`2026-09-12T03:15:29Z` as `f4b63e3b80ba142d8650e2d2d033881e7ba04a9e`. -It expanded the per-claim-scope ambiguity report from counts to named rows. -That report and `tree_bare_census_for_root` are different producers; sharing the -word “census” does not make the older RSS attribution apply to this change. - -Two nearby completed main runs before the merge and two after it give: - -| Main run / floor job | source SHA prefix | relation to #11078 | completed fold wall, ms | completed ledger resolve wall, ms | observed claim CPU total, ms | -| --- | --- | --- | ---: | ---: | ---: | -| [34665537725 / 103478308337](https://github.com/gunb-ai/gunbc/actions/runs/34665537725/job/103478308337) | `b2bda4edda7d` | before | 526589 | 44878 | 44027 | -| [34668064603 / 103484007739](https://github.com/gunb-ai/gunbc/actions/runs/34668064603/job/103484007739) | `c25ab6d87477` | before | 524850 | 34747 | 46789 | -| [34669916077 / 103489226361](https://github.com/gunb-ai/gunbc/actions/runs/34669916077/job/103489226361) | `6cdebf53c4b1` | after | 558781 | unobserved: refused | unobserved: no summary | -| [34672874238 / 103499666826](https://github.com/gunb-ai/gunbc/actions/runs/34672874238/job/103499666826) | `88c67ace130ef` | after | 487936 | 40831 | 42202 | - -Membership is verified with `git merge-base --is-ancestor` against #11078's -merge commit, not inferred from job completion time. The before runs also lack -the added `names_distinct` field; the after runs print it. Cancelled runs are -excluded and this convenience sample is not a population trend estimate. - -Producers: `run_required_floor` prints the completed -`floor: evaluating N / N (Xms)` fold span; `publish_terminal_ledger` prints -`resolve_ms`; the `[cross-claim-demand] claims_absorbed=...` summary prints -`claim_cpu_observed_total_ms`. The CPU totals cover 3754, 3781 and 3760 completed -claims respectively, with zero censored claim rows in those three summaries. -They are marginal claim-evaluation CPU, **not whole-floor CPU or census CPU**; -they cannot measure the cost of scope construction or reporting outside the -claim window. Nor is ledger `resolve_ms` a pure typecheck timer. - -The completed post-merge ledger resolve, 40.831s, lies inside the pre-merge -34.747–44.878s range. Post-merge fold times straddle the pre-merge range; the -completed post-merge claim CPU total is lower. Hosts and source populations -differ, and one post-merge ledger resolve is censored. These observations do not -identify a regression caused by #11078, nor rule out a smaller increment hidden -by those differences. Its specific cost needs a producer-local bracket; -whole-run wall/CPU cannot supply that attribution. No retained-state experiment -or new heavy run was added for this comparison. - -## Next-rung capability and reproduction - -**Next-rung trigger:** the floor's own receipts publish per-phase resident-memory -readings on refusing runs as well as successful ones, sufficient to compare the -terminal-ledger typecheck phase's peak with the admitted budget. The readings must -carry subject identity, phase boundaries, resource/budget scope and unavailable -or censored standing. A named file or access grant alone does not discharge this -capability. The existing failure-mode row -`gunbc.recurring_failure_mode.instrument_blind_spot_anti_correlated_with_severity` -carries this specimen; it does not assert that the memory behavior is repaired. - -Fetch each of the five jobs once, preserving its id in the filename: - -```sh -gh api repos/gunb-ai/gunbc/actions/jobs/103498744149/logs --allow-escape-sequences > 103498744149.log -gh run download 34670663495 -n required-floor-claim-cost -D cost -gh run download 34670663495 -n required-floor-cross-claim-demand -D demand -``` - -For each job log, select `[floor-cgroup]` lines whose `level` ends in the runner -`.service`, take the maximum printed `peak`, then the median across the five job -values. Convert bytes to GiB by division by `2**30`; convert the difference from -`high` to MiB by division by `2**20`. Preserve the measurement grain stated above. -Read the actual refusal lines, not the echoed shell grep that classifies them. - -A new measurement must retain the real floor's baseline, bracket **typecheck** -within this entry resolve (not all preparation), record current and peak process -RSS separately from cgroup charge, retain missing/censored distinctions, and -name exact source and binary identities. An isolated entry run is a useful -closure-increment control, but cannot replace the in-floor observation. Repeat -the same subject to report a phase-peak median; a refused attempt cannot supply -its unobserved completing demand. Keep the existing MemoryStall refusal active. - -Two execution routes were checked in this session and could not provide that -measurement: - -- [BuildBuddy envelope probe](https://app.buildbuddy.io/invocation/b6c1a5db-8f85-4197-a915-cea73a5745ee) reached amd64, but `/proc/self/cgroup` read `0::/` and both - `/sys/fs/cgroup/memory.high` and `memory.max` were absent. The command stopped - before building or running the workload. A separate - [invalid-Cargo-flag control](https://app.buildbuddy.io/invocation/e56f48bc-d69a-4d8c-a6fc-d02fcf2c079d) - reached remote Cargo and failed as expected; remote transport itself - was working. No budget was invented to pass admission. -- The parent's bounded local alternative was - `systemd-run --user --scope -p MemoryHigh=15G -p MemoryMax=16G ...`. - This container has no `systemd-run`, its cgroup directory is not writable, and - it has no built local interpreter. Its visible 31 GiB container limit is not - permission to consume the shared host slice. No local heavy run was started. - -An arm64 scoped control, if subsequently enabled, establishes a cost-shape lead; -it does not establish an amd64 cap. The exact runner phase peak and median remain -the outstanding measurement, explicitly reported to `bright-eagle-728`. - ---- - -## Outstanding state of the sizing response, 2026-09-14 - -This report concluded that no cap change was justified *by its own findings*. The -sizing change was authorised separately and is carried on `session/slot-sizing-26-25` -(PR #11204). It is **not converged**, and this section records what remains so the -state is readable without reconstructing it from the branch. - -**Live fleet, unchanged.** srv1 carries `MemoryHigh=16106127360` over 21 units. No -host effect has been performed by this work: no cap write, no slot retirement, no -canary. Every claim below is about source and CI, never about a host. - -**What the branch establishes in source.** The slot ceiling moves to 26 GiB -`MemoryMax` / 25 GiB `MemoryHigh`; `cores_per_slot` rises 6 → 10, so CPU binds every -host at 12 and the fleet commits 36 rather than 68. A raise is gated behind a -retirement transaction: the required-retiree set is derived from a declared prior -width (`gunbc.runner_width_transition`) and reads no observation, so it cannot shrink -when a unit stops being listed; operator interruption authority is checked as -*coverage over* that obligation and never contributes members to it; `mask --now` is -rendered into the hashed fleet artifact and executed only by fleet apply; and -completion comes from an independent readback of `UnitFileState`, `ActiveState` and -the real cgroup. Retirement disposition participates in the plan/apply fingerprint, -so a plan built while a unit read complete refuses if apply reobserves it populated. - -**srv2 is refused, not narrowed.** Its width resolves to a typed refusal and no -`RunnerHostDeploy` is constructed for it at all; slot planning, cap construction and -the cap writer all consume that same admission, and its generated sudoers projection -renders teardown and host-wide grants only — no slot-install, no activation, nothing -that could widen a host nobody can size. - -**What is NOT established, and is the work remaining.** - -1. **No wet proof.** Nothing has executed `mask --now` on a host. Every retirement - result is hermetic. The srv4 canary — interrupt → observed retirement → guarded - raise → effective-limit readback → no-change second pass — is the first real - proof and has not been run or authorised. -2. **No terminal green.** The last exact-head required run was red. Two failures are - measured as inherited rather than assumed, each reproduced at the committed head - with the branch's later changes stashed: - `witness_fabric_cell_absent_address_plans_add_and_never_change` and - `srv4_sccache_prereq_pins_version_and_digest`. -3. **One acceptance condition is specific and unmet.** - `test.claim.runner_slot_provision.no_deploy_row_out_commits_the_memory_budget` - must appear in the terminal disposition as `planned` (or - `planned_as_changed_witness`) **and** `passed`. At `890c264be36` it was - `declined_outside_required_gate` — its whole module carried no admitted - prefix — so the aggregate `planned=3938 executed=3938` was true and said nothing - about it. The prefix is now enrolled; the run proving it selected has not happened. - -**The instrumentation gap this report opened is still open.** Nothing here measures -the terminal-ledger phase peak. The sizing change was justified by the floor's -*uncensored demand* reading, not by the phase measurement this document says is -missing, and raising the ceiling does not close that gap. diff --git a/docs/plans/floor-shared-fill-ledger.md b/docs/plans/floor-shared-fill-ledger.md deleted file mode 100644 index 952bd2e7a66..00000000000 --- a/docs/plans/floor-shared-fill-ledger.md +++ /dev/null @@ -1,226 +0,0 @@ -# The floor's shared-fill ledger — measuring what a slow witness actually costs - -gunbc#8455 established that the floor's per-row wall time is not always the row's cost and named -the repair it did not perform: *attribute shared cost to the entry rather than its first row*. -This is that repair, plus the defect's mechanism root-caused to source. - -## The mechanism, named - -The floor builds a **fresh evaluation frame per claim**, so the sharing #8455 measured is not an -eval memo (a per-claim memo cannot survive between claims) but a population of -**process-global corpus-scan caches in the seed**, each filled at most once per process and read -free thereafter: - -| cache | what the fill is | -|---|---| -| `reference_edges` | one O(corpus) tokenize+parse pass over the whole pool | -| `module_graph_facts` | the live module graph over the pool roots | -| `module_path_index` | module → path over the source roots | -| `inert_carrier_data`, `complexity_linearity_audit`, `complexity_linearity_wildcard`, `fallback_arm_census`, `non_fold_residue`, `doc_graph_report`, `test_migration_behavior_discovery`, `test_migration_debt` | whole-corpus census reports | - -The #8455 specimen resolves exactly onto the first row. `g2_data_reference_under_selection` -calls `specimen_projection` → `v2.lens.live_read_classification` -`live_read_selection_manifest_live` → `dependency_resolution_facts_live`, whose seed realization -`cli_run.rs` `reference_resolution_facts` performs that whole-pool parse behind -`REFERENCE_EDGE_CACHE`. The first claim pays the pass (12197ms); the second reads the map -(31ms). Same call, 393x, 35ms apart. - -## Why the existing number cannot answer the paring question - -Nothing is computed twice — the caches are correct and the sharing is the saving. What is wrong -is the **attribution**, and it matters because the ceiling refuses per row: - -- A refusal names the **first toucher**, whose own cost may be milliseconds. -- **Splitting** such a row moves the charge to whichever fragment runs first - (`gunbc.witness_row_cost` `witness_decomposition_does_not_reduce_entry_cost_note`, observed - rather than predicted). -- **Removing** such a row does not remove its cost either: the next claim to touch the fill pays - the same seconds, so a quarantine decided on the per-row number can trade a named refusal for - an unnamed one in a module that was fast before. - -## What the ledger records, and the quantity it yields - -`cli_run/shared_fill.rs` records, per fill: the cache, the key it is declared on, what the fill -cost, which claim paid it, and every later claim and module that read it. `[floor-shared-fill]` -lines are emitted once at the end of the fold. The disposition on each line is derived by -`gunbc.witness_row_cost` `shared_fill_disposition` from the payer and the consumer breadth — -three states because three remedies: - -- `outside-fold` — preparation's cost. No witness can be pared to recover it. -- `exclusive` — one module consumed it. It is removable with that module. -- `shared` — more than one module consumed it. It survives any single removal. - -From that, joined against the `[floor-witness-slow]` rows the floor already prints: - -``` -what removing module M saves = M's rows' wall time - - fills M paid that are `shared` - (+ nothing for `outside-fold` fills at all) -``` - -## What this is not - -It refuses nothing, skips nothing, changes no verdict, makes no witness faster, and takes no -position on which rows to pare; it prints a table (DESIGN §5's sanctioned stopped-line audit — it -reports, it does not green) measuring the quantity that decision needs and nobody currently has. - -`unattributed_hits` on the total line is the honest bound: reads served by a fill this ledger did -not observe. Nonzero means the shared figure is a **lower** bound on the sharing. - -## Two bounds on reading the output - -**Run-to-run variance is real and is not this ledger's subject.** `where_refinement_cast_literal_oci_other_digest_algorithm_accepts` measured 1505ms, 1274ms and 1364ms — the first two on *byte-identical* input (a rerun of one frozen merge ref) — an ~18% spread straddling a 1500ms ceiling (sharp-raven-273, 2026-08-18, runs 32155779798 / 32153078487). For a row within ~20% of its ceiling, which side it lands on is partly a fact about the runner; a smaller fill difference is not readable from one run. - -**A right-censored row cannot be decomposed.** Exclusive-vs-shared subtracts from a *completion* -cost, so a row whose figure is its interrupt point has nothing to decompose. On main run -32177951514, 84 of the 97 budget-refused rows sit pinned at 1552–1560ms and must be excluded from -any ranking; the deadline is cooperative (polled every 4096 evals), so the other 13 ran past it -with real overshoots — up to `grounding_lens_whole_tree` at 21868ms. The `[floor-witness-slow]` -channel is the better census (720 rows, all with measured elapsed), on the same condition: its -~85 rows in the pinned band are censored, the rest are measurement. - -**The signature to look for, and the one that refutes the model.** A shared fill looks like *one -large row and cheap siblings* — the g2 pair's 12197ms then 31ms. Several siblings at roughly the -*same* cost is the opposite evidence: nothing was amortized, each pays its own way. The eight -`effect_reach_test` rows clustered at 1788–2134ms are therefore a prior *against* shared fill in -that module, and the ledger says so directly rather than by inference. - -## Receipt — run 32192150969, 9425 claims, one fold - -19 fills. The red is main's inherited (`failed=0 stale_quarantine=5 budget_refused=102`), not -this diff's; the ledger reported from a complete fold. - -**Read `fill_ms` as SELF time.** The first receipt exposed an instrument defect, fixed in the same -PR: these caches compose, so an outer fill's wall contains its inner fills'. A 17890ms -`module_graph_facts` fill contained a 12054ms `module_path_index` fill and a 5141ms -`reference_edges` fill — ~695ms of its own. The first receipt's `TOTAL fill_ms=302362` was an -inclusive over-count; the corrected self-time total is **~180s**. The per-fill -payer/consumer/disposition columns were never affected. - -### The finding: one row's entire cost is a fill 29 other modules read - -``` -cache=module_path_index key=dag+src/v2 fill_ms=51508 - paid_by test.claim.dissolution_census_witness_test.unbound_dissolution_empty_literal_refuses - read by 139 claims across 29 modules disposition=shared -``` - -That payer is one of the five rows in the corpus with an *exact* cost: **51620ms**, of which the -fill is 51508ms. So **99.8% of that witness's measured cost is a computation 29 other modules -consume**, and it is the run's only `shared` fill. Removing the witness recovers ~112ms and hands -51.5s to whichever of the 29 runs first — the paring question answered at identity grain, on the -row where it matters most. - -### And the opposite finding, which matters just as much - -The 86741ms row — `extdeps_scope_placement_gate_loudness_witness` -`red_seed_runner_failure_detail_projects_located_receipt`, the most expensive exact row in the -corpus — paid a 29017ms `module_graph_facts` fill and a 24965ms `reference_edges` fill, **both -`exclusive`**: no other module reads either. Its cost is its own and it is a real paring target. -Two rows adjacent on a slow list, opposite answers; per-row wall time cannot distinguish them. - -### Preparation's share, which no paring can recover - -Three fills totalling ~108s inclusive are `outside-fold` — a 55392ms `module_graph_facts`, a -35060ms `module_path_index`, a 17646ms `reference_edges`, all paid before any claim ran. That -cost is preparation's; quarantining every witness on the floor would not move it. - -### Second receipt — run 32196069889, 9001 claims, 8 fills: the relocation, at identity grain - -The first receipt reported consumer *counts*; this one names them, and caught the same fill -changing hands. - -``` -run 32192150969 cache=module_path_index key=/dag + /src/v2 fill_ms=51508 - paid_by dissolution_census_witness_test.unbound_dissolution_empty_literal_refuses - read by 139 claims across 29 modules disposition=shared - -run 32196069889 cache=module_path_index key=/dag + /src/v2 fill_ms=45075 - paid_by emitter_nested_refinement_cast_witness_test - .single_refinement_carrier_emits_no_unsupported_cast - read by 94 claims across 20 modules disposition=shared -``` - -Same cache, same key, `paid_by` changing hands after #8457 quarantined the first payer. The second -payer is **main's only failing witness**: 45784ms on this run, of which 45075ms is this fill — -~98.5%. **Pin that share to the run**: the same row measured 45784, 45941, 46714, 50532 and 52982 -across five runs the same day, so the fill *fraction* is robust and the denominator is not. Under -every reading, nearly all of the remaining red is one build that 20 other modules read for free. - -### The consumer set shrank, which prices the quarantine itself - -The two receipts also price what #8457's quarantine bought on this axis — close to nothing for -part of the population: - -``` -pre-quarantine read by 139 claims across 29 modules -post-quarantine read by 94 claims across 20 modules -``` - -Nine modules and 45 claims left the consumer set. A consumer is a **free rider** — it reads a fill -someone else built, so its marginal cost for that fill is ~zero. Quarantining a rider removes -coverage and recovers nothing here; quarantining the *payer* handed the bill to the next module and -produced main's sole red. - -**The honest limit:** the first receipt reported consumer *counts* only, so those nine modules -cannot be named retroactively — the delta is consistent with riders having been quarantined, not -proof of which. That is why the line now names consumers; from here the question is a set -difference, not an inference. - -The 20-module list is two lists at once: who inherits the bill if the current payer is quarantined, -and who is first to come **back** once the fill belongs to preparation. - -Evaluation order is alphabetical and the riders are named, so the prediction is checkable: -quarantining this payer hands ~45s to `emitter_optional_payload_cast_witness_test` next — the -fourth hop of a chain that has already run three times (`dissolution_census` → `emitted_lib_rs` → -this row). - -It remains the **only** `shared` fill — 8 fills this run, one with more than one consuming module. -The fix is `gunbc#8470`, which pays this index during preparation; the self-verifying -post-condition is that this row reappears as `paid_by= disposition=outside-fold`. - -The nesting correction shows here too: `module_graph_facts key=dag` reports -`fill_ms=2990 inclusive_ms=55048` — 52s of it was the path-index and reference-edge scans it -triggered, which the first receipt's total counted twice. - -### The prediction, confirmed on an experiment nobody had to build - -When #8457 quarantined 102 over-budget witnesses, this model predicted the cost would not leave -with them — the next module to touch the fill would pay it. Main before (run 32177951514) against -main after (run 32193032348), **one module, one run, one tree**: - -``` -single_refinement_carrier_emits_no_unsupported_cast 746ms -> 45941ms 61.6x -nested_refinement_carrier_deficit_remains_observable 728ms -> 763ms flat -``` - -Neither row changed; 38 modules were removed from the fold ahead of them. Were the 45.9s the row's -own work it would have been 45.9s before; were it a general slowdown the sibling would have moved -too. It is the first-touch signature exactly: the fill that row now pays was previously paid by -one of the quarantined 102, and the sibling rides it free in both eras. **Quarantining a first -toucher relocates the bill; it does not retire it.** (Measured by tidy-lark-471. It landed in the -failures channel rather than the refused one, which is why a scan for new refusals read the run as -refuting the prediction.) - -### What `exclusive` does and does not establish - -`exclusive` means **no other module in the executed population read this fill on this run** — -not that none ever would. Remove the payer and a would-be consumer could pay the same fill next -run: the first-toucher trap one level up, invisible from inside one run. - -Two things bound that doubt. The `key` is the fill's *declared inputs*, so an exclusive fill on a -key no other consumer requests is exclusive by construction, not luck — the 29017ms -`module_graph_facts` above is keyed on `dag+src/v2` roots that the shared 51508ms -`module_path_index` fill also serves, while the 17890ms one is keyed on a four-root pool only its -own module asks for. And the answer is decidable by execution: run the fold with the payer absent -and read the ledger again. Until then an `exclusive` row is one run's observation, and a recovery -estimated from it is an upper bound. - -A second edge corrects a reading of the same rows from isolation. A witness whose SUBJECT is the -corpus is not made cheap by shrinking the corpus: the 71s row measures 70–85ms in a 67-module -closure and 71060ms against the floor's 2376-module subject, with the witness unchanged. -Isolation structurally cannot price that class, which is why the ledger measures on the floor -path (quiet-ibex-39, 2026-08-18). - -`unattributed_hits=1`: one read was served by a fill this ledger did not observe, so the shared -figure is a lower bound. Small, but reported rather than rounded away. diff --git a/docs/plans/floor-time-attribution-2026-10-02.md b/docs/plans/floor-time-attribution-2026-10-02.md deleted file mode 100644 index 6cb311473d2..00000000000 --- a/docs/plans/floor-time-attribution-2026-10-02.md +++ /dev/null @@ -1,156 +0,0 @@ -# Where the required floor's time goes (2026-10-02) - -Status: investigation and plan. No code lands from this note; the repairs are dispatched separately. - -Trigger: #12506 (N7-2) was refused by `v2.workflow.floor_enrolment_margin` (55 of 96 newly enrolled -identities over margin). The question asked was where the floor's time goes, and whether that -overage is one case of a general cause. Raising the budget is not on the table. - -## Instrument and subject - -The subject is main at `d8eebaea3f`, merge_group run 37070592712, `floor` job 111048851355. The -#12506 reading is run 37071586760, job 111052035724. - -**No new instrument was minted.** The floor already prints every carrier this needs. They appear -in the job log: - -| carrier | grain | producer | -| --- | --- | --- | -| `[floor-seam-cpu] seam= thread_cpu_ms=` | cumulative thread CPU at each seam boundary | `claim_executor --required-ci` | -| `[floor-shared-fill] claim= marginal_cpu_ms= fill_cpu_ms=` | per claim, split into its own work and the shared fill it paid for | `cli_run::shared_fill` | -| `[floor-shared-fill] cache= key= fill_ms= consumer_claims=` | per shared computation, with the count of claims that read it | `cli_run::shared_fill` | -| `[cross-claim-demand] producer=

claims= evals=` | per producer, recomputation across claims | `claim_executor` | -| `compile. done in` | each host compile stage | seed compile pipeline | - -To re-derive, fetch the log with -`gh api --allow-escape-sequences repos/gunb-ai/gunbc/actions/jobs//logs`. Then difference -consecutive `floor-seam-cpu` values, and sum `fill_cpu_ms` and `marginal_cpu_ms` per claim and per -module. - -**Instrument defect found along the way.** The two durable carriers are no longer uploaded: -`required-floor-claim-cost` and "the cross-claim demand TSV this run uploads" (that phrase is the -log's own). On both runs above, the only artifact is `required-ci-measurement-receipt`. -`tools.floor_cost_distribution_instrument` therefore has no input, and the log's sentence about the -upload is false. Restoring the upload is PR 3 below. Until then the attribution can only be read -from the log, which is a scrape. - -## Structural predictions, stated before the confirming read - -I wrote these down from DESIGN §2, §3 and §6b before differencing the seams: - -- P1. Very little floor time is claim marginal work. Most of it is computation shared by many - claims, paid either in preparation or by whoever touches it first. -- P2. The witness families that compile fixtures pay a compile of the fixture's whole live import - closure once per distinct fixture, even though preparation has already compiled that closure. -- P3. The N7 overage is the same shape one layer up: a pure function of module-constant text, - evaluated once per claim, with no provider joining the claims. -- P4. Some preparation work is demanded by the gate closure and read by no claim the run plans. - -## Attribution (one reading; the carriers above are the authority) - -Floor job wall time was about 47.5 minutes. Thread CPU, differenced by seam: - -| slice | CPU | what it is | -| --- | --- | --- | -| build + parse + declarations | ~4m wall + ~61s | `cargo build`, parse of 7347 files | -| diff planning (base decl census, edits, nominal seeds) | ~100s | | -| **prepare-closure-resolve** | **~646s** | seed compile of the 2585-module gate closure; `compile.reconcile` alone took 8 minutes wall | -| **prepared-subject-warm** | **~377s** | 343 `cross_claim_pure_share` fills ≈ 316s, plus effect inputs | -| bare-reference edge index + discovery + site projection | ~120s | includes a second full parse of 7286 files (`reference-reading-parses full_parses=7286`) | -| **claim-evaluation-fold** | **~715s** (732s wall) | 673 claims | -| publication (wet witnesses) | ~6.5m wall, ≈0 thread CPU | `[local-repo-wet]` rows | - -Inside the fold, the 197 claims with fill lines account for about 667s. Of that, about 666s is -`fill_cpu_ms` and about 1s is marginal. The fill is host compiles: 90 `compile.emit` runs (~539s) -plus the frontend and reconcile runs of 244 fixture compiles. Fourteen v1-checker witness modules -pay all of it, led by `test.claim.declared_type_inhabitance_direct_call_witness` (~210s), -`infer_function_value_argument_arrow_witness_test` (~103s) and -`declared_type_expected_type_path_witness` (~84s). Summed across all claims, the run's own -`claim_cpu_observed_total_ms` is about 10s. - -Of the 343 pure-share fills paid in preparation, **298, totalling about 277s, report -`consumer_claims=0`**: no claim this run planned read them. The 45 that some claim did read total -about 39s. - -P1, P2 and P4 hold on this reading. P3 holds on #12506's `[cross-claim-demand]` rows: -`cref_assemble claims=6 evals=6`, `fps_assemble claims=5 evals=5`, `mbp_assemble claims=2 evals=4`. -Each claim re-runs `assemble_program_from_ingest` on the same module-constant fixture, at roughly -0.6–0.8s per evaluation and about 250k–325k eval steps per claim. The new-witness budget is 72,300 -steps. - -## Causes, ranked by floor time removed - -**C1. A fixture compile re-compiles the live closure the floor has already prepared (~10 min).** -The earliest unjustified boundary is `cli_run::emit_host` `compile_dag_diagnostic_census_uncached` -and its sibling `compile_dag_rust_emit_check`. For each distinct fixture they rebuild -`build_module_path_index_from_witness_roots`, resolve the fixture's imports from disk, and run -`compile_sources` over the whole closure. The contract only requires the fixture module to be new; -the closure is the prepared subject, which is already compiled under the same inventory digest -these memos key on. The memo is keyed per source, so it only absorbs repeats of the same text and -cannot reach the shared work across sources. -Repair class: **share a context at the least common ancestor.** Compile the fixture module against -the prepared closure's resolved and reconciled state, so only the new module is compiled. This is -#11401's R1 at fixture grain. Keep one claim that runs a fixture compile cold, as the inhabitance -claim for the real path. - -**C2. The whole gate closure is reconciled for a small diff (~8–11 min).** Here `seeds=259` -produced `closure=2585`, and `compile.reconcile` took 8 minutes. I have not located the boundary -yet. The reconcile cost overlaps the type_env PR-2 slowdown, which calm-pike-525 owns (#13008, -#13009). Take that attribution as the input here; I am not redoing it. The open question for this -plan is narrower: does any planned claim demand a reconciled closure, or only a resolved one? If -only resolved, reconcile beyond the planned claims' reach is redundant demand. -Repair class: **delete redundant demand**, if that question confirms it. - -**C3. The warm phase fills producers no planned claim reads (~4.6 min).** The roster -`v2.workflow.floor_pure_producer_share` is warmed for everything in the gate closure, but the run -plans 673 of 26,150 declared claims. The earliest unjustified boundary is the warm phase's -demand: it is keyed on the roster intersected with the closure, not on the roster intersected with -the planned claims' reach. -Repair class: **delete redundant demand.** Warm only producers that some planned claim reaches. -`body-reach-selection` already computes that reach. -Before cutting, one caveat needs checking. `unattributed_hits=399` counts hits outside the fold, -so confirm the zero-consumer rows are not being read by preparation itself. - -**C4. Small items.** There is a second full parse of the pool after preparation (~1 min; share -the parse). The ~6.5 minutes of wall-bound publication and wet-witness time is not analysed here. - -## Is #12506's overage a general cause? Yes: C1's shape, one layer up - -The N7 witnesses intentionally execute the real front end over a fixture; their header argues -that the red is not authorable against a supplied index. A real path is legitimately demanded once -per fixture. What is not legitimate is paying it once per **claim**. The floor isolates claims, so -every claim is an isolated consumer of a pure function of module-constant text. DESIGN §2 says -such consumers create **one reuse obligation at their least common visible ancestor**. Today that -obligation is discharged only by hand. An author must make each producer nullary, roster it, and -re-verify, which is the restructure swift-wren-597 and bright-ant-369 are now doing. The roster -has 343 entries, and that number grows with every PR in the class. The next N7 PR will hit the -same wall for the same reason. - -Two facts make it general. - -1. **The obligation is derivable but authored.** `[cross-claim-demand]` already computes the - signal: claims ≥ 2, keyed arguments, module-constant input. Rostering by hand is a second - authority over a fact the demand graph carries (§3). -2. **The margin charges the shared fill to every claim.** `floor_enrolment_margin` is an honest - policy budget under §5. It is denominated in deterministic eval steps, from a declared p90 - envelope. But it is applied to marginal work plus fill. Without a provider, every claim of the - module re-pays the fill. So splitting a witness into more claims multiplies cost rather than - isolating it, and the budget measures the absent provider rather than the claim. - It is the first-toucher defect of gunbc#8455. - -## Recommended first PRs - -1. **C1: compile fixtures against the prepared closure** in `emit_host`. This is the largest - removal, about 10 of 12 fold minutes. Postcondition: the 14 modules' `fill_cpu_ms` collapses - to the fixture module's own compile, and `[floor-shared-fill]` shows one closure fill - `paid_by=`. -2. **C3: scope the pure-share warm to the planned claims' reach.** This is small and about 4.6 - minutes. Postcondition: `prepared-subject-warm` reports no `consumer_claims=0` fills. -3. **Derive the pure-share obligation from `[cross-claim-demand]` and restore the upload of the - claim-cost and cross-claim TSV artifacts.** This also restores the named instrument this note - had to read from the log. Postcondition: an N7-class witness module whose claims share one - fixture is admitted with no rostering edit, and `floor_cost_distribution_instrument` runs - against a current run. - -Item 3 is the one that releases the N7 lane in general. Items 1 and 2 are the larger time -removals. diff --git a/docs/plans/gcp-iam-bootstrap.md b/docs/plans/gcp-iam-bootstrap.md deleted file mode 100644 index 0f16550d167..00000000000 --- a/docs/plans/gcp-iam-bootstrap.md +++ /dev/null @@ -1,280 +0,0 @@ -# Commission the approval-gated GCP IAM workflow - -The existing `gcp_iam_converge` fleet workflow needs its own cloud identities -before it can request approval and configure other workflows. This bootstrap -commissions that foundation from the existing IAM declarations. It does not -depend on the workspace allocation branch. - -## Entry points - -`gunbc.auth.gcp_iam_bootstrap.iam_bootstrap_plan` prints the exact roles, -permissions, bindings, deny permissions, and workflow admission condition without -cloud effects. `iam_bootstrap_apply` uses an explicitly supplied operator token -file and a fresh receipt identity: - -```bash -export GUNBC_GCP_ACCESS_TOKEN_FILE=/path/to/private/operator-token -export GUNBC_IAM_BOOTSTRAP_RECEIPT=operator-bootstrap-unique-attempt -export GUNBC_IAM_BOOTSTRAP_AUTHORITY_ID=stable-authority-operation -export GUNBC_IAM_BOOTSTRAP_AUTHORITY_EXPIRES_AT=2026-09-28T23:00:00Z # illustrative; set once for the intended operation -export GUNBC_IAM_BOOTSTRAP_PROBE_ID=stable-probe-operation -export GUNBC_IAM_BOOTSTRAP_PROBE_EXPIRES_AT=2026-09-28T23:00:00Z # illustrative; set once, at most one hour away -systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=0 --quiet \ - ./target/release/gunbc run --source-root dag --source-root src/v2 \ - --entry dag/gunbc/auth/gcp_iam_bootstrap.dag \ - --function iam_bootstrap_apply -``` - -`iam_bootstrap_plan` (same roots and entry, `--function iam_bootstrap_plan`) prints the -operator-supplied intent variables the run reads; this block illustrates them. - -The token file must be private and short-lived. No token belongs in a command -argument, source file, receipt, or GitHub variable. The ongoing workflow never -falls back to this operator token. No service-account key is created. - -The missing deny-policy permission is a modeled dependency, not a console task. -Under the operator's explicit authorization, the organization IAM writer can publish a -time-limited `roles/iam.denyAdmin` grant to `user:brian@gunb.ai`, install the -protection policy, then remove exactly that grant. GCP documents -[Deny Admin](https://docs.cloud.google.com/iam/docs/deny-access) and -[time-limited conditional bindings](https://docs.cloud.google.com/iam/docs/managing-conditional-role-bindings). -The account used for continuing automation never receives this grant. - -The operation ID and absolute UTC expiry identify one desired authority lease. -The maximum remaining duration is one hour. Retries retain both values and the -journal; they do not recalculate the expiry. A new lease requires a new explicit -intent. The current credential must be able to read/write organization IAM; if it -cannot, convergence reports that upstream dependency. This is an observed -boundary, not a declaration that organization IAM authority must forever be manual. - -## What bootstrap owns - -1. Observe and enable the declared APIs with independent readiness readback, then - create or exactly read back the dedicated IAM workflow pool/provider and its - observer/apply accounts. Create only bare pools and accounts for declared - target federations, so resource-local administrative roles can be attached. -2. Create or exactly read back the existing four apply role definitions and - three resource-specific observer roles plus one single-permission bootstrap probe role. Existing drift refuses adoption. -3. For an absent deny policy, converge the temporary operator authority, install - and read back the policy, and retire the exact temporary grant. Cleanup is - attempted on ordinary failure too; later capabilities and workflow trust require - successful cleanup. Existing exact deny policies need no new authority grant. -4. Add declared capability bindings with policy etags, preserving foreign cells. -5. Converge a time-bounded probe grant for the verified operator on only the - observer/apply accounts. The custom role contains only - `iam.serviceAccounts.getAccessToken`. Check access using 600-second tokens: - observer reads must work, and the apply account must receive HTTP 403 reading - a credential that the observer just successfully read. -6. Remove and independently verify absence of both temporary probe grants. Cleanup - also runs after ordinary failure and has a standalone recovery entry. Issued - tokens retain their original 600-second lifetime; deleting a grant is not - revocation of an already minted token. -7. Only after these checks and cleanup, add workload-identity impersonation - bindings for the existing, pinned IAM workflow. - -Target providers, workload impersonation, and target secret-access grants remain -in the existing approval-gated convergence plan. Bare target containers do not -activate those workflows. The target roster comes from `gcp_iam_converge_targets`, -including its current private-publisher and Namecheap entries; it is not copied -from the older allocation branch. - -Permission probes are diagnostics, not an authorization oracle. Configuration -readback and the real observer/apply secret-read control are separate checks. -Successful bootstrap still requires a real GitHub OIDC/approval/apply run before -the ongoing workflow can be called operational. - -## Recovery and evidence - -Each invocation creates `target/gcp-iam-bootstrap--started.txt` before -effects, then a separate receipt for every attempted stage. A refusal stops -later stages. A process loss or asynchronous creation may require a new attempt -identity; it rereads named resources and their current policies. A started -receipt is not completion evidence. Existing policy drift never authorizes -automatic privilege expansion. - -The existing `fleet_converge_workflow.dag` job already performs observer WIF, -request/approval, apply WIF, live approval recheck, and independent readback. -Bootstrap supplies its missing cloud prerequisites rather than adding another -approval workflow or a pasted-token path to secret consumers. - -## Validation - -The witness `test.claim.auth.gcp_iam_bootstrap_witness_test` is discovered and -executed by the required floor lane (`claim_executor --required-ci --required-lane -witnesses`) on every pull request and merge group that touches its closure; no -hand-assembled source root is needed. Qualification must also record -the binary hash, source revision, pure plan, missing-credential refusal, cloud -stage receipts, and a real approval-workflow run when available. - -API contracts follow Google Cloud's official references for -[custom roles](https://docs.cloud.google.com/iam/docs/reference/rest/v1/projects.roles/create), -[project policy updates](https://docs.cloud.google.com/resource-manager/reference/rest/v1/projects/setIamPolicy), -[workload identity pools](https://docs.cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools), -and [deny policy creation](https://docs.cloud.google.com/iam/docs/reference/rest/v2/policies/createPolicy). - -## Current commissioning standing - -Bootstrap completed successfully at source `85bdb4e19` on 2026-09-29. All ten -stages passed: required API readiness, identities, roles, deny protection, -temporary deny-authority cleanup, capabilities, temporary probe authority, -effective access checks, probe-authority cleanup, and workflow trust. The observer -read the pinned approval credential and the writer received exact HTTP 403 for it. -Both temporary probe grants and the earlier Deny Admin grant were verified absent. -The operator token file was removed. See -`receipts/gcp-iam-services-2026-09-29/README.md` for evidence and limitations. - -The existing approval-gated workflow was then dispatched on main as run -`36505895733`. Bootstrap completion does not establish that workflow's approval -or apply acceptance, nor VM commissioning. Earlier refusal receipts remain as -historical evidence of the dependencies repaired along the way. - -## Temporary-authority dependency and recovery - -```mermaid -flowchart TD - P[Read project ancestry; verify pinned organization] --> R[Read organization IAM] - R --> W[Organization IAM write authority] - W --> I[Persist fixed operator lease intent] - I --> E[Elect one publication] - E --> G[CAS grant and read back exact conditioned cell] - G --> D[Install and read back deny policy] - D --> C[CAS remove owned cell and read back absence] - G --> F[Deny installation refuses or lease expires] - F --> C - C --> T[Persist retirement; continue bootstrap] - E --> U[Interrupted or unreadable publication] - U --> O[Reobserve the same intent and cell] - O --> C - U --> Q[Absent cell with unresolved write: keep obligation outstanding] -``` - -`target/gcp-iam-authority--*.txt` records the exact project, organization, principal, -role, condition, and expiry before effects. Keep these records with the operator -recovery workspace; they are local bootstrap state, not yet protected fabric -storage. Intent drift or unreadable records refuse. A create-only election means -at most one grant request is issued for this operation, even across retries. -An acknowledged successful publication or a grant observed present closes that publication; cleanup can recover a crash -between removal and recording retirement. A definitive publication HTTP 4xx also -closes the no-effect attempt. Other unresolved write outcomes remain outstanding -when a read sees absence. Neither a timeout nor expiry proves physical cleanup. -The cloud condition independently bounds effective access after process loss. - -The shared policy reconciler preserves unrelated grants and conditions, uses -version 3, and requires etags. Cleanup owns only the exact conditioned cell; -unconditional pre-existing administrator access survives. A retired operation -cannot publish again. `iam_bootstrap_authority_cleanup` drives withdrawal alone, -using the same authority ID, expiry, journal, and operator token-file input. - -The downstream account-impersonation readback still requires its actual authority. -That is a separate dependency to classify when reached, not permission to invent -an approval or give the continuing workflow self-elevation rights. - - -### Role grant scope correction - -The first dependency attempt was rejected with HTTP 400 because Deny Admin cannot -be granted on a project. The current model uses the role's documented -[organization-only grant scope](https://docs.cloud.google.com/iam/docs/roles-permissions/iam#iam.denyAdmin). -A read-only Resource Manager observation established that active -`projects/582015116396` (`gunbai-secrets`) belongs to -`organizations/266638272282`. The authority declaration pins that organization; -publication rereads the project and follows validated folder parents, refusing -unknown, changed, cyclic, or incomplete ancestry. No project move or organization -creation is inferred from missing ancestry. - -This temporary role is organization-scoped and time-limited; it is not represented -as project-scoped access. Only the named human operator receives it. The modeled -actuator still installs only the exact project deny policy. Cleanup addresses the -pinned organization even if the project later moves. These local journals support -one shared operator recovery workspace; they are not a distributed election across -independent workspaces or hosts. The original project-level attempt was rejected, -not an effective grant, and its historical receipt remains preserved. - - -A newly read-back authority binding may not yet be effective. Deny installation -permits at most 30 attempts separated by ten seconds, only after an explicit HTTP -403 and while the original lease remains live. Transport/server faults are not -retried as if nothing committed. An accepted create is followed by at most seven -readbacks separated by ten seconds, with no additional creation request. Exhaustion -or clock/deadline refusal enters the same authority cleanup obligation. - - -Before publishing temporary authority, the native Google UserInfo operation must -establish subject `116084671989231734979` (previously confirmed by the operator), -verified email, and the pinned IAM member `user:brian@gunb.ai`. Live UserInfo readback -identified this primary email; the earlier `briansrls@gunb.ai` spelling is not used -as the authority identity. A different subject with the same email, missing claims, -or changed transport email refuses publication. Cleanup does not require the old -operator credential's identity: an authorized recovery credential may remove the -already-pinned exact cell. No access token is put in a URL or command argument. - -## Exclusive initialization of an empty pool policy - -On 2026-09-29 the operator confirmed that no other administrator will update -these pool policies during bootstrap. This supplies an operational exclusivity -assumption, not a provider-enforced CAS token. Normal policy updates still require -an observed nonempty etag. - -The one-off initializer is limited to pool resources already derived by -`iam_bootstrap_capability_bindings`: `github-heal-publisher`, -`github-heal-publisher-private`, `github-mtcollins1-boot`, -`github-namecheap-dns`, and `github-gcp-iam-converge`. Each initial policy contains -all and only that pool's declared capability cells. The last pool receives -observer capability only; the apply principal cannot administer its own trust. -It does not install target OIDC providers or account impersonation grants. - -An invocation must explicitly select the confirmed window using -`GUNBC_IAM_BOOTSTRAP_POOL_WINDOW=exclusive-pool-policy-initialization-20260929`. -The native initializer verifies the same stable Google administrator identity, -requires an empty policy without a revision, elects one publication through a -create-only local journal, and performs independent readback. Completion requires -exact declared cells and a returned revision before later CAS updates are allowed. -A failed or ambiguous write retains the journal and cannot issue another -unconditional request. Recovery can close it only after matching readback. -Keep the same journal workspace across retries; do not delete election records or -run independent copies. The operator's exclusive window must still be in force -at actual execution. This is not a standing permission for future initializations. - -This slice has not been applied to Google yet. An approval receipt does not -create an administrator credential. The normal `gcp-iam-converge` workflow first -federates as `iam-observe`, files its request, and only after approval federates as -`iam-converge`. Both trust grants are deliberately the final bootstrap stage. -Consequently that workflow cannot bootstrap its own initial trust. An existing -administrator credential is a remaining explicit dependency; this environment -has neither gcloud nor a configured readable GCP token file. No credential or -refresh token is stored in these sources or receipts. - - -## Temporary probe authority - -The live pool run completed all capability grants, then the administrator token -received HTTP 403 for `iam.serviceAccounts.getAccessToken` on `iam-converge`. -That is now another explicit bootstrap dependency, not a console instruction. -`GUNBC_IAM_BOOTSTRAP_PROBE_ID` and `GUNBC_IAM_BOOTSTRAP_PROBE_EXPIRES_AT` pin its -operation and deadline, at most one hour away. The two exact-account conditional -grants name the verified human identity. They share the deny authority's journal, -publication election, etag update and cleanup machinery. No signing, key-creation, -policy-write or project-wide impersonation permission is included. - -Google documents the [access-token permission](https://docs.cloud.google.com/iam/docs/service-account-permissions). -Permission propagation retries accept only explicit HTTP 403, at most 85 attempts -with five-second waits (seven minutes of waiting), and stop at the original deadline. Other failures are not -converted to propagation. `iam_bootstrap_probe_cleanup` can recover both exact -grants without rerunning bootstrap; keep its journal and original environment. -Trust requires cleanup success. A retired probe operation cannot grant again. - -The first probe lease was removed after 30 token-mint refusals over roughly one -minute. That did not establish a permission-design defect: Google documents policy -propagation as typically two minutes, potentially seven or longer. The bounded -retry now permits seven minutes within the original lease; no role is widened. - -### Re-running after an interrupted probe-authority stage - -The probe stage first reads back the operator identity, and only then does -`iam_bootstrap_cell_authority_reconcile` pin an intent or elect a publication. A run that -stops inside that readback therefore leaves no `gcp-iam-probe-*` journal and no grant. -Re-run with a fresh `GUNBC_IAM_BOOTSTRAP_RECEIPT` (the started receipt is create-only): -earlier stages read back and adopt what exists (the deny stage elects no temporary -authority when its policy already reads back), and the probe stage elects fresh under -the same or a new probe intent. Any `gcp-iam-probe-*` or `gcp-iam-authority-*` journal -that does exist must be kept with its original environment, as above. diff --git a/docs/plans/git-plumbing-extdeps-authority-design.md b/docs/plans/git-plumbing-extdeps-authority-design.md deleted file mode 100644 index f81f0bd5381..00000000000 --- a/docs/plans/git-plumbing-extdeps-authority-design.md +++ /dev/null @@ -1,233 +0,0 @@ -# Git plumbing as an extdeps authority - -LANDED, in part, by `GIT-PLUMBING-0`. This note scoped the dissolution of a split Git authority; -the operation family and the whole devboot migration are now in tree, and §7 records exactly which -acceptance rows landed and which did not, so the note reads neither as a plan for done work nor as -a receipt for undone work. - -## 1. The finding - -`extdeps.git` models Git's **porcelain and read** surface. `gunbc.devboot` privately models Git's -**object-store plumbing** surface, as raw argv. Neither fact is written down, and together they are -one authority split at the surface grain (DESIGN §3), not one un-lifted operation. - -Measured against the current tree: - -- `extdeps.git` `git` service declares ~35 operations — `CommitInRepo`, `MergeNoEditInRepo`, - `RevParseInRepo`, `LsFilesUnmergedInRepo`, the diff family, config, fetch. It declares **no** - `write-tree`, `commit-tree`, `hash-object`, `update-ref`, `checkout-index`. The sole `update-ref` - spelling in `extdeps/` is an argv helper that hardcodes `FETCH_HEAD` and carries no expected-old. -- `gunbc.devboot` carries the plumbing as argv: `update-ref` ×6, `hash-object` ×5, `cat-file` ×5, - `read-tree` ×3, `write-tree` ×2, `unpack-file` ×2, `rev-parse` ×2, `commit-tree` ×1. -- `extdeps.git.object_store` `git_model_update_ref_exact` is the **pure** exact-old transition, and - it is already rich: recursive symbolic-ref dereference with the symbolic ref retained, stale, - missing, duplicate, symbolic cycle, repository-invalid, format mismatch, target unavailable, and - target-kind-refused arms. Its own `git_modeled_ref_transition_boundary_note` already rules that - P3 must consume an executing transport observation and never promote the model to `Applied`. - -So the pure model exists and is good; the *operation* does not; the only realization in the tree -is private to one service. - -## 2. Why this is a mis-aimed dissolution, not merely missing work - -`gunbc.devboot.transport` is disciplined: all 30 raw argv sites are registered `RetainedWitnessRun` -rows with a reason, and the module note claims no unmarked `WitnessBin.Run` call survives review. -Every row declares the same dissolution target — `gunbc.host_effect_realize` `host_effect_apply`, -shell→intent Phase 2. - -That target is correct for roughly 13 rows (`rm_rf`, `mkdir`, `cp`, `mv`, `chmod`, `sleep`, `uname`, -`ldd`, `sha`, `cargo_build`, `rustc_version`, `execute_artifact`, `rm_file`) and **wrong for -roughly 17** (the git plumbing above). A Git operation's terminus is a modeled `extdeps.git` -service operation — DESIGN §3 (a), the dependency's interface shape, which extdeps owns — while -`host_effect_apply` is §3 (b), a transport. Routing `git write-tree` into a generic host-effect -actuator dissolves the *transport* and leaves Git's interface permanently unmodeled. - -The roster thus conflates two dissolution classes under one target, and the git half's trigger -cannot fire correctly. This PR discharges declared debt rather than adding surface. - -## 3. The operation family - -One closed mutation family, three arms. Create, exact advance and exact delete are distinct -transitions with distinct preconditions in Git's own interface, not distinct authorities. - -``` -GitExactRefMutation - = GitCreateRefIfAbsent { name, new } - | GitAdvanceRefIfExpected { name, expected_old, new } - | GitDeleteRefIfExpected { name, expected_old } -``` - -`expected_old: Optional` is **refused**. An absent expected-old would conflate four -states with different remedies — create-only, update unconditionally, the caller did not supply the -fact, the caller does not know the current value — the state-space conflation DESIGN §5 names. The -empty string devboot passes today is a transport encoding of Git's protocol, never a semantic value, -and disappears from the model. - -The pure model generalizes to the family while preserving every existing arm; `git_model_update_ref_exact` -becomes the advance arm's caller-facing spelling, not a second authority. Create and delete must -inherit — not re-derive — repository decode, symbolic dereference, format consistency and -target-kind validation. - -### Outcome and read-back - -The operation does not decode Git's stderr into semantic causes. Devboot establishes why: losing a -race and failing to reach the store both exit 128 (`produce.dag` records this by measurement), so -exit codes do not discriminate and messages are not a structural interface. The outcome is decided -by **observation**, not by the process result alone: - -``` -exit 0 + desired state observed => Applied -nonzero + expected state no longer held => PreconditionNotHeld -nonzero + expected state still held => ExecutionRefused -any exit + state not observable => ObservationRefused -exit 0 + desired state not observed => ReadBackMismatch -``` - -`Applied` is unreachable without an independent read-back, as the existing boundary note already -requires of P3. Consumers project their own remedies from the common fact — a refused advance whose -observed value differs is a stale parent and re-evaluates internally; a refused create whose ref is -present is an attach. - -## 4. The migration is the discriminator - -Replacing devboot's argv sites is the point of the PR, not a cleanup tail. Each site must declare -which transition it is; one already fails that test: - -**`gunbc.devboot.build` `publish_produced_tree` writes the produced-answer ref unconditionally.** -The ref is per-request-token and holds one build answer; the function is reached from both the -success path and `serve_refusal`. A re-serve of one token therefore silently overwrites a previous -answer, and a client that read the old answer and one reading the new disagree with no signal. The -ref is write-once by intent, so the correct transition is `GitCreateRefIfAbsent`, and a second -publish for one token is a real conflict that must refuse loudly. - -No unconditional arm is added to preserve that call. A site that can name neither create nor exact -advance refuses until its intended transition is modeled. - -## 5. The cut is the whole plumbing surface, not one operation - -DESIGN §3's replacement-migration rule is greedy root-first: attempt the maximal cut and descend -only on an exact refusal — Y-incomplete, an escaping consumer, or opaque — because a separate -deeper cut pays admission surfaces, receipts and X-compatibility work the root cut never pays. - -An earlier draft cut at ref mutation alone and left the other seven plumbing families private to -`gunbc.devboot`, with no refusal behind it. Ref mutation's interesting transactional guarantee is a -reason to sequence the *proof* there, not to leave the authority split standing. Descoping would -have left devboot the private authority for Git plumbing while each later family paid its own -admission round — the attractor the rule exists to prevent. - -So the cut is the split itself: **no `gunbc.devboot` declaration constructs a Git plumbing argv -list, and the corresponding `RetainedWitnessRun` rows delete rather than being repointed.** The -census in §1 is the deletion population, not a work-list for later. - -The upstream-shaped surface, joining `extdeps/git/`'s existing decomposition rather than arriving -as one generic operation: - -``` -object database HashObjectWrite · CatFileExists · CatFileSize · CatFileRead · UnpackObject -index and tree ReadTreeIntoIndex · CheckoutIndexToPrefix · WriteTreeFromIndex -commit CommitTree { tree, parents: NoParents | Parents { first, remaining }, message } -ref store ObserveRef · CreateRefIfAbsent · AdvanceRefIfExpected · DeleteRefIfExpected -``` - -Repository-scoped operations that already exist — `RevParseInRepo`, `CatFileBlobInRepo` — are -consumed, never reintroduced under a plumbing module. - -Acceptance: - -1. The three-arm pure ref model in `extdeps.git.object_store`, preserving every existing refusal arm - (repository-invalid, stale, missing, duplicate, symbolic cycle, format mismatch, target - unavailable, target-kind refused). Create and delete inherit that decode and dereference; they do - not re-derive it. -2. Repository, index path, ref, OID, parent set and output path are typed operation inputs. Git's - empty-old encoding exists only in the `CreateRefIfAbsent` transport projection. -3. Executed against real repositories, with independent read-back on every success: create succeeds; - a second create refuses with the original value intact; exact advance succeeds; stale advance - refuses; exact delete succeeds; stale delete refuses. -4. Devboot's behavior survives unchanged except where the migration intentionally exposes a defect - (§4). -5. The ~17 Git rows leave `gunbc.devboot.transport`; the ~13 host-effect rows remain, still pointing - at `host_effect_apply`. - -Out of scope: any SCM behavior. This PR makes the plumbing an authority with one real consumer cut -over; M0, in its own PR, is the second consumer and proves the surface is not devboot-specific. - -## 6. Deliberately undecided - -Whether `gunbc.devboot` should adopt `std.materialization_provider` is **open**, and the roster -argues against assuming it. All seven rows of `gunbc.materialization_provider_targets` are -compiler-internal caches identified by a `CacheInterfaceId` from a cited catalog row; devboot is a -cargo-build artifact service with no such row and a different grain. The roster already carries a -declared exclusion of this exact shape — `extdeps.realization.artifact_store_fs` sits *under* the -contract as a transport and cannot inhabit the target's cache field at all — and devboot's Git -object database plausibly occupies the same position. `ArtifactRequest` is additionally a closed -coproduct with an explicit `request_fold`, so a new arm edits every consumer. - -Two facts found while scoping make the question not merely open but currently **unanswerable in -the adoption direction**, recorded so the next author need not rediscover them. Devboot decides -reuse by `build_subject_equal` over the whole `BuildSubject` after the digest narrows to a candidate -— the digest is documented there as an index, not a decision — whereas `provider_serve` decides on -`ContentHash` equality alone. And `MaterializedArtifact` retains only `request_key` plus its parts, -so the contract holds no subject that could differ from a request's: the same-key-different-subject -state is not representable, which is why the provider's witness suite has no collision test. -Routing devboot into the contract as it stands would lower a wall rather than share one. - -That is a finding about `std.materialization_provider`, not about devboot or this PR, and belongs -to its own lane with its own operator ruling — the module is contract-stable after four review -passes and seven consumers are scheduled to adopt it. Nothing here depends on its outcome: if the -contract later grows an exact-comparison door, devboot's fold is the reference implementation; if -not, devboot remains a build-domain materializer whose storage realization is Git. Either way its -uniformity work is exactly §5's cut and nothing more. - -## Dissolution trigger (DESIGN §6) - -This note dissolves into the carriers it names when the family lands: the model and operations in -`extdeps.git`, and the deleted rows in `gunbc.devboot.transport`. The §6 materialization finding -does not dissolve with it; it is owed a lane of its own. - -## 7. What landed, and what did not (added when `GIT-PLUMBING-0` merged) - -Landed: - -- `extdeps.git.plumbing` — `service git.Plumbing`, seventeen operations covering the object - database, index and tree, commit, repository init and the ref store. Repository addressing is one - parameter (`GitRepositoryAddress`, worktree `-C` versus store `--git-dir=`) rather than two - operation families; `GIT_INDEX_FILE` is an operation-owned transport prefix rather than an `env` - spelled at a call site. Every operation reports `exit_code` and `stderr`, never a `success` Bool, - and none declares an `exit` block — both per the notes in `extdeps.git` that already ruled it. -- `extdeps.git` gains `FetchForcedRefInRepo` and `PushForcedRefInRepo`. These are not plumbing and - are not publication; they are the repository-addressed forced-refspec exchange devboot's git-only - channel runs on, which no operation in the corpus could express. -- `GitExactRefMutation` — the three arms, with `expected_old` refused as an `Optional` exactly as - §3 argued. Git's empty-string old value now appears once, inside `CreateRefIfAbsent`'s transport - row. -- The outcome table of §3, as `git_ref_mutation_outcome`, decided from an independent read-back - rather than from the exit code. `Applied` is unreachable without the read-back. -- All thirty-five git argv sites in `gunbc.devboot` are gone; the seventeen `RetainedWitnessRun` - rows are deleted rather than repointed. The thirteen host-effect rows remain, still pointing at - `host_effect_apply`. -- §4's discriminator: `publish_produced_tree` wrote the per-token answer ref unconditionally and now - creates it, so a re-serve of one token refuses loudly instead of silently replacing an answer a - client may already have read. It returns a typed `ProducedPublication` rather than a Bool, because - the Bool could not carry which of the five outcomes occurred. The lease take and the three - artifact bindings are creates for the same reason; the stranded-lease break is an exact delete. - -Did NOT land, and is not a stall: **acceptance row 1** — generalizing the *pure R0 model* -`git_model_update_ref_exact` in `extdeps.git.object_store` to the three-arm family. Left undone -deliberately, not forgotten: that model simulates a repository state, nothing in this cut consumes -it, and devboot decides from an executing observation instead, so generalizing it here would have -added authority with no consumer — the §6 tell this repository treats as a finding. -NEXT TRIGGER: the first consumer that simulates a ref transition rather than performing one, which -is the P3 boundary `git_modeled_ref_transition_boundary_note` already names. Until then the R0 model -answers exactly the question it has always answered, and the execution-side family answers the other -one. - -**Acceptance row 3 (execution against real repositories with independent read-back on every -success) is likewise NOT claimed by this PR; the bound is stated rather than left to be inferred -from a green witness suite.** What executes is the DECISION: `git_ref_mutation_outcome` -takes the three values a caller holds after an attempt, so all five arms — including the pair that -share exit 128 — are reachable hermetically and are claimed in -`test.claim.git_plumbing_ref_mutation_witness_test`. What does not execute is any -`git.Plumbing` operation, so a change that broke a transport argv while leaving the decision intact -would keep those witnesses green. The residue is the transport spellings; the wet receipt that would -close it is the same shape as -`test.manual.git_upstream_model_execution`'s and belongs with a live devboot run. diff --git a/docs/plans/glm-group-b-workload-qualification.md b/docs/plans/glm-group-b-workload-qualification.md deleted file mode 100644 index 89578838827..00000000000 --- a/docs/plans/glm-group-b-workload-qualification.md +++ /dev/null @@ -1,163 +0,0 @@ -# GLM group B: workload qualification after the variant-e capture - -Status 2026-09-28. Supersedes the "per-step timing experiment" proposal. External review -2026-09-27 (serving mechanisms) supplies the corrections this plan encodes; the variant-e -capture (2026-09-27, watchdog ts=1790547523) is the evidence base. - -## What the capture established — and what it did not - -Established: - -- Six concurrent cold 262K submissions: five completed (~5 min apart), the sixth's 1800s - client bound expired while the engine was still making progress. Zero preemption log lines - anywhere; the preemption/recompute-storm hypothesis is DISPROVED for this arm. -- The watchdog stacks show the engine core waiting on workers with a worker on-CPU — no - deadlock shape. -- The budget arithmetic closes the observation: 6 × 262,144 = 1,572,864 fresh tokens at the - observed ~874 tok/s aggregate ≈ 30 minutes of engine work; the five completions plus the - bound expiry fit inside it. - -Corrections now law (from the review): - -- Six cache seats are SIX REQUEST STATES, not six compute lanes. TP4 splits every operation - across the four ranks; "the engine serializes seats" is not a mechanism, it is one shared - compute pool under one shared 2,048-token step budget (running-before-waiting admission in - the pinned scheduler). -- A max_tokens=1 completion latency is a COMPLETION latency (input processing, queueing, - prefill, sampling, delivery), not an instrumented GPU-prefill duration. -- Completion order alone cannot distinguish driver-serial from budget-starved from - shared-throughput. Every timing claim binds to the frozen run plan / launch receipt, never - to the live spark_active_workload_variant selector (a mid-flight flip or a second launcher - — both happened 2026-09-27 — must not be able to relabel a run's mode). Runs interrupted by - another controller are CONTENDED evidence and excluded from latency claims. - -The remaining open question: is ~874 tok/s aggregate (≈2.3s per 2,048-token step) an -EFFICIENT execution of that work on this realization? That is an attribution question — -sparse-indexer, attention, collectives, host gaps — and it is answered by operation-level -timing on representative steps, not by more repro campaigns. - -## Corrections adopted 2026-09-28 (review of the W1 reading) - -1. **W1's observation is narrower than first reported.** One cold 262K completing in 323.16s - without six-way contention establishes that the slow near-full-window request OCCURS - without the cohort. It does not establish compute-bound execution and does not make - ~5 minutes an immutable price. The observation stays bound to its exact - runtime/profile/workload subject; W1b attribution remains REQUIRED. -2. **The "26k tok/s" engine log lines are reconciled and retired as a progress signal.** The - pinned metric producer credits prompt tokens at FIRST-OUTPUT processing divided by the - logging interval — completion accounting. Per-step progress evidence must come from - scheduled-token/per-step observation (W1b), never from those lines. -3. **gap_mib=2 does NOT discharge any rank fragmentation obligation.** The present producer - measures head-host maximum-process footprint growth above a post-prefill reference — not - allocator fragmentation — and observes the head host only, not the other ranks. The - observation is preserved under that actual meaning; the instrumentation-to-obligation join - needs repair (an allocator-level, per-rank measurement) before any fragmentation obligation - can close. -4. **Wet work is strictly sequential under exclusive cohort ownership.** W1b and other - investigations are prepared independently, but no second run may replace the engine whose - cache or timing another run is observing (the 2026-09-27 mid-flight teardown is the named - counterexample). -5. **W2 is an explicit cache-reuse qualification, not a two-seat latency comparison.** Cold - prime, exact replay, genuine suffix continuation, and a controlled prefix-miss leg, all - bound to the same admitted engine/cache incarnation, recording ACTUAL cached and computed - token counts (the response's prompt_tokens_details.cached_tokens, not inferred reuse) plus - request-level timing. A fast replay alone does not establish free general follow-ups. - -## The frozen objective and the unified qualification (2026-09-28 steering) - -**Objective: restore GLM at a qualified 262,144-token x 8-seat configuration, then stop.** One -deliverable: an operational eight-seat service with its exact acceptance bundle and persistent -readback. DeepSeek reuse continues independently and never becomes a deployment prerequisite. - -The closing qualification is ONE frozen eight-seat candidate and ONE exclusively owned -execution — not separate campaigns: - - freeze candidate (eight requested; executable derived; the old summary-based six-seat - derivation retained as floor only) - -> all-rank startup + resolved allocation evidence (the eight-seat candidate's OWN - allocation plan and per-rank phase bounds; the six-seat run's 1,035-block inventory is - a conditional expectation, never the verdict) - -> W2 cache-reuse legs (cold prime / exact replay / suffix continuation / prefix miss) - BEFORE any unrelated churn can evict the prefix under test - -> occupancy high-water evidence: live request identities, resident context/block demands, - concurrent decode/workspace shape, per-rank memory standing — eight submitted - max_tokens=1 requests do NOT establish eight simultaneously resident full-window - contexts; the run records the population it actually exercised - -> real decode + at least one real harness protocol interaction (one-token completions do - not establish tool-call parsing, multi-turn continuation, or sustained decode) - -> settlement: cancellation, its observed completion, deterministic stop - -> promotion through persistent convergence; deployed configuration, generation, and route - verified after the existing readiness/access gates - -The execution budget covers the WHOLE sequence (startup + readback + queued and executing -requests + reuse controls + settlement). A client deadline is never an engine failure, and a -client's disappearance is never proof its engine work or reservation was released — -cancellation and its observed completion ride the shared request lifecycle. - -Deployment gate vs follow-on (the release distinction): - - REQUIRED before normal serving: exact runtime/checkpoint/config/rank identity; complete - applicable memory bounds including the allocator allowance; ENFORCED context/concurrency - limits (the first operational admission policy is conservative about NEW cold-prefill - work, and the restriction is enforced, never aspirational); correct generation and - required protocol behavior; exclusive ownership, settlement, restart, readback; route - withdrawal and cancellation/recovery; any latency/reuse guarantee actually advertised. - - FOLLOW-ON unless explicitly promised: W1b performance attribution (blocks only if it - uncovers a correctness/resource defect or an explicitly required operating contract); - maximizing concurrent cold-prefill throughput; broad benchmark matrices; an unadvertised - "nearly free warm continuation" objective (a cache miss is not a deployment failure when - cold execution is safe); DeepSeek's complete deployment. - -Safety-term closure order for each open term: derive a defensible bound from the mechanism; -use a justified conservative bound where exact derivation is unnecessary (a conservative bound -covers the candidate's actual allocation behavior — never an arbitrary allowance beside a -green result); measure the specifically unresolved term where needed. - -## The three qualification workloads (replace the single cold stress test) - -W1. ONE COLD FULL-WINDOW REQUEST — the attributable cold-prefill cost. One 262K request, - nothing else admitted. Per-request timing: submission, engine arrival, first scheduled - work, prefill completion / first output, request completion, scheduled tokens per step, - plus operation-level durations and rank waits for a small number of representative - steps. The output is the cold-prefill service time with its dominant term named - (compute / indexer / communication / host / cache-recompute / unresolved). - -W2. CACHE-REUSE QUALIFICATION, four legs on one admitted engine/cache incarnation: (1) COLD - PRIME — a full-window body A, the cold reference; (2) EXACT REPLAY — A again, the pure - reuse measurement; (3) SUFFIX CONTINUATION — A plus a small new suffix, the genuine - follow-up shape; (4) PREFIX MISS — a disjoint body B, the control that must NOT reuse. - Each leg records the response's actual cached/computed token counts - (prompt_tokens_details.cached_tokens) and driver timing. Verdict shape: replay near-free - AND suffix near-free AND miss cold-priced means reuse is real and general for this - subject; a fast replay with a cold suffix means replay-only reuse; a fast MISS means the - measurement is lying. No leg's speed alone establishes free general follow-ups. - -W3. COLD PREFILL ALONGSIDE ONGOING DECODES — does admitting new work damage useful - interactive progress? N decode sessions established and streaming; one cold 262K prefill - admitted; the decode-latency standing before/during is the verdict's subject. - -## Service-promise shape this feeds - -The capacity intent stops being "262144 × N uniform seats" and becomes separate quantities: - - resident context capacity (the CacheFitProved wall — done: 6 proved, 8 hard ceiling) - active decode population (W3's subject) - new-prefill admission policy (W1's price, W3's coexistence standing) - per-step token budget (2048 today; raising it is an OPTIMIZATION CANDIDATE that - changes the activation/workspace subject — it goes through - the memory model before any service promise reads it) - cold-start latency objective - decode-latency objective while prefill is active - -The serving-liveness verdict (ArmServingLivenessVerdict, one open obligation) consumes W1–W3 -evidence; the watchdog remains capture-only instrumentation and never mints a verdict. The -stall predicate ("no forward progress within T under admitted occupancy") is a named modeled -term over the engine's own counters before any further watchdog-armed run — see the owner -directive 2026-09-27 on watchdogs not substituting for modeling. - -## Non-goals - -No preemption-flag tuning, no max_num_batched_tokens change, and no service-shape restoration -decision until W1 attributes the cold cost. Group B stays down in the interim; the arm stays -Unestablished. diff --git a/docs/plans/google-workspace-identity-convergence.md b/docs/plans/google-workspace-identity-convergence.md deleted file mode 100644 index 6f462549ca9..00000000000 --- a/docs/plans/google-workspace-identity-convergence.md +++ /dev/null @@ -1,108 +0,0 @@ -# Google Workspace identity/profile convergence (Cut 4) - -Owner directive 2026-09-27: the Google-side configuration the tracker depends on is modeled -and converged — "we need to model this via convergence"; the owner performs only the literal -initial clicks. External review 2026-09-27 (Workspace onboarding) supplies the acceptance -boundaries; this plan is its convergence-shaped reduction. - -## Law - -1. Every Google-side setting the product depends on is a DECLARED DESIRED FACT in the model. -2. Where Google exposes a supported READ, the setting is OBSERVED through it and the - observation is what standing claims consume. -3. Where Google exposes a supported WRITE, the setting is CONVERGED through the fleet's - approval-gated credential path (org_admin_credential, gcp_secret_access, the PR #12421 - IAM pattern). -4. Where Google exposes NEITHER, the setting is a MANUAL-ADMINISTRATION OBLIGATION with a - readback receipt — never a fabricated convergence API, never "established because login - worked once". -5. Authentication and profile acquisition are SEPARATE operations. A tenant selects ONE - profile source by policy: UserInfo (ordinary OIDC) or the authorized Directory source - (managed Workspace org). Never a runtime fallback chain. -6. The principal stays the only identity. Names/photos are mutable profile projections with - explicit source, provenance, freshness, and display-audience policy. - -## Surfaces and their convergence class - -| # | Google-side fact | Read API | Write API | Class | -|---|---|---|---|---| -| 1 | OAuth client registration (client id/secret) for the tracker | no supported read found in the reviewed Auth Platform documentation (2026-09-28) | none | MANUAL obligation (owner's initial clicks); the secret rides Secret Manager via the existing gcp_secret_access path; the deployment config is already modeled (oidc_deployment_config) | -| 2 | Auth Platform audience = Internal (project inside the gunb.ai org) | console readback (documentation reviewed 2026-09-28) | console-only | MANUAL obligation + readback receipt | -| 3 | API controls: THIS client's app access — Specific Google data (openid/email/profile scopes), top OU | console readback (documentation reviewed 2026-09-28) | console-only | MANUAL obligation + readback receipt | -| 4 | Directory sharing: authenticated-user basic profile fields (minimum); org-data sharing ONLY if assignee directory discovery requires it — the reason is recorded either way | Cloud Identity Policy API: `directory.external_directory_sharing` | no supported mutation | MANUAL change obligation + effective Policy API readback | -| 5 | Profile-editing policy (photo) org-wide | console readback (documentation reviewed 2026-09-28) | console-only | MANUAL obligation (already set 2026-09-27) + readback receipt | -| 6 | Managed user photos (assignee avatars, header) | Admin SDK users.photos.get (read-only scope admin.directory.user.readonly) | n/a (read-only source) | OBSERVED: periodic observation → profile projection refresh | -| 7 | Directory people discovery (assignee picker population) | People API people.listDirectoryPeople (directory.readonly) | n/a | OBSERVED: same projection, pagination + deletion semantics preserved (incremental sync lags writes; a completed sync is not a post-change readback) | - -The authorized backend identity for 6/7 (service account + domain-wide delegation, if chosen) -is a SEPARATELY APPROVED integration — never silently acquired, never required for basic -login. - -## Model shape - -- extdeps.google.workspace grows from a citation anchor into the external model of these - admin controls and directory/profile operations — Google's documented semantics only, no - gunbc policy inside. -- extdeps.auth.oidc keeps claims + UserInfo protocol + the sub-equality admission (a UserInfo - result whose sub differs from the ID token's sub is rejected whole). -- gunbc profile projection: revisioned, source-bound, with a declared refresh policy; a new - login MAY update it but is not its only producer; identity history is stable across profile - change. -- Photo vocabulary preserved end-to-end: PhotoReturned / ProviderDefaultPhotoReturned / - PhotoNotReturned / PhotoReadRefused / PhotoNotObserved. PhotoNotReturned never becomes - AccountHasNoPhoto; omission is never labeled PolicyDenied without a policy observation. -- Display audience is an access-policy term: self-header vs assignee-picker vs comment-thread - viewers are different audiences; admitted images serve through a controlled application - route with bounded caching — no tokens in image URLs, no open URL-fetch proxy. - -## Acceptance (behavioral matrix, from the external review) - -Admin-managed non-public photo retrievable via the authorized Directory path or a located -access refusal (never "make it public"); ID token omitting picture leaves authentication -valid with no photo-absence inference; UserInfo sub mismatch rejects the profile; API -denial/timeout is a distinct refusal, never stored as absence; provider default-photo marker -preserved; photo lost between serialization and readback fails that boundary's propagation -control; photo change/removal updates under the declared refresh policy; revoked profile -access follows the retention policy; child-OU/group overrides reported as effective policy; -second tenant isolated; email change keeps principal history; avatar absence never blocks -issue/approval authorization. Tested with at least one NON-ADMIN managed account. - -## Sequencing - -C4.1 — Workspace external model + convergence declarations for rows 1–5 (manual-obligation -machinery with readback receipts; VERIFY each row's API standing before classing it). -C4.2 — Profile projection: revisioned store, refresh policy, the five-valued photo -vocabulary, session mint becomes one producer. -C4.3 — UserInfo profile operation (sub-equality admission) for OIDC tenants. -C4.4 — Directory source (rows 6–7) with the separately-approved backend identity; assignee -picker consumes the projection. -C4.5 — Display-audience policy + controlled image route; the header, picker, and comments -share the one projection. -C4.6 — Receipt completion: the structured propagation trace (payload member standing → -parsed claim → verdict → projection → persisted readback → render selection → image-load -standing), plus the positive end-to-end control. - -## Done in this lane already (2026-09-27) - -- Login claims receipt: post-verdict writes only, typed publication standing - (LoginClaimsReceiptPublished/PublicationRefused), observed-boundary comment. -- Session profile projection carries name + picture as optional members end-to-end; the - header prefers the display name and paints the photo with the initials fallback. -- The direct evidence discipline held the diagnosis: three consecutive receipts established - provider-side omission before any account-level claim was made. - -## C4.1 API qualification and receipt boundary (2026-09-28) - -The source and limits of this classification are recorded in -[the C4.1 research receipt](receipts/google-workspace-admin-api-2026-09-28.md). -`extdeps.google.workspace` owns the external subjects, values and API standing. -`gunbc.auth.google_workspace_admin_convergence` declares the five obligations and feeds -subject-bound, time-bounded readback into `std.goal_assessment`. It does not mutate Google -configuration or alter login. Rows 6–7 remain separate integrations. - -The receipt producer must authenticate its source and persist the referenced evidence before -calling the assessor. These types and tests establish the assessment contract, not a deployed -observer or proof that the live organization matches it. No live admin receipt is supplied by -this change. Missing observations, unread overrides, wrong tenant/client/OU and expired -receipts refuse assessment. The photo obligation is scoped to the declared OU: an organization-wide -claim additionally needs resolved child-OU/group coverage; a top-OU receipt alone is insufficient. diff --git a/docs/plans/group-a-authority-establishment-plan.md b/docs/plans/group-a-authority-establishment-plan.md deleted file mode 100644 index 827a030fe7c..00000000000 --- a/docs/plans/group-a-authority-establishment-plan.md +++ /dev/null @@ -1,144 +0,0 @@ -# Establishing Group A's pair-serving authority on the fabric log - -Plan for the D0 prerequisite. Nothing here has been run. Every claim cites the symbol it was read from. -Anything not read from the live store on srv1 is marked as an inference. - -## What refused, and why - -Run 36220729318 (fleet-converge `spark_v41_engram_access_probe`, srv1) read -`heads/pair-serving-authority-group-a.1` and `…-group-b.1`, and refused srv8 with -`gunbc.spark.host_commitment` `group_standing_over`'s `GroupStandingUnread` arm. That arm fires when a -group's partition folds to `AuthorityUnestablished` **and** `gunbc.spark.pair_serving_authority` -`pair_serving_authority_for` finds a source row for the group. - -- **group-a:** `spark_pair_serving_authorities` declares - `PairServingActive { group: FabricGroupA, exact_realization: PairRealizationUnestablished {…} }`. The - log has no `AuthorityEstablished` event for it, so its standing is Unread. -- **group-b:** the roster has **no** group-b row. So an unestablished group-b partition reads - `GroupStandingUnclaimed`, and the probe doesn't refuse on it. **This corrects the brief:** the probe - output doesn't show that group-b is *established*. It only shows group-b isn't claimed. (Inference: no - repository route has ever written group-b's partition except the witness fixtures.) - -**Why group-a was never established.** There's no route for it. The only entry point is -`gunbc.spark.pair_serving_authority_log` `pair_serving_authority_establish_wet`, and its annotation says -"run once per group by an operator on an executor with the event log placed". No fleet-converge mode -invokes it: the mode list in `.github/workflows/fleet-converge.yml` has `pair_serving_d0` but nothing for -establishment, and no run has ever carried it. The roster row landed in #11501 and the log route in -#11555, and the one-time actuation between them was never done. - -## What the run requires - -``` -gunbc run --source-root dag --source-root src/v2 \ - --entry dag/gunbc/spark/pair_serving_authority_log.dag \ - --function pair_serving_authority_establish_wet --arg group=group-a -``` - -Inputs: only `group`. The authority is the source row, never an argument. The actor is the executor's -short hostname (`observe_executor_reach`). The store is `gunbc.fabric_event_log_host` -`event_log_store_for_host`, so it must run on a host with a declared dashboard instance. For in-process -access to `/opt/gunbc/fabric-storage`, that host is srv1. - -Preconditions, all enforced by `pair_serving_authority_establish` and `preparation_refusal`: - -1. The group-a partition folds to `AuthorityUnestablished`. A second run refuses at `established`. -2. `establishment_refusal`: the row commits hosts, and at least one is named (srv5, srv6, srv7, srv8 via - `fabric_group_hosts`, cage 1). The row holds no lease. -3. On `host-placement`: no pending group-a preparation, and group-a's live commitment is empty (no - finalization yet). -4. **No live host-effect claim on srv5–8.** A live claim, such as an in-flight V4.1 build, checkpoint - materialize or row-store encode, makes the run refuse at `host-effects`. Run it when those modes are - idle. -5. No srv5–8 host is fenced by group-b. - -## What it writes (a saga across two partitions) - -1. `host-placement`: `PlacementPrepared { operation: "establish group-a", previous: [], next: [srv5..8] }` -2. `host-placement`: `PlacementAppendClaimed { preparation, intent }` -3. `pair-serving-authority-group-a`: `AuthorityEstablished { authority: , placement: }`, - compare-and-set at `HeadAbsent` -4. `host-placement`: `PlacementFinalized`, with up to 3 attempts. If finalization fails, the run exits - non-zero and says to run `host_placement_finalize_wet`. A stale write or a refused write aborts the - run's own preparation (`saga_abort_own`). - -## Consequence to decide before consenting - -After step 4, srv5–8 are **fenced** by group-a (`group_fenced_hosts` ⊇ `group_live_commitment`). -`host_effect_admit_at` then refuses **every** new host-effect claim on those hosts with -`committed: … is fenced by an authority: group-a`. This covers the `spark_v41_*` modes that claim a -Group A host, such as `v41_runtime_image_converge`. - -**Establishing the authority does not unblock the Engram probe on srv8.** It changes the refusal from -"unread" to "committed". (Inference: the exact wording depends on the probe's admission route, which -wasn't traced to its end.) So establishment is correct for D0. For the V4.1 staging modes it's a -**regression unless they admit through the group's authority** (`admit_host_held_by_subject`) rather than -through a free-host claim. Parent: sequence staging work against this. - -## Operator authorization - -The code has **no consent gate** on this route. `pair_serving_d0_door`, by contrast, files an escalation. -It is still an operator-consented act, because: - -- It is a one-off, irreversible, privileged write to the shared production log. The only way out of the - state is a transition, never a re-establishment. -- It fences four production hosts against all other lanes. - -Under §3b's authorization-pattern selection (`gunbc.auth.authorization_pattern_selection`), a one-off -effect is **one operator approval**. The two ways to run it both need the operator: - -- **(A) Recommended.** The operator runs the command above on srv1 as the serve principal (briansrls). - briansrls writes under its own umask, so the file-mode defect is avoided. Actor = `srv1`. -- **(B)** Add a fleet-converge mode for it. This is a new modeled route in the workflow emission and - needs operator sign-off. It also writes as ghrunner under `umask 077`, so **it must wait for #12342** - and #12342's area-ensure repair. Otherwise the serve side can't read the four new objects or the - group-a head: they'd be 0600. - -Under (A), #12342 still matters downstream. D0 runs as ghrunner in CI (`pair_serving_d0`), and its later -appends are written 0600 until #12342 lands. - -## Verification - -- The command exits 0. -- Re-dispatch the read-only admission that refused: the Engram probe's admission, or D0's - `pair_serving_d0_admit_executor`. It must now read group-a as `GroupStandingEstablished`, generation 0, - `PairServingActive`. It must not say "has not been established". -- On srv1: `heads/pair-serving-authority-group-a.1` exists and names the `AuthorityEstablished` object. - The `host-placement` head has advanced by three events, ending in `placement-finalized`. -- Run the command a second time. It must refuse at `established`. This is the discriminating control. - -## Sequencing (parent ruling) - -Establishment fences srv5–8, so it runs **after** the remaining V4.1 staging: the production image -build on srv8, distribution to srv5–7, and the image probe. The operator runs it on srv1 at that point, -under route (A). - -## What follows Established, for the V4.1 launch - -The launch admits through `gunbc.spark.pair_serving_authority` `pair_serving_successor_may_launch`. The -states below are read from `gunbc.spark.pair_serving_d0` (`d0_transaction`, `d0_settle`, `d0_decide`). - -1. **Established:** `PairServingActive`, generation 0 (the genesis event). may_launch = **false**. -2. **D0 first write:** a transition at the exact head that was read, to - `SuspensionPendingReconciliation { hosts: grant.subject.hosts, transaction, authorization_binding, lease }`. - - The lease epoch is minted for generation + 1. - - No grant is carried yet. - - D0 refuses first if the grant's hosts are not the group's current population (an identity join). - - may_launch = **false**. -3. **D0 settle:** records an entry-state receipt, then makes **one** compare-and-set from the pending - state to one of three terminals: - - `SuspendedForAuthorizedSuccessor { hosts, authorization: , exact_candidate_realization: PairRealizationKeyed { key: grant.subject.successor }, lease, cleanup }`. - This is reached only when the incumbent didn't answer, the head is quiet, and the declared occupant - has drifted. Only this terminal carries a `LeaseGrant`: - - `reference` = transaction - - `fence` = (the admitting pending event, the pending generation) - - `max duration` = `grant.subject.term` (`d0_lease_policy`) - - `PairServingActive`, restored unchanged, when the incumbent answers as the declared realization. - may_launch = false. - - `FencedRefusal` for any unread or unidentified occupancy. may_launch = false; only - `pair_serving_may_finish` is true. -4. **Launch admission:** true only for `SuspendedForAuthorizedSuccessor` whose lease's **observed** - state is `LeaseRunningExpected`, which maps to `Converged`. The observed state is derived **at read - time** from the grant and the current instant (`lease_observed_at`). The launch must therefore read - `current_pair_serving_authorities` (the log), never the source row. An expired term reads - `LeaseRunningStale`, which maps to `Drifted`. That blocks START, while cleanup continues through - `pair_serving_may_finish`. diff --git a/docs/plans/harness-work-lifecycle.md b/docs/plans/harness-work-lifecycle.md deleted file mode 100644 index bb6c39c1651..00000000000 --- a/docs/plans/harness-work-lifecycle.md +++ /dev/null @@ -1,634 +0,0 @@ -# Harness work lifecycle (sketch → structure) - -Status: DRAFT v2 — owner-approved direction, external review folded in (2026-09-20). -Born of the srv2-deploy dispatch experiment: infrastructure proven end-to-end; the -missing thing is an authoritative lifecycle for coding work, so the known-mandatory -journey is owned by the workflow and never depends on an agent remembering it. - -Guiding line: **make the known obligations unavoidable, keep agent judgment -flexible, and make every claimed improvement visible in the real execution path.** - -## Axiom - -Work is **delivered** per the delivery contract declared at intake: - -- a **code-change task** is delivered only when its submitted candidate is - published to its PR; -- an **orientation task** delivers an orientation result; -- a **review task** delivers a review. - -The worker may not redefine a code-change task as "analysis completed" to escape -publication; orientation and review processes are never forced to manufacture PRs. - -The five evidence states are distinct and never collapse: - -**Ready ≠ Verified ≠ Published ≠ Approved ≠ Merged** - -## Anchors - -| Anchor | Owner | Structural (workflow) | Judgment (worker) | -|---|---|---|---| -| Intake | roadmap authority | accepted task + delivery contract + context bundle + base sha | — | -| Orientation | workflow (centering contract) | task, source pointers, constraints, exemplars, verification route; a typed way to report missing context | understand the problem; name uncertainty | -| Scoped editing | worker | bound workspace + permitted effects; nothing outside the scope is reachable | implementation, local debugging, local test iteration | -| Candidate handoff | worker submits, workflow binds | the handoff operation CAPTURES the candidate; the harness materializes its commit identity and carries the acknowledgment onto exactly that candidate | readiness claim + explanation + limitations; or blocked/needs-context | -| Verification | workflow (belt) | declared oracle runs against the captured head in a detached checkout; receipt retained | interpreting failure (feeds the next editing pass) | -| Publication | workflow (helper, credentialed) | push captured head, create-or-update the one PR per logical work item, receipt bound to the OBSERVED PR | PR body explanation | -| Review | owner/fleet | feedback bound to the reviewed head; changes route back as a new candidate | approval/merge decision (human) | - -## Rules - -1. **Delivery obligation.** A code-change task is never reported complete while a - publication obligation is outstanding. `Ready` (submitted) enables publication - and leaves the obligation open; passing verification is not publication; - approval and merge are later, separate states. -2. **Submission ≠ checkpoint, and binds the CAPTURED candidate.** The belt may - commit a dirty tree as a checkpoint (preservation only, no readiness claim). - A submission is a declared act whose handoff operation captures the candidate - as it stands THEN; the acknowledgment binds to that capture — never to - whatever a later belt tick sweeps up. Budget exhaustion without a submission = - `Yielded`, not ready. "A later edit inherits nothing" means old - candidate-specific evidence does not admit the new candidate; the old evidence - is PRESERVED, current eligibility recomputed. -3. **Head binding.** Verification and review bind to an exact commit. Revisions - update the SAME PR (the obligation belongs to the logical work item). -4. **Authority allocation is modeled and enforced by effects.** Who may commit, - publish, approve is a model fact; the worker's available effects, credentials, - the execution path, AND the rendered instruction all derive from it. Generating - "the worker may not publish" enforces nothing by itself — the selected effects - must make the restriction true; the prompt only explains it. -5. **The worker may iterate; the workflow owns obligations.** Local test/debug - loops are the worker's business. The authoritative verification, its receipt, - publication, review routing, and per-failure-class retry ownership are the - workflow's. Failure classes route by owner: CandidateFault → editing - (continuation with verdict); VerificationInfra → retry/repair the verifier; - PublicationInfra → blocked-on-publication with a named owner and next action. - Never fold all three back onto the worker. -6. **Typed blockers have owners.** `BlockedOnPublication(cause)` preserves the - pending obligation across restart, owns retry/escalation, and reconciles - uncertain remote success before creating anything: if the PR was created but - the local process died before recording it, resumption DISCOVERS the existing - PR — it never creates a duplicate. -7. **Tests are behavioral, at the real execution boundary.** The suite must show: - terminal response without submission is not ready; submission + pass discharges - into publication with no discretionary request; publication failure stays - visible and resumable; uncertain remote success reconciles without a duplicate - PR; a later edit cannot reuse a prior head's evidence; the worker cannot invoke - an effect the model assigns to the harness. Marker/projection comparisons are - secondary. -8. **Agent-facing documents are a justified argument, not just generated text.** - Once documentation is an input to execution, its correctness is part of the - work's correctness. Every consequential instruction must have traceable - support: WHY it must happen (applicable requirement/dependency), WHO may/must - (modeled authority allocation), WHEN it applies (task/role/stage/conditions), - HOW (the actual operation and admitted route), WHAT ESTABLISHES it happened - (observation bound to its subject), and WHAT IF it cannot (typed blocked/ - repair/retry/escalation). Explanatory prose may stay authored, but it may not - introduce an unmodeled permission, obligation, exception, or success claim. - Deriving prompt and docs from one source removes drift; it does NOT remove - consistent-wrongness — composition is checked, not just generation. -9. **The composed context must be consistent, not just each document.** The load - phase admits an applicable instruction set; individually valid documents can - compose into contradiction (case on file: worker prompt forbids claim_batch, - DESIGN.md teaches it — the repair is declaring the SCOPE distinction in the - model: the verifier uses the batch route, the worker submits candidates and - never runs authoritative verification; both documents then express it - correctly). Ordering one instruction before another does not repair meaning. - The check covers brief, orientation, stage instructions, available tools, and - observations: "publish the PR" is not a plan for a worker with neither - publication authority nor a handoff to its owner. -10. **Policy, observed fact, and plan never render as the same standing.** - "Publication is required", "publication succeeded", and "we intend to - implement publication" are three different premises. Stage instructions - reflect what is established NOW, not the aspirational pipeline. (The - experiment's own trace called oracle-green "the full loop" with the PRs - unopened — that class of unsupported conclusion is what this rule exists to - expose.) -11. **Contradiction reporting is a first-class, consumed route.** The worker - contract: when applicable instructions conflict, or a required conclusion - lacks its stated support, report the conflict BEFORE acting on anything that - depends on resolving it — never silently choose a convenient interpretation - or claim satisfaction. The report names the conflicting statements and their - sources, the affected operation, and the missing decision/evidence; a - suggested reconciliation is a proposal until the responsible authority - accepts it. The workflow preserves the affected obligation, surfaces the - blocker, and routes it to the owner of the contradictory instruction or - missing capability — a warning buried in a transcript is not consumption. - Two complementary protections: modeled contradictions are caught BEFORE - dispatch where possible; ambiguities found in execution are returned during - it. The agent is an additional reviewer of the supplied argument, not the - sole consistency checker, and it must never absorb our defect and disguise - it as successful execution. - -## The load phase (required dependency of every model-backed session) - -Before a model-backed process starts — worker, planner, or reviewer — the harness -resolves its declared context manifest against the applicable source snapshot, -assembles the required contents in the declared order, and supplies that assembly -to the ACTUAL model request. - -Honest boundaries (conceptual, not new modules): - -- **Context declared**: which sources and instructions are required. -- **Context assembled**: those contents were resolved from the source snapshot, - in order. -- **Context supplied**: the real provider request consumed the assembly (verified - against the request as sent, including provider rendering and tool definitions — - not against an intermediate prompt string). -- **Orientation produced**: the task-specific interpretation produced its output. - Load is the common foundation; orientation applies it to this task. Neither - proves the agent understood anything. - -The initial manifest: (1) full DESIGN.md (the canonical READING surface; -AGENTS.md/CLAUDE.md/README.md symlink to it), (2) genuinely additional standing -instructions — NOT a duplicate of the operational-essentials section already -inside (1), (3) the anchor-relevant context for this session. The reported file -size is a reason to measure token usage and context fit, not authorization to -substitute a smaller subset. - -Ordering law: stable project prefix first, role- and task-specific material last, -wherever the provider request format permits. - -## Alignment ladder (owner direction, 2026-09-21): cadenced alignment at every level - -Alignment is a RECURRING workflow responsibility across three scopes: the root -principles, the parent project's purpose and approach, and the task's outcome. -Functional decomposition exists so this is checkable: a decomposition must -PRESERVE each task's contribution to the levels above it — every node carries not -just its own contract but the derived statement of what that contract contributes -to its parent's intent. Alignment checks at any level are readings against that -carried contribution, never a fresh interpretation invented at check time. - -A budget is NOT a failure measure — it is the alignment cadence. When a turn -exhausts its budget the question is never "did it fail" but "is the work still -aligned." Orientation establishes the initial grounding; alignment MAINTAINS that -grounding as the work develops. Verification, publication, and review remain -distinct obligations — an alignment check never discharges them and they never -discharge it. - -The levels — each a different authority, and a check at one level never -substitutes for another: - -- **L0 root principles** — DESIGN.md (the axioms and what follows from them). - Question: does the work's approach still conform (single authority, fail-closed, - displaced cost, no workaround)? Carried by the load phase. -- **L1 project intent** — the decomposition node's declared purpose, approach, and - this task's carried contribution to it. Question: does the work still serve the - arc's intent, by its declared approach? -- **L2 task contract** — the node's boundary / red control / handback plus the - pinned witness. Question: is the current diff advancing THIS contract, or - drifting (scope creep, adjacent improvements, goalpost edits)? -- **L3 the live work** — the candidate and its submission against L2: the - submission's explanation is the worker's own alignment claim; verification and - goal audit are the independent ones. - -Cadence points — each cheap, each evidence-based, each producing an alignment -receipt: - -1. **Dispatch** — orientation + load phase (initial alignment). -2. **Each work interval / continuation turn** — a brief direction check. It - CONSUMES the actual work and its observations (the current diff, the receipts - produced so far, the worker's own account), never the worker's self-report - alone. Its verdict is a MAGNITUDINAL standing — a Fermi estimate, order-of- - magnitude and evidence-cited, never false precision (owner direction, - 2026-09-21): **ALIGNED (+0/−0)** — no measurable drift; **MISALIGNED - (direction)** — a named drift with a named correction; **EXTREMELY - MISALIGNED** — the work is off the task/project contract; **CATASTROPHICALLY - MISALIGNED** — the work violates a level above it (a root principle, the - project's declared approach). Each magnitude maps to exactly one action: - ALIGNED → justified continuation (evidence named); MISALIGNED → concrete steer - binding the next interval; EXTREMELY → escalate to the supervisor (plan-defect - territory; the cadence interval drops to its floor); CATASTROPHIC → stop the - line (the supervisor is convened immediately; work does not continue on the - candidate). The outcome AFFECTS the next work performed: a check whose answer - cannot change what happens next is ceremony, and ceremony here is worse than - absence because it reads as diligence. -3. **Budget checkpoints** — the Yielded path: the supervisor's adjudication IS the - alignment verdict (Nominal = aligned but slow; Plan defect = misaligned by plan; - Stuck = misaligned by reality). -4. **Submission and review/audit** — the worker's claim, then the independent - verdicts. - -Rules: - -- **A direction check is never pressure to manufacture completion.** The check - measures alignment and direction, not proximity to done; a check that punishes - an honest "not yet converging" teaches workers to fabricate convergence, which - is the same failure as budget-as-failure wearing a friendlier face. "Aligned - and still working" is a complete, acceptable answer. -- Alignment content is DERIVED from the modeled artifacts — DESIGN.md from - gunbc.design_document, arc intent and task contribution from the node's declared - fields and its parent's, the task contract from the node and its pinned - contract — never hand-typed prose that can drift (the submission-schema defect - is the standing specimen of prose drift). -- **The cadence is adaptive — a step function over verdicts (owner direction, - 2026-09-21).** Each level's interval starts at its magnitudinal base (scaled by - the chain's depth — more levels above a task means more to stay aligned to), - then steps with the VERDICT'S MAGNITUDE: ALIGNED widens that level's interval by - a declared step (+2 turns), MISALIGNED shortens it by the same step (sooner - re-check), EXTREMELY drops the interval to its floor outright, and - CATASTROPHICALLY stops the line rather than tuning anything. A RoutedConflict - (instructions disagree — not a worker signal) does not tune cadence at all. Every - level's interval stays bounded: never rarer than its magnitudinal ceiling (root - principles still re-check at their epoch regardless of streaks), never more - frequent than a declared floor that keeps probe cost subordinate to work. The - cadence state is per attempt-lineage, recorded on the alignment receipt (the - interval used and the verdict that moved it), so a later reader can replay why - the checks came when they did. The step reads only typed verdicts — never - vibes — and verification/publication/review gates are untouched by it: cadence - tunes HOW OFTEN we look, never what advancing requires. -- Alignment is not re-planning: the probe measures drift against existing - contracts; re-deriving the plan is the supervisor's job on escalation. -- What it is not: not a per-round interrupt, not a second full orientation, and - never a vibe check that can stall progress unbounded. - -First mechanical slice: the continuation brief carries the projected three-level -preamble; the answer rides in the submission's explanation; the belt records the -cadence point. Deeper slices: alignment answers as a typed per-turn artifact -consuming the diff and receipts, not just self-report; supervisor verdicts citing -alignment receipts. - -## Warm-prefix reuse - -**Warm-prefix reuse is an optimization to qualify and measure, not a guarantee.** -vLLM prefix caching matches processed token prefixes of resident blocks; eviction -and data-parallel cache scoping mean actual reuse depends on the serving route -and cache state, and must be OBSERVED. A canonical shared prefix makes sibling -requests ELIGIBLE for reuse — that is the whole claim. Consequences: - -- No warm-up agent, no conversation-fork mechanism: the first real request - supplies the initial demand; reuse shares eligible prefix computation, never - another session's mutable conversation or conclusions. -- **Missing required context blocks the request. A cache miss must never weaken - the context contract** — the declared cold route is taken when admitted. -- Cache savings are reported only when measured; prefill savings do not bound - generation cost. -- No "stability class" field until it has a concrete consumer; exact source - identity + declared order are what matter now. - -## Slice 3's authority shape (the extdeps distinction) - -DESIGN.md is the reading surface, not a second command authority. The intended -derivation is: - -**modeled operation + applicable policy → executable invocation, documentation, -and worker-facing explanation** - -— not one paragraph copied into both DESIGN.md and the prompt while execution is -authored elsewhere. Batching rules, witness population selection, wet-execution -requirement, executable provenance, and memory-budget requirements govern the -chosen execution ROUTE; the docs and prompt explain that same route. External -interface shapes stay in extdeps; the harness lifecycle composition and policy -stay in the workflow; the lifecycle does not move into extdeps to look modeled. -Slice 3's acceptance is the real execution boundary: at least one control fails -when the production connection between model facts and the session request is -removed. - -## Resource policy (subordinate, not organizing) - -**Budgets flow down the decomposition DAG; they are never flat.** A root carries -a declared bandwidth (rounds/tokens/wall-clock — the total effort the owner -allocates to that arc). A decomposition node allocates fractions of its -bandwidth to its children; a leaf's worker budget is what its parent dealt it, -not a global constant. Escalation sessions run one to two magnitudes above the -leaf budget they supervise — the supervisor must be able to re-read the -checkpoint, adjudicate, and still have runway to act, which a same-size budget -cannot do. Effort expended is therefore proportional to the project's bandwidth -by construction: big arcs afford big attempts; a leaf of a small chore node -gets a small one. Every budget figure is a declared allocation on the node, -visible on the dashboard — never a literal buried in harness code. - -Budget exhaustion is NEVER a bare "task failed". It produces a defined outcome: - -1. **Checkpoint** — the attempt state (transcript, worktree, receipts-so-far) is - preserved as a checkpoint. If the worker left a submission, it advances - normally; without one the attempt is `Yielded`, not failed. -2. **Escalation** — a Yielded attempt escalates to the supervisor session for - the node it was dispatched under (leaf → its parent decomposition node's - supervisor; the root arc's supervisor is the top). The supervisor resumes - from the checkpoint (the attempt's warm context/state — a KV checkpoint, - not a cold re-read) and adjudicates one of three routes: - - **Nominal** — the worker was progressing; issue a continuation turn with a - renewed budget. (Cheap tasks legitimately finishing late take this route; - attempt 1 of wave 1 would have.) - - **Plan defect** — the brief/centering sent the worker wrong (scope too big, - exemplar missing, contradictory instructions). Routes to the node's owner - as a plan defect — the worker is not re-dispatched against a known-bad - plan, and rule 11's contradiction machinery carries it. - - **Genuinely stuck** — the task itself can't advance (missing capability, - external blocker). Typed `Blocked` with the supervisor's stated cause. -3. **Supervisor verdicts are evidence**: the route, its basis, and the - continuation count are recorded per attempt. Continuation caps remain - tunable policy attached to the anchor; a cap hit escalates again rather than - failing silently. - -The supervisor is a modeled session role (like worker/reviewer/auditor), not a -human pager: it runs on the same harness, with its own guidance contract and the -load phase applied. Escalation depth is bounded by the decomposition DAG — a -supervisor that itself yields escalates to its parent, terminating at the root. - -## Metrics - -Outcome metrics: cost per delivered code-change result, publication success rate, -operator interventions per delivery, review rework per delivery. Rounds-per-diff -is retained as a DIAGNOSTIC (it is how orientation/repair cost movements are -observed); it is no longer a success metric. - -## Substrate ruling (owner, 2026-09-20): gunbc SCM inside, git at the edge - -The internal process substrate uses gunbc SCM (`dag/gunbc/scm/*`: object store, -write spine, staging, proposals) for everything it can cover: candidate capture, -the submission's head binding, attempt state identity, and the fanin merge of -worker candidates into the project's integration head (SCM proposals, typed -conflicts, modeled merge verdict — not textual three-way git merge). Git is used -ONLY at the final publish layer: translating the project's integration head to a -git push + GitHub PR. The repo being git-hosted and GitHub publication are the -edge; nothing internal speaks git where an SCM operation exists. Belt commit/ -verify/publish seams sit behind an explicit interface so the SCM realization is -the authority and the git realization is edge-only. - -## Delivery structure: recursive decomposition (owner ruling 2026-09-20) - -There is no separate "project" mechanism. A project IS a node — one whose work is -the completion of its children. The work graph is a DAG with the same structure -as the substrate itself: any node may decompose into child nodes (recursively), -a child may serve more than one parent (DAG, not tree — the import graph's law, -acyclicity, is the only structural rule), and every dispatchable node must have -at least one parent. Totally flat items — no parent — are not dispatchable work; -dispatch admission refuses them with a typed cause. - -Consequences: - -- **Fanout**: dispatching a decomposition node dispatches its ready descendants - (capacity-bounded), each attempt producing a verified candidate. -- **Fanin is a fold, recursively**: a decomposition node's own candidate is the - integration of its children's verified candidates (SCM proposal merges, typed - conflicts); its evidence is complete when every child is delivered. The fold - bottoms out at leaf nodes (ordinary single-worker tasks). -- **Delivery obligation binds at the grain you publish**: a leaf is a - decomposition of one (the degenerate case — per-attempt PR behavior falls out - without special-casing); a batch like the shell→dag ten publishes ONE PR at - the decomposition node's integration head; a whole arc could publish one PR - at its root. -- Evidence states still bind per-candidate-head at every grain; a parent head is - Published only when its integration carries every included child's - verification receipt. - -Slice 2's publication machinery is unaffected in mechanism — the obligation -subject is "the node whose delivery contract was dispatched", at any depth. - -## Distribution layer (owner direction, 2026-09-20): stateless executors, fabric-resident state - -Agents are stateless step functions over stored state; any conforming executor on -the fabric can run the next slice of any attempt. The worker's signature: - -> input: (attempt identity, transcript head ref, workspace ref, budget slice) -> output: appended events + typed terminal (Continue | Yielded | Submitted | Blocked) - -Nothing an attempt needs may live only in an executor's local filesystem. Attempt -state maps onto the fabric's storage tiers — each class into the store that is -already shaped for it, never a new per-lane persistence invention: - -| State class | Fabric storage home | Status (2026-09-20) | -|---|---|---| -| Transcript events (append-only, ordered, replayed on resume) | Fabric event log — the transcript becomes a hash-chained event stream with a head ref | Log exists; attempt transcripts not yet written to it | -| Indexes over events (transcript heads, attempt-by-node, executor fencing) | The fabric DB (gunbc#11723 — shared dependency with the group-A seat pool) | In flight | -| Workspace / candidate / receipt objects (immutable, content-addressed) | SCM object store — the belt's candidate capture already writes here | Landed (e9f0683adb) | -| Bulk artifacts (worker logs, large dumps) | Artifact store (artifact_store_fs realization today; storage-tiering policy later) | Exists | -| Executor leases / seats | Fabric cells (fabric_cell_acquire) | Exists for fleet work, not yet for dispatch | - -Rules: - -1. **Exactly one active executor per attempt**, fenced by a fabric lease. Two - executors continuing one transcript is the corruption case; the fence, not - politeness, prevents it. -2. **The belt observes fabric state, not host state.** Today it polls - attempt-state dirs on the dashboard host; the same facts read from the store - make it restart-safe and host-independent (a belt on srv1 cannot see srv2's - disk). -3. **Placement is a claim, not a hardcode.** Dispatch mints work + state refs; - executors (systemd units today, microVMs later) claim seats through the cell - machinery. A microVM worker is pool member N behind this seam — never a - bespoke spawn path. -4. **Executor switches pay cold prefill, stated honestly.** Prefix caches are - per serving engine; a resumed attempt on a different executor recomputes its - prefill. The slice-3 stable load-phase prefix is what keeps that cheap; it is - a cost to measure, not a reason to pin attempts to hosts. -5. **Transcripts compact.** Long attempts append a compaction event - (summary + reference to the compacted range) rather than growing unboundedly; - the resume reads the compaction head. -6. **Transcripts carry private facts.** Store placement and access for attempt - state inherit the private-facts boundary that ctrl/ used to be; this is a - placement decision, not an afterthought. - -The supervisor/escalation design (Resource policy) rides this substrate: a -supervisor session is just another executor reading the same refs — "resume -from the KV checkpoint" is, mechanically, load transcript head + workspace ref + -a warm prefix. - -Graduated path, each step independently useful: - -1. **State into the store** — transcripts and receipts content-addressed - alongside candidate capture; the belt dual-reads (store first, disk - fallback). -2. **Checkpoint/resume on one host** — a budget-exhausted worker exits Yielded - with a transcript head; a fresh unit resumes from the ref. (The continuation - routing built 2026-09-20 is this step's decision layer.) -3. **Executor pool seam** — spawn targets a claimed seat instead of the - dashboard host by name. -4. **Second executor + microVM** — another host or a microVM joins the pool; - nothing above this line changes. - -## Workflow execution (owner ruling, 2026-09-21): durable obligations, replaceable processes - -Repair the current failure, but the global belt pass is NOT the permanent -architecture. The durable facts are: accepted work, each outstanding obligation -(capture, verification, publication, review, audit, alignment check), its state, -its due time, and its last result or located failure. Processes, systemd units, -timers, and page bodies are replaceable realizations over those facts — never -the facts themselves. - -1. **Per-item advancement, per-operation persistence.** Each work item's - operations advance when INDEPENDENTLY eligible — not when a global pass - reaches them. Every operation's result AND its located failure persists at - the moment it is produced, never at pass end. A blocked capture is an - item-scoped fact by construction: it must never prevent unrelated - verification, publication, or alignment from advancing. And a required - operation never disappears as a legacy skip — an unperformable operation - stays outstanding with a typed cause and an owner, because a skip that sinks - an obligation is a silent drop wearing a receipt. -2. **Scheduling: wake-ups, due times, bounded reconciliation.** Targeted - wake-ups (submission written, worker terminal, receipt landed) advance the - affected item's next operation promptly — the timer is not the critical - path. Due times live ON obligations (retry-after, the alignment cadence) so - anything time-driven is data, not a loop's period. Bounded reconciliation - (the sweep) is the RECOVERY backstop that re-derives from durable state — - never the primary driver. Missing a notification or restarting a controller - is a non-event: recovery replays from the durable obligations and no user - action is ever required to resume. -3. **The page: fast, honest, decoupled.** Observations are decoupled from - execution success — a failed, poisoned, or hung item still renders - truthfully. Staleness is EXPLICIT (rendered-at and observed-at per region), - never implied by a fresh-looking body. -4. **Acceptance demonstrations.** (a) One submitted change reaches its PR - while another attempt is poisoned or hung. (b) Restart and executor-loss - recovery through the same path — kill the controller mid-obligation and - watch recovery resume it without user action. - -5. **The contract (owner ruling, 2026-09-21): durable, versioned handoff of - responsibility across execution boundaries.** This generalizes clauses 1-4 - beyond the belt - and it does NOT mean queueing every local function call. - The authority is a modeled module in the repo (lands as - `gunbc.workflow.durable_handoff` with its first real consumer); this document - REFERENCES it and must never become the only place the rule exists. The - contract: - - **Inbound**: a handler performs bounded admission, then confirmed durable - acceptance, and returns an operation reference - it finishes there. The - accepted work, its required inputs, and its continuation survive the caller - and the executing process. - - **Processing**: consumes identified inputs and advances state - conditionally; settlement records an attributable result, a refusal, or a - durable transfer of responsibility. - - **Three separate facts**: transport acknowledgment, execution-resource - release, and historical retention are distinct - a delivery may be settled - once durable responsibility transfers, but settlement never erases the - obligation or the information recovery needs. - - **Outbound**: the same contract applies to effects at the upstream - boundary, with guarantees limited to what the upstream API actually - provides - modeled as observed guarantees, never invented ones. - - **Composition, not invention**: the contract composes the existing work, - storage, identity, ownership, and observation authorities. -6. **The missing conformance question lands with the first real inbound - consumer.** The §3b roster (gunbc.design_argument conformance_domains -> - roadmap_review_criteria) has no row whose question is "does this boundary - hand off responsibility durably - bounded admission, durable acceptance, - conditional advance, honest settlement?" - so this week's failure classes had - no mechanical reviewer: a pass-killing item failure, a hung tick freezing a - pipeline, an undecidable artifact sinking a candidate, a fresh-looking body - hiding stale observations. The boundary with neighbors: leasing (§3b) owns - the lease as a PRIMITIVE; fabric/compute owns allocation; the new row owns - the handoff architecture that uses them. Per §3b's own rule a row names homes - that EXIST: the durable_handoff authority lands first with its executable - controls, and the conformance row is added naming it plus the lease - authorities - until then this is a modeling obligation, not a reviewer. These - classes are its seed tells. - -## The belt demoted (owner ruling, 2026-09-22): wake-up and anti-entropy only - -The belt tick is NOT the workflow authority, NOT the unit of execution, and NOT -the source of current state. It is demoted to two duties: delivering wake-ups -and bounded anti-entropy repair. Every accepted issue, attempt, candidate, and -external observation has its OWN durable authority and version — that is where -state lives. - -The working shape: - -- An event or a due refresh recomputes the SMALLEST affected projection, - conditionally publishes it (publish only when the version moved — never - republish unchanged), and creates ONLY the next eligible obligation. -- Duplicate, lost, and out-of-order wake-ups are harmless: every consumer is - idempotent and version-checked, so a wake-up is a hint, never a fact. -- The expensive operations (verification, escalation convening, integration, - publication) are per-item obligations invoked on wake-up — never phases of a - monolithic pass whose completion bound the corpus scale will always - eventually break. The 90-minute tick that dies unfinished is the standing - specimen. - -## The frontend as an issue tracker (owner ruling, 2026-09-22) - -**Issue intake is fabric-resident (owner ruling, 2026-09-23).** Created issues -never touch the git tree: content versions go to the SCM object store, intake and -state-change events to the fabric event log, and the current-state index -(issue id → current version + standing) to the fabric DB (the shared in-flight -dependency — group-A seats and attempt-state indexing consume the same -substrate). The roadmap projection consumes the fabric-resident intake as an -overlay onto the .dag-authored nodes; the two populations present identically -downstream. `POST /issues` is the first real inbound consumer of -`gunbc.workflow.durable_handoff`: bounded admission (fields valid, a parent -required — the no-flat-items ruling), durable acceptance, an issue reference -back. - -The frontend IS an issue tracker over the same authorities — and the interface -itself is an external authority, modeled in extdeps with its real vocabulary: -**Google's Issue Tracker (Buganizer)** (`extdeps.google.issue_tracker` — Issue, -Component, Status, Assignee, CC, Star, Upvote, Hotlist, SavedSearch, -BookmarkGroup, BlockingRelation, and its stock sidebar views: Assigned to me, -Starred by me, Upvoted by me, CC'd to me, Collaborating, Reported by me, To be -verified, plus Hotlists and Browse components). The roadmap frontend is a -MAPPING from our domain authorities onto that modeled interface (interface in -extdeps; mapping and policy in the workflow) — never a homegrown tracker shape. - -- **Components express domain placement; parent issues express decomposition; - blocking edges express dependencies; attempts express execution history.** -- The default view is a searchable issue table. Each issue has a detail page - and a separate Work tab. A project-tree view uses nested title-in-progress - bars (the landed taskbar component, promoted to the tree grammar). -- **Four facts stay separate**: planning status, the current workflow - obligation, the attempt outcome, and the delivery evidence. The page derives - a concise PRESENT-TENSE explanation from them ("verifying candidate - 2ccf4dc72f", "blocked: turn 2's artifact undecodable; turn 3 re-declared") — - it NEVER flattens them into one label like "Submission failed" plus a row of - misleading pending stages. A pending stage names what it waits on, not just - that it waits. - -## Review ruling (2026-09-23): branch held on two blockers; the conservation laws - -The first full review of the integration branch returned HOLD with two blockers -(the launch census reads only tmux while production attempts run in systemd -units — an invisible-population defect on the live admission path; and -ContinuationFresh conflating "no lineage" with "lineage active/unresolved" — -an effectful spawn answer for non-fresh states) plus high findings (alignment -model parallel-not-authoritative; submission capture not self-authenticating; -launch admission not an atomic cross-invocation reservation; the actuation root -too broad to audit). The laws it minted are now doctrine: - -1. **Observation conservation**: every identity discovered at an external - boundary survives into exactly one modeled output arm — classified, - unclassified/unknown, foreign, or malformed. No arm may mean "drop it." - Classification failure weakens what is KNOWN about the identity; it never - erases the identity. -2. **No refusal-to-neutral conversion**: Refused or Unknown must never become - [], 0, false, or none at a safety boundary. -3. **Current policy does not identify a historical occurrence**: an existing - attempt is read through the provider/realization/profile it was launched - with (its persisted spawn identity), never through the currently selected - policy. -4. **Adjacency is not a join**: a digest, subject, locator, or authority - supplied beside a payload must be derived from it or checked against it. -5. **Weaker evidence cannot improve admission**: replacing an observed fact - with unread/unknown keeps the verdict equal or more restrictive. - -The recurring-failure-mode row to file on the public side: "an observed member -disappears when classification refuses," with the static census tripwire over -helper-layer shapes (Refused => [], Unknown => none, Absent => accumulator). - -**The reopening gate is conjunctive (review ruling, 2026-09-24):** model-backed -dispatch resumes only when ALL of: the population-conservation + continuation -composition proof is accepted (an active systemd attempt reaches zero spawn -effects), the memory proof reaches FitProved for the declared capacity intent, -and the exact combined revision is deployed and read back. Until the launch -lease lands, launch authority stays single-controller/manual — a bounded -intermediate state, never evidence of cross-invocation exclusivity. - -## Slices and their production exit criteria - -| Slice | What must be true before calling it complete | -|---|---| -| 1. Orientation requirement (in flight: agent-13, roadmap_centering.dag) | The real dispatch path consumes the applicable orientation input; absent or inapplicable orientation cannot be bypassed by a presence flag. | -| 2. Handoff and delivery | A submitted candidate is captured, authoritatively verified, and published to its associated PR without another discretionary request. Yield, publication failure, restart, and uncertain remote success all remain truthful. | -| 3. Justified instruction contract + load phase | The real session request consumes the ordered, source-bound context. Consequential build/test and submission/delivery claims derive from the same modeled facts that govern execution (traceable support per rule 8); the composed instruction set is consistent per rule 9 (the claim_batch scope distinction is the demonstration case, declared in the model and expressed correctly in both surfaces); the worker has the rule-11 contradiction route and it is consumed. At least one control catches a DELIBERATELY INTRODUCED conflict. | -| 4. Evidence-driven refinement | Actual dispatch records show where cost and failures occur. Improvements are evaluated against those records, including unsuccessful attempts and review rework. Cache savings reported only when measured. | - -Serialization note: `roadmap_belt_actuate.dag` edits serialize behind agent-13; -preparation, source inspection, acceptance-case design, and non-conflicting work -do not. - -## The review deliverable - -The owner-facing PR contains the harness changes ACTUALLY EXERCISED (identifying -the source head that ran), links the resulting task PRs and evidence, and shows -at least one real dispatch reaching its task PR: this dispatch used this source -and context, submitted this candidate, verified this head, created/updated this -PR, reached this review state — plus which failure controls were exercised and -which capabilities remain unproven. A dashboard restart is an operational step, -not evidence. Plan text and dashboard status are not the deliverable. diff --git a/docs/plans/host-network-attachment-converge-design.md b/docs/plans/host-network-attachment-converge-design.md deleted file mode 100644 index d5d5ae53456..00000000000 --- a/docs/plans/host-network-attachment-converge-design.md +++ /dev/null @@ -1,115 +0,0 @@ -# Host network attachment as a converged fact — retiring the router click-path - -**Status:** design, operator-directed 2026-08-27 ("can we figure out how to do this stuff -via fleet converge in the future? i don't really want to poke at my router anymore"). - -## 1. The displaced cost - -Paid on 2026-08-27: moving two DGX Sparks from ethernet to wifi meant hand-editing DHCP -reservations in the CR1000A web UI — delete two entries, wait on a stale dynamic lease the UI -refuses to remove, re-author two static rows against MACs read off the hosts by hand. Each act -is a fleet fact, none is expressible in the model, and the model's record was wrong in a way -nobody could catch from inside the repo — `dgx_procurement.dag` asserted two -`StaticDhcpReservation` rows while the router had been handing out ordinary dynamic leases the -whole time. A fact the repo cannot read is a fact the repo will eventually misstate. - -The cost to displace: **a host's address is currently owned by a device the fleet cannot -observe, cannot converge, and cites only through a user-guide PDF.** - -## 2. Two paths, and why one is declined - -### Path A — the host owns its address (RECOMMENDED) - -Take DHCP out of the loop for fleet hosts. A host's address becomes a netplan fact, authored -from the model and applied over the existing `host_effect_apply` path — the transport that -already converges hostname, toolchain, runner slots and build cache. The router's only remaining -involvement is a **one-time** DHCP-pool shrink so the static range cannot collide; after that it -is never touched. - -This fits the machinery already in tree: - -- `extdeps.netplan.netplan` already models the applier half, including the load-bearing - fact that presence in `/etc/netplan` is sufficient for application regardless of writer. -- `host_effect_apply` already delivers typed effects to these exact hosts. -- `membership_reconcile` is the spine: desired vs observed attachments, keyed by - `(host, interface)`. Added/Modified → upsert, Removed → teardown-or-refuse. A new medium is - a new instantiation, not a forked reconcile. -- **No new transport.** §3 permits one as a Realization handler, but the cheapest correct - move needs none. - -### Path B — model the CR1000A admin transport (DECLINED, recorded) - -Converge DHCP reservations by driving the router. `Cr1000aAdminTransport` today is -`WebUiManual | UnknownTransport`, so a real handler is the structurally sanctioned addition. -Declined on cost and on §5: - -- The CR1000A's local admin API is undocumented. The module's cited authority is a Verizon - user-guide PDF describing the UI, not an API — any handler would be reverse-engineered from - a SPA's traffic and **could not be cited**, the grounding requirement `extdeps/` exists to - hold. -- Firmware-fragile: a Verizon-pushed update can change it with no notice and no version to - declare. -- Buys strictly less than Path A: the address still lives in a device that is not the fleet, - and the model still has to transcribe it back. - -Recorded rather than left implicit because the next reader will otherwise re-derive it: the -router *is* the obvious place to converge a DHCP reservation, and the reason not to is not -obvious. - -## 3. What is missing - -Four gaps, in dependency order. None is large; the first is what everything else waits on. - -**(a) A per-host interface roster, and its producer.** No model of "srv6 has `enP7s7` -(ethernet, dark) and `wlP9s9` (wireless, up)". `InterfaceObservation` is a shape with **no live -producer** — `host_network_diagnosis.host_network_observation` returns `none` for every host, -and the module says so, counting the whole fleet as an honest deficit. Landing the producer is -not new scope: an existing model is already waiting for it, and its tally falling is the -receipt. - -**(b) Wireless in netplan.** `extdeps.netplan.netplan` models the renderer and the interface, -not config *content* — no `addresses:`, no `wifis:`/`access-points:`. The wifi block needs an -SSID and a key. **The PSK is a secret and must be a `SecretRef`**, never an inline literal, on -the reasoning `nvidia_dgx_spark_setup` already applies to the per-unit sticker credential: a -shared home SSID password in source is the FactoryLogin mistake with a different subject. - -**(c) MAC grounding.** `MacAddress` is still the anemic `NonEmptyStr where brand` in -`extdeps.dhcp.v4`, with a named dissolution to an unwritten `extdeps/network/mac.dag`. Two MACs -per host makes this load-bearing: the model must say *which interface's* MAC a row binds, and -today it cannot. The 2026-08-27 incident is the witness — a reservation silently kept naming a -dead port. - -**(d) A medium axis on the attachment.** `NetworkPortMedium` carries `WirelessLan`, but as a -*hardware catalog* fact (what the box ships with), not an *attachment* fact (what the host is -using). `product.network_topology.NetworkLocality` has `Lan` with no wired/wireless -distinction, so the topology cannot express the change that just happened. - -## 4. Staging - -- **Phase 0 — read.** Land the `InterfaceObservation` producer over `host_effect_apply`: - name, permanent MAC, admin state, carrier, medium, addresses. Discharges the counted - deficit in `host_network_diagnosis` and makes (c) and (d) authorable from measurement - instead of transcription. No writes. -- **Phase 1 — ground.** `extdeps/network/mac.dag` (hex-octet parse, dissolving the brand); - medium on the attachment; the roster keyed by `(host, interface)`. -- **Phase 2 — write, wired first.** Netplan `addresses:` content + apply, converged through - `membership_reconcile`. Wired is the safe pilot: a mistake leaves the box reachable on - wifi. Requires the one-time pool shrink. -- **Phase 3 — wireless.** `wifis:`/`access-points:` with the PSK as a `SecretRef`. - -**The standing hazard:** every phase after 0 can strand a host. These two boxes have no BMC and -both ethernet ports are now dark, so a bad wireless apply is a physical trip. Wireless converge -must land behind the same read-back-and-revert discipline as the rest of the converge path, and -Phase 2's wired-first ordering exists so there is a second way in before the first is edited. - -## 5. Acceptance - -- Phase 0 RED: a host whose observation cannot be produced answers `UnknownRefused` and is - **counted**, never defaulted to a plausible interface list. -- Phase 2/3 RED: an attachment whose desired address collides with the router's live DHCP - pool **refuses** rather than applying — decidable from the pool bound and the desired set, - so a construction wall, not a post-check. -- The flagship: srv5/srv6's addresses become derived from converged attachment rows, and - `dgx_procurement.dag`'s reservation rows stop being the address authority — at which - point the CR1000A rows are observation-only, and the click-path is retired by having - nothing left to click. diff --git a/docs/plans/human-identity-authorization.md b/docs/plans/human-identity-authorization.md deleted file mode 100644 index edb4213b1fa..00000000000 --- a/docs/plans/human-identity-authorization.md +++ /dev/null @@ -1,194 +0,0 @@ -# Human identity and authorization (owner ruling 2026-09-25) - -> **Network placement decides who can reach an endpoint. Google OpenID Connect -> establishes which human is present. Application policy decides what that -> principal may do.** - -Tailnet no longer authenticates people for the tracker or approvals app. It -remains transport: development network, TLS/reverse-proxy, a reachability -boundary. A `Tailscale-User-Login` header is optional transport/audit -metadata; it never mints an application principal and never authorizes a -mutation. (The tailnet model itself flags the fragility: the header is -unsigned and trustworthy only when the proxy is provably the sole route.) - -## Three separate concepts - -```text -Transport: Direct HTTPS | Tailnet proxy | local development -Authentication: Google Workspace principal -Authorization: assign | comment | approve | administer | merge -``` - -## One qualified-principal authority - -```text -PrincipalRef { authority, namespace, subject } - -principal:google-oidc/accounts/ -- authority https://accounts.google.com, subject = ID-token sub -principal:gunbc/workflows/fabric -- authority gunbc, namespace workflows -``` - -Email, display name, picture are `PrincipalProfile` facts with a -profile_revision — never identity inputs. `sub` is the durable identifier -(email can change; Workspace membership comes from the `hd` claim, not email -parsing). Consequences: email changes don't mint people; email reuse doesn't -inherit authority; avatar failures can't weaken auth; one human = one -principal across tracker, web approvals, native approvals. - -The known contradiction at a3fd5eb is BANNED going forward: -`principal_from_email` setting `subject = email` keeps the selector as -durable identity. Comments must not copy that shape. - -## Authority placement - -Buganizer extdeps keeps `User { email }` (the interface's visible user). -gunbc's principal/profile/auth authorities live OUTSIDE extdeps: - -```text -extdeps.google.openid_connect -- Google's protocol + claim vocabulary -extdeps.google.workspace -- Workspace org/domain interface facts -gunbc.identity.principal -- PrincipalRef, workflow principals, profile refs -gunbc.auth.google_workspace_login -- gunb.ai policy over Google OIDC -gunbc.auth.session -- application session identity + lifetime -gunbc.auth.request_authentication -- raw HTTP request → authenticated request -``` - -(The existing oauth2.Google.Refresh is API-token refresh, not a login model.) - -## Authentication vs authorization (Google's own split) - -Login requests ONLY `openid email profile`. No Gmail/Contacts/Directory/Drive -scopes for login. Profile enrichment for the assignee picker is a separately -authorized directory producer feeding the internal principal-profile cache. -Avatar order: internal profile → Workspace directory → cached → initials; -absent/unread never blocks auth, assignment, comments, or approval. - -## Web flow (tracker + web approvals) - -Authorization-code flow with PKCE (implicit is deprecated-insecure). Routes: -`GET /auth/login`, `GET /auth/google/callback`, `POST /auth/logout`, -`GET /auth/session`. Steps: state+nonce+PKCE verifier persisted as a -short-lived transaction → redirect → verify state → exchange code → verify ID -token (signature/JWKS, iss, aud, exp, nonce, email_verified, hd == gunb.ai) → -mint opaque `AuthenticatedSession` → redirect only to an admitted relative -return path. - -Browser gets an opaque cookie: Secure, HttpOnly, SameSite=Lax, bounded -lifetime, rotated on authentication. State-changing forms additionally require -application CSRF proof (login state ≠ CSRF token). - -Redirect URIs: exact-match owned domains (`tracker[-dev].gunb.ai`, -`approvals[-dev].gunb.ai`); dev names may resolve only inside the tailnet. -Separate OAuth clients per app × environment × platform (tracker web -dev/prod, approvals web dev/prod, approvals iOS/Android); one admitted -audience roster, no silent cross-app token acceptance. - -## Approvals: three proofs, never one - -```text -Human authentication — Google OIDC principal -Decision authority — one-time approval capability + approval policy -Device/app integrity — App Attest / Android equivalent / enrolled device -``` - -GET confirmation page never decides; POST decision is the only mutation, -requiring: authenticated session + CSRF proof + one-time capability + -expected current decision state + approval authorization (an explicit -approver roster/role — `hd == gunb.ai` alone is org membership, not approval -authority; first production cut = an exact stable-principal roster, even of -one). Native apps: platform Google Sign-In → ID token to the backend -validated against the native client audience; the mutation requires Google -principal + device integrity + capability. - -## Event-log migration - -`roadmap-event/v3`: `author: PrincipalRef`; `Claimed { assignee: -PrincipalRef, return_to: PrincipalRef }`; `CommentCreated { ..., author: -PrincipalRef }`. Historical v1/v2 email strings decode as -`LegacyEmailPrincipal { email, authentication_unestablished }` — readable, -never authorizing, NEVER auto-matched to a current `sub`. New v3 writes -require an authenticated principal; missing/invalid session refuses. - -Assignee picker stays email-centric in display, but suggestion rows submit -the stable PrincipalRef. Typed email → directory lookup → exactly one -admitted principal or typed refusal (none / multiple / external domain / -unverified). Fabric: selector `fabric@gunb.ai`, stored -`principal:gunbc/workflows/fabric`. - -## Comments and planning behind the seam - -Order: stable principal model → Google OIDC authentication → authenticated -comment append → Ask Fabric / planning work requests. The comment CARRIER may -be built against PrincipalRef immediately; its production POST stays blocked -until Google session auth is wired. The one-principal one-work-request design -stands: Issue (durable outcome) / Comment (narrative or explicit request -origin) / IssueWorkRequest (the exact work requested now) / -DeliverableContract (code candidate | issue-revision candidate | -investigation report | review verdict). No planner role, no Planning type. - -## The canonical work graph - -```text -A. Human identity and authorization - A1 PrincipalRef authority - A2 Google OIDC external model - A3 server session + CSRF boundary - A4 tracker assignment migration to event v3 - A5 durable comments - A6 issue-revision/planning work contracts - A7 web approval migration - A8 native approval-client identity - A9 directory/avatar enrichment -B. Stage0 driver iteration (identity split, immutable cache + private - overlay, A/B qualification, freeze, incremental closure, fixed point) -C. Group B serving (cache proof → prefill bound → FitProved intent → - persistent convergence/readback) -D. SCM fanin (conversion → generation/CAS save → structural locator → - integration-head advance) -E. Workflow reliability (population, continuation, alignment, capture, - launch lease, actuation split) - -A1→A2→A3; A3→A4→A5→A6; A3→A7→A8; A1→A9. -B independent of A. C independent of A,B. D independent of A,B,C. -E constrains broad autonomous dispatch. -``` - -The stage0 (B) acceptance contract is preserved verbatim from the ruling: -never hand-edit the tracked stage0 mirror; scratch driver rendered by -interpreting emit_source_root_eval_driver_main_rs; one exact materialized -library identity; Library/DriverA/DriverB mutation receipts; immutable -content-addressed library + private mutable overlay (overlay output is never -authority until copied under the full content key); freeze the rendered main -digest after qualification (instrumentation lives at the modeled runtime -boundary, not in 05_emit_rust.dag); landing = regen-round receipt + stage0 -partition rebuild decision; final acceptance = A′/B′ parallel on two hosts, -full two-build fixed point paid once, generated driver digest == fast-loop -qualified digest. - -## Acceptance controls - -- A forged Tailscale-User-Login establishes no application principal. -- Direct HTTPS and tailnet-proxied requests authenticate to the same - principal. -- Wrong iss/aud/exp/nonce/hd or unverified email refuses. -- Same sub + changed email = same principal; same email + different sub ≠ - same principal. -- Comment events store PrincipalRef, not email. -- Historical email-only events readable but unverified. -- Valid capability + wrong principal refuses; GET never decides; POST - requires capability + principal + authorization + CSRF. -- Avatar/directory failure renders fallback, never affects authorization. -- Service/machine writers cannot use a human Google session as workload - identity. -- Tailnet loss changes reachability, not stored-event identity semantics. - -## Immediate sequencing (owner-set) - -1. Amend the comments lane: author is PrincipalRef, not email/Tailnet. -2. HOLD deployment of the assignment POST at a3fd5eb until the v3 migration - lands there. -3. Build A1–A3 (principal authority, OIDC model, session/CSRF). -4. Cut tracker assignment + comments over (A4, A5). -5. Cut the web approval broker over (A7), then native audiences (A8). -6. Demote tailnet identity to transport observation on user-facing routes. -7. B, C, D continue independently. diff --git a/docs/plans/infer-checking-mode-plan.md b/docs/plans/infer-checking-mode-plan.md deleted file mode 100644 index c636899ed37..00000000000 --- a/docs/plans/infer-checking-mode-plan.md +++ /dev/null @@ -1,140 +0,0 @@ -# Checking mode in v2 infer: the expected type reaches a row before it decides - -Owner: smart-newt-725. Status: plan only; nothing here is built. It is sequenced after -eager-newt-412's site-keyed facts store (branch `eager-newt-412/facts-site-key`, -[keying-relation-design.md](keying-relation-design.md) §3e), because both restructure the same fold. - -## The defect this answers - -`v2.compiler.infer` is one bottom-up fold (`v2.std.node` `fold_node` in `infer_entries_for_tree`). -A row decides from its children alone. A generic record construct is typed from its field values -alone. When no field value fixes a type parameter, the construct stays untyped and counted, even -when the position consuming it declares the instantiation. That happens for a phantom parameter -(`Ph { n: Int }`), or for a parameter that occurs only in a field whose value is itself such a -construct (`Two { a: Ph, b: T }`). - -The controls are the two claims in -`v2.test.claim.compiler.infer_expected_type_record_instantiation_witness_test`, enrolled -expected-red (`v2.workflow.floor_expected_red` `floor_expected_red_chunk_infer_checking_mode`), -plus the discriminating refusal below, which this work ENROLLS when it lands. It is done when all -three pass. - -The refusal is not enrolled expected-red today because, while it is red, infer does not refuse the -program and runs the whole member, which costs about 75.8k eval steps against the 72.3k floor -budget for a new witness. Once checking mode refuses at the field, the same claim stops early and -fits. Its specimen, to enroll with this work as `etr_a_field_value_disagreeing_with_the_contexts_instance_refuses_holds`: - -``` -module v2.test.etr_two_bad - -import v2.std.logic { Bool } - -type Ph { - n: Int -} - -type Two { - a: Ph - b: T -} - -data t: Two = Two { a: Ph { n: 1 }, b: 3 } -``` - -Infer only the `data` member `t` (handed the module's own indexes) and require a refusal with -`record_field_value_does_not_inhabit` at `b`. The nested `a: Ph` must stay: it is what leaves -`T` fixed only by the context, so the claim discriminates checking mode rather than plain -field-value instantiation. - -## The rule that shapes everything: deliver before deciding, never revise - -The site-keyed store keeps one fact per site. A row that enters a different second fact at an -occupied site refuses `infer_facts_key_conflict`. So the expected type cannot be applied after a -child has decided, by re-judging it or overwriting its fact. It must reach the child's row before -the row decides. - -A consumer-side re-judgment that worked after the fact was built and withdrawn (#12935). It would -have been a second route that this plan deletes. - -## The mechanism: the expected type is fold context, not a fact - -`v2.std.node` already has the fold this needs: `fold_node_topdown` with -`NodeFoldTopDown { init, child_context, step }`. `child_context(parent, ctx, edge)` computes what a -child is told from its parent and the parent's own context. Checking mode runs infer's gather on -that fold, with the context carrying at most one expected type. - -- **It is an input, not a fact.** It is never stored in the facts trie, and no site gets a second - entry because of it. -- **A child's site is the parent's site plus one step**, which the site-keyed store makes directly - addressable. That is why this goes second. - -## Where an expected type enters (`child_context` arms) - -Each arm states the one edge it speaks for. Every other edge passes **no** expectation: context is -never inherited blindly through a node that does not declare a type for that child. - -| parent | edge | expected type for the child | -|---|---|---| -| bodied Arrow | `^arrow_body_edge` | the declared return, at its denotation, unless it mentions the Arrow's own type parameters | -| annotated Bind (`let x: T = e`) | the bound value | the annotation | -| record construct | a field edge | the field type the record declares. If the construct itself has an expected `R` of its own record, the record's binders are first instantiated to `A..`. | -| list introduction | an element | `T`, when the list's own expected type is `FreeMonoid` | -| application | an argument | the formal's declared type, only while the callee's Arrow is known (inline today; named calls after #12506) | - -The record-construct arm is the one that matters for the controls, and it is recursive. The -construct's own expectation instantiates its binders, which instantiates the field types, which -become the expectations of nested constructs. - -## Which rows consume it (first slice: one) - -Only **the record construct row** consumes the expected type in the first slice. When its context -is an Instantiation of its own record with every argument present, it starts the existing -instantiating walk (`infer_judge_formal_args`) from those instances, not from empty. A field value -that disagrees with an instance the context fixed is refused at the value by the same relation and -reason as any field. The construct forms `R` itself, so the consuming position's existing -judge sees an equal type and decides. - -That one row needs two supporting changes, both written once in #12935 and withdrawn with it: - -- a starting-instances parameter on `infer_judge_formal_args`; -- the instantiating walk judging a formal that mentions an already-instanced type parameter at that - instance (`inner: Ph` with `T` fixed reads as `Ph`). - -Other rows that could consume an expectation are later slices, each with its own red. Two examples: -an empty list literal `[]`, which is on the frontier today, and an unannotated `let`. - -## What does not change - -- **There is no second judgment.** The consuming position still judges produced against declared - through `v2.std.inhabitance` `declared_type_inhabitance`. Checking mode only lets the child - decide with more information. -- **No type is guessed.** A declared type that is a declaration reference (possibly an alias), an - Instantiation of a different record, the wrong arity, or one mentioning an enclosing function's - type parameters gives no expectation. The construct stays counted exactly as today. -- **String literals stay out of scope.** They are untyped until work item adhoc-3e4eee5a-d66, so - the map-literal case stays counted on that item. - -## Cost shape - -`child_context` runs once per edge. For a construct, the record's declared fields must not be -re-derived per field edge, which would be quadratic in the field count. The context for a -construct's children carries the instantiated field-type map, built once when the construct's own -context is computed. Reading the record declaration is a guarded index lookup -(`v2.std.symbol_index` `symbol_index_lookup`) against the subject's `resolved_declarations`. - -## Evidence when built - -- The two enrolled controls pass unchanged and leave `floor_expected_red` in the same change, and the - refusal above is enrolled green, as - that roster's monotone arm requires. -- Existing record-construct, declared-return, application-argument and optional-at-required - witnesses stay green. -- A native-route census, base against head: N reach / M changed, at identity grain. -- A cost reading on a construct with many fields, showing the field map is built once. - -## Sequencing - -1. eager-newt-412's site-keyed facts store lands. It converts the gather rows to per-site entries. -2. Checking mode: the gather moves to `fold_node_topdown`, adds the `child_context` arms above, and - changes the construct row as the only consumer. One PR, with the controls flipping in it. -3. Later slices add other consumers one at a time, each with its own red. diff --git a/docs/plans/issue-boundary-cuts-review.md b/docs/plans/issue-boundary-cuts-review.md deleted file mode 100644 index 360f3ae9964..00000000000 --- a/docs/plans/issue-boundary-cuts-review.md +++ /dev/null @@ -1,86 +0,0 @@ -# Issue boundary cuts and project focus — review index - -Base: `e286b29962c3b89904277c64d0e0c7cc25ddb2d0` (PR #12387). -Branch: `codex/issue-cut1-toposort`. Implementation is isolated from the serving checkout. -Nothing in this branch is merged or deployed. The auth flow, compiler, fleet configuration and -held allocation PR #12465 are untouched. - -## Review order - -1. `d446000e2d6` — Cut 1: command/state boundary. Private carrier snapshots, exact remote-ref - CAS, atomic event + operation receipt, original-result retry, native URL-encoded forms and - truthful detail-history refusals. [Detailed receipt](receipts/issue-cut1-command-boundary-2026-09-28.md). -2. `b09db59f677` — Cut 4.1: five Workspace administration obligations over the existing goal - assessment vocabulary. Row 4 has a supported Policy API read, but manual mutation. The - others use bounded console readback. No Google setting or credential was changed. - [API qualification](receipts/google-workspace-admin-api-2026-09-28.md). -3. `abd2d6857f2` — Cut 2: independent nesting, shared by worker alignment, supervision, - integration and presentation. A single event transition produces standing and assignment; - progress actors and attempt existence do not assign ownership. Typed ticket fields survive - presentation; description uses the brief; fleet placement leaves Google's extdeps model. -4. `8e8796ea0d5` — Project focus: clicking a title focuses its recursive project, displays the - recent three completed items and upcoming work in dependency order, and offers “…” for - earlier work. Open selected issue retains detail navigation; modifier-click keeps native - link behavior. Undo restores order/focus/expansion, column sorting exits topological mode, - and cycles refuse before changing the view. - -The first authored nesting migration is shell → DAG batch 1: the ten `shell-dag-*` work items -belong to `shell-typed-invocation`, as the existing batch contract explicitly states and the -operator selected for this acceptance case. Other reverse dependency links were **not** copied -into ownership. Their scheduling meaning remains intact. Additional projects require authored -membership; issues with no nesting stay independent. Both ends of nesting are the same work-item -kind. There is no separate Epic/Task hierarchy; the redundant hardcoded Task chip is removed. - -The old tree's shared-prerequisite links remain dependency links, but do not inflate owned-child -progress counts. This prevents the old “0 of 1” project claim for work owned somewhere else. - -## Validation boundaries - -The changed workflow has focused pure, actual carrier and browser controls. The full page -suite still reports the pre-existing ticket-brief budget failure. A run against an untouched -worktree at the base reproduced these six violations: `2-scm-native-authority-program` (134), -`floor-ceiling-roster-cut-completeness` (123), `frontend-eval-training-program` (177), -`fleet-mtcollins1-first-host` (153), `fleet-slot-retirement-wet-proof` (116), and -`cardinality-vertical-slice` (119). The limit is 100 words. This branch does not weaken it or edit -unrelated ticket contracts. - -The full serve witness closure previously exceeded the unchanged 6 GiB limit (exit 137). -Focused passes are not a green integrated floor. This branch remains a draft for review, not a -merge/deployment recommendation. Each receipt names the executed boundary; a typecheck is not -reported as an executed HTTP acceptance test. - -Known boundaries retained from the handoff: first-time Fabric assignment still invokes the -existing inline launch path; this cut does not install a durable launch-obligation consumer. -Per-issue attempt observation remains unavailable with an explicit rendered reason. Workspace -readback types are not a deployed Google observer; receipt producers must authenticate and -retain their evidence. Broad protected-storage cutover remains in the held allocation lane. - -## Reproducing the narrow controls - -Each control is a `.dag` entry, run with `gunbc run --source-root dag --source-root src/v2 ---entry --claim-run`. The Git CAS/race/replay control is -`test.manual.issue_command_wet`. The emitted clients come from `test.manual.issue_assignment_client` -(`write_issue_sort_script`, `write_assignment_client_script`). The form codec is -`test.claim.http.form_urlencoded_witness_test`. The Python browser drivers and closure copier that -were used locally had no `.dag` authority and no consumer, so they were removed rather than landed -as unmodeled harnesses (DESIGN §6). The browser-side controls have no committed instrument until -they are modeled. - -## Final local results - -- Page: 83 PASS; 1 FAIL, the six brief overruns reproduced on the untouched base. -- Presentation: 57 PASS. Alignment: 42 PASS. Continuation: 22 PASS. Submission: 34 PASS. -- Event-state: 19 PASS. Tracker: 34 PASS. Fleet component projection: 2 PASS. -- Recursive nesting: 3 PASS. Workspace administration: 5 PASS. Form codec: 2 PASS. -- Actual Git CAS/race/replay commands: PASS. Native Chromium form → DAG decoder: PASS. -- Emitted assignment retry client: PASS. Emitted project-focus/sort client with served CSS: PASS. - -These results are re-derived by the entries named above; the transcribed logs are not committed -(DESIGN §6: name the instrument, never transcribe its output). The late detail-parameter forwarding and whitespace cleanup are -small follow-ups to the executed page snapshot; they do not change the default served inputs. - -The ordinary push was rejected by `.githooks/pre-push`: inherited Rust formatting drift at -`src/v1/stage0/src/cli_run.rs:20257`. `cargo fmt --all --check` reproduces it on the untouched -base. The hook explicitly documents `git push --no-verify` as its override. That override is -used only to publish this draft for review; Rust source and the hook are unchanged. No landing -check is represented as passing because the push was allowed. diff --git a/docs/plans/issue-page-architecture.md b/docs/plans/issue-page-architecture.md deleted file mode 100644 index 15e7959ef9a..00000000000 --- a/docs/plans/issue-page-architecture.md +++ /dev/null @@ -1,108 +0,0 @@ -# The issue page architecture (owner mandate 2026-09-25) - -The per-issue page copies Buganizer's information architecture nearly -literally, retaining gunbc's stronger workflow evidence. The page must answer -at first glance: what is this, who owns it, what blocks it, what changed, what -happens next. It feels alive because people and Fabric visibly participate in -one narrative — not because more diagnostics are on screen. - -## Layout law - -- Title is the primary object/link; the canonical slug is secondary identity. -- Header carries ONLY planning status + type + priority (`[Done] [Task] [P2]`). - No competing status strings (`done`, `claimed by…`, `accepted`, standing - lines all at once is the defect). -- Default tab is **Comments**: description pinned above a chronological - comments/activity stream with author avatars, comment numbers, timestamps, - and a real composer. Tabs: `Comments · Dependencies · Work · History` - (Duplicates later, when duplicate semantics exist). -- Right metadata rail: status, assignee (the editable control), reporter, - component, parent, blocked-by/blocking, delivery, return-to, modified, - observation. -- Authored fields are not seven equal rows: Description (brief/motivation/ - outcome), Acceptance criteria (red control, hand-back), Current plan - (current state, first slice — collapsed once done), Scope (collapsible), - Work (implementation instructions and evidence). An empty - `verified · last verified` row never renders. -- Work tab: attempts, incarnations, provider selection, candidate heads, - verification receipts, raw diagnostics. Comments tab: discussion, assignment - changes, important blockers, publication, review handoff, status changes. - Never raw provider/belt noise in Comments. - -## One qualified-principal model - -```text -QualifiedPrincipal { subject, primary_email, display_name, - kind: Human | Workflow, avatar } -``` - -Email is the user-facing selector, never the durable identity — aliases can -change without rewriting history. The assignee control is an email -autocomplete (search, recents first from durable assignment history, avatar + -display name + email + kind, keyboard navigation, dedupe by stable principal, -`fabric@gunb.ai` pinned near top). NO model names anywhere in the assignment -UI (no glm/kimi/codex); Fabric picks the worker through capability/cost/ -capacity/serving authorities. The realization may appear in the Work tab. - -Avatar resolution order: internal principal profile → Google Workspace -directory/Contacts photo → cached prior → deterministic initials. Served via -`/avatar/` with TTL/ETag. Standing: -`AvatarResolved | AvatarAbsent | AvatarUnread`; absent and unread render -initials and NEVER refuse assignment, comments, or rendering. Fabric gets a -stable service avatar with a workflow badge — it must not masquerade as human. - -## Assignment IS the execution entry point - -```text -AssignIssue { issue, expected_issue_revision, assigned_by, assignee, return_to } -``` - -- Assign to human → transfer responsibility, no dispatch. -- Assign to `fabric@gunb.ai` → transfer responsibility + durably request - execution (`IssueAssignedToWorkflow → WorkRequested → launch admission`). - No Start Work button anywhere. -- Admission refusal does NOT undo assignment: the issue stays assigned to - Fabric and renders the located waiting condition ("blocked — no admissible - serving offer; retry when an offer becomes available"), not "assignment - failed". -- Re-assigning the same issue revision to Fabric joins the existing request - rather than minting another attempt. - -## Return-to-merge - -Record `requester`, `assignee`, `return_to/merge_owner` explicitly (default -`return_to = requester` — never derived from whichever event was first). On -OBSERVED publication: durable publication event → one compact automated -comment with PR/head → status Ready for review → assignee becomes return_to. -Merge remains the human completion action; publication never closes the issue. - -## Comments are durable events - -`IssueCommentCreated / Edited / Deleted` — each comment carries issue id, -comment id, author principal, body (or body ref), created/edited times, -visibility. Edits/deletes append events, never invisible mutation. Stable deep -links `/issue/#comment-7`. Composer with markdown/code/@mentions; retries -never duplicate. The stream interleaves only meaningful lifecycle events -(assignment, work accepted, attempt started, blocking decision needed, -candidate submitted, PR published, review requested, reassigned for merge, -closed/reopened). - -## Dependencies stay dimensional - -Dependencies tab preserves separate sections: parent/children, blocked -by/blocking, related, supersedes/superseded-by — never one flattened list. -Header may carry a compact summary (`Blocked by 2 · Blocking 1 · 4 children`) -that opens the tab. - -## Acceptance controls (12) - -1. Title primary link, slug secondary. 2. Assignee field opens avatar-backed -email autocomplete with fabric + humans + recents. 3. Human assignment never -dispatches. 4. Fabric assignment = exactly one work request, no Start Work. -5. Serving/capacity refusal leaves Fabric assigned with the located blocker. -6. Reload preserves assignments and comments. 7. Comment retries never -duplicate. 8. Missing avatars render initials, never block operations. -9. Publication posts one handoff comment and reassigns to the stored merge -owner. 10. Dependencies separate from parent/child. 11. No raw provider/belt -noise in Comments. 12. Work tab keeps exact attempt/candidate/verification/ -publication/incarnation evidence. diff --git a/docs/plans/kv-capacity-proof-abstract.md b/docs/plans/kv-capacity-proof-abstract.md deleted file mode 100644 index 5de1b94a1ff..00000000000 --- a/docs/plans/kv-capacity-proof-abstract.md +++ /dev/null @@ -1,129 +0,0 @@ -# KV capacity proof: the model-agnostic abstract (for the DS4.1 lane) - -Distilled from the GLM-5.3-Flash / group-B work (2026-09-24/25). Everything -here except section 4 is model-agnostic; section 4 is the worked GLM instance -to imitate. Governing doctrine: docs/plans/moa-memory-modeling.md. - -## 1. The question the proof answers - -Given a qualified subject — model, runtime, topology, parallelism, allocator, -workload, generation — decide, WITHOUT launching: - -```text -CacheFitProved / CacheFitRefused / CacheFitUnestablished -``` - -for a workload intent (sequence_length × seats) against every rank's allocator -inventory. The whole-arm verdict (ArmMemoryFit*) is a CONJUNCTION of subproofs -of which this is one: static residency, cache capacity, prefill transient, -decode transient, load transient, supply reserve. CacheFitProved never implies -ArmMemoryFitProved while another subproof is open. - -## 2. The one algebraic law - -A hybrid model's persistent cache is NOT a fungible token inventory. It is a -sum over cache GROUPS with different growth shapes: - -```text -PersistentCache(rank, length, seats) = - seats × Σ(fixed-per-sequence groups) (recurrent/state groups) - + seats × Σ(per-token groups) × length (full attention) - + seats × Σ(windowed groups) × min(length, window) - + seats × Σ(pooled-index groups) × ceil(length / pool_every) - + allocator grouping / padding (per-group block rounding) -``` - -The four growth shapes (extdeps.vllm.kv_layout's KvGroupKind): - -| shape | scales with | example | -|---|---|---| -| FullAttention | length × seats | dense attention layers | -| SlidingWindow | min(length, window) × seats | windowed attention | -| Mamba/KDA recurrent | seats ONLY | KDA/linear-attention state | -| KPoolIndex | ceil(length / pool_every) × seats | compressed sparse index | - -Demand: `seats × cold_admission_blocks(exact_layout, length)` per group, -folded against `allocator_blocks` and `arena_bytes` on EVERY rank, joined by -the LIMITING rank (ranks are asymmetric — measure or derive per rank, never -average). - -### The three forbidden moves - -1. **No full-context-equivalent division.** The engine's printed "GPU KV cache - size: N tokens" is a projection at one profile, not an inventory; - `N / new_length` is a CacheCapacityCandidate, never a proof. -2. **No linear rescale of allocated arena.** "KV memory in use" is the - RESIDUAL allocation (envelope − weights − activation − graphs), not demand - of the configured seat. -3. **No negative or composite terms as credits.** A negative component - observation is an attribution refusal (lower 0, upper open); - `weights + non-torch` is a composite observation, not a placement receipt. - -## 3. The proof machinery (all reusable) - -- **MemoryProofSubject** (7 axes): model@checkpoint-digest, runtime@source-sha - +image-digest, topology@host-census-revision, parallelism policy, - allocator@configuration-digest, workload@intent-revision, generation. -- **MemoryTerm**: { component, placement_law, scaling_law, phase, lifetime, - bound, derivation } — every byte names its mechanism. -- **Receipt-subject compatibility**: computed FROM the scaling law. Static - residency transfers across workloads; per-sequence-fixed transfers across - lengths at equal seat counts; every workload-sensitive term refuses across - subjects; a TP4 receipt refuses TP2 at the topology axis. Generation is - provenance, not a gate. -- **Measurement standings**: DerivedExact / DerivedConservativeInterval / - MeasuredUpperBound / ObservationOnly / ModelFalsified. A point sample never - becomes MeasuredUpperBound by being the largest seen once. -- **The allocation plan as the durable instrument** - (ResolvedKvAllocationPlan): the engine emits its group roster — kinds, - layer identities/counts, block sizes, page bytes, tokens-per-state, - grouping/padding — and the plan MUST reproduce the engine's printed capacity - as an independent control. After that, any workload point evaluates offline; - wet runs become falsification, not sizing. -- **Discriminating controls** (witness these in any new instance): - same arena + different roster → different capacity; - missing group → Unestablished; - pool_every 4→8 → derived capacity changes; - cross-topology receipt → refused; - cross-workload transient receipt → refused; - negative observation → cannot improve headroom. - -## 4. The GLM worked instance (what to swap for DS4.1) - -GLM-5.3-Flash @ TP4 (GB10 unified pool, fp8 KV, 0.8567 envelope): - -```text -architecture (from config.json, cited): - 45 layers = 34 KDA linear-attention + 11 sparse-attention (~3:1 interleave) - KDA: 64 heads × dim 128 → recurrent state ∝ seats (≈1 MiB/layer/seq/rank) - sparse: 512-dim MLA latent (∝ tokens), index_kpool 4 (∝ tokens/4), per-req tail -measured (bounded experiment, subject-bound per rank): - pools 119.69–121.69 GiB; weights+nontorch 77.0–81.6 (composite, asymmetric); - limiting rank TP3 (pool 119.69, arena 17.03 GiB) -engine control print: 1,805,689 tokens @ 64K profile → 27.55x at 65,536 -candidate (NOT a verdict): floor(1,805,689 / 262,144) = 6 seats at 262K -open: 262K prefill transient (sparse-indexer buffer ≈ 40 × 262,144 × 132 B - ≈ 1.289 GiB/rank, formula from the pinned runtime source) -``` - -For DS4.1, swap exactly: the architecture facts (layer census by attention -kind, head counts/dims, latent widths, any pooling), the pinned runtime -revision, the topology/host census, and the envelope policy. The fold, the -verdicts, the standings, the compatibility law, and the controls do not -change. DeepSeek-family specifics to resolve in the roster: MLA latent -dimensions, whether a pooled/index state exists, MoE expert placement under -the chosen EP policy (active-params is compute, NOT residency). - -## 5. Module map (reuse, don't fork) - -- `dag/extdeps/vllm/kv_layout.dag` — growth shapes, cold_admission_blocks, - layout mint refusals. Add DS4.1's group roster HERE as cited architecture - facts beside GLM's. -- `dag/extdeps/vllm/kv_group_metadata.dag` — the emission-gap model; the - ResolvedKvAllocationPlan rung. -- `dag/gunbc/spark/arm_memory_fit.dag` — the three-arm verdicts, MemoryTerm - census, receipt join, compatibility law. -- `dag/gunbc/spark/arm_memory_experiment_observed.dag` — the wet-bundle - binding pattern (how receipts admit with standings). -- `dag/gunbc/spark/group_b_serving_capacity.dag` — the capacity-intent → - derived-profile pattern (intent is the owner's decision; profile derives). diff --git a/docs/plans/lowering-occurrence-projection-design.md b/docs/plans/lowering-occurrence-projection-design.md deleted file mode 100644 index d08c8ae4a08..00000000000 --- a/docs/plans/lowering-occurrence-projection-design.md +++ /dev/null @@ -1,176 +0,0 @@ -# Lowering occurrence projection (XL-2 prerequisite `LoweringOccurrenceProjection`): design - -Status: design only. No compiler behaviour changes in the PR that lands this file. Tracked by -`gunbc.recurring_failure_mode.lowering_rebuilds_an_authored_atom_without_its_occurrence`, carrier arm -`gunbc.compiler_frontend_program_status` `LoweringOccurrenceProjection`. - -## 1. What was measured, and how - -**The instrument for the totals** is the census: -`v2.compiler.reference_conservation_census` `reference_conservation_census_for_paths`, run over -`reference_conservation_stratified_sample_paths` in batches of 25 paths. The whole sample in one -process does not fit in memory. Its per-module summary is the authority for `conserved`, -`locus_erased`, `dropped` and the channels, and this document does not copy those numbers. The -figures that were measured when this design was written are a dated receipt in the PR that landed -it, not a fact kept here. - -**The per-site split has no committed instrument yet, on purpose.** Nothing on a lowered node says -which function built it, so the census cannot attribute an erased atom to a site. The split in the -PR receipt came from a one-off instrumentation: - -- Each synthetic-minting site on the normalize route was tagged with a site id carried in - `OccurrenceProjected`, which the census treats exactly like `OccurrenceSynthetic`. -- A probe recorded which pool entry each locus-erased atom consumed: a rebuilt atom with its tag, - a `Named` edge label, or an atom lowered from the wrong source. - -That tagging is an edit to the compiler, so it cannot be an entry point, and it was not committed -(DESIGN §6: a one-off is not an instrument). **Nothing below depends on its numbers.** §3's -exclusion rests on grammar role alone, and §4's order uses only a coarse size comparison. PR 3 of -§4 makes the split re-derivable without tagging: the census reports each erased atom's grammar role -and its encoding (label, rebuilt atom, or wrong-source atom) as typed dispositions. - -### The sites, by grammar role and encoding - -Each row names the function that builds the node and how the locus is lost. The classification -is structural, from reading the lowering: which grammar role the atom has, and whether the -lowering encodes it as a label or as a rebuilt atom. - -| site | how the locus is lost | grammar role | XL-2 population? | -|---|---|---|---| -| `v2.std.node_query` `construct_tag_edge`, called from `body_lower_try_record_literal` (record tag `Uri { .. }`) and `body_lower_pattern_suffix_lowered` (pattern constructor `Present { .. }`) | the atom exists but is lowered from the **enclosing captured shell**, so it carries the shell's occurrence instead of the tag token's | constructor or type reference | **yes** | -| `v2.std.qualified_name` `qualified_name_spine_node`, reached from `body_lowering_fold` `body_lower_qualified_name_spine_node` | every segment atom is built with `node_synthetic`; the `occurrence_id` parameter reaches only the spine's root, and the input is already `List`, so there is no segment node to carry | dotted reference or type path | **yes** for the head segment and type paths; tail field projections are not | -| `body_lower_field_init_edge`, `body_lower_field_pattern_edge` | the name becomes a `Named` edge label | field name | no | -| the module body's `Named` edges for `data`, `fn`, `test fn` and `type` names; parameter lists; field declarations; variant declarations; named-argument labels | label | binder or label | no | -| `body_lower_let_expr` let binders | atom lowered from an enclosing shell | binder | no | -| module-header segments (namespace graft; the census matches them against its `header_pool`) | header pool | the module's own name | no (see §3; to be proven in PR 3) | -| `body_lower_method_operator_atom` (`.first` and similar method selectors) | `node_synthetic` | method selector, resolved through the receiver's type | no | - -**The reading that matters, and it holds without the counts:** - -- Only two constructors rebuild a *reference*. Everything else in the table is a binder or a - label. -- The largest reference site does not use `node_synthetic` at all. Its atom **is** lowered from a - source, just the wrong one. -- In the one-off measurement, `node_synthetic` inside `body_lowering_fold` itself was a negligible - site. The loss is in two `v2.std` constructors and in the readers that feed them. -- On the pinned sample, `dropped` is larger than `locus_erased`. The census shows this directly: - most of it is in modules that normalize refuses, which belong to other `xl2_prerequisites` - arms, and the rest is atoms `absent` from modules that normalize accepts. - -## 2. #11985's mechanism, and the one this carrier reuses - -#11985 added `v2.std.node` `node_lowered_from(kind, children, source: Node)`. It builds the new -node with `source.occurrence_id`, taken whole. A lowered node stands for the same authored text at -the same position, so it keeps that occurrence. A source that had no authored origin gives a node -that still has none, so the constructor can only preserve attribution and never invent it. - -**`OccurrenceProjected { caused_by }` is not the right carrier, and the RFM row's ceiling text is -wrong to name it.** `std.occurrence_identity` reserves `OccurrenceProjected` for a node minted in a -*different allocator*, meaning an insertion with no counterpart in the base, whose cause must be a -`ScopedOccurrenceRef`. It states outright that "a carried-forward node gets no arm here". Lowering -within one graph owns its source's occurrence, so using `OccurrenceProjected` there would create a -second identity for a fact `OccurrenceMinted` already carries. That is the parallel identity the -brief rules out. - -**The one mechanism:** every lowering site that rebuilds an authored atom builds it with -`node_lowered_from(source: )`. The reused type is -`std.occurrence_identity` `NodeOccurrenceIdentity`, the `OccurrenceMinted` arm, carried unchanged. -No new type, arm, or index is added. The measurement shows two ways a site gets this wrong, and the -same mechanism fixes both: - -1. **The source is the enclosing shell, not the terminal** (rank 3). `construct_tag_edge(tag: - Symbol, source: captured)` has a source, but it is the whole captured literal. The fix passes the - tag's own terminal node. That means `body_lower_record_literal_tag_optional` and the - pattern-suffix reader return the tag **node** instead of `Symbol`, and the tag atom is built with - `node_lowered_from(source: tag_node)`. -2. **The input no longer holds the nodes** (rank 4). `qualified_name_spine_node(qn: QualifiedName)` - receives `List`. By then there is nothing to carry, and `node_synthetic` is the honest - answer. Re-deriving the chain (DESIGN §6b) puts the earliest unjustified boundary at the reader - that turns segment nodes into symbols (`body_lower_postfix_chain_read` and its `segments`), not - at the spine builder. The fix carries the segment terminals to the builder, and the builder makes - each segment atom with `node_lowered_from(source: segment)`. - - `v2.std.qualified_name` `declaration_reference_node`, the marked carrier that resolve mints, keeps - the occurrence on its marker only, and does so on purpose. That is a separate post-resolve - decision and this design leaves it alone. - -**Climbing from mitigation to construction.** Once each reference site takes a `Node` source, the -constructors that only take a `Symbol` leave the lowering surface. A lowered atom is then built by a -constructor that *requires* its authored source, and "rebuilt without its occurrence" cannot be -written on the lowering route, with no lens needed. `body_lower_type_atom_node(sym, source)` already -has this shape. The remaining writable path is `node_synthetic` used with an `Atom` kind inside -lowering, and the last build PR closes it for reference positions. - -## 3. Binders and edge labels: not in the XL-2 population - -The XL-2 residual is the set of **resolve refusals after the typed import strip** -(`gunbc.compiler_frontend_program_status` `ExactOccurrenceResidualCensus`). XL-5's waves rewrite -those references. An atom is in the population only if removing imports can make resolve refuse -*at it*. - -- **A binder** (the name of a `fn`, `data`, or `type`, a variant declaration, a parameter, a field - declaration, a `let` name) introduces a name. Stripping imports cannot make it refuse, and no wave - rewrites it. -- **A field or argument label** (`Uri { uri: .. }`, `f(path: ..)`, `R { value: v } =>`) resolves - against the declaration of its type or callee. When that declaration is imported and stripped, - resolve refuses **at the type or callee reference** (rank 3 or 4), not at the label. A namespace - wave renames modules and declarations; it never renames a field or a parameter. -- **Module-header segments** are the module's own name. The graft consumes them, not resolve. - -So `EdgeLabel` (`v2.std.node` `Named { name: Symbol }`) does **not** need an occurrence for XL-2. -Adding one would change a substrate type that participates in structural equality and content -hashing, for a population XL-2 never reads. - -There is one real caveat. An occurrence-exact rename of a *declaration*, which XL-5 is not, would -need binder occurrences. That is a separate future carrier and should be modelled when a consumer -exists, not before (DESIGN §3c). - -Two consequences for the tracking rows, both corrected in this PR: - -- The RFM trigger "`locus_erased` reads zero on the stratified sample" is **over-scoped**: most of - that count is binders and labels, which no XL-2 fix will or should move. The capability is - stated as: *every reference-position atom that lowering rebuilds carries its authored terminal's - occurrence*. -- Deleting the declaration-scoped spelling pool in `v2.compiler.reference_conservation` needs a - second thing: binders must be told apart from references **by a grammar fact**, namely the atom's - role in its production, instead of by a spelling match. Until then the pool can shrink to binders - and labels, but it cannot be deleted. That work is PR 3 below. - -## 4. Rollout order and the regression check - -In order of size and risk: - -1. **Constructor tags: `construct_tag_edge` sources.** Change one shared constructor and its two - call sites: record literal and pattern suffix. It goes first because it is the smallest change. - In the one-off measurement it was about the same size as the spine site. -2. **Qualified spines: segment nodes reach `qualified_name_spine_node`.** This is larger: the chain - reader's result type changes, and so does a `v2.std.qualified_name` constructor that other - callers use. -3. **Census and closure.** Split the census's `locus_erased` into a typed binder/label channel - (counted, like `import_channel`) and a reference-position count, using the grammar role of the - atom. Take the `Symbol`-only atom constructors off the lowering route. Shrink the spelling pool. - -**The check: one controlled-fixture control per site, not a ratchet over the live sample.** Each -build PR brings a fixture module that exercises its site. A claim runs -`reference_conservation_of_subject` on it and asserts that the site's reference atoms are -`conserved`. The number is grounded in the controlled fixture, so it is a real oracle (DESIGN §5). -The red is run before the fix: on the base, the same claim reports the atom as `locus_erased`. The -positive control is a fixture atom that already conserves, such as a type atom (#11985). - -A conserved-share floor over the pinned sample is **not** recommended. That literal would be copied -from the current tree, which makes it a change detector. It is also not a closed universe: six of -its 315 paths have already been deleted. The sample census remains the observation that ranks the -sites, and PR 3 lets it report the split without the probe. - -## 5. Size - -- **In-population sites to change: 3** (the record-tag call, the pattern-constructor call, and the - qualified spine), in **2 constructors**: `construct_tag_edge` and `qualified_name_spine_node`. - Readers that change type: `body_lower_record_literal_tag_optional`, the pattern-suffix tag reader, - and `body_lower_postfix_chain_read`. -- **Out-of-population sites, recorded but not changed:** 8 label and binder encodings, the - let-binder atom, and the method-selector atom. -- **PRs: 3**, as in §4, each with its site fixture and a pre-fix red. PR 2 is the largest because - it changes a `v2.std.qualified_name` signature, so it has to enumerate that constructor's other - callers first. An optional fourth PR could cover the 7-atom method-selector site, but it is not in - the population. diff --git a/docs/plans/machine-intake-design.md b/docs/plans/machine-intake-design.md deleted file mode 100644 index b04e8ad6888..00000000000 --- a/docs/plans/machine-intake-design.md +++ /dev/null @@ -1,413 +0,0 @@ -# Machine intake: hardware qualification, provisioning, placement commissioning - -Operator ruling of 2026-08-29, transcribed as the authority for the `gunbc.machine_intake_*` -lane. The `.dag` modules cite this document; this document does not restate what they declare. -Where a section below names a type, the type is the authority and this is its rationale. - -Status: **INTAKE-0 and BOOT-DELIVERY-0 modeling in review** (gunbc#9690), reworked under the -operator's second ruling of 2026-08-29, the third ruling of 2026-08-30 -([machine-intake-ruling-3.md](machine-intake-ruling-3.md)) and the fourth ruling of 2026-08-30 -([machine-intake-ruling-4.md](machine-intake-ruling-4.md), whose ten-item final approval gate is -the merge gate: neither slice is recorded complete until they hold). Under ruling 4 the request's -`BootDeliveryTarget` (subject × protocol-neutral `BmcControllerEndpoint`) is BOUND to the access -context before any candidate is judged (`bind_boot_delivery_target`; subject and controller -mismatches are separate refusals), every transport-local standing (configfs via -`target_bound_reinstall_path`, whose observations are bound to a target at the observation and whose standing is derived by the producer — an empty population is Unestablished; UEFI HTTP; network boot) carries the target it was established for -and is refused as `CandidateEvidenceForOtherTarget` otherwise; the profile's capability row is -the exact-release `BmcFirmwareReleaseCapabilityRow` so raw "0.32" inhabits a valid profile (a -positive control proves it); a promoted profile binds only beside `ProfilePromotionVerified`, -which no producer can construct until INTAKE-AGENT-0A's evidence store — so every promoted -profile refuses today; the Redfish probe receipt is one coherent reading (payload-free ladder arm at the floor AND a nonempty `admitted_protocols`; a receipt saying one without the other is refused as a contradiction), boot CONTROL is established on its own evidence (`SurfaceBootControl` in the profile/observation intersection plus exactly one OBSERVED control route — `BmcBootControlRouteObservation`, its bytes in the evidence manifest, the route named by the profile — carried on the plan as `boot_control_route`; no observation refuses, an observed route the catalog does not name refuses, two observed routes are a typed ambiguity, never a catalog preference) and the observation -receipt carries an evidence MANIFEST that must name both the discovery bytes and the nested -probe's bytes; the plan's provenance preserves the selected candidate's own evidence. The -changed-witness execution sublane #9717 requires is dispatched as its own CI PR (child work -item of this lane). Under ruling 3 the solver reads a -`BootDeliveryRequest` (target unit and attempt, exact `BootArtifact`, staging offers each naming -the digest they serve), a `BoundBootDeliveryEvidence` whose access context -`gunbc.machine_intake_access` has already bound to this attempt (profile ∩ observation, capability -row read off the profile, observation receipt digest checked), and a `BootDeliveryPolicy`; it -answers with a `BootDeliveryPlan` that repeats target and artifact, derives the Redfish locator and -protocol from the probe RECEIPT, requires the MegaRAC route's `image_redirection` fact, and names -the profile provenance and observation receipt its eligibility came from. The capability-only -legacy solver, its `FirmwareUpdateThenVirtualMedia` shortcut, `srv3_install_mechanism`, -`fleet_install_server_specs` and the ProxyDHCP artifact path are deleted; `gunbc.os_install_mechanism` -is a compatibility projection that refuses at `InstallAccessProfileNotLookedUp`. Firmware release -identity is the raw vendor string (`BmcFirmwareReleaseIdentity`, so "0.32" is representable); -`FirmwareTransition*` lives in `gunbc.bmc_firmware_transition` and `NetworkBoot*` in -`gunbc.network_boot_delivery`. Under ruling 2: the Mt. Collins family binding is an EXPECTATION -(`MtCollinsBmcImplementationExpected`) until a workflow-layer `BmcImplementationObserved` -receipt exists; the one solver reads `BmcAccessProfile × BmcAccessObservation` through a total -profile lookup (`BmcAccessProfileStanding`), plans the Redfish arm only at -`TransferProtocolAdmitted` or above on the probe ladder, and takes its transport preference as an -explicit policy input; `derive_fleet_admission` consumes only a `ValidatedIntakeReceiptLedger` -(one genesis, linked, one subject and attempt, monotone); the in-substrate receipt hash is named -a structural fingerprint, with SHA-256 reserved for the durable producer; and -`gunbc.bmc_onboarding` is quarantined as non-authoritative legacy vocabulary. Executed -Mt. Collins and ASRock boots reaching the nonce-bound intake-agent callback are BOOT-DELIVERY-0's -acceptance and are still owed; INTAKE-AGENT-0, MEMORY-0 and SOAK-ADMISSION-0 are unbuilt. - -## 0. The ruling, in one paragraph - -The correct system is one hardware-independent qualification transaction with multiple -platform-specific access and boot-delivery realizations. "Virtual CD" is not a phase. Redfish, -OEM NBD/WebSocket, OpenBMC configfs USB gadget, UEFI HTTP, and PXE are alternative constructors -for one obligation: *deliver this content-addressed boot artifact to this identified host for one -boot, prove that the intended artifact executed, and restore the prior boot-control state.* -(`gunbc.boot_artifact_delivery`.) - -Four corrections to the original brief: - -1. Board serial is the durable unit key but is not enough to key a qualification. A repaired - machine keeps its board serial while every DIMM changes. Qualification is keyed to the board - serial plus an assembly-manifest digest (`gunbc.machine_intake_subject`). -2. Hardware qualification, OS provisioning, and placement commissioning are separate - transactions. They share boot delivery and receipts; success in one must not fabricate success - in another (`gunbc.machine_intake_phase`). -3. NFS traffic, a redirection session, boot-rail activity, KVM pixels, and a consumed boot - override prove activity, not test success. A memory verdict needs a machine-readable report or - an intake-agent receipt (`attest_diagnostic_boot`). -4. BMC-reported watts are not automatically wall watts, and EDAC reports are not automatically - DIMM-attributed. Both need explicit provenance types (SOAK-ADMISSION-0, MEMORY-0). - -Operationally: run all return-window-sensitive hardware testing immediately on arrival, before -shelving; run a second placement-specific commissioning soak after installation in its actual -power, cooling, and network environment; nothing serves until both have succeeded. srv2 is the -current candidate realization of an abstract staging service, not its identity -(`gunbc.machine_intake_staging`). - -## 1. The subject being qualified - -``` -QualificationSubject = UnitKey × AssemblyManifestDigest × FirmwareManifestDigest -UnitKey = validated unique baseboard serial -AssemblyManifest = chassis identity × CPU × DIMM × accelerator × storage × NIC × PSU identities -FirmwareManifest = BMC implementation/version × BIOS/UEFI × CPLD/SCP/platform firmware × configuration digests -MachineIntakeSubject = QualificationSubject × IntakeAttemptId -``` - -| Event | Consequence | -|---|---| -| BMC IP changes | Same unit; no identity change | -| Machine moves racks | Same unit and assembly; placement receipt becomes stale | -| One DIMM changes | New assembly subject; previous admission cannot apply | -| BIOS/BMC firmware changes | New firmware subject; affected qualification must be repeated | -| Motherboard changes | New unit key | -| Board serial absent, duplicated, or contradictory | Identity refuses; no fleet key is fabricated | - -The chassis part number is a first-class identity fact (platform-variant discriminator), not the -unit key. A physical DIMM label is bound to its SPD identity (`PhysicalDimmLabelBinding`); an -absent or duplicated SPD serial is preserved as a limitation, never papered over by the label. - -## 2. Platform facts vs live access observations - -`extdeps.bmc.access_profile`: `BmcAccessProfile` is platform/catalog data keyed by baseboard × -`ChassisApplicability` (an explicit arm, never absence-means-all) × implementation × exact raw -firmware release (`BmcFirmwareReleaseIdentity`), carrying its routes as inhabitants -(`BmcAccessRoute`: Redfish locators, IPMI lanplus, the MegaRAC REST route with its typed -`image_redirection` fact, the OpenBMC shell, the OpenBMC NBD WebSocket path) so a no-Redfish -build is representable without inventing locators, plus the capability row that release carries -(a catalog row whose row firmware disagrees with its key is refused at lookup). Provenance is -`ProfileFromExternalAuthority | ProfilePromotedFromObservation`, never the rendering declaration. -`BmcAccessObservation` is per endpoint with its raw response digest and a resource-bound -`RedfishVirtualMediaProbeReceipt?`; the lookup answers `Known | Uncatalogued | Ambiguous | -CatalogRowRefused` (external facts only). The controller is named protocol-neutrally -(`BmcControllerEndpoint { host }`, ruling 4 §3): one Mt. Collins boot drives one controller over -MegaRAC REST for delivery and IPMI for boot control, so an identity carrying a protocol has -already selected a route. The capability row a profile carries is the exact-release -`BmcFirmwareReleaseCapabilityRow` (ruling 4 §2), so a release with no semantic reading can be -catalogued. Which attempt the observation belongs to, whether the lookup ran, whether the -evidence manifest names every blob read, and whether a promoted profile's receipts were -verified are WORKFLOW standings: `gunbc.machine_intake_access.bind_bmc_access_context` produces -the one `BoundBmcAccessContext` the solver may read, and `ProfilePromotionVerificationStanding` -refuses every promoted profile until the INTAKE-AGENT-0A evidence store produces -`ProfilePromotionVerified` (ruling 4 §4). - -`BmcFirmwareFamily` gained `AmiMegaRac` (`extdeps.bmc.endpoint`, gunbc#9678); the implementation -module is `extdeps.bmc.megarac` and the vendor row `extdeps.vendor.ami`. The family-keyed -dispatchers moved out of `extdeps.bmc.openbmc` into `gunbc.bmc_implementation_dispatch` so a -MegaRAC change never edits the OpenBMC module (DESIGN §3 external upstream decomposition). -Family-grain MegaRAC facts are cited (admin/admin published default, IPMI-only protocol floor); -build-scoped facts live on the build binding `extdeps.ampere.mt_collins_product_brief.bmc` — -the observed Foxconn Mt. Collins build (BMC 0.32) serves NO Redfish and boots through the -proprietary REST remote-media surface, so the Mt. Collins delivery arm is -`MegaRacRestRemoteMedia`, not the DMTF pull. A profile/capability row per exact firmware is -still to be authored from that observation. - -## 3. The boot boundary - -Boot control (set one-shot target × reset × observe override consumption × restore) and artifact -delivery (visible to firmware × bytes proven × intended host consumed it × detach) are -independent obligations. `BootArtifactDelivery` is the coproduct -`RedfishVirtualMediaPull | MegaRacRestRemoteMedia | OpenBmcNbdProxyWebsocket | -OpenBmcConfigfsUsbGadget | UefiHttpBoot | PxeChainBoot`; `extdeps.bmc.virtual_media` gained the -`RedfishVirtualMediaTransport` and `MegaRacRemoteMediaTransport` arms and -`extdeps.bmc.redfish_virtual_media` models the DMTF shape. Boot purpose is data -(`BootPurpose`); the artifact set is per architecture (`IntakeArtifactSet`), never a bi-arch -invariant. - -Platform mapping: - -| Platform observation | Candidate delivery | -|---|---| -| MegaRAC build with live-observed Redfish VirtualMedia | RedfishVirtualMediaPull | -| Mt. Collins MegaRAC (observed build 0.32: no Redfish) with live-observed REST remote media | MegaRacRestRemoteMedia | -| ASRock/OpenBMC with live-observed OEM NBD WebSocket | OpenBmcNbdProxyWebsocket | -| ASRock/OpenBMC with BMC SSH, NBD client, configfs gadget, usable UDC | OpenBmcConfigfsUsbGadget | -| Firmware with observed UEFI HTTP support and reachable artifact service | UefiHttpBoot | -| Firmware with all network-boot requirements established | PxeChainBoot | - -`gunbc.os_install_mechanism` is a compatibility projection of the delivery solver with no solver -of its own; its consumers migrate to `BootDeliverySolution` and it is then deleted (its frozen note -carries the trigger). A `BootDeliveryPlanned` result is a candidate PLAN bound to target and -artifact — insert, host consumption, eject and detach are established only by execution receipts. -The boot-control target is protocol-neutral (`BootControlTarget`); which BMC operation sets it is -realization bound to the route the profile exposes. The request's `BootDeliveryTarget` is bound -to the access context first (`BoundBootDeliveryTargetContext`, ruling 4 §1) and the transport-local -standings each carry their own target, so a plan can never name a subject or controller other -than the one its evidence was gathered for; the plan's `BootDeliveryEligibilityProvenance` names -the profile provenance, the evidence manifest, the selected candidate's own evidence -(`SelectedCandidateEvidence`) and every candidate's standing. - -## 4. The workflow: one `MachineIntake`, three transactions - -**A — arrival hardware qualification** (immediately after delivery or repair, before shelving): - -| Phase | Required result | -|---|---| -| AccessDiscover | BMC implementation, exact firmware, supported protocol surfaces, BMC clock observed | -| IdentityBindProvisional | Board/chassis/BMC identities collected without contradiction | -| PriorLifeBoundary | Existing logs archived, then cleared or a precise immutable baseline cursor established | -| BmcSecure | Unique managed credential works; published factory credential no longer works | -| BootDeliveryEstablish | One eligible delivery transport selected from live observations | -| DiagnosticBootAttest | Intended intake artifact calls back with the attempt nonce and repeats machine identity | -| InventoryConform | BMC, SMBIOS/OS, and platform slot-table observations reconcile | -| MemoryFastQualify | Parseable pre-OS memory-test result | -| MemoryAuthoritativeQualify | Linux stress plus ECC/RAS before-and-after delta | -| SubsystemQualify | CPU, storage, network, accelerator, PSU, cooling obligations for the expected component set | -| AcceptanceSoak | Return-window heat/load test succeeds | -| ArrivalCleanup | Media detached, override cleared, temporary access revoked, desired power state restored | - -A machine that completes this is HardwareQualified. It still cannot serve. - -**B — provisioning:** OsArtifactDelivered → OsInstalled → InstalledSystemBootedFromLocalTarget -→ RuntimeIdentityMatchesIntakeSubject. OsInstalled needs a post-install producer (the installed -agent calling back after a normal local boot); a consumed override, a read ISO, or a DHCP lease -is not sufficient. - -**C — placement commissioning** (in the serving location): PlacementIdentityBound → -ActualCoolingAndFanPolicyObserved → ActualNetworkPathsQualified → PlacementLoadSoakQualified → -WallPowerMeasured → ServiceRuntimeAdmitted → FleetAdmitted. Moving the machine, changing its PSU -feed, cooling, or fan policy invalidates this receipt without invalidating memory qualification. - -`gunbc.bmc_onboarding` combines factory credentials, rotation, OsInstalled and FabricJoined in -one vocabulary; extracting those into the three transactions is a follow-up, not a second -lifecycle. - -## 5. Identity and prior-life handling - -Identity is provisional until the Linux intake environment repeats collection from the host -side and BMC identity agrees with host-visible SMBIOS/SPD/PCI identity agrees with platform -expectations. Disagreement is a named finding: BoardSerialDisagrees, ChassisPartNumberDisagrees, -BmcAndHostMemoryPopulationDisagree, DuplicateDimmSerial, ExpectedComponentAbsent, -UnexpectedComponentPresent (InventoryConform, unbuilt). - -Prior-life boundary: before clearing anything, preserve BMC clock, SEL/event logs, Redfish log -collections, audit/account events, current boot override, virtual-media state, power state, and -a sensor snapshot as an immutable content-addressed capture. Then -`PriorLifeBoundaryEstablished = LogsArchivedAndCleared | LogsArchivedWithBaselineCursor`; the -second arm only where platform policy explicitly admits an uncleared append-only boundary. The -receipt carries the pre-clear archive digest and the post-clear observation. - -## 6. Credential qualification - -Factory credentials are a bootstrap mechanism, not an ordinary credential state. BmcSecure: -probe published bootstrap credential → authenticated session → create/rotate unique per-unit -credential → verify new works → verify published fails → record secret reference and credential -epoch. The receipt never contains the password. A non-working factory credential is an access -state (CredentialStateUnknown, PreviouslyRotatedCredentialRequired, -AccountManagementUnavailable) → PartsHold, not ReturnWindow, absent other evidence. - -`gunbc.machine_intake_bmc_secure` is the authority for that standing: `derive_bmc_secure` folds -one `BmcCredentialRotationObservation` — the bootstrap reading, the rotation application, and the -two post-rotation readbacks — into a `BmcSecureStanding`, and `bmc_secure_phase_receipt` projects -that onto the receipt spine so `derive_fleet_admission` reads it like any other phase. The -standing is `sole_constructor` and carries the `MachineIntakeSubject` it was adjudicated for, and -the receipt reads its subject off the standing rather than taking one: a standing derived for one -attempt has no argument left to swap, so it cannot be re-addressed as another attempt's evidence. -Both post-rotation readbacks must be ordered after the rotation they claim to verify — an ordering -among the supplied readings, which establishes nothing about whether a BMC was probed — and a -readback that did not establish a credential state refuses rather than widening into the good arm. -No refusal cause is owned by `UnitHardware`. The module observes nothing; the effectful rotation -actuator that produces the observation is the declared frontier, and it is the same producer the -ruling gates on the `gunbc.bmc_onboarding` split. Evidence: -`test.claim.machine_intake_bmc_secure_witness_test`. - -## 7. Proving the diagnostic environment booted - -Each attempt mints a nonce; the intake agent returns `IntakeAgentBootReceipt { attempt_nonce, -artifact_digest, architecture, board_serial, assembly_observation, firmware_observation, -booted_at }`. That receipt, and nothing else, is what a future attestation would rest on — -but nothing constructs DiagnosticBootAttested today, and -`gunbc.machine_intake_receipt.attest_diagnostic_boot` does not: a matching singleton returns -DiagnosticBootCompletenessUnestablished, because an observed population is not a closed one. -The positive arm waits on an acquisition producer that CLOSES the callback population — bound -issuance, single ingress, close-before-snapshot, gap-free transcript, evidence from that same -acquisition. The module is the authority for which arms exist. NFS plateaus, redirection sessions, -power signatures, reset completion, SOL streams and KVM pixels are supporting observations that -ride in the refusal. KVM is optional forensic capture. - -## 8. Memory qualification (MEMORY-0, unbuilt) - -Fast gate: `FastMemoryQualified { artifact, report_digest, pass_count, tested_bytes, error_count } -| FastMemoryRefused`; a delivery session plus power activity without a report is -`MemoryExerciseObserved`. Authoritative gate: before snapshot, test plan, after snapshot -(SMBIOS/DMI, SPD, Redfish Memory, EDAC topology, EDAC CE/UE before/after, rasdaemon, MCE/APEI/ -GHES, stressapptest, stress-ng); deltas during the interval, not lifetime counters; thresholds in -policy rows (new UE → defect; new CE → refuses, swap diagnosis; zero → gate may hold). -Attribution is a separate standing: `DimmAttributed | RankAttributed | ChannelAttributed | -ControllerAttributed | Unattributed` — a channel-level error fails the gate but cannot construct -DimmDefective. Swap-pass diagnosis binds every stick label to SPD first; every physical swap -changes the assembly digest and starts a new subject. - -## 9. Beyond memtest (SOAK-ADMISSION-0, unbuilt) - -`ExpectedSubsystemSet` derives from platform and assembly inventory; an expected component cannot -silently disappear from testing, and a platform with no accelerator derives no accelerator -obligation rather than a Skipped row. Per-subject observations: CPU topology/load/throttling/RAS -deltas; memory population/stress/CE-UE/temperature; storage identity/SMART/self-test (destructive -only on declared-disposable storage); NIC MAC/link/errors/throughput to staging; accelerator -PCIe/device memory/compute/ECC-Xid-AER/thermal; fans tach presence and response; PSU inventory, -telemetry, external wall measurement where required; BMC sensor completeness, clock, log service, -account state. - -## 10. Load soak and power (SOAK-ADMISSION-0, unbuilt) - -Two policy rows: ArrivalAcceptanceSoak (return-window defects) and PlacementCommissioningSoak -(environment-specific). Both record load recipe digest, duration, ambient, sample interval, -temperature maxima, fan extrema, throttling, RAS deltas, power samples, cooldown. -`PowerMeasurementPoint = WallAcInput | PsuAcInput | PsuDcOutput | BoardRail | BmcUnspecified`. -Only a WallAcInput observation (or a justified calibrated conversion from a precisely identified -point) may retire the economics model's 350 W wall-power assumption; without one, emit -WallPowerUnestablished. `PowerTelemetryAgreement { wall_ac, bmc_reported, difference }`. - -## 11. Disposition - -`MachineIntakeDisposition = Admitted | ReturnWindow | PartsHold` with -`QualificationRefusalOwner = UnitHardware | BmcAccess | StagingInfrastructure | PlatformModel | -QualificationPolicy | OperatorAction`. Mapping (`gunbc.machine_intake_disposition`): observed -hardware defect ∧ window open → ReturnWindow; defect ∧ window closed → PartsHold{RepairOrCull}; -qualification unestablished → PartsHold{cause, owner}; placement pending → -PartsHold{PlacementPending}; all gates ∧ subject current ∧ cleanup confirmed → Admitted. -`FleetAdmissible` is derived from receipts; a hand-authored Admitted row cannot substitute. - -## 12. Receipt architecture - -Every phase emits one normalized envelope (`IntakeReceiptEnvelope`) plus evidence refs by -digest. Wire schema `gunbc.machine-intake.receipt/v1`; raw archive -`/srv/bmc/intake/artifacts//`, `attempts//manifest.json`, `phase/*.json`, -`evidence/`. The JSON is an observation transport, not a second decision authority. -Required properties: canonical digest; attempt identity and exact subject; tool/agent/artifact/ -policy identities; start/end; evidence by digest; success or typed refusal; hash-chain link; -staging identity; no credentials or one-time tokens. - -## 13. The `intake ` executable (unbuilt) - -One operator command: acquire endpoint lock → discover access surface → bind provisional identity -→ acquire unit/attempt lock → ask the modeled solver for the next phase plan → execute through the -selected adapter (redfish-standard, ipmi-fru, ami-megarac, openbmc-nbdproxy, openbmc-configfs, -uefi-http, pxe) → append receipt → resume until terminal disposition. Phase order and disposition -live in `.dag`, not in `if chassis == ...` branches. Needs idempotent resume, per-endpoint then -per-unit locks, one-shot callback tokens, content-addressed artifacts, cleanup/compensation after -every failed delivery, no secrets in logs, a dry plan mode, a typed terminal receipt on staging -failure. It emits observations; it does not author `.dag` rows. Dissolution: delete it when the -modeled fleet-ops executor consumes the same plan, performs every effect, and produces -byte/schema-equivalent receipt envelopes for both the Mt. Collins and ASRock transport fixtures. - -## 14. Integration map - -| Existing authority | Change | -|---|---| -| `extdeps.bmc.endpoint` | `AmiMegaRac` arm (landed via gunbc#9678 and this PR) | -| `extdeps.bmc.megarac`, `extdeps.vendor.ami` | implementation module (gunbc#9678: cited admin/admin default, IPMI floor, proprietary REST remote-media operations, executed Mt. Collins boot recipe) and vendor row | -| `extdeps.bmc.access_profile` | profile/observation carriers and intersection (landed) | -| `extdeps.bmc.redfish_virtual_media`, `extdeps.bmc.virtual_media` | DMTF shape; `RedfishVirtualMediaTransport` arm (landed) | -| `gunbc.boot_artifact_delivery` | the one delivery solver: request × bound evidence × policy → plan (landed, ruling 3 §1-§2) | -| `gunbc.machine_intake_access` | attempt-bound access context producer; `BootDeliveryTarget`, evidence manifest, promotion verification standing (landed, ruling 3 §1, ruling 4 §1/§4/§5) | -| `extdeps.bmc.endpoint`, `extdeps.bmc.capability` | `BmcControllerEndpoint`; `BmcFirmwareReleaseCapabilityRow` (landed, ruling 4 §2-§3) | -| `gunbc.install_transport_qualification` | `TargetBoundReinstallPath` — the host transport standing joined to an intake target (landed, ruling 4 §1) | -| CI changed-witness execution sublane | dispatched as its own PR (child work item; ruling 4 "#9717 is not an unrelated CI incident") | -| `gunbc.bmc_firmware_transition`, `gunbc.network_boot_delivery` | standings cut out of the solver into their own authorities (landed, ruling 3 namespace ruling) | -| `gunbc.os_install_mechanism` | compatibility projection; capability-only solver and `FirmwareUpdateThenVirtualMedia` deleted (landed, ruling 3 §3) | -| `gunbc.os_install`, `gunbc.generated_artifact` | `fleet_install_server_specs`, `InstallServerSpec`, `gunbc.install_server_emit` and the `ProxyDhcpDnsmasqArtifact` path deleted (ruling 3 §3) | -| `gunbc.bmc_implementation_dispatch` | family dispatch moved out of `extdeps.bmc.openbmc` (landed) | -| `gunbc.machine_intake_*` | subject, phase, receipt, disposition, staging (landed) | -| `gunbc.bmc_onboarding` | extract BMC access/security, provisioning, fabric joining (follow-up) | -| `gunbc.nbd_proxy_virtual_media_install` | realize `ActuatorRedfishInsertMedia` through the Redfish arm (follow-up) | -| `extdeps.bmc.capability` | inventory, log, telemetry, cleanup capabilities (follow-up) | -| private `strategy.*` | per-unit normalized receipts and return-window facts (follow-up) | - -srv1 vs srv2 as staging host is resolved by observation (`intake_staging_realization_today` -is `StagingUnobserved`), not by prose. - -## 15. Implementation sequence - -- **INTAKE-0 (modeling in review; ruling 3 gate 5 — lifecycle-valid ledger, admission - provenance — in this PR):** subject, manifests, attempt, phase standing, refusal owner, - disposition, fleet-admission derivation, receipt envelope; controls in - `test.claim.machine_intake_subject_witness_test` and - `test.claim.machine_intake_disposition_witness_test`. -- **BOOT-DELIVERY-0 (modeling landed; execution owed):** MegaRAC identity, Redfish VirtualMedia - transport, solver consuming configfs/WebSocket, UEFI HTTP split from PXE; controls in - `test.claim.boot_artifact_delivery_witness_test`. Acceptance: one executed Mt. Collins boot and - one executed ASRock boot reaching the same nonce-bound intake-agent callback. -- **INTAKE-AGENT-0A (next, per ruling 3):** deterministic x86-64 and AArch64 artifacts with - recorded content identities; a callback carrying attempt nonce, exact artifact digest, - architecture, raw board serial, assembly/firmware observation digests and boot time; replay and - duplicate-callback refusal; wrong-nonce/artifact/unit/architecture REDs; canonical durable receipt - bytes and their SHA identity; raw EDAC/RAS, inventory and sensor capture only; local execution of - the callback contract without a BMC. Then ACCESS-OBSERVE-0 / BOOT-DELIVERY-0E on hardware: - observation → profile lookup → plan → execute → agent callback → cleanup, on Mt. Collins and ASRock. -- **MEMORY-0:** fast report ingestion, authoritative plan, EDAC/Redfish deltas, attribution, - label/SPD binding, swap-pass diagnosis. -- **SOAK-ADMISSION-0:** subsystem recipes, thermal/fan policy, wall-power producer, two soak - profiles, final fleet-admission consumer. - -The smallest useful MVP reaches the Linux agent, inventory conformance, and authoritative memory -evidence; automating media attachment while the verdict stays manual is not it. - -## 16. Discriminating controls - -| Perturbation | Required result | Where | -|---|---|---| -| Change one DIMM while retaining the board serial | Old admission no longer applies | subject + disposition witnesses | -| Leave factory credential working after rotation | BmcSecure refuses | bmc secure witness | -| Remove Redfish VirtualMedia from live observation, keep it in the profile | Redfish transport not selected | boot delivery witness | -| Stage a URI serving digest B for a request naming digest A | `CandidateStagedArtifactMismatch`, no plan | boot delivery witness | -| Hand the solve an observation receipt from another attempt | `AccessContextObservationForOtherAttempt`, no plan | boot delivery witness | -| Receipt manifest omits the observation's raw response | `AccessContextObservationEvidenceMismatch` | boot delivery witness | -| Receipt manifest names the discovery bytes but not the nested probe's | `AccessContextProbeEvidenceMismatch` | boot delivery witness | -| Request target for attempt two, context bound for attempt one (same controller) | `DeliveryTargetSubjectMismatch`, no plan | boot delivery witness | -| Request target for controller .61, context bound for .60 (same subject) | `DeliveryTargetControllerMismatch`, no plan | boot delivery witness | -| Configfs path / network boot established for another target | `CandidateEvidenceForOtherTarget` | boot delivery witness | -| Profile promoted from an observation, receipts unresolvable | `AccessContextPromotedProfileUnverified`, no plan | boot delivery witness | -| Raw release "0.32" with an exact-release OEM row and matching observation | context binds, MegaRAC plan (positive) | boot delivery witness | -| Probe arm below the floor while the population lists HTTPS | `CandidateRedfishProbeContradictory`, never resolved by preferring one side | boot delivery witness | -| Live observation lacks `SurfaceBootControl` while catalog names override and media is live | `DeliveryBootControlSurfaceUnestablished`, no plan | boot delivery witness | -| Profile names Redfish and IPMI; boot control observed over IPMI only | plan carries IPMI, never Redfish | boot delivery witness | -| No route-specific boot-control observation / both routes observed | `DeliveryBootControlRouteUnobserved` / `DeliveryBootControlRouteAmbiguous`, no plan | boot delivery witness | -| Boot control observed over MegaRAC REST (named by the profile, no control operation) | `DeliveryBootControlRouteNotControlCapable` | boot delivery witness | -| Control-route observation bytes absent from the manifest | `AccessContextControlRouteEvidenceMismatch` | boot delivery witness | -| Empty configfs observation population | `CandidateConfigfsUnestablished` with all eight probes missing — no authored establishment | boot delivery witness | -| Every current phase in the three rosters | exactly one rank, all distinct (positive) | disposition witness | -| Catalog row whose capability firmware differs from its key | lookup refuses the catalog | boot delivery witness | -| Probe admitted NFS only, staging offers HTTPS | `CandidateRedfishProtocolNotAdmitted` | boot delivery witness | -| MegaRAC route with `image_redirection` Unset | `CandidateOemParameterUnset`, no plan | boot delivery witness | -| Remove virtual media and provide no network-boot evidence | No implicit PXE | boot delivery witness | -| Preserve NFS/media activity, remove the report/callback | Boot verdict refuses | disposition witness | -| Make EDAC unavailable | Memory standing unestablished, not zero-error | MEMORY-0 | -| Channel-attributed error only | Gate fails; no DIMM convicted | MEMORY-0 | -| BMC watts without wall meter | Wall power unestablished | SOAK-ADMISSION-0 | -| Fail the staging artifact service | PartsHold{StagingInfrastructure}, never ReturnWindow | disposition witness | -| Leave media attached or override active | Cleanup refuses; admission impossible | ArrivalCleanup (unbuilt) | -| Re-run from a stale attempt after a repair | Receipt subject mismatch refuses | disposition witness | -| Manually write Admitted without receipts | Derivation refuses | disposition witness | diff --git a/docs/plans/machine-intake-ruling-3.md b/docs/plans/machine-intake-ruling-3.md deleted file mode 100644 index 577e67a7dd2..00000000000 --- a/docs/plans/machine-intake-ruling-3.md +++ /dev/null @@ -1,421 +0,0 @@ -# Machine-intake ruling 3 — review of #9690 @ d4b1e6a104 (2026-08-30) - -Transcribed verbatim from the design side-chat (turn c6f87ffa). Citations stripped. - -# Review verdict - -**REQUEST CHANGES / HOLD at `d4b1e6a104ce8bf78e147642742c259e98994a59`.** The major repairs are real, but ruling 2 is not fully discharged. I would not yet record either **INTAKE-0 complete** or **BOOT-DELIVERY-0 modeling complete**. - -The following parts now satisfy the ruling: - -- `MtCollinsBmcImplementationExpected` no longer presents the family as an Ampere-authored platform fact. -- The solver returns `BootDeliveryPlanned`, not an execution establishment. -- `derive_fleet_admission` accepts only `ValidatedIntakeReceiptLedger`. -- `intake_staging_realization_today` remains `StagingUnobserved`. -- The `bmc_onboarding` split may remain a follow-up. fileciteturn35file0L2-L2 fileciteturn31file0L2-L2 fileciteturn46file0L2-L2 fileciteturn50file0L2-L2 - -Four ruling-level blockers remain. - -## 1. The new solver is not bound to **this artifact on this host** - -`BootArtifact` correctly names purpose, architecture, digest, format, entrypoint, and build identity. But `BootDeliveryEvidence` contains no `BootArtifact`, target unit, target host identity, or intake attempt, and `BootDeliveryPlanned` contains only a transport plus the considered verdicts. The selected Redfish URI, MegaRAC image name, NBD export, or network-boot evidence is therefore not proven to represent the `BootArtifact` defined earlier in the same module. The plan also drops the endpoint from which eligibility was derived. fileciteturn51file0L2-L2 fileciteturn31file0L2-L2 fileciteturn52file0L2-L2 - -This admits a straightforward false plan: - -```text -requested artifact digest = A -Redfish image URI actually serves digest = B -profile + probe + params are otherwise eligible -result = BootDeliveryPlanned -``` - -Nothing in the solver compares A and B because A is not an input. - -The solver needs a request and a result closer to: - -```dag -type BootDeliveryRequest { - target: BootDeliveryTarget - artifact: BootArtifact - access: BoundBmcAccessContext - staging: BootArtifactStagingBinding -} - -type BootDeliveryPlan { - target: BootDeliveryTarget - artifact: BootArtifact - delivery: BootArtifactDelivery - boot_control: BootControlPlan - eligibility_provenance: BootDeliveryEligibilityProvenance -} -``` - -Every transport-specific staging binding must name or derive the exact artifact digest. The eventual callback then repeats the digest from the same plan. - -### The access evidence can also be spliced across identities - -`BootDeliveryEvidence` independently accepts: - -```text -BmcFirmwareCapabilityRow -BmcAccessProfileStanding -BmcAccessObservationStanding -``` - -The capability test reads `ev.row`; surface eligibility reads only the profile and observation. There is no check that: - -```text -row.firmware -= profile.key.firmware -= observation.identity.firmware -= observation.firmware -``` - -A capability row for firmware A can therefore contribute `CapabilityVirtualMedia` while a profile and observation for firmware B establish the surface. `BmcFirmwareCapabilityRow` itself carries only firmware and a capability list—not board identity or source provenance. fileciteturn31file0L2-L2 fileciteturn47file0L2-L2 - -`BmcAccessObservation` also carries the firmware twice—inside `identity.firmware` and again as `firmware`—with no constructor proving they agree. It is described as per-attempt, but carries neither an attempt ID nor a qualification subject, and it contains no raw-response evidence digest. An observation from a previous attempt can therefore be handed to the current solve as long as its interpreted identity still fits. fileciteturn30file0L2-L2 - -Required shape: - -```dag -type BoundBmcAccessContext { - subject: MachineIntakeSubject - endpoint: BmcEndpoint - identity: BmcObservedIdentity - capability_row: BmcFirmwareCapabilityRow - profile: BmcAccessProfile - profile_provenance: BmcAccessProfileProvenance - observation_receipt: EvidenceRef -} -``` - -Its producer must prove all identity joins before the delivery solver can run. Better still, place the exact capability row inside `BmcAccessProfileCatalogRow`, with a constructor proving that its firmware equals the profile key. Then remove the independently supplied `row` from `BootDeliveryEvidence`. - -Add RED controls for: - -- capability-row firmware different from the profile; -- `observation.firmware != observation.identity.firmware`, if the duplicate field remains; -- a live observation from another attempt; -- a profile whose declaration exists but whose underlying observation receipt is absent. - -### The real Mt. Collins firmware cannot currently inhabit the key - -The retained historical report calls the Mt. Collins firmware `0.32`, while `bmc_firmware_version_from_wire` accepts exactly three numeric components and every profile/observation key requires `BmcFirmwareVersion`. A live wire value of `0.32` therefore either refuses or must be fabricated as `0.32.0`. fileciteturn35file0L2-L2 fileciteturn47file0L2-L2 fileciteturn30file0L2-L2 - -Preserve an opaque exact vendor release identity: - -```dag -type BmcFirmwareReleaseIdentity { - family: BmcFirmwareFamily - raw_version: NonEmptyStr - semantic_version: FirmwareSemanticVersion? -} -``` - -Exact profile lookup should use the raw vendor identity. Semantic parsing is an optional derived view, never a prerequisite for observing the firmware. - -## 2. A planned transport can disagree with the route that was observed - -### Redfish protocol and resource are unbound - -`TransferProtocolAdmitted` carries the admitted protocol, but `redfish_virtual_media_candidate` discards that payload and merely checks that separate `RedfishVirtualMediaParams` are present. Those params independently carry a media locator and protocol. Thus: - -```text -probe admitted NFS -params request HTTPS -``` - -still produces `BootDeliveryPlanned`. The same is true of the media member: the probe carries no locator, while the params may name any member. `InsertSucceeded` and `EjectSucceededAndDetached` are also accepted as plan-floor evidence even though those arms have lost both locator and protocol. fileciteturn32file0L2-L2 fileciteturn31file0L2-L2 fileciteturn44file0L2-L2 - -The probe must be resource-bound: - -```dag -type RedfishVirtualMediaProbeReceipt { - locator: RedfishVirtualMediaLocator - progress: RedfishVirtualMediaProbe - admitted_protocols: List - raw_response: EvidenceRef -} -``` - -The solver should derive the locator and protocol from that receipt. It should accept only the artifact URI from staging, rather than accepting complete final params that can contradict the observation. - -### The MegaRAC constructor omits the parameter its own authority calls required - -`extdeps.bmc.megarac` says the start-media request **must** carry `image_redirection: 1`. The design document likewise says that OEM quirk belongs in the access profile. Yet `MegaRacRemoteMediaParams` has no such field, the solver never examines `profile.oem_parameters`, and the positive Mt. Collins control uses `oem_parameters: []` while still selecting `MegaRacRestRemoteMedia`. fileciteturn36file0L2-L2 fileciteturn37file0L2-L2 fileciteturn44file0L2-L2 fileciteturn34file0L2-L2 - -That is a direct false-green against the observed operation contract. Encode the requirement structurally, preferably as a typed field rather than a generic name/value pair: - -```dag -type MegaRacRemoteMediaBinding { - ... - image_redirection: MegaRacImageRedirectionEnabled -} -``` - -The positive test must fail when that binding is absent. - -### The access-profile shape cannot honestly represent the no-Redfish platform - -Every `BmcAccessProfile` is required to provide three nonempty Redfish locators, even for the Mt. Collins build whose retained report says Redfish is absent. The synthetic control therefore invents `/redfish/...` locators for an OEM-only profile. Meanwhile `BmcEndpoint.protocol` has only `Redfish | Ipmi`, and the no-Redfish MegaRAC OEM-REST fixture is labeled `protocol: Redfish`. fileciteturn30file0L2-L2 fileciteturn33file0L2-L2 fileciteturn34file0L2-L2 - -Replace mandatory Redfish fields with route inhabitants: - -```dag -type BmcAccessRoute - = RedfishRoute { ... } - | IpmiLanplusRoute { ... } - | MegaRacRestRoute { ... } - | OpenBmcShellRoute { ... } - | OpenBmcNbdWebsocketRoute { ... } -``` - -The BMC host identity should be separate from the routes it exposes; one controller may expose IPMI and OEM REST simultaneously. The boot-control target should also be protocol-neutral. Presently every delivery, including the IPMI-controlled no-Redfish MegaRAC path, projects to `RedfishBootSourceOverrideTarget`. fileciteturn31file0L2-L2 - -Also replace `chassis_part_number: None means match every chassis` with an explicit applicability: - -```dag -type ChassisApplicability - = ExactChassisPartNumber { part_number: NonEmptyStr } - | AllChassisVariants -``` - -Absence must not silently mean universality. - -## 3. There are still two delivery solvers - -The frozen projection is not the offending path. The offending path is the still-live original solver: - -```dag -fn solve_install_mechanism(...) -``` - -It selects virtual media from a capability row alone and defaults to PXE in its final `else`. `srv3_install_mechanism` still calls it, and `gunbc.os_install.fleet_install_server_specs` still consumes that result. The generated-artifact registry then consumes the install-server roster for `ProxyDhcpDnsmasqArtifact`. This remains an independent live modeled answer beside `solve_boot_artifact_delivery`. fileciteturn28file0L2-L2 fileciteturn29file0L2-L2 fileciteturn23file0L2-L2 fileciteturn25file0L2-L2 - -That chain must be deleted or migrated now: - -```text -solve_install_mechanism -srv3_install_mechanism -fleet_install_server_specs_for -fleet_install_server_specs -the stale ProxyDHCP artifact path, if its consumer census remains empty -``` - -Do not retain a second solver merely because the generated registry cannot express a refusal. Given that the proxy-DHCP artifact is currently `NotCommitted`, deleting the dead consumer is preferable to growing an additional standing solely to preserve it. fileciteturn25file0L2-L2 - -There is a second bypass inside the purported projection: `firmware_transition_precursor` returns `FirmwareUpdateThenVirtualMedia` when a transition is established and **some** catalog row supports VM. It ignores the exact `to` version and performs no post-transition profile lookup or live observation. A transition to an unrelated, non-VM release can therefore select the combined mechanism. fileciteturn29file0L2-L2 - -A firmware transition is not a delivery. The result should be: - -```text -FirmwareTransitionRequired / FirmwareTransitionPlanned -``` - -After executing it, the workflow must re-observe the endpoint and call the one delivery solver again. It may not preselect “then virtual media.” - -### Ruling on `AccessProfileNotLookedUp` - -**The refusing projection is honest and should not be deleted blindly.** `srv3_os_install_actuate_scope` and `gunbc.nbd_proxy_virtual_media_install` still consume its refusal-bearing solution and currently fail closed because of it. fileciteturn26file0L2-L2 fileciteturn27file0L2-L2 - -Keep that compatibility adapter until those consumers read `BootDeliverySolution` directly. Delete the capability-only total solver now. Also rename: - -```text -InstallVirtualMediaLiveSurfaceUnestablished -``` - -because `AccessProfileNotLookedUp` is not a live-surface observation failure. `InstallBootDeliveryEvidenceUnestablished` or a specific `InstallAccessProfileNotLookedUp` arm would preserve the actual cause. - -## 4. The receipt ledger is link-valid, but not yet lifecycle-valid - -The raw-list admission defect is fixed: admission now receives only `ValidatedIntakeReceiptLedger`, and the validator rejects empty, disconnected, differently linked, mixed-subject/attempt, and decreasing-start-time ledgers. The FNV value is also honestly named a structural fingerprint. Those are substantial corrections. fileciteturn38file0L2-L2 fileciteturn39file0L2-L2 fileciteturn46file0L2-L2 - -But the prior ruling also required valid phase ordering or explicit retry semantics. The validator does not read the declared phase order at all. It checks only subject, predecessor, and nondecreasing `started_at`; admission later searches the entire ledger independently for each required phase. fileciteturn38file0L2-L2 fileciteturn39file0L2-L2 fileciteturn40file0L2-L2 fileciteturn46file0L2-L2 - -A counterexample that currently admits: - -1. Put every required receipt in reverse phase order. -2. Give them increasing timestamps. -3. Recompute every predecessor fingerprint. -4. Validate the ledger. -5. Admission finds one success for every required phase and returns `FleetAdmissible`. - -A subtler counterexample is a complete successful lifecycle followed by a new successful `AccessDiscover`. Last-wins makes the new access receipt current, while all downstream qualifications still predate that new discovery. - -Add either a declared prerequisite relation or a monotone phase rank. For a serial first implementation, the simplest rule is: - -- phase rank may remain the same for an immediate retry; -- phase rank may advance; -- phase rank may never regress after a later phase has been recorded; -- retrying an earlier phase requires a new attempt or explicitly invalidates all descendants. - -The validator also needs: - -- `started_at <= ended_at`; -- a declared time-order rule across receipts; -- policy-digest compatibility across the attempt; -- producer-version compatibility or a typed producer-transition receipt; -- a RED for each violation. - -### The generic ledger still cannot validate the promised SHA chain - -The comment says the eventual durable producer will emit SHA-256, but validation always calculates the expected predecessor with `intake_receipt_structural_fingerprint`. Cross-family comparison refuses. Consequently, a genuine SHA-linked ledger cannot pass this validator. The receipt note also still names the nonexistent `intake_receipt_digest` and calls the ordering immutable. fileciteturn38file0L2-L2 - -Either: - -- explicitly name this `StructurallyValidatedIntakeFixtureLedger` and do not claim it is the durable admission ledger; or -- introduce `IntakeReceiptRecord { envelope, identity }`, have the producer establish the canonical SHA identity, and validate links against the supplied current-record identities. - -Finally, `FleetAdmissionReceipt` drops the ledger’s terminal identity and the exact receipt identity for each satisfied phase. It carries only subject, phase names, and derivation time. The derivation reads the producer ledger, but its output does not preserve which producer ledger it read. Add the terminal ledger identity, policy identity, and phase-to-receipt bindings. fileciteturn46file0L2-L2 - -# Layer-placement rulings - -## `BootDeliveryPreference` - -**Correctly belongs in `gunbc`, not `extdeps`.** It is product/workflow policy, not a vendor or protocol fact. Keeping it in `gunbc.boot_artifact_delivery` is acceptable while there is only one local policy. - -However, it should not be a field of a type named `BootDeliveryEvidence`. Separate fact from policy: - -```dag -fn solve_boot_artifact_delivery( - request: BootDeliveryRequest, - evidence: BoundBootDeliveryEvidence, - policy: BootDeliveryPolicy, -) -> BootDeliverySolution -``` - -Move it to `gunbc.boot_delivery_policy` only when a second workflow or policy row needs independent governance. - -## `BmcAccessProfileStanding` - -The current type fuses two layers. Split it: - -**Keep in `extdeps.bmc.access_profile`:** - -```text -BmcAccessProfile -BmcAccessProfileKey -BmcAccessProfileCatalogRow -BmcAccessProfileLookup = - Known | Uncatalogued | Ambiguous -``` - -**Move to a workflow module such as `gunbc.machine_intake_access`:** - -```text -ProfileLookupNotRun -AccessUnobserved -AccessObservationCaptured -ProfileBoundToObservation -ProfileObservationIdentityMismatch -``` - -`AccessProfileNotLookedUp` is workflow progress, not an external fact. `AccessProfileIdentityMismatch` is presently not produced by `bmc_access_profile_for` at all; the lookup produces only known, uncatalogued, or ambiguous, while a different function produces the profile-observation mismatch. That arm should either acquire a real producer or disappear from the lookup standing. fileciteturn30file0L2-L2 - -Also, `DeclarationRef` is not sufficient profile provenance. It identifies the catalog declaration—the renderer—not the observation or cited producer that made the profile admissible. Preserve both: - -```dag -type BmcAccessProfileProvenance - = ProfileFromExternalAuthority { authority: ExternalAuthority } - | ProfilePromotedFromObservation { - observation_receipt: ContentHash - promotion_receipt: ContentHash - } -``` - -# Namespace ruling - -The 31 rows use the correct **mechanical disposition** for the movement that actually occurred: the referenced declarations changed target, so `TargetChanged` is the right classification. fileciteturn43file0L2-L2 - -The target cut is not final, however. - -### `FirmwareTransition*`: move to its own module now - -This one is mandatory. `boot_artifact_delivery` itself says a firmware transition is **not** a delivery, and the new delivery solver does not consume `FirmwareTransitionStanding`; only the frozen legacy projection does. It belongs in something like: - -```text -gunbc.bmc_firmware_transition -``` - -That module should own the six requirements, the establishment producer, exact target-row binding, and execution receipt. fileciteturn31file0L2-L2 fileciteturn29file0L2-L2 - -### `NetworkBoot*`: separate module is the cleaner final cut - -I would also move the network-boot establishment vocabulary to: - -```text -gunbc.network_boot_delivery -``` - -Its four evidence axes have independent producers—client firmware mode, serving infrastructure, boot artifacts, and boot control—and form an independently useful standing consumed by the compositor. `gunbc.boot_artifact_delivery` should import that standing and select the `PxeChainBoot` constructor. - -This is less categorical than the firmware-transition move: keeping it beside its sole constructor would not violate ruling 2. But the separate module is the better final authority boundary and avoids turning the 605-line solver into the home of every candidate’s evidence lifecycle. - -**Do not preserve the current 31 rows and then move the subjects again.** Make the final module cut, rerun the required namespace instrument, and transcribe the resulting final-target admissions. - -# `bmc_onboarding` quarantine - -Deferring the split remains approved. The header quarantine says the correct things. fileciteturn42file0L2-L2 - -The new witness is not discriminating, though. It proves: - -```text -legacy lifecycle reaches FabricJoined -empty receipt list does not validate -``` - -Those are independent facts. A future function converting `FabricJoined` into an intake receipt could be added and the witness would remain green. fileciteturn41file0L2-L2 - -Replace or supplement it with a dependency/producer wall that derives and requires: - -```text -consumers of BmcOnboardingPhase in gunbc.machine_intake_* = [] -producers from BmcLifecycleState to intake receipt/standing/disposition = [] -``` - -That correction is small and does not require doing the full split now. - -The `MtCollinsBmcImplementationExpected` correction itself is accepted, but the adjacent strings still call the unpreserved `0.32` report an “observation,” say it was “grounded by execution,” and describe fallback to a platform default. The same module now admits that the raw producer was not preserved. Rename those to an explicitly unverified historical operator-report note and remove any claim that a workflow may use them as fallback authority. fileciteturn35file0L2-L2 - -# Next slice - -**INTAKE-AGENT-0 is next. Not MEMORY-0, and not the full `bmc_onboarding` split.** - -MEMORY-0 would otherwise model report ingestion, attribution, and verdicts before the producer capable of returning the report exists. The project’s own sequence places the minimal multi-architecture agent before MEMORY-0, and BOOT-DELIVERY-0 acceptance explicitly requires both physical transports to reach that common nonce-bound callback. fileciteturn45file0L2-L2 - -I would make the next serial gate: - -## INTAKE-AGENT-0A — artifact and callback contract - -Exit requires: - -1. Deterministic x86-64 and AArch64 artifacts with recorded content identities. -2. A callback carrying attempt nonce, exact artifact digest, architecture, raw board serial, assembly-observation digest, firmware-observation digest, and boot time. -3. Replay and duplicate-callback refusal. -4. Wrong-nonce, wrong-artifact, wrong-unit, and wrong-architecture RED controls. -5. Canonical durable receipt bytes and their SHA identity. -6. Raw EDAC/RAS, inventory, and sensor capture only—no memory pass/fail verdict yet. -7. Local execution of the callback contract without needing a physical BMC. - -Then run a narrow **ACCESS-OBSERVE-0 / BOOT-DELIVERY-0E** slice on hardware: - -```text -Mt. Collins observation → profile lookup → plan → execute → agent callback → cleanup -ASRock observation → profile lookup → plan → execute → same callback → cleanup -``` - -Only after those producers exist should MEMORY-0 consume the real agent evidence. The `bmc_onboarding` split remains a gate before the general effectful `intake` executor or before live `BmcSecure`, `OsInstalled`, or `ServiceRuntimeAdmitted` producers—not before building the diagnostic agent artifact. - -## Merge gate from this review - -Before approval of #9690: - -1. Bind target and exact `BootArtifact` into the solve and plan. -2. Replace the independently supplied capability/profile/observation tuple with one validated, attempt-bound access context carrying real producer provenance. -3. Bind Redfish locator/protocol and MegaRAC OEM requirements into the selected transport. -4. Remove the capability-only solver and the `FirmwareUpdateThenVirtualMedia` shortcut; keep only the fail-closed compatibility projection until its real consumers migrate. -5. Add phase/prerequisite, interval, policy, and producer compatibility to ledger validation; preserve ledger provenance in admission. -6. Split `FirmwareTransition*`, preferably `NetworkBoot*`, and regenerate namespace admissions. -7. Replace the onboarding tautology with a dependency/producer wall and quarantine the unpreserved Mt. Collins prose as historical report only. - -This review is against the requested exact head. Current `main` has since advanced to `1b59e7d3de3d11d1de7e1551cfbac092067660a7`, so after the architectural corrections the branch must be brought current and the namespace census and claims rerun on the final integrated head. fileciteturn2file0L12-L16 fileciteturn48file0L2-L2 diff --git a/docs/plans/machine-intake-ruling-4.md b/docs/plans/machine-intake-ruling-4.md deleted file mode 100644 index d8e7490ed08..00000000000 --- a/docs/plans/machine-intake-ruling-4.md +++ /dev/null @@ -1,265 +0,0 @@ -# Machine-intake ruling 4 (2026-08-30) — reviewer second pass on head cdbf4611bb - -Transcribed verbatim from the side chat (https://chatgpt.com/c/6a933560-5590-83ea-bc5c-731d17bfa2e3). The two short interim notes precede the ruling. - -> The structure is much stronger, but one splice remains: `request.target` is not proved equal to the bound access subject/endpoint. I’m checking transport-local evidence for analogous joins. -> The deleted solver chain and module cut look sound; keep `LedgerPhaseUnranked` and the fail-closed projection. #9717 instead exposes that this PR’s changed witnesses never ran. - -## Ruling - -**HOLD / request changes on exact head `cdbf4611bbc472275130722cd6dc3da3d12fea42`.** This is substantially closer than the prior head: the second solver is gone, the compatibility layer is honestly fail-closed, the ledger now enforces lifecycle ordering, and the authority split is mostly right. The remaining blockers are narrow joins and provenance gaps rather than another redesign. - -Also, all seven gate items are not yet present on this exact head. The onboarding dependency wall and Mt. Collins prose correction are still absent; the current witness remains the earlier “legacy terminal plus empty ledger” test, and the current Mt. Collins module still describes the unpreserved run as an executed observation and a fallback platform default. fileciteturn76file0L2-L2 fileciteturn77file0L2-L2 - -## What is accepted - -The deletion decision is accepted. `solve_install_mechanism`, `FirmwareUpdateThenVirtualMedia`, the generated proxy-DHCP path, and the capability-only default-to-PXE chain are gone. `gunbc.os_install_mechanism` now contains only projections of `BootDeliverySolution` and per-host fail-closed standings. **Keep that projection until its two real consumers migrate; do not delete it merely because it currently refuses.** It is no longer a second solver. fileciteturn65file0L2-L2 - -The module cut is accepted. `gunbc.network_boot_delivery` is the right home for network-boot establishment, and `gunbc.bmc_firmware_transition` correctly states that a transition is not a delivery and is not consumed by the delivery solver. Its unbuilt producer can remain a declared next rung because nothing uses an `Established` value to authorize delivery today. fileciteturn62file0L2-L2 fileciteturn73file0L2-L2 - -The ledger climb is accepted at its stated rung. It now checks one subject and attempt, one genesis, links, interval direction, nondecreasing start time, phase rank, policy identity, and producer identity. Admission preserves the terminal ledger fingerprint, policy digest, and exact receipt selected for every phase. The source is also explicit that this is structural-fingerprint validation, not the future durable SHA-256 ledger. fileciteturn68file0L2-L2 fileciteturn70file0L2-L2 - -**Keep `LedgerPhaseUnranked`.** Its current refusal is unreachable because every present `IntakePhase` is enrolled, but it is the fail-closed arm that becomes reachable when a future phase variant is added without being placed in the required ordering. That is a legitimate totality guard, not speculative product state. Add a positive witness that every current phase has exactly one rank; an authorable negative fixture is not required. fileciteturn69file0L2-L2 - -## 1. The request target is still not bound to the access context - -`BootDeliveryRequest.target` carries a subject and endpoint. `BoundBmcAccessContext` independently carries a subject and endpoint. The access binder correctly joins the observation receipt to the subject it was asked to bind, but the delivery solver does not compare the resulting context with `request.target`; it uses the context to select a transport and then copies the request target into the output plan. The existing witnesses always build both sides from the same helper, so they do not discriminate this splice. fileciteturn59file0L2-L2 fileciteturn58file4 fileciteturn58file5 fileciteturn64file0 - -As written, this can plan: - -```text -access observation and profile for subject/endpoint A -+ -boot-delivery request for subject/endpoint B -= -plan that names B but was admitted using A -``` - -Add a binding step before candidate evaluation: - -```dag -type BoundBootDeliveryTargetContext - = DeliveryTargetContextBound { - target: BootDeliveryTarget - access: BoundBmcAccessContext - } - | DeliveryTargetSubjectMismatch { ... } - | DeliveryTargetControllerMismatch { ... } -``` - -The solver should consume only the bound arm. Required REDs: - -1. same endpoint, different attempt or qualification subject; -2. same subject, different BMC controller. - -The same target binding is needed for the other evidence inputs. `ReinstallPathEstablished` is currently accepted while its `qualified_by` population is ignored; UEFI HTTP evidence carries an artifact digest but no target; network-boot establishment carries four evidence references and a digest but no unit, endpoint, or attempt. Evidence from one machine can therefore authorize another machine’s request even after the access-context join is repaired. fileciteturn58file0 fileciteturn62file0L2-L2 - -Each established transport standing should carry the `BootDeliveryTarget`, or consume a target-bound observation type whose sole producer performs that join. Artifact-digest agreement is necessary, but it does not bind the host. - -The plan’s positive provenance should also preserve the evidence particular to the selected candidate. The generic `profile_provenance + observation_receipt + considered` product is sufficient for the profile-shaped candidates, but it drops configfs `qualified_by` evidence and UEFI HTTP’s evidence list. A `CandidateEligible` verdict alone does not tell an executor or reviewer what made that candidate eligible. - -## 2. `"0.32"` is representable but still cannot inhabit a valid profile - -The opaque `BmcFirmwareReleaseIdentity` repair is right. `"0.32"` remains exactly `"0.32"` and has no fabricated semantic patch component. However, every `BmcAccessProfile` still embeds a semantic `BmcFirmwareCapabilityRow`, and catalog admission requires the raw release’s optional semantic view to equal that row’s semantic firmware. For `"0.32"`, the semantic view is absent, so the match always refuses. fileciteturn72file0L2-L2 fileciteturn60file0L2-L2 - -The new witness demonstrates exactly that: `"0.32"` is representable, but both profile lookup and access-context binding refuse it. That is a useful RED, but it means the observed Mt. Collins firmware can never progress to `AccessProfileKnown` under this model. fileciteturn64file1 fileciteturn64file2 - -Introduce an exact-release capability carrier: - -```dag -type BmcFirmwareReleaseCapabilityRow { - release: BmcFirmwareReleaseIdentity - capabilities: List -} -``` - -and make the new access-profile path consume it. The legacy semantic capability row can remain for existing OpenBMC consumers. - -The required positive control is: - -```text -raw release "0.32" -+ exact-release OEM-remote-media capability row -+ matching profile -+ matching synthetic observation receipt -→ bound access context and MegaRAC delivery plan -``` - -That is a fixture proving inhabitability, not a seeded fleet observation. - -## 3. Controller identity is still fused to one protocol - -The profile now correctly carries route inhabitants, including Redfish, IPMI lanplus, and MegaRAC REST. But `BmcEndpoint` itself remains `{ host, protocol: Redfish | Ipmi }`, and that route-specific value is used as the intake target and access-context endpoint. fileciteturn71file0L2-L2 - -One Mt. Collins boot needs at least two routes against one controller: - -```text -MegaRAC REST for artifact delivery -IPMI lanplus for boot control/reset -``` - -A single `protocol` field cannot identify that controller without selecting one route in advance. The current fixture demonstrates the problem indirectly: the supposedly no-Redfish OEM profile is observed through a `BmcEndpoint` constructed with `protocol: Redfish`. fileciteturn64file0L2-L2 - -Do not expand `BmcProtocol` with another arm. Introduce a protocol-neutral intake identity, for example: - -```dag -type BmcControllerEndpoint { - host: NonEmptyStr -} -``` - -Use it in `BootDeliveryTarget`, `BmcAccessObservation`, and `BoundBmcAccessContext`. The profile’s `BmcAccessRoute` population remains the authority for how that controller is reached. Existing legacy users of `BmcEndpoint` need not migrate in this PR. - -## 4. Promoted profiles must refuse while their provenance is unverified - -You are right not to fabricate a receipt store. The conclusion, however, is not that `ProfilePromotedFromObservation` may produce `AccessProfileKnown` while the gap is merely described. - -At present, two arbitrary hash-shaped values can be authored as `observation_receipt` and `promotion_receipt`, placed on a catalog row, and accepted by lookup and access binding. The positive fixtures do exactly this with synthetic content hashes. No producer establishes that either receipt exists or that the observation describes the profile being promoted. fileciteturn60file0L2-L2 fileciteturn64file0L2-L2 - -No store is required to fail closed now. Put verification in the workflow layer: - -```dag -type ProfilePromotionVerificationStanding - = ProfilePromotionVerified { ... } - | ProfilePromotionUnverified { - observation_receipt: ContentHash - promotion_receipt: ContentHash - } -``` - -`bind_bmc_access_context` may bind: - -- a profile grounded directly in an external authority; or -- a promoted profile accompanied by `ProfilePromotionVerified`. - -Without the future store, every promoted profile produces `ProfilePromotionUnverified` and refuses. That gives the missing discriminating control today without pretending that receipt lookup exists. - -INTAKE-AGENT-0A can later introduce the store-backed producer that constructs `ProfilePromotionVerified`. - -## 5. The Redfish probe still has two protocol authorities - -`RedfishVirtualMediaProbeReceipt` carries both: - -```text -progress = TransferProtocolAdmitted { protocol: ... } -admitted_protocols = [...] -``` - -The solver uses the progress arm only to decide whether the plan floor was reached, then decides protocol eligibility from `admitted_protocols`. Consequently, the receipt can claim `TransferProtocolAdmitted { NFS }`, list only HTTPS, and admit an HTTPS offer. fileciteturn63file0L2-L2 fileciteturn58file1 - -Use one representation: - -- make the progress arm payload-free and let a nonempty admitted-protocol population establish the plan floor; or -- put the admitted protocol population directly on the progress arm and remove the sibling field. - -The nested probe’s `raw_response` also needs to be part of the observation receipt’s verified evidence manifest. The access binder currently checks the overall observation `raw_response` against one `EvidenceRef`, but the transport plan derives its locator and protocols from the nested probe receipt’s separate hash. There is no structural statement that this second blob was among the captured evidence. A receipt manifest containing all referenced evidence digests is the clean correction. - -## Ledger and durable-receipt ruling - -`IntakeReceiptRecord` may remain deferred to **INTAKE-AGENT-0A**. The current code is honest that `ValidatedIntakeReceiptLedger` is an in-substrate structural ledger and cannot validate the future SHA-linked wire ledger. That is sufficient for the INTAKE-0 semantic model, but it is not sufficient for a live admission producer. fileciteturn68file0L2-L2 - -The PR body is now stale: it still claims a “canonical digest and hash-chain check” and still describes the superseded live-surface refusal rather than `InstallAccessProfileNotLookedUp`. Update it only after the final code head is fixed. fileciteturn79file0L8-L8 - -The gate before any effectful intake/admission executable is: - -```text -IntakeReceiptRecord { - envelope - canonical_bytes_identity: Sha256Digest -} -``` - -with links checked against those supplied record identities, not recomputed FNV fingerprints. - -## Onboarding and Mt. Collins worker - -The proposed worker direction remains correct, but it must land before this review can close. - -The dependency wall must inspect the live producer/consumer relation—no path from `BmcOnboardingPhase` or `BmcLifecycleState` into intake standings, receipts, disposition, or admission. The exact-head witness currently proves only that the legacy state reaches `FabricJoined` and that an unrelated empty receipt list does not validate; adding a conversion function would not make that witness red. fileciteturn76file0L2-L2 - -For Mt. Collins, retain `MtCollinsBmcImplementationExpected`, but rewrite or delete the adjacent rows that currently say: - -- the stack was “established by execution”; -- `0.32` and no-Redfish are an “observation”; and -- workflows may fall back to this platform default. - -The raw producer was not preserved, so these may survive only as an explicitly unverified historical operator report with no authority path into lookup or planning. fileciteturn77file0L2-L2 - -## Namespace ruling - -The final module cut is approved, but do not transcribe admissions from the present intermediate head. The exact head still carries the earlier 31-entry roster describing the old move directly into `boot_artifact_delivery`. Those are not the final targets after the split into `network_boot_delivery` and `bmc_firmware_transition`. fileciteturn74file0 - -The sequence is: - -1. land the target/context, raw-release, endpoint, provenance, probe, and onboarding corrections on the branch; -2. merge current `main`; -3. run the namespace instrument against that exact integrated head; -4. transcribe only the resulting final `TargetChanged` identities; -5. rerun once to prove no unadjudicated or stale admissions. - -`main` has already advanced to `61c6dfde780430ea6fd20ac3985a72f5cdbd9384`, well beyond the PR’s recorded base, so the final census cannot be taken from `cdbf4611bbc472275130722cd6dc3da3d12fea42`. fileciteturn80file0L2-L2 - -## #9717 is not an unrelated CI incident - -The red is truthful. #9717 was specifically built because prior PRs added witnesses that passed remotely but executed **zero** times in the required floor. It now blocks every changed witness whose required-floor disposition is declined, missing, or nonterminal. fileciteturn81file0L8-L8 - -The 2026-08-29 compiler-floor ruling and #9717 can coexist: - -```text -static global required-gate roster = compiler floor only -dynamic per-PR changed-witness lane = exactly the changed witness identities -``` - -The honest repair is a changed-witness execution sublane: - -1. reuse #9717’s already-derived changed identity set; -2. execute exactly that set, without adding product prefixes to the global compiler gate; -3. write terminal outcomes into the same disposition ledger; -4. let #9717 project those outcomes. - -Do **not**: - -- weaken #9717; -- classify `DeclinedOutsideGateClosure` as green; -- add a blanket exception for #9690; -- expand the standing global compiler gate with every product namespace; -- convert the 53 identities to expected-red or transition admissions. - -The reported 74 remote greens are valuable development evidence, but they are not yet an exact-head required-CI receipt. They could substitute only if imported as subject-bound, identity-complete execution receipts that the required gate itself consumes. - -At the time I checked, the exact-head GitHub run was still nonterminal: Rust unit tests had succeeded, while the required build and floor jobs were still running. Thus there is not yet a final CI verdict on `cdbf4611bbc472275130722cd6dc3da3d12fea42` to classify. fileciteturn82file0L2-L2 - -The changed-witness execution repair can land as its own CI PR ahead of #9690. Until it does, #9690 remains correctly blocked rather than “green except for infrastructure.” - -## Next slice - -After these corrections and #9690’s merge, **INTAKE-AGENT-0A remains next**. - -That slice should produce the missing common substrate: - -1. deterministic x86-64 and AArch64 artifacts; -2. canonical SHA-256 `IntakeReceiptRecord` identities; -3. an evidence manifest/store capable of resolving observation and promotion receipts; -4. nonce-bound callback with artifact, architecture, unit, assembly, firmware, and attempt identity; -5. replay and duplicate-callback refusal; -6. raw inventory, EDAC/RAS, sensor, and access-observation captures without memory verdicts; -7. local callback-contract execution before hardware use. - -That slice supplies the producer needed to verify `ProfilePromotedFromObservation` rather than forcing #9690 to invent a generic receipt store. Then execute the Mt. Collins and ASRock access/boot callbacks. **MEMORY-0 follows those real agent receipts.** The full `bmc_onboarding` split remains required before the general effectful intake executor or live `BmcSecure`, `OsInstalled`, or `ServiceRuntimeAdmitted` producers, not before the agent artifact. - -## Final approval gate - -Approval requires one final head containing: - -1. request target ↔ access-context subject/controller binding; -2. target-bound configfs, UEFI HTTP, and network-boot standings; -3. a positive exact-raw `"0.32"` capability/profile path; -4. protocol-neutral BMC controller identity; -5. one Redfish protocol authority plus bound probe evidence; -6. fail-closed unverified promoted-profile provenance; -7. the real onboarding dependency wall and historical-report rewrite; -8. namespace admissions generated from the final current-main composition; -9. updated PR body; -10. terminal required CI in which every changed witness has an executed disposition. - -The architecture no longer needs another broad rewrite. These are the remaining seams where independently valid values can still be paired to manufacture a plan about the wrong subject, controller, release, route, or producer. diff --git a/docs/plans/managed-host-untangle.md b/docs/plans/managed-host-untangle.md deleted file mode 100644 index 5a0506e0b3a..00000000000 --- a/docs/plans/managed-host-untangle.md +++ /dev/null @@ -1,744 +0,0 @@ -# Managed-host untangle: mtcollins1 → unit / platform / silicon / BMC stack / fleet procedure - -*Work item adhoc-81aa03f8-bc8 (session warm-crane-577), measured at `26e99a9c7f`. Governing doctrine: DESIGN §3 (single authority; replacement migrations cut over at the root, consumers enumerated before deleting a root that is outside the required gate), §3b, §3c, and [the replacement migration doctrine](replacement-migration-doctrine.md).* - -## Why - -A second Ampere Altra platform, Mt. Jade 2U (unit `mtjade1`), is arriving. Most of what is named for `mtcollins1` is not specific to Mt. Collins: it is Ampere silicon knowledge, AMI MegaRAC knowledge, or fleet procedure for any BMC-managed host, with the unit hardcoded through a handful of constants. As long as the procedure is `mtcollins1_*`, Mt. Jade has two options: fork the procedure (a §3 nickname) or wait. The fix is the root seam below, plus one cut per authority. - -## How the census was taken (the instrument) - -This is an import census over every `.dag` module under `dag/` and `src/v2/`, not a grep for consumers: - -- **Population**: every module whose name or text matches `mtcollins`. That is 269 modules: 81 witnesses and 188 production/reference modules. -- **Consumers**: the reverse `import` edges into each module. -- **Gate**: transitive import closure from `v2.workflow.required_floor` `required_gate_prefixes`. -- **Constants**: the `mtcollins|mt_collins|MtCollins` symbols each module imports from another module. These are the unit/platform constants it depends on. - -The method is under **Census method** below. It is a one-off that each cut re-runs at its own head. - -### Findings that shape the plan - -1. **No layer-1/2/3/5 machine-intake module is in the required gate.** The 12 gate-reachable members are generic modules carrying prose or a single row: - - `extdeps.bmc.megarac` - - `extdeps.boards.types` - - `gunbc.fleet_intent_network` - - `gunbc.secret_provision` - - `gunbc.ci_layer_roots` - - `gunbc.durable_cas_file_store` - - `gunbc.floor_demand` - - `gunbc.roadmap_authority` - - `gunbc.runner_registration_labels` - - `gunbc.runner_throughput_qualification` - - `v2.workflow.floor_route_gap` - - `test.claim.action_use_admission_witness` - - **So every cut below deletes a root outside the gate.** Per §3, each child brief carries its consumer list by name, from the tables below, and must run each named consumer's witnesses itself. A green required run proves nothing about these modules (`docs/rung-drops/required_lanes_do_not_resolve_product_layer_modules_2026-09-20.txt`). - -2. **The unit enters through about ten roots.** These are the most-imported unit symbols: - - | Symbol | Home | Importers | - |---|---|---| - | `mtcollins1_endpoint` | `gunbc.machine_intake_mtcollins1_access_observation` | 10 | - | `operator_host_mtcollins1` | `gunbc.fleet_intent_network` | 7 | - | `mtcollins1_firmware`, `mtcollins1_capability_row`, `mtcollins1_access_observation` | access observation | 7 / 5 / 4 | - | `mtcollins1_bmc_gunbc_secret_ref` | `gunbc.secret_provision` | 6 | - | `mtcollins1_secured_account` | `gunbc.machine_intake_mtcollins1_bmc_secure_observation` | 6 | - | `mtcollins1_unit_hold_key`, `mtcollins1_unit_hold_store_host` | `gunbc.machine_intake_mtcollins1_maintenance_hold` | 6 / 4 | - | `mtcollins1_cdrom_selection` | `gunbc.machine_intake_mtcollins1_actuate` | 5 | - | `mtcollins1_host_nic_mac` | `gunbc.machine_intake_mtcollins1_netboot_client_observation` | 7 | - | `mtcollins1_boot_export_dir` | `gunbc.machine_intake_mtcollins1_boot_artifact` | 6 | - | `mt_collins_channel_population_roles`, `mt_collins_channel_connector_pairs`, … | `extdeps.ampere.mt_collins_*` (the board figure) | 10 / 5 | - - **Fleet procedure is unit-specific only through these roots.** Thread a host record through them and the procedure modules become host-generic without restating their logic. - -3. **`gunbc.host_reset_subject_roster` `ResetSubjectCandidate` is already the per-host record in embryo.** It carries: - - `host` (a `HostIdentity`) - - `observation` (a `BmcAccessObservation`, which carries the endpoint) - - `capability_row` - - `username` - - `boot_selection` - - `unit_hold` - - Its note says a second host joins "the day it authors an observation". But it is named and scoped for *reset*, and the boot, hold and census procedures each re-import the same unit constants independently. Those are four consumers holding four copies of one row. - -4. **`gunbc.machine_intake_subject` is not the home.** `MachineIntakeSubject` / `QualificationSubject` is the *identity of a unit under qualification*: unit key, assembly manifest, firmware manifest and attempt. It has no BMC endpoint, credential or stack. It stays what it is, and the new record references it rather than absorbing it. - -5. **`mtjade1` has no `HostIdentity` row.** `gunbc.fleet_intent_network` names `operator_host_mtcollins1` but no `mtjade1`. What exists for the unit: - - `gunbc.machine_intake_mtjade1_access_observation` carries `FamilyUnobserved`, a refused unit key, and `SecretLocusReservedNotOnAccessorRoster`. - - `gunbc.machine_intake_bmc_firmware_family_discriminator` and `bmc_first_contact_standing` already model "BMC family not yet observed". - -6. **The platform home exists.** `extdeps.boards.types` `BaseboardModel` / `ServerBaseboard` is in the gate, has 25 importers, and is where the board figure is keyed. - -7. **`.github/workflows/fleet-converge.yml` is a projection.** Its authority is `gunbc.fleet_converge_workflow` (271 references, 4,446 lines). It carries 8 `mtcollins1_*` mode literals in conditions and about 30 `mtcollins1_*` step/job identifiers. Renaming any mode needs operator sign-off. - -## The root seam (revised after the #13046 review) - -**Name.** The name was chosen by DFS of existing vocabulary. The corpus already says *managed* for "a host this fleet drives through its baseboard controller" (`gunbc.host_reset_subject_roster` contrasts it with "executor fleet host"). No `ManagedHost` type exists, and none of the existing `Managed*` types is the same concept. - -**The record is a projection over standings that already exist, not a new bundle.** Two joined authorities already cover most of it: - -- `gunbc.machine_intake_access` `BoundBmcAccessContextStanding` binds the `MachineIntakeSubject`, endpoint, observed identity (which includes baseboard and BMC family), capability row, profile, observation and evidence manifest. -- `gunbc.machine_intake_bmc_secure` `BmcSecureStanding` / `ManagedCredentialReference` carry the secured account, role, `SecretRef` and credential epoch. - -Restating any of that as loose `baseboard` / `bmc_stack` / `credential` fields would be a second authority and would lose the capability row that `reset_subject_admission` needs. So: - -``` -module gunbc.managed_host -type ManagedHost { - host: HostIdentity // gunbc.fleet_intent_network, the sole naming authority - access: BmcAccessObservationStanding // endpoint and observed identity (baseboard, BMC family) - capability_row: BmcFirmwareReleaseCapabilityRow // the pair oob_boot_handoff_admission consumes today - secure: BmcSecureStanding // managed credential: account, role, SecretRef, epoch - unit_hold: ResetUnitHold // moved here from host_reset_subject_roster -} -fn managed_hosts() -> List -fn managed_host_binding(h: ManagedHost) -> ManagedHostBindingStanding // THE join; effectful consumers read only this -``` - -**The seam is a join, not a bundle** (side-chat review, 2026-10-03). There are two invariants. - -1. **One joined standing.** The subject is derived from `secure.subject` and never stored again. `managed_host_binding` refuses, with a typed and located cause, unless all three hold: - - `access` is observed; - - the observed endpoint equals the `BmcSecured` endpoint; - - the secured subject's unit is bound to the row's `host`. - - Every effectful consumer (reset, boot, hold) reads the credential and the endpoint only through the bound result. Without the join, unit A's secured account could be used against unit B's controller. The discriminating RED uses a secure standing for endpoint A and an access observation for endpoint B. -2. **Operation admission includes the operation's policy.** A host is a member of an operation's roster only if that operation's policy is decided for it. For reset, one reset-specific authority takes the bound `ManagedHost` plus the reset boot selection and produces an admitted reset subject. The roster lists exactly those hosts, and `gunbc.host_reset_return_run` consumes the selection from that admitted subject with no second lookup. The RED is a capable host with no reset selection, which must be absent from the roster. Every later cut that adds an operation (boot, hold) follows the same shape. - -Rules for the record: - -- **Baseboard and BMC family are read through `access` (its observed identity), never stored beside it.** A procedure that needs MegaRAC matches the bound identity and refuses otherwise. -- **The silicon field arrives in cut 1** as a standing authored there, not as a placeholder in cut 0. -- **Cut 0 has exactly one row, `mtcollins1`.** Its `subject` and `secure` come from `gunbc.machine_intake_mtcollins1_bmc_secure_observation` (`mtcollins1_intake_subject`, `mtcollins1_bmc_secure_standing`). Its `access` and `capability_row` are the unit's existing `mtcollins1_access_standing` and `mtcollins1_capability_row`. -- **Why `access` is the observation standing plus the capability row, not `BoundBmcAccessContextStanding`** (measured by cut 0, 2026-10-03): - - `gunbc.machine_intake_access` `bind_bmc_access_context` has no production caller. - - There are no production `AccessObservationReceipt` values. - - No `BmcAccessProfileCatalogRow` exists outside type declarations. - - Binding mtcollins1 would require authoring a catalog row whose provenance cannot be honestly decided today. `ProfileFromExternalAuthority` has no source, and `ProfilePromotedFromObservation` binds only beside a `ProfilePromotionVerified` that no producer makes. - - So the row carries the pair that the consumed admission (`oob_boot_handoff_admission`) actually reads today. This is a **declared frontier, not a second authority**. Its trigger: the first production producer of a `BoundBmcAccessContextStanding` for a managed host, which needs a catalog row with a decided provenance. In that change, `access` and `capability_row` collapse into the bound context and these two fields are deleted. - -- **`mtjade1` gets no row yet.** It has no `MachineIntakeSubject` (its unit key is refused), no endpoint, no family and no hold decision. Fabricating any of them to populate a row is the §5 defect the review named. -- **mtjade1's absence is the honest standing.** Every procedure refuses a host that is not in `managed_hosts()`. -- **mtjade1 joins on a trigger, not by edit.** It becomes a row the day first contact authors its subject and access context standing (`gunbc.machine_intake_jade_first_contact_frontier`) and a hold decision is made. That is the trigger, and it is not a cut of this program. -- **`ResetSubjectCandidate` dissolves.** `gunbc.host_reset_subject_roster` becomes a filter over `managed_hosts()` with the same admission fold, and `boot_selection` returns to the reset arm as reset policy. - -**Operation-keyed policy is not host identity** (ruling (b), 2026-10-03). The boot federation is policy for the pair (managed host, operation): - -``` -ManagedHostBootFederationStanding {host, operation, pool, provider, service_account, environment, claim_pins, secret_grants} -``` - -- `mtcollins1` is bound to the existing live names (`github-mtcollins1-boot` pool and provider, the `mtcollins1-boot` service account and GitHub environment, and the grants), which are not renamed. -- Any other host is explicitly unprovisioned, and the generic job refuses it. -- None of these ids appear on `ManagedHost`. - -## Rulings recorded (operator, via eager-gull-22 side-chat review, 2026-10-03) - -- **(a)** The fleet-converge mode recut is approved in principle as the **last** cut: one atomic transition, no aliases, the eight old literals deleted in the same change. - - Do **not** overload the existing `host` input, which is the executor host. Add a distinct managed-subject input and keep the executor axis. - - Names are derived after cuts 2–6 expose the axes. Census-QEMU modes are executor-toolchain operations, census image publish/readback are artifact operations, and boot/fan/UI/KVM are managed-host operations. - - The final names still need operator sign-off. -- **(b)** Keep the live GCP IAM names. Model them as the operation-keyed standing above. - -## Ordered cut list (re-cut vertically) - -**The governing rule** (review item 3): *a module may not acquire a generic name while its answer is still transitively fixed to mtcollins1.* Each cut therefore moves a module only after everything it transitively imports from the `mtcollins1` population is either already generic or is a layer-1 unit observation that the generic module now receives as a **parameter** or reads off the `ManagedHost` row. - -The boot vertical is large. Its transitive closure from `gunbc.machine_intake_mtcollins1_boot_run` reaches 50 census modules, including: -- the layer-1 `mtcollins1_memory_census_observation`; -- the layer-3 `mtcollins1_smpro_observation`; -- the layer-4 `mtcollins1_kvm_still`, `mtcollins1_bmc_sensor_observation` and `mtcollins1_bmc_secure_observation`. - -So the BMC and silicon leaves it needs move first. - -| # | Cut (one authority) | Root deleted | Sequencing | -|---|---|---|---| -| **0** | **Seam**: `gunbc.managed_host` as above, `mtcollins1` row only; dissolve `ResetSubjectCandidate`. | `reset_subject_candidates`, and the roster's direct `mtcollins1_*` imports | Now. Disjoint from #13025 and #13041. | -| **1** | **Silicon**: Ampere-generic SMpro/PMpro, boot-stage, `CP:` and CCIX decoding moves out of `mtcollins1_smpro_observation` into `extdeps.ampere.*`, next to the existing `ampere_{dram,socket}_console_observation`. Adds the silicon standing to `ManagedHost`. | the generic decoders inside the unit module | After #13025, and after cool-ant-760's error-record decoder lands. Public-tree rule: no SCP UM, no CHANGELOG.txt, nothing disassembled. | -| **2** | **Maintenance hold**: host-generic over `ManagedHost.unit_hold`. | `mtcollins1_unit_hold_*`, `mtcollins1_maintenance_hold_{take,release}` | After 0. The env var `GUNBC_MTCOLLINS1_MAINTENANCE_REASON` is a workflow surface: keep it until cut 7. | -| **3** | **Boot-critical BMC leaves**: `mtcollins1_kvm_still`, `mtcollins1_media_attach` (the MegaRAC virtual-media half), `mtcollins1_bmc_sensor_observation`, and the secured-account projection of `mtcollins1_bmc_secure_observation`. These become MegaRAC-scoped modules that take a `ManagedHost` and refuse a non-MegaRAC bound identity. The unit's own readings stay unit-named. | those modules' `mtcollins1_` procedure roots | After 0. | -| **4a** | **Boot subject leaves**: artifact, image fetch, milestone, phase timing, admission, authorization, actuate. `MtCollins1BootSubject` becomes a boot subject over `ManagedHost`. | `MtCollins1BootSubject`, `mtcollins1_boot_subject*`, `mtcollins1_cdrom_selection`, `mtcollins1_boot_export_dir` as procedure inputs | After cuts 1 and 2. **Not after cut 3**: measured on main 2026-10-04, the 4a modules import none of the cut-3 modules. The census-image import in the boot authorization becomes the boot **medium parameter**, so 4a does not wait on 4b either. The runner-image medium arm and its terminal milestone (quiet-stag-623's lane) land on top of 4a. | -| **4b** | **Census boot image chain**: the 7 `mtcollins1_census_*` modules. Toolchain and QEMU modules are executor-host operations, so they are keyed by executor host, not managed host. | the `mtcollins1_census_*` procedure roots | After 4a. | -| **4c** | **Boot federation standing**: `ManagedHostBootFederationStanding` replaces `gunbc.auth.mtcollins1_boot_federation*`, live names kept as the `mtcollins1` binding (ruling b). | `mtcollins1_boot_*` federation decls | After 0. Independent of 4a/4b. | -| **4d** | **Boot run, dry realization, diagnostic bundle**: the vertical's root. Unit observations it reads today (`mtcollins1_memory_census_observation`, `mtcollins1_access_observation`) become row reads or parameters, so the dependency direction is procedure ← unit. | `mtcollins1_boot_run`, `_boot_dry_realization`, `_boot_diagnostic_bundle` | After 1, 4a, 4b, 4c. Likely split run/dry and bundle at dispatch. | -| **O1** | **Arrival convergence, factory state through an admitted managed host** (operator requirement and rulings, 2026-10-03). PRs: O1a route standing, O1b state-shaped `BmcSecure` and observer clock, then O1c in three parts (the shared step-sequence fold with its first consumer; the prior-life archive; `BmcSecure` wiring and the admission population). See its own section below. | the family-keyed rotation route; the rotation-event-only `BmcSecure` derivation; the supplied `BmcSecureStanding` in the firmware convergence; the source-roster `managed_hosts()` | After 4c. Does not need the generic boot run. Each unit's live credential write needs its own operator go-ahead; mtcollins1 goes first. | -| **O2** | **Boot enters through the arrival convergence** (replacement migration): the `mtcollins1_boot` mode is re-rooted onto the convergence in one transition, and the old boot entry is deleted. | the boot run's own entry (the root that assumes `BmcSecured`); the caller-supplied password path into the boot wrappers | **After 4d.** The convergence is host-generic, so it may not call a boot run whose answer is still fixed to mtcollins1. | -| **5** | **Remaining BMC-stack observations**: fan observe, UI bundle observe, KVM observer, SOL notice, served-UI catalog, BMC fan, which the boot does not need. | those `mtcollins1_` procedure roots | After 3. | -| **6** | **Platform**: physical orientation, DIMM connector and platform observation become logic over the baseboard read through `ManagedHost.access`. The Mt. Collins figure stays in `extdeps.ampere.mt_collins_*`. | the board bindings in those modules | After #13025 (it edits `mtcollins1_physical_orientation`). | -| **7** | **Workflow modes**, per ruling (a). | the 8 `mtcollins1_*` literals and their steps in `gunbc.fleet_converge_workflow` and its projection; `mtcollins-canary.yml` dispositioned in the same change | Last. Operator sign-off on names first. | - -### Cut O — arrival convergence (plan delta, 2026-10-03; revised for the side-chat review of `b42944791e`) - -**Requirement** (operator, via eager-gull-22). Boot is to be entered from onboarding, starting at **factory state**. Today's boot run assumes `BmcSecured` already holds. First subject by requirement: `mtjade1` (MegaRAC at 192.168.1.246, factory `admin`/`admin`, BMC clock reading the year 2000). First **wet** subject by ruling: `mtcollins1`. - -**Framing (operator ruling, 2026-10-03): a consolidation, not a new mode beside the boot.** It is a replacement migration under DESIGN §3. -- The arrival convergence becomes the one entry for boot, and no second boot route survives beside it. -- No new mode, no new job, no renamed mode. The literal `mtcollins1_boot` and its job stay byte-identical until cut 7. -- Boot is the gap-intolerant boundary, so the staged form applies: the convergence is built and witnessed first (O1), then **one transition** re-roots the mode and deletes the old entry together (O2). - -**Module name.** `gunbc.machine_intake_arrival_converge` (approved). It avoids "onboarding" because `gunbc.bmc_onboarding` is quarantined legacy (ruling 2026-08-29). That module is not imported, and the quarantine witness keeps holding. - -#### Scope: a prefix of the existing `ArrivalPhase` order, exactly - -`gunbc.machine_intake_phase` `arrival_phases_all` orders twelve phases. Cut O is the **prefix through `BootDeliveryEstablish`**: five phases, in the authority's own order, with none omitted and none added. The later phases (`DiagnosticBootAttest` through `ArrivalCleanup`) are not in this cut and keep their present standing. The fold is over `arrival_phases_all`, cut at `BootDeliveryEstablish` by `intake_phase_rank`. It is not a second, shorter list. - -Each phase yields one result and one receipt. Supporting operations are **nested under the phase that consumes them**; they are not phases. - -| Phase | Goal, read back independently | Effects, and whether each is a write | Nested support | -|---|---|---|---| -| `AccessDiscover` | access observation, firmware family, **and the sealed route standing** (below) | read-only probes | the controller's own clock reading, recorded as an observation and never used for ordering | -| `IdentityBindProvisional` | the exact `MachineIntakeSubject` (below) and its `HostIdentity` binding | read-only: FRU, firmware versions | subject construction; host allocation lookup | -| `PriorLifeBoundary` | `LogsArchivedWithBaselineCursor`: the prior-life carriers are archived as content-addressed evidence and a cursor is derived from that archive (below) | **read-only arm**: archive, derive the cursor, clear nothing. **`LogsArchivedAndCleared`**: a write with its own admission and operator sign-off, **not in this cut**. | per-carrier completeness standings | -| `BmcSecure` | the existing conjunction: the managed credential is accepted **and** the published credential is refused on every LAN member of the channel census | **write**: the typed account action, operator-gated per unit | secret generation, store and exact-version fetch **before** the write (below) | -| `BootDeliveryEstablish` | **one eligible delivery transport selected from live observations**: a `gunbc.boot_artifact_delivery` `BootDeliveryPlan`, bound to subject, controller and artifact. **This is not the boot run.** | read-only observations; the selection | managed-host admission (below) | - -`PriorLifeBoundary` has no implementation today: the phase is declared and nothing produces it. A factory or repaired unit cannot skip it, so O1c builds its read-only arm. That is new work this plan had omitted. - -**What the `PriorLifeBoundary` read-only arm is** (`docs/plans/machine-intake-design.md` §5: `PriorLifeBoundaryEstablished = LogsArchivedAndCleared | LogsArchivedWithBaselineCursor`). It is an **archive**, not a bare cursor. -- **Carriers archived**, each as content-addressed evidence with its own completeness standing: the BMC clock, the SEL and event logs, the Redfish log collections, audit and account events, the current boot override, virtual-media state, power state, and a sensor snapshot. -- **Cursor:** derived **from that archive**. -- **Nothing is cleared.** -- **Incomplete archive:** a required carrier that cannot be archived makes the phase refuse, or records a typed gap. It is never silently omitted. -- **Policy admission:** the design admits this arm "only where platform policy explicitly admits an uncleared append-only boundary". That admission is an authored policy row per platform. It is a decision this cut must state for Mt. Collins and Mt. Jade, not assume. -- **Control:** only the SEL final record id, with the other carriers absent → not established. - -**What `BootDeliveryEstablish` is, and is not.** It is phase 5: a delivery plan or standing. The boot run is not part of it, and establishing it completes no later phase. -- **O2:** consumes the established delivery to enter the existing boot run. -- **Controls:** - - delivery selected → `BootDeliveryEstablish` established **and `DiagnosticBootAttest` unestablished**; - - no `BootDeliveryEstablish` → the boot entry cannot start. - -**O1 and O2.** O1 runs the prefix through `BmcSecure` and ends at an admitted managed host. O2 adds `BootDeliveryEstablish` (the delivery standing) and re-roots the boot mode so that the existing boot run is entered only from an established delivery. It lands only after cut 4d has made the boot run host-generic. The boot run itself, and every phase after phase 5, stays what it is. mtcollins1's wet `BmcSecure` control needs no boot, so it runs at O1. - -#### The subject (`IdentityBindProvisional`) - -A FRU serial yields only a `UnitKeyStanding`. A `MachineIntakeSubject` is a `QualificationSubject` (unit key, assembly-manifest digest, firmware-manifest digest) plus an `IntakeAttemptId`. Each part has a named producer: - -| Part | Producer | Evidence | -|---|---|---| -| unit key | `gunbc.machine_intake_subject` `bind_unit_key` over `BoardSerialObservation`s | committed FRU capture | -| provisional assembly manifest | `AssemblyManifest` of the `ComponentIdentity` rows readable before boot (board, BMC, and what the FRU and the BMC inventory expose), digested by `assembly_manifest_digest`. Provisional is the phase's own word: `InventoryConform` later re-derives it from the booted host. | the same captures | -| live firmware manifest | `FirmwareManifest` of the versions read back from the controller now, digested by `firmware_manifest_digest` | committed version readbacks | -| attempt identity | one `IntakeAttemptId` minted per convergence run | the run's receipt | -| subject | `qualification_subject_of`, then `MachineIntakeSubject` | the above | - -**That exact subject flows through `BmcSecure`, managed-host admission and boot.** A firmware reflash changes the firmware-manifest digest, so `compare_qualification_subject` reports `FirmwareManifestStale`. A `BmcSecureStanding` bound to the old subject is then not current and is never reused: re-entry after a reflash re-derives the subject and re-runs the phases against it. - -**Host identity.** A `UnitKey` is not a fleet-operation identity. The `HostIdentity` comes from `gunbc.fleet_intent_network` (naming) and `gunbc.hostname_allocation` `gunbc_fleet_hostname_allocations` (the allocation row). The binding of a `HostIdentity` to a subject is recorded in the admission record below. mtjade1 has neither a name row nor an allocation today, and both are authored decisions, not derived. - -#### The route standing (`AccessDiscover`), corrected first (O1a) - -`gunbc.bmc_implementation_dispatch` `rotation_route_for_family(AmiMegaRac)` answers `RotationRouteUnavailable` for a whole family. That is wrong in the direction the evidence shows, and the replacement must not be another family-keyed answer. The evidence is exactly this, and no wider: - -- **mtcollins1**: an IPMI user-management rotation was **executed** on that controller and build (`artifacts/bmc/mtcollins1-bmc-user3-route-2026-09-13.txt`). -- **mtjade1**: a Redfish Manager **read** is committed (#13065). It proves a Redfish surface. It does not prove a Redfish AccountService write, nor an IPMI user-management request, on that controller. - -So the route is a **sealed standing bound to an endpoint, a firmware build and evidence**, derived **inside** the convergence after `AccessDiscover`. It is not passed in, which withdraws this plan's earlier "selected before and passed in" sentence: that would have been a second observation authority. The arms: -- **grounded by an executed request on this controller and build** (carries the evidence); -- **grounded by a cited source for this build** (carries the citation); -- **ungrounded.** - -An ordinary `BmcSecure` Apply consumes **only a grounded standing**. mtcollins1's IPMI route does **not** authorize mtjade1, whose route is ungrounded today. - -**Grounding a route is its own effect, not the Apply.** The Apply cannot produce the premise that admits it, so there is a bootstrap with its own authorization: - -`RouteUngrounded` → `RouteQualificationCandidate { endpoint, firmware_build, request shape, evidence }` → `RouteGroundedByExecutedRequest { endpoint, firmware_build, request receipt, response receipt }` - -- The transition is made by a **separately authorized route-qualification effect**. It is classified as its own privileged effect through `select_authorization_pattern`, and it has its own operator go-ahead per controller. -- The candidate names the exact request shape and the evidence that makes it worth trying (an observed surface, a public standard's operation). It authorizes nothing by itself. -- Only the grounded arm enters ordinary `BmcSecure` Apply. -- **RED:** `RouteUngrounded` + operator approval of the rotation + ordinary Apply → refuse. - -This is the MegaRAC rotation operation that the runner-bringup gap analysis lists first. The route standing is keyed by endpoint and build and carries request and response receipts, so the same model serves that backlog's other consumers without a second route vocabulary. - -Controls: -- two `AmiMegaRac` controllers with different observed and executed surfaces produce different route standings; -- a Manager GET alone cannot construct an account-write route. - -`rotation_route_consumption_frontier` stands until the held O1b transition: O1b supplies the consumer's code (`gunbc.machine_intake_bmc_secure` `plan_bmc_account_action` computes each write's admission with `admit_rotation_apply` and refuses on every other arm), but not yet its production route. - -#### Request shapes, and what a grounding establishes (O1a-2, ruled 2026-10-04) - -The route standing is per **request shape**, per controller and build. Two shapes exist. - -- **Set User Password** (`IpmiSetUserPassword`). mtcollins1's committed execution grounds it **at BMC 0.32 only**. The controller is now on 0.45.3, so for the current build this route is another build's and the Apply refuses (`RouteForAnotherBuild`). -- **Set User Access, privilege-limit-only form** (`IpmiSetUserPrivilegeLimit`). It is needed because the `BmcSecure` conjunction requires the published credential refused on every LAN member, and a password write cannot set a channel privilege to no-access. mtcollins1 is a **candidate** only, and nothing is grounded. - -What the model holds: -- **One shape never admits another.** A route grounded for one request shape does not admit an Apply of the other. -- **A grounding records what it established.** `EffectObserved`, or `AcceptedEffectUnobserved`, computed from before and after readings. A privilege-**lowering** write is admitted only over an effect-observed grounding. -- **The lockout guard runs before the first dangerous write, including the qualification write itself.** The live transport may be driven only from a sealed admitted qualification request. For the privilege-limit shape, that request requires a sealed, evidence-bound readback from the same run showing the goal's managed account at administrator on that channel. The readback must be for the same controller and build, with one unambiguous row. -- **mtcollins1's qualifying request is non-destructive.** It re-asserts the value already there. That grounds acceptance only, so it does **not** admit closing the factory administrator. - -**What mtcollins1's wet transition therefore needs, in order.** Each item is an operator-gated act; eager-gull-22 batches them into one go-ahead. -1. A committed read-only post-reflash probe: firmware version, channel info for every channel, and user access and user name for every user id on each LAN channel. The last of these is what lets a candidate name a spare slot. -2. Qualify Set User Password at 0.45.3. -3. Converge the managed account and read it back. -4. Qualify the privilege-limit shape with a **discriminating, harmless** request: a spare user slot changed, read back, then restored. -5. Only then, the closing write on the factory administrator. -6. The independent re-read from which `BmcSecure` is derived. - -The convergence refuses at each step that is not yet grounded. It never widens. - -#### mtjade1's wet sequence (operator ruling, 2026-10-06) - -**The ruling.** Given by the operator directly in eager-gull-22's session and relayed in dashboard message `msg_7402f8df`: live operations on mtjade1 are authorized whenever and indefinitely, with no per-run approval. That covers BMC reads and writes (history archive, login and credential change), boots, and firmware qualification on Mt. Jade. **The scope is mtjade1 only**: mtcollins1's wet steps still need their own go-ahead. - -**Where it lives.** The ruling is recorded as a standing grant in `gunbc.auth.standing_operator_grant`, not here. That module's `StandingOperatorGrant` is scoped today by a Spark `FabricGroup`, so its scope is re-derived to also name a managed-host subject, with Group A's meaning unchanged. The grant carries only effects that a gate consumes (§3c). Each covered effect is classified by executing `gunbc.auth.authorization_pattern_selection` and gets its row in `gunbc.auth.privileged_effect_census`. An effect whose consuming gate has not landed is a declared frontier, not a row. - -**What the ruling does not change.** It discharges the per-run approval only. Every other wall stands: -- the route standing; -- the lockout guard before the first dangerous write; -- the sealed, evidence-bound readbacks; -- the per-build grounding; -- **real readings only.** Anything unmeasured stays a refusal; nothing is defaulted or transcribed from another unit or another build. - -**Order.** Each step lands as a committed, digest-cited capture or receipt, taken from srv1 the way the 2026-10-04 Manager and FRU reads were. A step whose consuming code has not landed waits for it. - -| # | Step | Consumed by | Waits on | -|---|---|---|---| -| J0 | The standing grant row for mtjade1 | the gates below | its own PR (no hardware) | -| J1 | Read-only probe at the current build (Wiwynn/AMI MegaRAC SPX, firmware `2.11.104000`): channel info for every LAN channel; user access and user name for every user id; SDR record names and readings; the controller clock | O1a route standing, O1b `BmcSecure` observe, `gunbc.megarac_operation_standing` `BuildReading` | J0 (reads only) | -| J2 | **Prior-life archive**, read-only on the BMC: all eight carriers captured from the box, archived and read back by digest; the cursor derived from the archive | O1c-2 `PriorLifeBoundary` | #13419 landed | -| J3 | **Per-build MegaRAC operation evidence** at `2.11.104000`: virtual-media attach, a KVM canvas still, SDR socket naming; each an executed receipt, never a recovered or transcribed one | `gunbc.machine_intake_megarac_operation_evidence`, `admit_megarac_host_at_current_build` | J1 | -| J4 | **Login and credential change**: `BmcSecure` Apply through the route O1a grounds at this build, the lockout guard first, then the independent re-read from which `BmcSecure` is derived; then mtjade1's `ManagedHostAdmission` record | O1c-3 (the gate discharged by the J0 grant; the admission population) | O1c-3 landed, J1 grounding the routes | -| J5 | **Board figure and BMC-stack readings measured on the box**: SMBIOS type 17 locators and the DIMM population; fan and sensor names | a Mt. Jade figure for cut 6's board selection, from the cited Mt. Jade guide joined to these readings; cut 5's family-selected fan policy | J1, and the board-identity decision below | -| J6 | **Boots** | the host-generic boot run | cuts 4b-ii and 4d, then O2 | - -**mtjade1's board is not `GigabyteMp72Hb0`.** Its FRU capture (`artifacts/bmc/mtjade1-ipmitool-fru-print-192.168.1.246-2026-10-04T110207Z.txt`) reads Board Mfg `WIWYNN`, Board Product `Mt.Jade Motherboard`, part `B81.03010.0041`. `extdeps.boards.types` `GigabyteMp72Hb0` is a different board: a candidate reference row with no fleet instance. - -Cut 6's selection is keyed by the closed `BaseboardModel`. `extdeps.ocp.mt_jade.platform` deliberately keeps Mt. Jade out of that enum: adding a board must not edit a generic product enum (§3, external upstream decomposition). So J5 has two prerequisites, and it does not add an enum arm: -- **Board identity:** decide how a bound host's board identity reaches the per-board authority (`extdeps.ocp.mt_jade` and `extdeps.ampere.mt_jade_getting_started_guide`), read from that host's own FRU. -- **Re-key the selection:** cut 6's figure selection moves to that identity. If it stays on the closed enum, the closed enum is a §3 hub. - -Until both are done, mtjade1's orientation refuses. - -**What stays a refusal until measured:** -- Mt. Jade's DIMM figure: none is authored from the guide alone, and nothing comes from proprietary documents. -- A MegaRAC operation not executed at `2.11.104000`. -- A route not grounded at this build. -- mtjade1's managed-host membership, until J4's record exists. - -#### `BmcSecure` as a desired state (O1b) - -Today `derive_bmc_secure` is rotation-event shaped. A rejected bootstrap credential is `PreviouslyRotatedCredentialRequired`, an accepted one with no rotation is `CredentialRotationNotApplied`, and only `RotationApplied` with an advanced epoch can mint `BmcSecured`. **So a correctly secured controller cannot be observed as a Noop through the existing fold.** The phase is therefore decomposed in `gunbc.machine_intake_bmc_secure`, with its conjunction unchanged: - -1. **Observe** the current managed-account and published-account state: the managed probe, plus the published probes joined to the channel census. This is independent of whether this run rotated anything. -2. **Noop only when the full existing conjunction already holds.** -3. Otherwise **Apply** a typed account action over the grounded route. -4. **Re-read independently** and derive `BmcSecure` from that readback. - -**Secret order, structurally.** The new credential generation is generated and stored under the unit's `SecretRef`, and its **exact resolved version is fetched**, before the account write and before the managed probe. Later consumers materialize that same `SecretRef` generation through `gunbc.auth.secret_ref_credential` `fetch_secret_ref_credential`. The earlier table's separate "managed secret materialized" row after `BmcSecure` was wrong: the rotation path cannot first obtain the secret after it has supposedly completed. - -Controls: -- already secured → Noop; -- managed accepted and factory accepted → not Noop; -- reflash-restored factory access → the same Apply and readback path, with no second recovery authority. - -**Ruling on mtcollins1's standing (eager-gull-22, 2026-10-04): staged, option C.** -- **Measured by O1b.** The committed `BmcSecured` standing is the 2026-09-13 observation on BMC 0.32. The 2026-10-02 reflash to 0.45.3 changed the subject and re-enabled the factory `admin`. No post-reflash readback is committed. So the honest standing for the current subject is not secured, and landing that alone would empty `managed_hosts()` and stop boot, reset, hold and federation admission. -- **O1b lands in shadow.** Boot is gap-intolerant, so O1b lands the state-shaped derivation, the carriers, the clock and the controls, with mtcollins1's live observation and the rotation-event path untouched and frozen. -- **One later transition** lands the honest re-expression and deletes the old path, **together with** a committed read-only post-reflash probe and the operator-approved wet `BmcSecure` Apply on mtcollins1. -- **Declared drop.** Until then, main's stale claim is a declared drop under `gunbc.rung_drop`, whose trigger is that capability. -- **What the state-shaped fold refuses** (side-chat reviews of #13221). The firmware build that admits a route is read from the observation, never supplied. The published account's replacement is the goal's exact break-glass generation, checked before the write, in the assessment and in the post-read. An unaddressed LAN channel, or channel access not closed, is refused with a typed cause naming the missing operation; it is not planned as a password write. -- **`rotation_route_consumption_frontier` stays open** until the planner is on a production path. This shadow cut supplies the consumer's code, not its production route. -- **Order on the hardware:** the operator's DIMM change, then cut 2 (#13131) landing, then the probe and the wet `BmcSecure`, then the census boot the runner lane needs, on a secured BMC. - -**mtcollins1 is not assumed satisfied.** This withdraws the earlier sentence that it "enters at its satisfied state and every step is a Noop". After the 2026-10-02 reflash the `gunbc` user and IPMI admin were restored by hand and the factory `admin` was not disabled (eager-gull-22, 2026-10-03). The readback is therefore expected to find managed accepted and factory accepted: not Noop. - -**Firmware re-entry.** `gunbc.fleet.mtcollins_firmware_converge` `BmcCredentialReestablishment.ipmi` is today a supplied `BmcSecureStanding`. It becomes this phase's result, for the post-reflash subject. - -#### Managed-host admission: a durable, named population (O1c) - -`managed_hosts()` is today a source roster with one construction-confined row. A runtime convergence cannot add a row, and the earlier sentence "mtjade1 joins by converging, not by edit" is withdrawn. Membership needs a durable authority, and the repository already has the mechanism: an intake fact is a **git-tracked observation consumed at mint time**. So: - -- **`ManagedHostAdmission`**: one committed record per unit. It is authored from that unit's arrival receipts in the unit's own observation module, the way the mtcollins1 observations are today. It retains: - - `HostIdentity` ↔ the exact `MachineIntakeSubject`; - - the access and capability standing; - - the `BmcSecureStanding`; - - the unit-hold decision; - - the receipt identity and its currency. -- **`managed_hosts()` becomes a projection** over the admission records. A unit is a member only when its record is complete, its standings are bound to the same subject, and that subject is current. -- **Absent, not defaulted.** A secured unit with no hold decision, with no host allocation, or with a receipt for another subject, is absent. -- The boot and reset consumers enumerate that same population (they already read `managed_hosts()`). -- `managed_host_binding` stays what it is: a pure join over a row. It is not the membership event. - -So mtjade1 joins when its receipts are committed and its admission record is authored, which is an edit of evidence and never of the roster's logic. - -#### Receipts and clock: domain carriers, not `gunbc.ensure` alone (O1b, O1c) - -`gunbc.ensure` says of itself that its before and after observations and its satisfaction Booleans are caller-supplied, that one observation can be passed twice, and that it does not prove the decided plan is the effect that ran. So "ensure-style" is the decision shape only. For every effectful phase, the effect authority mints a **construction-confined domain carrier** that binds: - -`subject + phase + admitted plan + application receipt + independent post-read + evidence` - -The dry realization over `gunbc.bmc_model` `BmcWorld` exercises those carriers, not only the generic Noop / Apply / Refuse arms. - -**Observer clock.** `bmc_secure` carries bare `EpochMs`, so a comment cannot make a controller-sourced time unconstructible. O1b introduces an observer-clock timestamp whose only producer is `gunbc.clock_read`, and every ordering field in the phase consumes it. The controller's year-2000 clock is preserved as an `AccessDiscover` observation and can never be the ordering clock. Control: a controller-sourced year-2000 timestamp cannot satisfy the rotation and readback ordering, and an observer timestamp can. Setting the BMC clock is a write and is out of scope. - -#### The convergence fold: one shared home, landed with its first consumer (O1c-1) - -**Why this is here** (eager-gull-22, 2026-10-03). The Spark serving bring-up (valiant-crab-775) needs the same thing cut O does: several effectful steps run as one convergence instead of separately dispatched modes that a human sequences. The operator pointed that lane at this one. The home lands **once**, with cut O and the Spark arm as its consumers. It does not go inside `gunbc.machine_intake_arrival_converge`. - -**What already exists (DFS before naming anything).** -- **The per-step lifecycle has a consumed home.** `gunbc.host_convergence_protocol` is the canonical protocol from the CONVERGENCE-ONE program (`docs/plans/convergence-one-program.md`): select, observe goal-blind, assess, plan, admit, apply, independently read back, terminal verdict. - - `HostEffectDomainProjection { observe, assess, decide }`, `inspect_via_host_effect_projection` and `reconcile_from_read_attempt` are the step. - - `gunbc.ensure` is a projection of it. - - That program's own rule: "anything that calls itself convergence without running that lifecycle is a second convergence algebra". -- **The roster of persistent host effects has a home.** `gunbc.host_convergence_census` `host_convergence_census_rows`. Each arrival phase's effect, and each Spark step, is a row there. It is not a parallel list. -- **The sealed receipt is the domain's.** `std.realization_reconcile` states that std cannot seal the relation decided plan ↔ performed plan ↔ evidence without accepting a law from the caller it would be checking, and that the binding "is the DOMAIN's obligation, discharged by a domain carrier minted only by the authority that performed the effect" (worked example: `gunbc.typed_remote_file_write` `RemoteFileConverged`). So there is **no generic sealed receipt** to build. O1b's `BmcSecure` receipt and Spark's receipts stay domain carriers. - -**What is still open in that home, stated honestly** (side-chat review of `5044d42`). Ordered composition is not the only missing piece. -- **The protocol's apply side is an awaiting frontier.** `host_effect_decide_apply_frontier` is `FrontierConsumerAwaiting`: `converge_apply_for_host` bypasses `HostEffectDomainProjection` and calls `host_effect_apply` directly. That is CONVERGENCE-ONE's unbuilt C8. -- **`gunbc.ensure` `ensure_reconcile` does not execute the lifecycle.** It classifies reports. - -**The boundary this plan takes: (B).** The fold **composes already-executed, domain-owned step outcomes**. It does not execute the protocol and does not claim to. -- Each domain runs its own step (observe, decide, apply, read back) and mints its own sealed receipt. -- The fold orders step instances, checks preconditions against those receipts, and produces the run's ledger. -- **C8 stays open.** It is discharged only by a production route through the projection's apply side. Nothing in cut O claims to discharge it. If an O1c PR does route a real apply through the projection, it says so and retires the frontier by its own trigger. - -It goes in the workflow layer, `gunbc.fleet`. It does not go in std, because lanes, principals and resumability are fleet policy. The module is named by DFS when built. - -**The runtime steps are joined to the census, so the census stays the roster.** -- `StepInstanceKey { run, step_identity, subject }` identifies a step instance. -- Every runtime step maps to **exactly one** `HostConvergenceCensusRow`. -- A missing, extra, duplicate or unresolved identity refuses. -- A duplicate `(run, step, subject)` also refuses. - -Without that identity join the runtime list would become the real roster and the census would be commentary. - -**Its shape, tested against two real consumers:** cut O's five arrival phases, and the Spark arm's steps S1–S9 (image produce and distribute, checkpoint seed and replica, sudo grants, claim recovery, supersede, launch). valiant-crab-775 reviewed the shape on 2026-10-04, and its six corrections are in the table. - -| Requirement | Shape | Driven by | -|---|---|---| -| A step | an identity; a `HostEffectDomainProjection`; its domain receipt type; its preconditions. **A step instance is step × subject**: the Spark steps are per (host, artifact), the arrival steps per unit. | both | -| Preconditions | step identities **with a quantifier over subjects**: all-of or any-of, each over a stated subject set. So "3 of 4 hosts converged" is representable as partial progress and is not read as a refusal. | Spark S9 (S3, S5, S6 on all hosts); S5 (S4 verified on some peer) | -| Order | derived from preconditions by an identity join (an unknown identity or a cycle refuses). **Where an order authority already exists, each step is bound to one member of it and the derived order must agree with that authority's ranking, or refuse.** For cut O, each step is bound to one `IntakePhase` and checked with `intake_phase_rank`. The roster is a projection of the authoritative order (2 phases in O1c-1, 4 in O1, 5 with O2), not the 12-phase list itself. | arrival (authority), Spark (derived) | -| Per step | the canonical protocol, unchanged: Noop / Apply / Refuse, independent readback, the domain receipt minted by the effect authority | both | -| Precondition satisfied | only by a **readback in this run** of the precondition instance: a receipt for the same subject. Never by an earlier step's return value. **Which readback is sufficient is the domain's declaration**, and the fold does not force the most expensive one. A domain may declare a cheap identity readback that binds to an earlier full-verification receipt, and it must then state what that cheap readback does and does not establish (§4b rung honesty). | both; Spark S5, whose full verify re-hashes the whole checkpoint on every host | -| Refusal | names the unmet step by identity. The fold may take that step **only if it is in the roster and ungated**. A gated step stops with a typed refusal naming the discharge it needs. | Spark S9 naming S6/S7; the arrival credential write | -| Gate | a step is ungated, or gated on an authorization discharged for that subject and that run. The pattern comes from `select_authorization_pattern`, selected **before** the fold (§3d: the fold chooses nothing). | arrival `BmcSecure`; Spark S7 | -| Principal | per step, from the same selection. A step whose principal is not bound refuses. | Spark S6/S7 vs S9 | -| No fallback arms | a step with no admissible realization refuses; it never widens (§5) | Spark S5: no verified peer, no Hub fallback | -| Uncertain completion | an applied step whose readback does not yet ground is **pending**, distinct from converged and from refused. A rerun re-observes and reattaches. A pending instance never satisfies a precondition. **Only its dependents wait.** Independent instances proceed in the same run; otherwise every run degenerates to one step. | Spark S2/S4 (hours, resumable) while S3 and S6 proceed; arrival archive | -| Deadline | every effect leg carries a deadline. A leg with none is unconstructible. | Spark's ssh leg that hangs instead of refusing | -| Lane | the set of hosts a run may mutate is derived from its step instances' **mutated subjects**, one lane per affected host. **The executor host that dispatches the run is not a lane key.** | Spark, where every mode dispatches from srv1 and collides there; the arrival unit hold | -| Verdict | one ledger per run: each step instance converged, pending, refused, or not reached. **A refusal stops the line. A pending instance does not.** | both | -| Selected before the fold | everything that is a choice: the route, the authorization pattern, the principal, and **roles** such as seed versus replica. A role derived inside a step is how a silent fallback happens. | arrival route (O1a); Spark S4/S5 roles | -| Not a step | a **measurement** is not a persistent host effect. It consumes a converged verdict and produces an observation receipt, outside the fold and outside the census. Otherwise "converged" comes to mean "benchmarked". | Spark S10 (load) | - -**Workflow surface.** Lane derivation changes concurrency groups in the generated workflow, which is an operator-visible surface. Cut O's use changes none: it runs inside the existing `mtcollins1_boot` job and its unit hold. The Spark arm's reshaping of its modes is that lane's proposal to the operator, not part of this cut. - -**§3c, and the pairing obligation: each capability lands with its first REAL consumer.** A fold with no consumer is a dangling declaration, and a capability exercised only by a designed fixture has no inhabitance claim (`DESIGN.md` §3, "a witness discriminates at one interface"). So the fold is not built whole in one PR. Each row of the shape table arrives in the PR whose consumer really exercises it: - -| PR | Real consumer | Capabilities it brings | -|---|---|---| -| **O1c-1** | the arrival prefix's two **read-only** phases, `AccessDiscover` (with the O1a route standing) and `IdentityBindProvisional` | the read-only scheduler core: `StepInstanceKey`; the census join and its refusals; step-to-`IntakePhase` binding and the rank check; preconditions satisfied by an in-run readback receipt; refusal naming the unmet step; the run ledger. **No apply, gate, lane, deadline or pending arm.** | -| **O1c-2** | `PriorLifeBoundary`'s archive (an effect that writes evidence and can be incomplete) | an applied step with its domain receipt; per-leg deadlines; incomplete-versus-refused | -| **O1c-3** | `BmcSecure` (O1b's receipt) and the `ManagedHostAdmission` population | the authorization gate and its discharge; principal per step; the mutation lane (the unit hold on the store host); apply followed by independent readback | -| **Spark arm** (valiant-crab-775's lane) | S1–S9 over four hosts | subject quantifiers (all-of, any-of); pending that blocks only dependents; the domain-declared cheap readback; multi-host lanes keyed on mutated subjects | - -**Controls land with the capability.** -- O1c-1's are: - - a step whose phase rank disagrees with the authority refuses; - - an unknown precondition refuses; - - a runtime step with no census row, or two rows, refuses; - - a duplicate `(run, step, subject)` refuses; - - a precondition is not satisfied by a receipt for another subject. -- The quantifier, pending and lane controls arrive with the consumer that inhabits them, not earlier with a fixture. - -valiant-crab-775 reviews O1c-1's shape before it lands. `DESIGN.md` §3d names the fleet admission spine as this cycle's first consumer, so the shape must not preclude it, and O1c-1's PR says how it fits. - -**What O1b changes.** Nothing in its scope. Its observe, assess and decide go through the canonical projection (`HostEffectDomainProjection`), and its effect is a census row. Its apply and readback are the domain's own, with its own sealed receipt, because the projection's apply side is the open C8 frontier. - -#### Authorization and what lands - -- **The rotation is a privileged effect.** It is classified by executing `gunbc.auth.authorization_pattern_selection` `select_authorization_pattern` over its real attributes, and it gets its row in `gunbc.auth.privileged_effect_census`. The pattern is what the selection returns. -- **The write is gated inside the single route.** The `BmcSecure` Apply arm refuses unless that authorization is discharged for that unit and that run. A run that finds the unit unsecured with no discharge stops with a typed refusal at `BmcSecure`. It never boots on a factory credential and never widens. -- **O2 preserves every refusal** the current boot entry makes (admission, authorization, maintenance hold, artifact, medium readback). Each is listed and witnessed in that PR. -- **The O1 PRs land** the route standing, the state-shaped `BmcSecure`, the observer clock, the convergence through `BmcSecure`, the admission population, and the dry realization with the controls above. **No live credential write** is part of any PR; each unit's write gets its own operator go-ahead, mtcollins1 first. -- **Password-path frontier.** The remaining wrappers (media attach, KVM, SOL, fan, UI bundle) cut over in cuts 3, 4a, 4d and 5. `mtcollins1_managed_secret_fetch_frontier` is retired when the last one refuses a caller-supplied path. -- **Workflow surface:** none added. O2's generated-workflow diff is shown in its PR. - -#### Decisions (eager-gull-22 for the operator, 2026-10-03) - -- **(i) Module name:** `gunbc.machine_intake_arrival_converge`. Approved. -- **(ii) Read-only reads of mtjade1 with the factory credential:** no separate sign-off; the operator approved hands-on access to the unit on 2026-10-03. **The rotation and any account write do need sign-off.** -- **(iii) First wet subject:** mtcollins1, with its unsecured state treated as likely real. -- **(iv) Workflow mode:** approved as a **re-root of the existing `mtcollins1_boot` mode**, not a new mode. The live credential write on each unit still gets its own go-ahead. - -### Terminal receipt owed by every cut (review item 5) - -These modules are outside the required gate, so a green required run proves nothing about them. Each PR's description carries all of the following: - -1. **Consumer witnesses, run by name.** The cut's complete consumer roster, recomputed at the PR head with the census method below and included in full in the PR description. Each named witness is executed with `gunbc run` against the head, with binary path and build sha printed. -2. **A same-path RED.** On the acceptance path the cut's witness actually runs, remove the binding the cut introduced (the `ManagedHost` row, the standing arm, the parameter) and show the named witness refusing. Then restore it and show it accepted. A RED reached by a different route does not count. -3. **A corpus sweep.** `git grep` at the head for every deleted module name and symbol, across `.dag`, `.rs`, `.yml`, `docs/` and `artifacts/`. Every remaining hit is classified with the dispositions in the non-import census below. -4. **No aliases.** No module re-exports, wraps or renames-through the deleted root, and no `mtcollins1_*` function is a one-line call into the generic one. - -## Non-import census (review item 4) - -These occurrences do not refuse through an import edge, so the import census above cannot see them. They are measured at `26e99a9c7f` and dispositioned by surface. **migrate** means the owning cut changes it. **receipt** means it is deliberately unit-named and stays. **residue** means it is historical prose and stays as written. - -| Surface | Population | Disposition | -|---|---|---| -| `.github/workflows/fleet-converge.yml` (projection of `gunbc.fleet_converge_workflow`) | 118 occurrences: 8 mode literals and about 30 step/job ids | **migrate**, cut 7 only, regenerated from the authority. Never hand-edited. | -| `.github/workflows/mtcollins-canary.yml` | a hand-authored scaffold by its own header; `runs-on: [self-hosted, mtcollins]` | **migrate or retire** in cut 7, and only with the operator's ruling. The runner label is a live registration (`gunbc.runner_registration_labels`). | -| `"mtcollins1_*"` string literals in `.dag` (mode names, step ids, effect rows) | 34 files. The largest are `gunbc.fleet_converge_workflow` (26), `gunbc.auth.privileged_effect_census` (11), `gunbc.ci_spec` (11), `mtcollins1_boot_run` (10), `mtcollins1_memory_census_observation` (10) | **migrate** in the cut that owns the named step. The privileged-effect rows follow cut 4c and 7. Unit-observation literals are **receipts**. | -| Entry-path strings `"dag/gunbc/machine_intake/mtcollins…"` in `.dag` | `gunbc.non_fold_residue` (45), `gunbc.ci_spec` (11), `v2.workflow.floor_unimported_bare_provider_debt_roster` (4), and 5 single occurrences | **migrate**: every cut that moves a file updates these rows in the same PR. Each is a path a deletion does not refuse. | -| `artifacts/bmc/mtcollins1-*` (17 files) | captured evidence; paths cited by unit observations | **receipt**: never renamed, because the content hashes bind the path. | -| `docs/probes/mtcollins*` (12), `docs/rung-drops/*` (1), `docs/design-rung-drops.md` | dated observations and declared drops | **receipt**. | -| `docs/plans/*` (14 files other than this one), `docs/recovered/*`, `ROADMAP.md` (2) | dated plans; the roadmap goal "Bring Mt. Collins unit 1 into service" is correctly unit-named | **residue / receipt**. Not edited by these cuts. | -| `src/v1/stage0/src/cli_run.rs` (1) | a comment | **residue**. | -| The IAM condition description string beginning `gunbc.auth.mtcollins1_boot_federation: the one version …` (kept by cut 4c in `gunbc.auth.managed_host_boot_federation`) | part of a **live** IAM binding condition | **receipt**: it must stay byte-identical until a deliberate re-provision. A name sweep must not "fix" it, although it names a deleted module. | - -Exact token `mtcollins1_boot`: 19 files at `26e99a9c7f`, all inside the populations above. - -## Census method: a one-off, re-run by each cut (not an instrument) - -No entry point in the tree derives an import-graph consumer roster, and building one is outside this program. So the census is a **one-off measurement** (DESIGN §6), and no snapshot of its output is the authority for anything. - -**Each cut re-runs it at its own PR head** over its own deleted roots, and puts the **complete** consumer roster in that PR's description, bound to that head sha. That roster is the §3 "enumerate the consumers by name before you delete" receipt for the cut, and it lives with the deletion it licenses, where it cannot go stale. - -The method: -1. Walk `dag/` and `src/v2/` for `.dag` files. -2. Read each `^module` and each `^import `. -3. The population is the modules whose name or text matches `mtcollins` (case-insensitive). -4. Consumers are the reverse import edges. -5. Gate membership is the transitive import closure from the modules matching `v2.workflow.required_floor` `required_gate_prefixes`. -6. Imported unit symbols are the names inside each module's `import … { … }` lists that match `mtcollins|mt_collins|MtCollins`. - -It is a regex over source text, so it censuses *import shapes*. The terminal receipt's corpus sweep and the non-import census above cover what an import edge cannot see. - -## Census - -The layer is assigned per module from its subject. -- **R** = mentions the unit only in prose or receipts. -- **W** = witness. - -Consumers list production importers by name, truncated after six with a count, plus the number of witness importers. **This table is a dated reading aid at `26e99a9c7f`, used to plan the order. It is not a consumer receipt: each cut recomputes its own roster at its head (see Census method).** - -### Layer 3 — silicon (3 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `gunbc.machine_intake_ampere_dram_console_observation` | 314 | 2 | — | `gunbc.machine_intake_ampere_socket_console_observation`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` · 1 witness | no | -| `gunbc.machine_intake_ampere_socket_console_observation` | 503 | 2 | — | `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_smpro_observation` | 395 | 36 | `mtcollins1_endpoint` | `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_phase_timing`, `gunbc.machine_intake_mtcollins1_boot_run` · 3 witness | no | - -### Layer 5 — fleet procedure (53 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `extdeps.provisioning.ubuntu_seeded_install_media` | 196 | 1 | — | `extdeps.provisioning.ubuntu_seeded_install_media_remaster`, `gunbc.machine_intake_megarac_media_attach`, `gunbc.machine_intake_mtcollins1_boot_authorization`, `gunbc.machine_intake_mtcollins1_census_image`, `gunbc.machine_intake_mtcollins1_census_image_publish`, `gunbc.machine_intake_mtcollins1_census_medium_readback` +4 more · 7 witness | no | -| `extdeps.tools.xorriso` | 151 | 1 | — | `extdeps.provisioning.ubuntu_seeded_install_media_remaster`, `extdeps.provisioning.ubuntu_seeded_install_media_toolchain`, `gunbc.host_effect_realize`, `gunbc.machine_intake_mtcollins1_census_member_readback` · 2 witness | no | -| `gunbc.auth.fleet_secret_accessor_roster` | 95 | 6 | `mtcollins1_bmc_gunbc_secret_ref` | `gunbc.auth.approval_mac_key_provision`, `gunbc.spark.secret_access_ensure` · 1 witness | no | -| `gunbc.auth.gcp_iam_converge` | 893 | 4 | `mtcollins1_boot_dedicated_federation` | `gunbc.auth.gcp_iam_converge_run`, `gunbc.fleet_converge_workflow` · 1 witness | no | -| `gunbc.auth.gcp_iam_converge_federation` | 67 | 1 | — | `gunbc.auth.gcp_iam_converge`, `gunbc.fleet_converge_workflow` | no | -| `gunbc.auth.heal_publisher_provision` | 500 | 1 | — | `gunbc.auth.gcp_iam_converge`, `gunbc.auth.mtcollins1_boot_federation_provision`, `gunbc.namecheap.federation_provision` · 2 witness | no | -| `gunbc.auth.mtcollins1_boot_federation` | 157 | 43 | `mtcollins1_bmc_gunbc_secret_ref` | `gunbc.auth.gcp_iam_converge`, `gunbc.auth.mtcollins1_boot_federation_provision`, `gunbc.auth.privileged_effect_census`, `gunbc.fleet_converge_workflow` · 1 witness | no | -| `gunbc.auth.mtcollins1_boot_federation_provision` | 39 | 28 | `mtcollins1_boot_secret_grants`, `mtcollins1_boot_service_account`, `mtcollins1_boot_service_account_display_name`, `mtcollins1_boot_service_account_id`, `mtcollins1_boot_service_account_member` … | `gunbc.auth.gcp_iam_converge` · 1 witness | no | -| `gunbc.auth.privileged_effect_census` | 1060 | 54 | `mtcollins1_boot_service_account_member` | `gunbc.census_closure_frontier` · 3 witness | no | -| `gunbc.fleet_converge_workflow` | 4446 | 271 | `gunbc_ci_mtcollins1_boot_admit_invoke`, `gunbc_ci_mtcollins1_boot_invoke`, `gunbc_ci_mtcollins1_boot_job_backstop_timeout_minutes`, `gunbc_ci_mtcollins1_census_image_publish_invoke`, `gunbc_ci_mtcollins1_census_image_step_timeout_minutes` … | `gunbc.generated_artifact_emit` · 11 witness | no | -| `gunbc.fleet_workflow_steps` | 929 | 4 | — | `gunbc.fleet_converge_workflow` · 2 witness | no | -| `gunbc.host_reset_return` | 1034 | 2 | — | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.host_reset_return_run` · 2 witness | no | -| `gunbc.host_reset_return_run` | 787 | 3 | — | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_boot_run` · 1 witness | no | -| `gunbc.host_reset_subject_roster` | 140 | 22 | `mtcollins1_access_observation`, `mtcollins1_bmc_username`, `mtcollins1_capability_row`, `mtcollins1_cdrom_selection`, `mtcollins1_unit_hold_key` … | `gunbc.fleet_converge_workflow`, `gunbc.host_reset_return_run` | no | -| `gunbc.hostname_allocation` | 111 | 3 | `operator_host_mtcollins1` | `gunbc.deployed_intent_v1`, `gunbc.host_reset_return_run`, `gunbc.network_identity_subsumption` | no | -| `gunbc.machine_intake_host_capture_envelope` | 749 | 5 | — | `gunbc.machine_intake_host_capture_historical_binding`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_milestone`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_census_image` · 6 witness | no | -| `gunbc.machine_intake_host_capture_historical_binding` | 348 | 14 | `mtcollins1_census_stages`, `mtcollins1_host_capture_artifact_path`, `mtcollins1_host_capture_byte_count`, `mtcollins1_host_capture_digest` | — · 2 witness | no | -| `gunbc.machine_intake_host_resource_observation` | 403 | 4 | — | `gunbc.machine_intake_mtcollins1_topology_goal` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_actuate` | 196 | 41 | `MtCollins1BootStart`, `mtcollins1_access_observation`, `mtcollins1_boot_start_of`, `mtcollins1_capability_row`, `mtcollins1_secured_account` … | `gunbc.host_reset_subject_roster`, `gunbc.machine_intake_mtcollins1_boot_authorization`, `gunbc.machine_intake_mtcollins1_boot_run` · 3 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_admission` | 52 | 15 | — | `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_artifact` | 58 | 5 | — | `gunbc.machine_intake_mtcollins1_boot_authorization`, `gunbc.machine_intake_mtcollins1_boot_image_fetch`, `gunbc.machine_intake_mtcollins1_census_image`, `gunbc.machine_intake_mtcollins1_census_image_publish`, `gunbc.machine_intake_mtcollins1_census_medium_readback`, `gunbc.machine_intake_mtcollins1_census_member_readback` +1 more · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_authorization` | 105 | 61 | `mtcollins1_boot_export_dir`, `mtcollins1_boot_image_sha256`, `mtcollins1_cdrom_selection`, `mtcollins1_census_image_stem`, `mtcollins1_census_volume_id` … | `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_census_medium_readback`, `gunbc.machine_intake_mtcollins1_census_member_readback`, `gunbc.machine_intake_mtcollins1_media_attach` · 9 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` | 2204 | 214 | `MtCollins1BootPhaseTiming`, `mtcollins1_boot_ipmi_kill_after`, `mtcollins1_boot_ipmi_read_deadline`, `mtcollins1_boot_phase_timing_not_taken`, `mtcollins1_boot_phase_timing_text` … | `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_media_attach` · 8 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_dry_realization` | 730 | 100 | `mtcollins1_sol_notice_ready_suffix`, `mtcollins1_sol_notice_token_env`, `mtcollins1_sol_notice_watcher_path` | — · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_image_fetch` | 119 | 37 | `mtcollins1_boot_export_dir`, `mtcollins1_diskless_image_name` | `gunbc.machine_intake_mtcollins1_boot_authorization` | no | -| `gunbc.machine_intake_mtcollins1_boot_milestone` | 133 | 28 | `mtcollins1_nproc` | `gunbc.machine_intake_host_capture_historical_binding`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_census_image` · 4 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_phase_timing` | 255 | 92 | — | `gunbc.machine_intake_mtcollins1_actuate`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_run` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_boot_run` | 2734 | 571 | `MtCollins1BootDiagnosticBundle`, `MtCollins1BootMedium`, `MtCollins1BootPhaseTiming`, `MtCollins1BootRoute`, `MtCollins1BootRunRecord` … | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_boot_dry_realization`, `gunbc.machine_intake_mtcollins1_sol_notice` · 8 witness | no | -| `gunbc.machine_intake_mtcollins1_census_image` | 220 | 76 | `mtcollins1_boot_export_dir`, `mtcollins1_census_stages`, `mtcollins1_diskless_image_name` | `gunbc.machine_intake_mtcollins1_boot_authorization`, `gunbc.machine_intake_mtcollins1_census_image_publish`, `gunbc.machine_intake_mtcollins1_census_medium_readback`, `gunbc.machine_intake_mtcollins1_census_member_readback`, `gunbc.machine_intake_mtcollins1_media_attach` · 9 witness | no | -| `gunbc.machine_intake_mtcollins1_census_image_publish` | 267 | 46 | `mtcollins1_boot_export_dir`, `mtcollins1_boot_export_host_target`, `mtcollins1_census_image`, `mtcollins1_census_image_stock_path`, `mtcollins1_census_pinned_image` | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_census_medium_readback` | 260 | 74 | `MtCollins1BootMedium`, `MtCollins1CensusMedium`, `MtCollins1StockInstallerMedium`, `mtcollins1_boot_export_dir`, `mtcollins1_boot_export_host_target` … | `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_census_member_readback` · 3 witness | no | -| `gunbc.machine_intake_mtcollins1_census_member_readback` | 483 | 34 | `MtCollins1BootMedium`, `MtCollins1CensusMedium`, `MtCollins1StockInstallerMedium`, `mtcollins1_boot_export_host_target`, `mtcollins1_boot_medium` … | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_census_image_publish` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_census_qemu_host_observe` | 294 | 11 | — | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_census_qemu_toolchain_converge` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_census_qemu_toolchain` | 172 | 1 | — | `gunbc.machine_intake_mtcollins1_census_qemu_host_observe`, `gunbc.machine_intake_mtcollins1_census_qemu_toolchain_converge` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_census_qemu_toolchain_converge` | 41 | 8 | — | — | no | -| `gunbc.machine_intake_mtcollins1_census_refusal_hypotheses` | 356 | 13 | — | — · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_diskless_boot_receipt` | 144 | 12 | — | — | no | -| `gunbc.machine_intake_mtcollins1_handoff_probe` | 63 | 14 | `mtcollins1_access_observation`, `mtcollins1_capability_row`, `mtcollins1_firmware` | — | no | -| `gunbc.machine_intake_mtcollins1_maintenance_hold` | 763 | 58 | `operator_host_mtcollins1` | `gunbc.fleet_converge_workflow`, `gunbc.host_reset_return_run`, `gunbc.host_reset_subject_roster`, `gunbc.machine_intake_mtcollins1_actuate`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe` +2 more · 5 witness | no | -| `gunbc.machine_intake_mtcollins1_media_attach` | 427 | 111 | `MtCollins1BootSubject`, `MtCollins1MediaAttachRecord`, `mtcollins1_boot_chassis_power`, `mtcollins1_boot_export_dir`, `mtcollins1_boot_ipmi_kill_after` … | `gunbc.machine_intake_mtcollins1_boot_run` · 4 witness | no | -| `gunbc.machine_intake_mtcollins1_memory_census_observation` | 916 | 172 | `MtCollins1ControllerFruIdentity`, `mtcollins1_controller_fru_identity` | `gunbc.machine_intake_host_capture_historical_binding`, `gunbc.machine_intake_mtcollins1_bmc_secure_observation`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_milestone`, `gunbc.machine_intake_mtcollins1_topology_goal` · 3 witness | no | -| `gunbc.machine_intake_mtcollins1_netboot_client_observation` | 73 | 11 | — | `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_observer`, `gunbc.site_uefi_arm64_pxe_edge` · 6 witness | no | -| `gunbc.machine_intake_oob_boot_handoff` | 542 | 2 | — | `gunbc.host_reset_return_run`, `gunbc.host_reset_subject_roster`, `gunbc.machine_intake_mtcollins1_actuate`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_phase_timing`, `gunbc.machine_intake_mtcollins1_boot_run` +1 more · 5 witness | no | -| `gunbc.machine_intake_pre_os_bringup_attempt` | 370 | 2 | — | `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation`, `gunbc.machine_intake_mtcollins1_sixteen_module_restore_observation` · 1 witness | no | -| `gunbc.machine_intake_pre_os_bringup_verdict` | 633 | 1 | — | `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_sixteen_module_restore_observation`, `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_observer` +2 more · 6 witness | no | -| `gunbc.machine_intake_pre_os_capture_replay` | 175 | 27 | `mt_collins_channel_population_roles`, `mtcollins1_first_own_image_boot`, `mtcollins1_host_nic_mac`, `mtcollins_3ds_capture_path`, `mtcollins_3ds_capture_sha256` … | — · 2 witness | no | -| `gunbc.machine_intake_pre_os_observer` | 139 | 1 | — | `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_inventory_assessment`, `gunbc.machine_intake_pre_os_timeline` · 3 witness | no | -| `gunbc.machine_intake_pre_os_timeline` | 392 | 3 | `mtcollins_dram_cycle_start` | — · 1 witness | no | -| `gunbc.machine_intake_sel_stimulus_listener` | 124 | 2 | — | `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_replay` · 2 witness | no | -| `gunbc.machine_intake_terminal_transcript` | 18 | 2 | — | `gunbc.machine_intake_host_capture_envelope`, `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_observer` · 1 witness | no | -| `gunbc.seeded_install_media_policy` | 28 | 1 | — | `gunbc.machine_intake_mtcollins1_census_image`, `gunbc.srv3_seeded_install_media_artifact` | no | -| `gunbc.site_uefi_arm64_pxe_edge` | 339 | 7 | `mtcollins1_host_nic_mac` | `gunbc.census_closure_frontier`, `gunbc.site_pxe_edge_converge` · 2 witness | no | -| `gunbc.verified_archive_install` | 235 | 1 | — | `gunbc.machine_intake_mtcollins1_census_qemu_toolchain`, `gunbc.runner_browser_toolchain`, `gunbc.runner_microvm_host_ready` · 3 witness | no | - -### Layer 4 — BMC stack (22 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `extdeps.bmc.http` | 350 | 1 | — | `gunbc.fleet_health_observe`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.srv3_boot_once_cd`, `gunbc.tools.bmc_health_reader_converge`, `gunbc.tools.bmc_intake_first_contact`, `gunbc.tools.bmc_onboard` | no | -| `extdeps.bmc.ipmi` | 410 | 1 | — | `ctl.pos_emit`, `ctl.pos_service`, `gunbc.machine_intake_bmc_fan_observation`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_dry_realization`, `gunbc.machine_intake_mtcollins1_boot_run` +4 more · 1 witness | no | -| `extdeps.bmc.ipmi_sel` | 83 | 1 | — | `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_sixteen_module_restore_observation`, `gunbc.machine_intake_pre_os_bringup_verdict`, `gunbc.machine_intake_sel_stimulus_listener` · 2 witness | no | -| `extdeps.bmc.ipmitool_observed_output` | 67 | 6 | — | `gunbc.bmc_dry_realization`, `gunbc.machine_intake_mtcollins1_boot_dry_realization` | no | -| `extdeps.bmc.megarac` | 664 | 5 | — | `extdeps.bmc.access_profile`, `extdeps.bmc.virtual_media`, `gunbc.bmc_implementation_dispatch`, `gunbc.bmc_megarac_web_adapter`, `gunbc.boot_artifact_delivery`, `gunbc.machine_intake_megarac_media_attach` +3 more · 7 witness | **yes** | -| `extdeps.bmc.megarac_observed_output` | 73 | 1 | — | `gunbc.bmc_dry_realization` | no | -| `extdeps.bmc.phosphor_fan_control` | 126 | 2 | — | — · 1 witness | no | -| `extdeps.bmc.redfish_memory_inventory` | 459 | 2 | — | `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` · 1 witness | no | -| `gunbc.bmc_model` | 409 | 2 | — | `gunbc.bmc_dry_realization`, `gunbc.bmc_megarac_web_adapter`, `gunbc.machine_intake_mtcollins1_boot_dry_realization` · 5 witness | no | -| `gunbc.machine_intake_bmc_fan_observation` | 519 | 2 | — | `gunbc.machine_intake_mtcollins1_fan_observe` · 1 witness | no | -| `gunbc.machine_intake_bmc_rotation_route` | 117 | 1 | — | — · 1 witness | no | -| `gunbc.machine_intake_megarac_media_attach` | 2107 | 9 | — | `gunbc.bmc_dry_realization`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_media_attach`, `gunbc.machine_intake_mtcollins1_ui_bundle_observe` · 5 witness | no | -| `gunbc.machine_intake_megarac_served_ui_catalog_observation` | 20 | 2 | — | `gunbc.machine_intake_megarac_ui_features`, `gunbc.machine_intake_mtcollins1_ui_bundle_observe` · 3 witness | no | -| `gunbc.machine_intake_megarac_ui_features` | 403 | 3 | — | `gunbc.machine_intake_megarac_media_attach`, `gunbc.machine_intake_mtcollins1_kvm_still` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_bmc_secure_observation` | 275 | 92 | `mtcollins1_bmc_gunbc_secret_ref`, `mtcollins1_controller_fru_identity`, `mtcollins1_endpoint`, `mtcollins1_firmware`, `mtcollins1_subject_binding` | `gunbc.machine_intake_mtcollins1_actuate`, `gunbc.machine_intake_mtcollins1_fan_observe`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe`, `gunbc.machine_intake_mtcollins1_media_attach`, `gunbc.machine_intake_mtcollins1_platform_observation`, `gunbc.machine_intake_mtcollins1_ui_bundle_observe` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_bmc_sensor_observation` | 96 | 25 | — | `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_fan_observe` | 264 | 39 | `mtcollins1_endpoint`, `mtcollins1_secured_account` | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe`, `gunbc.machine_intake_mtcollins1_ui_bundle_observe` | no | -| `gunbc.machine_intake_mtcollins1_kvm_observer_observe` | 276 | 71 | `mtcollins1_bmc_firmware`, `mtcollins1_boot_host_admission`, `mtcollins1_endpoint`, `mtcollins1_fan_credential_path`, `mtcollins1_fan_credential_path_env` … | `gunbc.fleet_converge_workflow` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_kvm_still` | 1180 | 43 | — | `gunbc.ci_spec`, `gunbc.fleet_converge_workflow`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle`, `gunbc.machine_intake_mtcollins1_boot_dry_realization`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe` · 6 witness | no | -| `gunbc.machine_intake_mtcollins1_sol_notice` | 438 | 52 | `mtcollins1_boot_sol_capture_path_env`, `mtcollins1_boot_sol_pid_path_env`, `mtcollins1_sol_client_diagnostic_path`, `mtcollins1_sol_delivery_suffix`, `mtcollins1_sol_incident_path` … | `gunbc.ci_spec` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_ui_bundle_observe` | 364 | 61 | `mtcollins1_boot_ipmi_kill_after`, `mtcollins1_boot_ipmi_read_deadline`, `mtcollins1_endpoint`, `mtcollins1_fan_credential_path`, `mtcollins1_fan_credential_path_env` … | `gunbc.fleet_converge_workflow` · 1 witness | no | -| `gunbc.machine_intake_sol_hold` | 344 | 17 | — | `gunbc.ci_spec`, `gunbc.machine_intake_mtcollins1_boot_dry_realization`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_sol_notice`, `gunbc.owned_process` · 2 witness | no | - -### Layer 2 — platform (16 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `extdeps.ampere.mt_collins_1u_product_brief.specifications` | 228 | 23 | `MtCollins1UPublicProductBrief`, `mt_collins_1u_brief_authority`, `mt_collins_1u_public_product_brief` | `gunbc.machine_intake_mtcollins1_expansion_attachment`, `gunbc.machine_intake_mtcollins1_expansion_candidates`, `gunbc.machine_intake_mtcollins1_expansion_observation` · 2 witness | no | -| `extdeps.ampere.mt_collins_1u_product_brief.subject` | 102 | 8 | — | `extdeps.ampere.mt_collins_1u_product_brief.specifications` | no | -| `extdeps.ampere.mt_collins_2u_user_guide.subject` | 68 | 7 | — | `gunbc.mt_collins_dimm_physical_identity`, `gunbc.specification_citation_read_provenance` · 1 witness | no | -| `extdeps.ampere.mt_collins_getting_started_guide.dimm_layout` | 92 | 32 | `mt_collins_gsg_authority`, `mt_collins_gsg_model_scope` | `gunbc.machine_intake_mtcollins1_dimm_connector_observation`, `gunbc.machine_intake_mtcollins1_physical_orientation`, `gunbc.machine_intake_mtcollins1_socket1_investigation_observation`, `gunbc.mt_collins_dimm_physical_identity`, `gunbc.mtcollins_memory_placement` · 3 witness | no | -| `extdeps.ampere.mt_collins_getting_started_guide.subject` | 92 | 3 | — | `extdeps.ampere.mt_collins_getting_started_guide.dimm_layout`, `extdeps.ampere.mt_collins_product_brief.memory_population`, `extdeps.ampere.mt_collins_product_brief.platform` · 1 witness | no | -| `extdeps.ampere.mt_collins_product_brief.bmc` | 59 | 5 | `MtCollinsPublicProductBrief`, `mt_collins_public_product_brief` | — | no | -| `extdeps.ampere.mt_collins_product_brief.memory_population` | 387 | 85 | `MtCollinsPublicProductBrief`, `mt_collins_gsg_authority`, `mt_collins_public_product_brief` | `gunbc.machine_intake_mtcollins1_dimm_connector_observation`, `gunbc.machine_intake_mtcollins1_physical_orientation`, `gunbc.machine_intake_mtcollins1_spare_screen_prediction`, `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.memory_configuration_evaluation`, `gunbc.mt_collins_dimm_physical_identity` +3 more · 9 witness | no | -| `extdeps.ampere.mt_collins_product_brief.platform` | 102 | 12 | `MtCollinsPublicProductBrief`, `mt_collins_gsg_authority`, `mt_collins_public_product_brief` | `gunbc.memory_provisioning` · 1 witness | no | -| `extdeps.ampere.mt_collins_product_brief.subject` | 77 | 8 | — | `extdeps.ampere.mt_collins_product_brief.bmc`, `extdeps.ampere.mt_collins_product_brief.memory_population`, `extdeps.ampere.mt_collins_product_brief.platform` · 1 witness | no | -| `extdeps.boards.types` | 89 | 1 | — | `extdeps.bmc.access_profile`, `extdeps.boards.asrock_rack`, `extdeps.boards.gigabyte`, `extdeps.cooling.dynatron`, `extdeps.cooling.types`, `extdeps.firmware.types` +13 more · 6 witness | **yes** | -| `gunbc.machine_intake_mtcollins1_dimm_connector_observation` | 105 | 22 | `MtCollinsDimmDiagramLegend`, `MtCollinsDimmPopulationIdentity`, `MtCollinsDimmPopulationRow`, `mt_collins_population_16_dimms` | `gunbc.machine_intake_mtcollins1_expansion_candidates`, `gunbc.machine_intake_mtcollins1_expansion_observation`, `gunbc.machine_intake_mtcollins1_physical_orientation` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_physical_orientation` | 856 | 293 | `MtCollins1ChassisEnd`, `MtCollins1CpuRelease`, `MtCollins1DiagnosisHypothesis`, `MtCollins1FirmwareSocketSummary`, `MtCollins1HypothesisStanding` … | — · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_platform_observation` | 225 | 33 | `mtcollins1_unit_key_standing` | — · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_topology_goal` | 18 | 7 | `mtcollins1_nproc` | — · 1 witness | no | -| `gunbc.machine_intake_power_supply` | 25 | 1 | — | `gunbc.machine_intake_mtcollins1_platform_observation` · 1 witness | no | -| `gunbc.mt_collins_dimm_physical_identity` | 173 | 44 | `MtCollins2uMemoryTopologyCitation`, `MtCollinsChannelConnectorPair`, `MtCollinsChassisFigureOrientation`, `MtCollinsDimmDiagramLegend`, `MtCollinsDimmFigureBank` … | `gunbc.machine_intake_mtcollins1_dimm_connector_observation`, `gunbc.machine_intake_mtcollins1_physical_orientation` · 1 witness | no | - -### Layer 1 — unit (17 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation` | 695 | 114 | `AmpereMtCollins`, `mtcollins1_firmware`, `mtcollins_training_capture_path`, `mtcollins_training_capture_sha256` | `gunbc.machine_intake_mtcollins1_sixteen_module_restore_observation` · 3 witness | no | -| `gunbc.machine_intake_mtcollins1_access_observation` | 294 | 45 | `AmpereMtCollins` | `gunbc.host_reset_subject_roster`, `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation`, `gunbc.machine_intake_mtcollins1_actuate`, `gunbc.machine_intake_mtcollins1_bmc_secure_observation`, `gunbc.machine_intake_mtcollins1_boot_authorization`, `gunbc.machine_intake_mtcollins1_boot_diagnostic_bundle` +8 more · 3 witness | no | -| `gunbc.machine_intake_mtcollins1_connectx4lx_observation` | 108 | 19 | `operator_host_mtcollins1` | `gunbc.machine_intake_mtcollins1_expansion_candidates` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_expansion_attachment` | 112 | 6 | `MtCollinsUnitExpansionEvidence`, `mt_collins_1u_ocp_slot` | `gunbc.machine_intake_mtcollins1_expansion_candidates` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_expansion_candidates` | 87 | 8 | `mt_collins_1u_ocp_slot` | — · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_expansion_observation` | 186 | 18 | `MtCollins1UExpansion`, `mt_collins_1u_expansion`, `mt_collins_1u_ocp_slot` | `gunbc.machine_intake_mtcollins1_expansion_attachment`, `gunbc.machine_intake_mtcollins1_expansion_candidates` · 2 witness | no | -| `gunbc.machine_intake_mtcollins1_memory_prediction` | 156 | 8 | — | — · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_sixteen_module_restore_observation` | 304 | 44 | `mtcollins1_pre_os_capabilities`, `mtcollins1_sol_idle_capture`, `mtcollins1_sol_reading`, `mtcollins1_sol_session_banner_digest` | — · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_socket1_investigation_observation` | 389 | 139 | `MtCollinsDimmFigure` | `gunbc.machine_intake_mtcollins1_physical_orientation` · 1 witness | no | -| `gunbc.machine_intake_mtcollins1_spare_screen_prediction` | 247 | 16 | `MtCollinsDimmPopulationRow`, `mt_collins_population_16_dimms` | — · 1 witness | no | -| `gunbc.memory_configuration_evaluation` | 396 | 11 | `AmpereMtCollins`, `MtCollinsChannelPopulationRoleStanding`, `MtCollinsPopulationRoleRefusal`, `mt_collins_channel_population_roles`, `mt_collins_socket_dimm_sizes_supported` … | `gunbc.memory_change_evaluation`, `gunbc.memory_change_report` · 2 witness | no | -| `gunbc.mtcollins_3ds_memory_change` | 132 | 59 | `MtCollinsChannelPopulationRole`, `MtCollinsPopulationRoleRefusal`, `mt_collins_channel_population_roles`, `mtcollins_dram_cycle_start`, `mtcollins_hynix_part` … | `gunbc.machine_intake_pre_os_capture_replay` · 1 witness | no | -| `gunbc.mtcollins_memory_change` | 197 | 57 | `AmpereMtCollins`, `MtCollinsChannelPopulationRole`, `MtCollinsPopulationRoleRefusal`, `mt_collins_channel_population_roles`, `mt_collins_socket0_channel_pairs` … | `gunbc.machine_intake_mtcollins1_32dimm_bringup_observation`, `gunbc.machine_intake_pre_os_capture_replay`, `gunbc.machine_intake_pre_os_timeline`, `gunbc.mtcollins_3ds_memory_change` · 4 witness | no | -| `gunbc.mtcollins_memory_placement` | 22 | 4 | `MtCollinsChannelPopulationRole` | `gunbc.memory_configuration_evaluation`, `gunbc.mtcollins_memory_change` · 2 witness | no | -| `gunbc.rung_drop.mtcollins1_boot_accepts_socket1_absent` | 46 | 27 | — | `gunbc.rung_drop.roster` | no | -| `gunbc.rung_drop.mtcollins1_boot_matrix_enrolment_dead_band_observed_only` | 44 | 15 | `mtcollins1_boot_matrix_enrolment_dead_band_observed_only`, `mtcollins1_boot_matrix_native_witness_capability` | `gunbc.rung_drop.roster` | no | -| `gunbc.rung_drop.mtcollins1_boot_matrix_new_witness_eval_step_cost` | 58 | 11 | — | `gunbc.rung_drop.mtcollins1_boot_matrix_enrolment_dead_band_observed_only`, `gunbc.rung_drop.roster` | no | - -### Layer R — reference only (keeps prose/receipt; no layer move) (77 modules) - -| module | lines | refs | unit/platform constants imported | consumers (import census) | gate | -|---|---|---|---|---|---| -| `extdeps.linux.edac` | 214 | 1 | — | `gunbc.machine_intake_mtcollins1_memory_census_observation` · 2 witness | no | -| `extdeps.linux.mlx5_core` | 33 | 1 | — | `gunbc.machine_intake_mtcollins1_connectx4lx_observation` · 1 witness | no | -| `extdeps.network.dell_0r887v` | 23 | 1 | — | `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.network.ibm_01ft753` | 40 | 1 | — | `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.network.mcx4121a_acat` | 53 | 1 | — | `gunbc.machine_intake_mtcollins1_connectx4lx_observation`, `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.network.mcx4411a_acan` | 24 | 1 | — | `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.network.mcx4421a_acan` | 24 | 1 | — | `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.network.mcx4621a_acab` | 24 | 1 | — | `gunbc.machine_intake_mtcollins1_expansion_candidates` | no | -| `extdeps.standards.nic_attachment` | 62 | 1 | — | `extdeps.ampere.mt_collins_1u_product_brief.specifications`, `extdeps.network.dell_0r887v`, `extdeps.network.ibm_01ft753`, `extdeps.network.mcx4121a_acat`, `extdeps.network.mcx4411a_acan`, `extdeps.network.mcx4421a_acan` +6 more · 3 witness | no | -| `gunbc.ci_layer_roots` | 2236 | 3 | — | `dag.test.claim.offline_local_recipe_witness`, `gunbc.ci_spec`, `gunbc.commit_workflow`, `gunbc.contributor_onboarding_path`, `gunbc.decoder_execution_identity`, `gunbc.derived_obligation_row` +34 more · 9 witness | **yes** | -| `gunbc.ci_spec` | 3825 | 194 | `mtcollins1_bmc_gunbc_secret_ref`, `mtcollins1_boot_sol_capture_path_env`, `mtcollins1_boot_sol_pid_path_env`, `mtcollins1_census_image_receipt_path`, `mtcollins1_fan_credential_path_env` … | `gunbc.compiler_gate_workflow`, `gunbc.fleet_converge_workflow`, `gunbc.fleet_release_bins_key`, `gunbc.fleet_workflow_steps`, `gunbc.generated_artifact_emit`, `gunbc.heal_publisher_workflow` +10 more · 10 witness | no | -| `gunbc.doc_graph_roots` | 1757 | 3 | — | `v2.test.lens_doc_reachability.doc_reachability_test` · 3 witness | no | -| `gunbc.durable_cas_file_store` | 856 | 1 | — | `gunbc.auth.approval_assertion_counter`, `gunbc.auth.approval_decision_store`, `gunbc.auth.approval_device_redemption`, `gunbc.auth.approval_keyring_converge`, `gunbc.auth.approval_store_receipt`, `gunbc.authorization_claim_slot` +9 more · 8 witness | **yes** | -| `gunbc.durable_exclusive_hold_file_store` | 369 | 1 | — | `gunbc.fabric_control_plane`, `gunbc.fabric_required_build_cell`, `gunbc.machine_intake_mtcollins1_maintenance_hold`, `tools.fabric_control_plane_live_probe` · 7 witness | no | -| `gunbc.fleet_intent_network` | 461 | 3 | — | `gunbc.approve_ios_project`, `gunbc.auth.approval_broker_endpoint`, `gunbc.auth.approval_device_enrolment_code_issue`, `gunbc.auth.approval_keyring_converge`, `gunbc.auth.approval_ntfy_deployment`, `gunbc.auth.approval_request_submission` +90 more · 101 witness | **yes** | -| `gunbc.floor_demand` | 1870 | 1 | — | `gunbc.ci_floor_measurement`, `gunbc.fabric_control_plane`, `gunbc.fabric_floor_dispatch`, `gunbc.fabric_witness_run`, `gunbc.floor_cold_build_receipt`, `gunbc.floor_memory_demand` +5 more · 8 witness | **yes** | -| `gunbc.guarantee_stall.pci_address_rendering_equivalence_stall` | 17 | 2 | — | `gunbc.guarantee_stall.roster` | no | -| `gunbc.non_fold_residue` | 2029 | 55 | — | — · 1 witness | no | -| `gunbc.owned_process` | 175 | 1 | — | `gunbc.bmc_megarac_web_transport`, `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe`, `gunbc.machine_intake_mtcollins1_kvm_still` · 3 witness | no | -| `gunbc.recurring_failure_mode.a_derivation_key_that_does_not_pin_bytes_across_build_hosts` | 26 | 4 | — | — | no | -| `gunbc.recurring_failure_mode.a_pending_member_vanishes_from_a_terminal_refusal` | 28 | 3 | — | — | no | -| `gunbc.recurring_failure_mode.a_termination_contract_is_met_by_a_trimming_transport` | 51 | 5 | — | — | no | -| `gunbc.recurring_failure_mode.absent_reads_identically_to_never_looked` | 70 | 3 | — | — | no | -| `gunbc.recurring_failure_mode.ad_hoc_hardware_probe_produces_no_receipt` | 26 | 1 | — | — | no | -| `gunbc.recurring_failure_mode.an_unspawnable_program_aborts_the_run_instead_of_refusing` | 22 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.collector_armed_after_the_transition_it_observes` | 14 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.control_plane_acknowledgement_minted_as_effect` | 14 | 1 | — | — | no | -| `gunbc.recurring_failure_mode.else_less_if_statement_has_no_lowered_form` | 28 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.empirical_association_promoted_to_decode` | 14 | 1 | — | — | no | -| `gunbc.recurring_failure_mode.enforcement_gate_positioned_after_the_effect_it_guards` | 15 | 1 | — | — | no | -| `gunbc.recurring_failure_mode.enrolment_dead_band_has_no_representable_standing` | 17 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.gunbc_run_pays_a_large_fixed_pre_entry_cost_per_invocation` | 23 | 4 | — | — | no | -| `gunbc.recurring_failure_mode.identity_hashed_from_a_shared_mutable_path` | 44 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.rest_response_nested_from_key_ignored` | 32 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.subject_and_its_digest_as_independent_parameters` | 22 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.the_checker_admits_a_cast_the_evaluator_refuses` | 17 | 2 | — | — | no | -| `gunbc.recurring_failure_mode.unbound_interpolation_in_service_argv_accepted_until_runtime` | 22 | 1 | — | — | no | -| `gunbc.recurring_failure_mode.upstream_non_endorsement_consumed_as_physical_prohibition` | 17 | 3 | — | — | no | -| `gunbc.recurring_failure_mode.wet_witness_keyed_to_the_runner_not_its_subject` | 28 | 1 | — | — | no | -| `gunbc.roadmap_authority` | 5415 | 4 | — | `gunbc.floor_non_verdict_enrollment`, `gunbc.generated_artifact_emit`, `gunbc.roadmap_belt`, `gunbc.roadmap_belt_actuate`, `gunbc.roadmap_closing_contract_authoring`, `gunbc.roadmap_forecast` +11 more · 15 witness | **yes** | -| `gunbc.rung_drop.roster` | 240 | 9 | `mtcollins1_boot_accepts_socket1_absent`, `mtcollins1_boot_matrix_enrolment_dead_band_observed_only_drop`, `mtcollins1_boot_matrix_new_witness_eval_step_cost` | `gunbc.design_ledgers`, `gunbc.jurisdiction_drop_accounting`, `gunbc.ledger_row_coherence`, `v2.workflow.floor_subject_seed_test` · 5 witness | no | -| `gunbc.runner.runner_bound_attempt_receipt` | 184 | 4 | — | — | no | -| `gunbc.runner.runner_cgroup_placement_receipt` | 169 | 1 | — | — | no | -| `gunbc.runner.runner_checkout_receipt` | 109 | 1 | — | — | no | -| `gunbc.runner.runner_first_job_receipt` | 237 | 3 | — | — | no | -| `gunbc.runner.runner_guest_egress_attempt` | 209 | 1 | — | `gunbc.runner.runner_cgroup_placement_receipt`, `gunbc.runner.runner_cpu_bandwidth_receipt` | no | -| `gunbc.runner.runner_guest_network_receipt` | 186 | 1 | — | — | no | -| `gunbc.runner.runner_host_first_boot_receipt` | 127 | 5 | — | `gunbc.machine_intake_pre_os_capture_replay` · 2 witness | no | -| `gunbc.runner.runner_host_hardware_observation` | 216 | 9 | — | — · 1 witness | no | -| `gunbc.runner.runner_host_jailed_kvm_receipt` | 200 | 2 | — | — | no | -| `gunbc.runner.runner_host_kvm_probe_receipt` | 122 | 5 | — | — | no | -| `gunbc.runner.runner_host_media_independence_receipt` | 137 | 10 | — | — | no | -| `gunbc.runner.runner_isolation_admission` | 292 | 2 | — | — · 1 witness | no | -| `gunbc.runner.runner_live_census_receipt` | 185 | 1 | — | — | no | -| `gunbc.runner.runner_observed_version_check` | 129 | 3 | — | — | no | -| `gunbc.runner.runner_qualification_receipt` | 177 | 4 | — | — | no | -| `gunbc.runner.runner_signed_envelope_receipt` | 114 | 3 | — | — | no | -| `gunbc.runner.runner_two_attempt_receipt` | 180 | 3 | — | — | no | -| `gunbc.runner_attempt_launch` | 1044 | 1 | — | `gunbc.runner_microvm_lifecycle`, `gunbc.runner_microvm_lifecycle_realize`, `gunbc.runner_microvm_slot_controller`, `gunbc.test.claim.runner.runner_microvm_lifecycle_witness_test` · 2 witness | no | -| `gunbc.runner_browser_toolchain` | 1381 | 3 | — | `gunbc.machine_intake_mtcollins1_boot_run`, `gunbc.machine_intake_mtcollins1_kvm_observer_observe`, `gunbc.machine_intake_mtcollins1_kvm_still`, `gunbc.wet_host_premise_readback` · 8 witness | no | -| `gunbc.runner_host_grants` | 479 | 5 | `mtcollins1_unit_hold_store_host` | `gunbc.fleet_converge_plan`, `gunbc.generated_artifact_emit`, `gunbc.host_credential_custody_converge`, `gunbc.runner_host_file_converge`, `gunbc.runner_microvm_network_apply`, `gunbc.runner_microvm_network_observe` · 4 witness | no | -| `gunbc.runner_jit_mint` | 159 | 1 | — | `gunbc.runner.runner_jit_perform`, `gunbc.runner_attempt_launch` · 1 witness | no | -| `gunbc.runner_registration_labels` | 94 | 1 | — | `gunbc.fleet_runner_connectivity`, `gunbc.runner_browser_toolchain`, `gunbc.runner_connectivity_recovery`, `gunbc.runner_unit_file` · 3 witness | **yes** | -| `gunbc.runner_throughput_qualification` | 1027 | 2 | — | `gunbc.runner_throughput_qualification_route`, `gunbc.runner_throughput_selection`, `product.fabric.node_qualification` · 1 witness | **yes** | -| `gunbc.runner_throughput_qualification_route` | 383 | 27 | `operator_host_mtcollins1` | — · 1 witness | no | -| `gunbc.secret_provision` | 603 | 6 | — | `gunbc.auth.approval_decision_store`, `gunbc.auth.approval_device_redemption`, `gunbc.auth.approval_mac_key_provision`, `gunbc.auth.approval_ntfy_deployment`, `gunbc.auth.approval_request_submission`, `gunbc.auth.approval_store_receipt` +13 more · 8 witness | **yes** | -| `gunbc.spark.pair_serving_d0_door` | 355 | 1 | — | `gunbc.fleet_converge_workflow` · 3 witness | no | -| `tools.approval_store_live_probe` | 236 | 1 | — | — | no | -| `v2.compiler.reference_conservation_census` | 816 | 1 | — | `v2.test.claim.namespace_xl0.reference_conservation_census` | no | -| `v2.test.floor_enrolment_margin` | 717 | 4 | `mtcollins1_boot_matrix_enrolment_dead_band_observed_only` | — | no | -| `v2.workflow.floor_cost_debt_admission` | 155 | 12 | — | `v2.workflow.floor_enrolment_margin` | no | -| `v2.workflow.floor_enrolment_dead_band` | 97 | 15 | — | `gunbc.rung_drop.mtcollins1_boot_matrix_enrolment_dead_band_observed_only`, `v2.test.floor_enrolment_margin`, `v2.workflow.floor_enrolment_margin` | no | -| `v2.workflow.floor_enrolment_margin` | 649 | 1 | — | `v2.test.floor_enrolment_margin` · 1 witness | no | -| `v2.workflow.floor_eval_step_cost_drop` | 416 | 31 | — | `gunbc.rung_drop.app_attest_interpreted_crypto_new_witness_eval_step_cost`, `gunbc.rung_drop.app_attest_verifier_new_witness_eval_step_cost`, `gunbc.rung_drop.dag_emit_round_trip_new_witness_eval_step_cost`, `gunbc.rung_drop.fleet_build_job_membership_new_witness_eval_step_cost`, `gunbc.rung_drop.live_deploy_apply_render_new_witness_eval_step_cost`, `gunbc.rung_drop.live_deploy_dark_install_render_new_witness_eval_step_cost` +7 more · 1 witness | no | -| `v2.workflow.floor_route_gap` | 1999 | 24 | — | `gunbc.native_frontier_ratchet`, `gunbc.witness_v2_native_route`, `v2.test.claim.local_repo_wet_terminal_test`, `v2.workflow.floor_terminal_ledger`, `v2.workflow.floor_terminal_ledger_wire`, `v2.workflow.floor_terminal_ledger_wire_test` +1 more · 1 witness | **yes** | -| `v2.workflow.floor_unimported_bare_provider_debt_roster` | 2209 | 7 | — | `v2.test.claim.floor_unimported_bare_provider_debt`, `v2.workflow.floor_unimported_bare_provider_debt` | no | -| `v2.workflow.local_repo_wet_terminal` | 2562 | 55 | — | `gunbc.wet_host_premise_readback`, `v2.test.claim.local_repo_wet_terminal_test`, `v2.workflow.required_floor`, `v2.workflow.witness_admission` · 1 witness | no | - -### Witnesses (81 modules) - -Each witness moves with the subject it imports; none is in the required gate except `test.claim.action_use_admission_witness` (3 prose refs only). - -`test.claim.action_use_admission_witness`, `test.claim.approval_request_submission_witness_test`, `test.claim.authorization_pattern_selection_witness`, `test.claim.boot_artifact_delivery_witness_test`, `test.claim.build_fulfillment_witness`, `test.claim.build_selection_witness`, `test.claim.durable_exclusive_hold_witness`, `test.claim.fan_policy_standing_witness`, `test.claim.fleet.fleet_converge_checkout_pin_witness_test`, `test.claim.fleet.site_pxe_edge_converge_witness`, `test.claim.gcp_secret_access_witness_test`, `test.claim.heal_publisher_provision_witness`, `test.claim.host_memory_qualification_witness_test`, `test.claim.host_reset_return_witness_test`, `test.claim.install_media_remaster_grub_cmdline_witness`, `test.claim.linux_edac_topology_authority_witness`, `test.claim.machine_intake.ampere_dram_console_observation_witness_test`, `test.claim.machine_intake.ampere_socket_console_observation_witness_test`, `test.claim.machine_intake.host_capture_sol_crlf_witness_test`, `test.claim.machine_intake.host_capture_sol_nul_fill_witness_test`, `test.claim.machine_intake.host_resource_observation_witness`, `test.claim.machine_intake.kernel_module_decompression_observation_witness_test`, `test.claim.machine_intake.megarac_media_convergence_witness_test`, `test.claim.machine_intake.megarac_ui_features_witness_test`, `test.claim.machine_intake.mtcollins1_bmc_sensor_observation_witness_test`, `test.claim.machine_intake.mtcollins1_boot_acceptance_matrix_test`, `test.claim.machine_intake.mtcollins1_boot_authorization_witness_test`, `test.claim.machine_intake.mtcollins1_boot_diagnostic_bundle_witness_test`, `test.claim.machine_intake.mtcollins1_boot_phase_timing_witness_test`, `test.claim.machine_intake.mtcollins1_boot_run_witness_test`, `test.claim.machine_intake.mtcollins1_census_image_publish_witness_test`, `test.claim.machine_intake.mtcollins1_census_medium_readback_witness_test`, `test.claim.machine_intake.mtcollins1_census_member_readback_witness_test`, `test.claim.machine_intake.mtcollins1_census_qemu_host_observe_witness_test`, `test.claim.machine_intake.mtcollins1_census_refusal_hypotheses_witness_test`, `test.claim.machine_intake.mtcollins1_kvm_observer_observe_witness_test`, `test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness`, `test.claim.machine_intake.mtcollins1_kvm_still_witness_test`, `test.claim.machine_intake.mtcollins1_maintenance_hold_witness_test`, `test.claim.machine_intake.mtcollins1_physical_orientation_witness`, `test.claim.machine_intake.mtcollins1_sixteen_module_restore_witness_test`, `test.claim.machine_intake.mtcollins1_smpro_observation_witness_test`, `test.claim.machine_intake.mtcollins1_stale_power_off_witness_test`, `test.claim.machine_intake.mtcollins1_ui_bundle_observe_witness_test`, `test.claim.machine_intake.mtcollins1_unit_hold_forged_probe_witness_test`, `test.claim.machine_intake.oob_power_action_witness`, `test.claim.machine_intake.pre_os_bringup_witness_test`, `test.claim.machine_intake.pre_os_sol_crlf_witness_test`, `test.claim.machine_intake.sol_hold_stdin_wet_witness`, `test.claim.machine_intake_ethernet_witness`, `test.claim.machine_intake_mtcollins1_bmc_secure_standing_witness_test`, `test.claim.machine_intake_predictive_claim_witness_test`, `test.claim.megarac_spx_ui_surface_artifact_integrity_witness`, `test.claim.memory_change_evaluation_witness`, `test.claim.memory_multiple_findings_witness`, `test.claim.micron_dram_module`, `test.claim.modeled_filesystem_witness_test`, `test.claim.mt_collins_dimm_physical_identity_witness`, `test.claim.mt_collins_population_role_witness`, `test.claim.mtcollins1_candidate_attachment_witness_test`, `test.claim.mtcollins1_census_image`, `test.claim.mtcollins1_census_image_local_wet`, `test.claim.mtcollins1_connectx4lx_observation_witness_test`, `test.claim.mtcollins1_expansion_witness_test`, `test.claim.mtcollins1_memory_census_witness_test`, `test.claim.mtjade1_access_witness`, `test.claim.nbd_proxy_virtual_media_install`, `test.claim.pci_identity_observation_witness`, `test.claim.pre_os_capture_replay_witness`, `test.claim.pre_os_observer_witness`, `test.claim.pre_os_replay_witness`, `test.claim.product.cohort_observation_witness`, `test.claim.refinement_cast_wall_witness_test`, `test.claim.runner_isolation_admission_witness`, `test.claim.runner_throughput_qualification_witness`, `test.claim.site_uefi_arm64_pxe_edge_witness`, `test.claim.srv3_os_install_actuate`, `test.claim.unit_hold_store_provision_witness`, `test.claim.workflow_dispatch_input_witness`, `test.claim.xorriso_path_list_witness`, `test.probe.unit_hold_proof_forged_probe` diff --git a/docs/plans/micron-mta36ads2g72pz-identification.md b/docs/plans/micron-mta36ads2g72pz-identification.md deleted file mode 100644 index ee15c525fa7..00000000000 --- a/docs/plans/micron-mta36ads2g72pz-identification.md +++ /dev/null @@ -1,209 +0,0 @@ -# MTA36ADS2G72PZ-2G1A1: what Micron says it is, and what the Ampere documents say about it - -Survey date: 2026-09-12. Lane: clever-heron-234, work item `node://adhoc-677e2a53-ae7`. -Landed authority: `extdeps.memory.micron` (rows `mta36ads2g72pz_2g1a1_catalog`, -`mta36asf2g72pz_2g1a2_catalog`), `extdeps.vendor.micron`, and the third value of -`extdeps.memory.types` `DramDieStacking`. This document carries the reading; the rows carry the -facts. Nothing observed on Mt. Collins unit 1 is authored into `extdeps`. - -## Verdict, in the brief's order - -1. **MTA36ADS2G72PZ-2G1A1 is a 16 GB, 2-rank, x4, DDR4-2133 CL15, ECC, registered, - VERY-LOW-PROFILE (18.75 mm) RDIMM built from 18 DUAL-DIE packages of two 4 Gb dies each.** - Every word of that is Micron's, from two documents (part-numbering decode and the SPD image - Micron publishes for the part), and the two agree at byte grain. Status: obsolete. -2. **ADS versus ASF is `DS` = "VLP Dual-Die w/Temp Sensor" versus `SF` = "FBGA w/Temp - Sensor".** Micron's DDR4 Module Part Numbering System (Rev. 05-Dec-2018) defines the - module-options field as a two-letter code where the first letter is the PCB height class - (`S` standard, `D` VLP, `H` 2U, `L` 4U) and the second the DRAM package (`F` FBGA - single-die, `S` dual-die, `Q` quad-die, `E` octal-die). The letter difference IS the answer: - the ADS part is the VLP dual-die build of the same 36-die, 2 Gig x 72, RDIMM, -2G1 family. -3. **"2DRx4" reads as "2 ranks, dual-die, x4", and that is verified, not adopted.** The - operator's hypothesis was that a VLP module needs dual-die packages to reach 16 GB with x4 - devices in 18.75 mm. Micron's SPD byte 6 for the part is `0x91`: non-monolithic, 2 dies, - MULTI LOAD STACK, i.e. the JEDEC class a DDP belongs to. Byte 128 (`0x04`, nominal height - ≤ 19 mm) confirms the VLP outline. The catalogue's "Component Config: 2Gb x4" names the - 8 Gb dual-die package; "Number of Components: 36" counts dies, matching the `36` in the - part number, whose field the numbering sheet labels "Number of Die". -4. **Ampere publishes nothing that admits or refuses a dual-die (multi-load stack) RDIMM.** - Altra Rev A1 Datasheet Issue 1.55 §2.10 lists "RDIMMs, LRDIMMs, and Stacked (3DS) RDIMMs" - and "8 Gb and 16 Gb DRAM devices"; the words DDP, dual-die, multi-load and 4 Gb do not - appear in the document. Neither does the Mt. Collins GSG, the OCP Mt. Jade spec, or the - AVL say anything about package construction. The OCP Table 3 "Different DRAM DIES" = Yes - cells govern MIXING die types across modules; they do not say a given die construction is - supported, and this survey did not let them. Note also that the datasheet's device list - (8 Gb, 16 Gb) does not name the 4 Gb die that BOTH Micron quartets use, and the monolithic - quartet trained; so "unlisted device density" is a fact about both quartets and cannot be - what separates them. The corpus now models the DDP class as *not among the listed classes* - (`altra_admits_module_class` answers false for Registered × DualDiePackage) and the - controller module's own prose keeps that distinct from a withdrawal such as UDIMM's. -5. **Neither part is on the Ampere Altra Family AVL Issue 1.15 (December 2025).** Table 3's - Micron rows are MTA18ASF2G72PDZ (16 GB 2Rx8), MTA18ASF2G72PZ-2G9E1 (16 GB 1Rx4), - MTA36ASF4G72PZ (32 GB 2Rx4, three suffixes) and MTA36ASF8G72PZ-3G2E1 (64 GB 2Rx4) — all - `SF`, all 8 Gb/16 Gb dies, all 2666 or faster. There is NO 16 GB 2Rx4 row from any vendor - and no 2133 row at all, so the AVL is equally silent about the ADS part and the ASF part - that trained. An AVL omission is not a prohibition and this survey does not read it as one. - -**Firmware corroboration, received from hw-first-host after this survey was drafted (SOL capture -`artifacts/bmc/mtcollins1-sol-dram-training-2026-09-12.txt`, sha256 `e5044f71…8a4c`, on that -lane's branch).** The Altra DRAM-init firmware prints the ADS part as -`RDIMM[2c:80] 16GB 2133 ECC 2R x4 RCD[b3:80] 36ADS2G72PZ-2G1A1` — rank count 2, exactly as -Micron's byte 12 says, the `D` being a package letter and not a rank — and refuses the mixed -socket with `ERR: CHANNEL Mismatch Byte 6 SLOT0[00] EXP[91] @MCU[4]` followed by -`ERROR: Non-identical DIMM mixture NOT supported!`. Two things follow, both about the observing -layer and neither authored into `extdeps`: (a) the module's SPD byte 6 reads `0x91` on the -board, agreeing with Micron's published image; (b) what the firmware enforces on that socket is -**byte-6 agreement across the socket's channels**, i.e. a MIXING rule, so the failure with -4 × ADS + 12 monolithic is a mixed-population refusal and says nothing about a uniform ADS -population, which has not been run. The trained quartet's label reads `36ASF2G72PZ-2G1A2II`, -matching the catalogue SKU carried here plus process code `II`. The firmware's `RCD[b3:80]` / -`RCD[32:86]` per-module variation is the same variation Micron's record carries across process -codes (bytes 133–135), which is why register vendor is not a field of the catalogue row. - -**Which of the three differing axes is operative — read carefully, because three bytes differ -and only one is named.** The refusal quotes byte 6. Bytes 128 (height) and 130 (raw card) -differ between the two parts just as much and are not mentioned. The strongest honest reading: -the axis the firmware is known to compare is package type / die count / signal loading — the -byte `DualDiePackage` is grounded on — and VLP height and raw card are not shown to matter. -What the message does NOT establish is that they are unchecked: a comparator that stops at its -first mismatch would print byte 6 whether or not it also compares 128 or 130 later in its -order. So "VLP is a passenger" is the reading the evidence favours, at inference strength; -proving it needs a population that differs at 128/130 but agrees at 6 (an `SS`/`DF`-coded part -against a monolithic full-height one), which has not been run. - -**What Micron's data leaves as the only differences between the two quartets:** package -construction (dual-die multi-load stack vs monolithic), PCB outline (18.75 vs 31.25 mm) and, with -the outline, the JEDEC raw-card reference design (SPD byte 130 `0x08` vs `0x01`) and the -module-attribute byte 131 (`0x05` vs `0x09`). Everything else — vendor, generation, capacity, -speed bin, CAS latency, ECC, buffering, device width, package-rank count, die density, DRAM -addressing — is byte-identical between the two SPD images. Which of those the Mt. Collins -training outcome tracks is not decided by any document read here; the firmware line above -names byte 6 directly, and that receipt belongs with the lane that ran the crossovers. - -## Documents read - -| Document | Revision | Locator | Digest / standing | What it establishes | -|---|---|---|---|---| -| Micron part catalogue record, MTA36ADS2G72PZ-2G1A1 | database record fetched 2026-09-12 | `www.micron.com/products/obsolete/obsolete-vlp-rdimm/part-catalog/part-detail/mta36ads2g72pz-2g1a1` (renders `…/_jcr_content.products.json/getproductinfo/-/-/-/en_US/-/mta36ads2g72pz-2g1a1`) | HTTP 200, no credentials, `"data-source": "DATABASE"`, `"sub-category": "obsolete-vlp-rdimm"` | Details table and full DDR4 SPD image (12 process codes JG/HK/JJ/JI/HG/HJ/II/HI/IG/IJ/IK/JK; they differ only at register-vendor bytes 133–135 and the bytes-128..253 CRC). | -| Micron part catalogue record, MTA36ASF2G72PZ-2G1A2 | same | `www.micron.com/products/obsolete/obsolete-rdimm/part-catalog/part-detail/mta36asf2g72pz-2g1a2` | HTTP 200 | The control's details table and SPD (process codes KI/KJ/KK/KG/II/IG/IJ/IK). The bare `mta36asf2g72pz-2g1` record answers with no process codes. | -| Micron DDR4 Module Part Numbering System | Rev. 05-Dec-2018 | `siewert-kau.info/micron/wp-content/uploads/2020/05/Nomenklatur_Micron.pdf` (distributor copy; `micron.com/numbering` → `assets.micron.com` answers "Request Rejected" to this session) | sha256 `7d52c2ad…5c6a2` | Field decode: family, die count, voltage, module options (`DS` = VLP Dual-Die w/Temp Sensor, `SF` = FBGA w/Temp Sensor), configuration, module type (`P` = 288-pin RDIMM), package code (`Z`), speed grade (`-2G1` = DDR4-2133, PC4-2133, 15-15-15, component -093E), die and PCB revision. | -| Micron 288-Pin DDR4 RDIMM Core Product Description | Rev. B 12/2021, CCM005-341111752-10541 | `media.digikey.com/pdf/Data%20Sheets/Micron%20Technology%20Inc%20PDFs/DDR4_SDRAM_RDIMM_Core_RevB_Dec2021.pdf` (DigiKey copy of Micron's PDF; Micron's own host rejects) | sha256 `ea054b3d…8abc` | Figure 1 RDIMM 31.25 ± 0.15 mm; Figure 2 VLP RDIMM 18.75 ± 0.15 mm; Table 12 module ↔ component speed grades; C0–C2 pin text distinguishing "a traditional DDP package, which uses CS1_n, CKE1, and ODT1 to control the second die" from single-load 4H/8H stacks. It says the per-part organization lives in the MPN-specific addendum. | -| Micron per-part addendum for MTA36ADS2G72PZ | — | **Not found.** Probed `…/data-sheet/modules/{vlp_rdimm,rdimm,parity_rdimm}/ads36a2gx72pz.pdf` and variants (404); search engines index no addendum for the ADS family; the catalogue page's download resource answers 404. | — | The SPD image is the strongest first-party organization statement reachable; the addendum would add placement and thermal figures, not organization. | -| Ampere Altra Rev A1 Datasheet | Issue 1.55, 2025-04-08 | `extdeps.cpu.ampere` `ampere_altra_rev_a1_datasheet_authority` (v1.55 locator) | 90 pages, read as text | §2.10 feature list; no DDP / dual-die / multi-load / 4 Gb text anywhere in the document. | -| Ampere Altra Family AVL | Issue 1.15, December 2025, AMP 2025-0116 | `extdeps.cpu.ampere` `ampere_altra_avl_authority` | sha256 `6199ae84…8d0d` (matches swift-bat-341's read) | Table 3 Micron rows listed above; no 16 GB 2Rx4 row; no 2133 row; §1.3 test suite is homogeneous 1DPC/2DPC. | -| OCP Mt. Jade Motherboard Spec | Rev 1.0 | `extdeps.ocp.mt_jade.memory_mixing` | already modelled | Table 3 is a MIXING matrix; "Different DRAM DIES" = Yes is not a construction-support statement. | -| swift-bat-341 survey | 2026-09-11 | [altra-cross-channel-rank-mixing-survey.md](altra-cross-channel-rank-mixing-survey.md) | — | Starting point for the AVL and the platform-guide reads; nothing there is contradicted here. | - -### Reading the two SPD images side by side - -| Byte | Meaning (JESD21-C Annex L) | ADS-2G1A1 | ASF-2G1A2 | -|---|---|---|---| -| 2 | DRAM device type | `0C` DDR4 | `0C` | -| 3 | Module type | `01` RDIMM | `01` | -| 4 | Density and banks | `84` 4 Gb/die, 4 BG × 4 banks | `84` | -| 5 | Addressing | `21` 16 rows, 10 columns | `21` | -| **6** | **SDRAM package type** | **`91` non-monolithic, 2 dies, multi-load stack** | **`00` monolithic** | -| 12 | Module organization | `08` 2 package ranks, x4 | `08` | -| 13 | Bus width | `0B` 64 + 8 ECC | `0B` | -| 18 / 125 | tCKmin / fine offset | `08` / `C2` → 938 ps (2133) | same | -| 24 | tAAmin | `6C` 13.5 ns → CL15 | same | -| **128** | **Nominal height** | **`04` ≤ 19 mm** | **`11` ≤ 32 mm** | -| 129 | Max thickness | `11` | `11` | -| **130** | **Raw card** | **`08`** | **`01`** | -| **131** | **RDIMM attributes** | **`05`** | **`09`** | -| 329–349 | Part number | `36ADS2G72PZ-2G1A1` | `36ASF2G72PZ-2G1A2` | - -Raw card letters and bytes 131/133–135 are recorded as bytes, not decoded: the JEDEC raw-card -letter table and the JEP106 register-vendor table are not authorities the corpus carries yet, -and reading them from memory would be exactly the uncited transcription §4d forbids. A JEDEC -SPD authority module (`extdeps.memory.jedec` currently carries only the generation ceilings) is -the named follow-up that would make the decode above typed rather than prose. - -## The OEM SEL family (second half) - -Payloads observed on Mt. Collins unit 1, all manufacturer `3a cd 00`, record type `C0`, raw -bytes preserved exactly as the brief carries them: - -``` -0fde7033ff10 0fde703bff10 2026-08-19, same timestamp, adjacent record IDs -0fde70330102 (x19) 0fde70330502 (x2) -0fde70331102 0fde703b1102 -``` - -### What is public - -The only public serializer of a `3a cd 00` / `C0` OEM SEL record with this byte shape is -Ampere's OpenBMC layer, `meta-ampere/meta-common/recipes-ampere/host/ac01-boot-progress/dimm_train_fail_log.sh` -in `github.com/ampere-openbmc/openbmc` (read at `f68588807957ffbc07371634ed41e534e7b416c2`, -2026-01-22; the script has three commits in its history and was created 2023-04-15 already -carrying the constants below — there is no earlier revision with a different sensor number). -It emits, via `IpmiSelAddOem` with OEM id `0x3a 0xcd 0x00` and SEL type bytes -`0x00 0x00 0x00 0xC0`: - -``` -byte 0 SENSOR_TYPE_SYSTEM_FW_PROGRESS = 0x0F -byte 1 SENSOR_BOOT_PROGRESS = 235 = 0xEB -byte 2 EVENT_DIR_ASSERTION | OEM_SENSOR_SPECIFIC = 0x00 | 0x70 -byte 3 (channel << 4) | (socket << 3) | BOOT_SYNDROME_DATA(=4) -byte 4 syndrome bits 15:8 (the SoC-BMC "Boot Stage Error Syndrome" byte 1) -byte 5 syndrome bits 7:0 (byte 0: [1:0] failure type, [4:2] physical rank, [7:5] syndrome 0) -``` - -The syndrome bytes it copies are defined by the Ampere Altra Family SoC-BMC Interface -Specification, Issue 1.43 (2023-05-30), Table 16 registers `0xB4` (slot select) and `0xB5` -(Boot Stage Error Syndrome): failure type 1 = PHY training, 2 = DIMM training; syndrome 0 for -PHY = setup / write leveling / read gate leveling / read leveling / software training; -syndrome 0 for DIMM = VREFDQ / LRDIMM DB / LRDIMM DB software; byte 1 for write leveling = -slice number and per-nibble "no rising edge" bits. The specification is a `connect-admin` -download that answered HTTP 200 without credentials (sha256 `34ccdcd2…`, 56 pages). The -companion "Altra Family Monitoring Events" v0.60 (2023-06-05, sha256 `23d84c2c…`) is a -Redfish message registry and defines no SEL byte layout. - -### What is not public, and therefore not decoded - -- **Sensor `0xde` (222) is not in any public Ampere source.** `ampere-openbmc/openbmc`, - `ampere-ipmi-oem`, and the Mt. Jade / Mt. Mitchell IPMI sensor YAMLs were searched; 222 and - 235 appear there only as ordinary temperature/current sensor numbers unrelated to boot - progress. Mt. Collins runs AMI MegaRAC (`extdeps.ampere.mt_collins_product_brief.bmc`), not - this OpenBMC layer, and AMI's Ampere port is not published. So the `0xde` serializer is - **not found**. -- **Byte 3's low nibble is `3` where the public schema writes `4`**, and the sensor number - differs, so the OpenBMC layout is a *related* schema and no byte of ours may be read through - it as a decode. Under that layout byte 3 = `0x33` would be channel 3, socket 0; `0x3b` - would be channel 3, socket 1 — which is consistent with the operator's inference that bit 3 - moved with the socket holding the intervened population across the two crossovers, and is - still an inference: the companions moved with it. -- **The tail is ambiguous even under the cousin schema**, and the ambiguity is why it is not - promoted. Reading `11 02` as `[syndrome hi][syndrome lo]` gives failure type 2 (DIMM - training), rank 0, syndrome0 = 0 with byte 1 = `0x11`; reading it as `[lo][hi]` gives failure - type 1 (PHY training), rank 4, syndrome0 = 0, byte 1 = `0x02` (write-leveling slice 2). The - two readings name different training steps and different ranks. Only a decode of register - `0xB5` on this platform, or the AMI serializer, settles it. The `ff 10` pair from 2026-08-19 - reads as failure type 3 (Reserved) under one ordering and failure type 0 (N/A — no failure) - under the other, neither of which is a syndrome a training-failure record should carry, which - is further evidence that the `0xde` family is not simply the `0xeb` family under another - sensor number. - -So the payloads stay raw. `gunbc.host_memory_qualification` `TrainingEvidenceStanding` already -has the right arms for what a receipt may claim: `AssociatedByIdenticalSignature` at best -(signature = the raw six bytes, source = the meta-ampere script as the related schema), never -`DecodedFromPlatformSyndrome`, until `0xB5` is read on the unit. - -## What this lane did not land, and why - -- **No `HostMemoryStanding` or `CausalExperimentReceipt` rows for the eight Mt. Collins - configurations.** The brief summarises them (durations, wattage, temperature, payloads) but - carries no instants, no DIMM masks, no slot locators; authoring receipts from a summary would - fabricate `observed_at` values and put prose in the mask field — the exact defect that module's - own notes record. The rows belong to the lane that holds the raw BMC log; they now have a - catalog row to bind to (`ObservedDimmCataloged { catalog: mta36ads2g72pz_2g1a1_catalog }` - reconciles, exercised by `test.claim.micron_dram_module`). -- **No typed SEL/OEM-record carrier.** One night's payloads under an unpublished schema are - not an upstream authority. A carrier is warranted when either the AMI serializer or a `0xB5` - read exists to ground it. -- **No `form_factor`/height field on `DramModuleCatalogRow`.** The VLP fact is carried in the - row's annotation and here; the field lands with its first consumer (a chassis DIMM-clearance - check), per §3c. -- **The Micron-hosted locators.** Both Micron PDFs are cited through distributor copies with - digests because `assets.micron.com` rejects this session's fetches; re-cite to `micron.com` - when reachable. The catalogue records ARE Micron-hosted. diff --git a/docs/plans/microvm-and-floor-wind-down-state.md b/docs/plans/microvm-and-floor-wind-down-state.md deleted file mode 100644 index 34dcc0dc7c1..00000000000 --- a/docs/plans/microvm-and-floor-wind-down-state.md +++ /dev/null @@ -1,310 +0,0 @@ -# microVM and floor: wind-down state, 2026-09-21 - -Written on operator instruction to wind down and record remaining items, so that v1 -performance and v2 migration can be prioritised. **This is a state record, not a plan**: it -says what is true on `origin/main`, what is owed, and what is blocked, so that resuming any -lane does not begin by re-deriving it. - -Every claim below was verified against `origin/main` at `7a145ef9ca` or read from the lane -that produced it. Where a claim is **contested**, it is marked, and neither version is -asserted — DESIGN §4d: a bet is typed as a bet, and the reader who consumes it as a fact is -the defect. - -**Consumer, and a declared frontier (DESIGN §3c).** This page is cited by path from the two -re-entry briefs that govern the parked programs, which is where a resuming session reads it. -It is **not** linked from `DESIGN.md`, and it deliberately does not add a -`gunbc.design_document` row to become so: it is a dated state record rather than a doctrine -a DESIGN section governs, and editing that authority is not a wind-down act. It therefore -joins the orphan population already rostered as `gunbc.guarantee_stall` -`doc_graph_orphan_population_stall` (`current: OutsideTheLadder`), and this paragraph is the -admission rather than a silent addition. **Trigger for the frontier:** whichever funded lane -resumes a program named here either links this page from the section that governs its work, -or supersedes it and deletes it. A wind-down record that outlives the wind-down is debt. - -**On the numbers below.** Where a figure is load-bearing it is stated with the producer that -re-derives it (DESIGN §6: name the instrument, never transcribe its output). Where a figure -is a lane's reported reading rather than a re-derivable instrument, it is attributed to that -lane and marked as such — those are receipts of a past run, not facts a reader may refresh, -and they are written so that staleness is visible rather than plausible. - ---- - -## 1. The floor reports SUCCESS over its own refusal — the highest-value open item - -`.github/workflows/witnesses.yml:67`: - -``` -if [ "$GUNBC_FLOOR_CLASS" = structural ] && [ "$GUNBC_FLOOR_EXIT" -eq 0 ]; then GUNBC_FLOOR_CLASS='none' -``` - -`claim_executor` **exits 0 over its own typed refusal**, so the class is downgraded to `none` -and the job concludes success. A job can therefore report SUCCESS over a run that executed -**zero witnesses**, with its own log saying `phases_failed=1`. - -**This is not merely a hidden defect — it admitted a new one.** #11731 landed a file that -could not parse *because* the floor was refusing and reporting success. The hollow gate is -upstream of the breakage, not downstream of it (§5: a failure arm must refuse, never widen; -the downgrade is an absorbing fallback executed in YAML). - -**Current standing.** The parse class is **closed**: the indented-annotation census is 0 -corpus-wide and the floor selects and executes its full planned set, with `not_attempted=0` -and `claims_failed=0`, where previously **zero** witnesses were selected. Closed by #11896, -#11897, #11880, #11920, #11842. **Re-derive rather than trust this sentence:** the producer -is the required floor itself — `claim_executor --required-ci --source-root dag ---source-root src/v2 --required-lane witnesses` — whose run prints its own -`planned/executed/not_attempted/terminal/passed` summary line. The counts move with every -landed witness, so a number copied here would rot within a day. - -**CONTESTED — do not propagate either reading.** Whether the downgrade swallows failures -*per phase* is disputed by two lanes: - -| reading | source | consequence if true | -|---|---|---| -| per-phase: parse reds the job honestly, declarations passes | `nimble-swift-273` | once parse cleared, a **declarations defect is invisible** | -| no such ordering | four run observations | the discriminator is unknown | - -The four observations: main `0de0b6137c7` 13 parse FAILs → **SUCCESS**; #11803 `70faed05501` -340 → **SUCCESS**; #11842 343 → **FAILURE**; #11884 317 → **FAILURE**. No ordering by count, -and parse failures did **not** always red the job. `eager-swift-412` independently reports -the discriminator as **cannot-tell from the logs**. - -**The discriminating test nobody has run**, and it is cheap: read `$GUNBC_FLOOR_LOG` — a -**file** the floor writes, *not* the job log, where every `class=` line is echoed script — or -the classifier itself, on **one PASS and one FAIL of the same phase shape**. Counting -signature strings in the GHA job log is **not** discriminating: a fired signature sets an env -var rather than printing, so identical counts appear whether the hypothesis holds or not. - -**#11836 and #11829 are competing repairs. Only one should land.** Neither is owned. - -## 2. microVM: the part that decides is built; the part that acts is not - -`gunbc.runner_microvm_lifecycle_realize` **landed** (#11803) and has executed against real -processes on srv1: the lane reported every claim group green at its landing head, including -the wet receipt under `--wet` and the argv-binding wall. That is a **lane receipt of a past -run**, not a standing fact — re-derive it with `claim_batch` over the module's witness -modules at the current head before relying on it. #11677 closed both launch frontiers, so -the JIT credential mint and jail staging are realized. - -**Five declared frontiers, all live in that one landed module.** Read them from the file, -not from here. - -| # | frontier | status | what is missing | -|---|---|---|---| -| 1 | `controller_main_pid_consumer_frontier` | **dispatchable, start here** | **nothing calls `run_controller`.** Until a slot unit execs it as MainPID the module is a library, not a controller. Every other frontier is worth less until this clears. | -| 2 | ~~`attempt_cleanup_realization_frontier`~~ | dissolved | `gunbc.runner_microvm_lifecycle_realize clean_attempt_resources` runs between the stop and the readbacks, only after a quiescent stop: it detaches every mount at or under the jail (deepest first, read from `/proc/self/mountinfo`), deletes the credential device, and removes the jail and attempt directory with `rm -rf --one-file-system`. It then runs the slot's own `runner_slot_teardown_flush_commands`. The existing absence readbacks and network lists stay the verdict, so an incomplete cleanup quarantines. | -| 3 | ~~`guest_bring_up_channel_frontier`~~ | dissolved (channel); live half re-declared | The controller binds the console at launch: the unit's Environment must name this attempt's invocation_id, and the unit's systemd InvocationID is captured then. After the terminal trigger it reads only that invocation's journal entries (`journalctl -u … _SYSTEMD_INVOCATION_ID=…`) and advances GuestBooted / RunnerListening through `advance_bring_up`. The receipt carries the phase. Reading it live, to decide SERVING while the attempt runs, is `slot_serving_live_readiness_frontier`; its consumer is the deferred warm pool. | -| 4 | `slot_network_readings_producer_frontier` | **BLOCKED — security** | no converged slot network on any host; every reading is `Unreadable`, which quarantines. | -| 5 | `workflow_effect_sequencing_frontier` | **not a lane — a design question** | `std` carries **no** effect-sequencing authority. Bigger than microVM: every effect sequence in the corpus rests on the answer. | - -**The consequence that governs planning:** the module states it itself — *"no cell settles -CellReady through this module today."* **A first full run quarantines by construction, and -that is not a bug in the readbacks.** A warm pool is impossible until (4) clears. - -> Do not let anyone "fix" (4) by omitting `SlotNetworkQuiescent` from the required facts. -> That is the empty-observation narrow `product.fabric.sanitation` exists to refuse. - -### Why (4) is blocked, stated so the reason survives - -**The converge principal IS the job principal.** Granting it install+restart over files it -also *stages* is granting root: `install /etc/systemd/system/…` -followed by `systemctl restart` is root-equivalence, however narrowly spelled. So -`microvm_network_granted_operations` deliberately carries **readbacks only**. - -To unblock, an administrator principal must (1) own the staged source so the job user cannot -write what root will install and execute; (2) install the nft loader unit and ruleset and -`daemon-reload` + restart it; (3) set `ip_forward` and create the bridge; (4) create and -destroy per-attempt taps — and be **unimpersonable by the job user**. That last is the whole -point, and is why the frontier trigger names a *capability* rather than an artifact (§4b(3)). - -#11751 (`microvm_network_observe`) landed. It **installs nothing**, reads every fact with its -own UNREAD arm, and mints `ConvergedNetwork` only when every fact was actually read. On every -host today it will **refuse**, because nothing has installed a tap or the table — that is -honest, not a failure. - -**It has now been run, and it refused exactly there.** fleet-converge run 35646413870, mode -`microvm_network_observe`, host srv1, 2026-09-21: the whole receipt is -`microvm-network-observe REFUSED: slot srv1-01: tap disable_ipv6 could not be read`. srv1 -carries no `gunbc-tap1`. That is the first end-to-end reading of the mode against a real host -and it establishes the mode works and the host is unconverged; it is **not** a -`ConvergedSlotNetwork` (that row stood until the converged run below). - -**The administrator principal has landed: `gunbc.runner_microvm_network_apply`.** It is the -apply half of (4), a fleet-converge mode of its own (`microvm_network_apply`, -`FleetSshKeyConsumed`), executing as `gunbc.fleet_bootstrap_principal` -`executor_bootstrap_principal` over the fleet SSH edge — the same edge -`gunbc.runner_host_file_converge` already uses for root-evaluated files. It refuses when that -administrator IS the job user on the host; it refuses unless the dispatch names the revision -the tree is checked out at, because the bytes it installs are rendered from that revision; it -verifies **every ancestor** of the staging root root-only far-side before staging a byte; it -writes each staged file `root:root 0600` through `converge_typed_remote_file` (stage, mode, -atomic rename, far-side content comparison); it verifies the staged files' own ownership; -then it runs the modeled install operations in order, stopping at the first failure, and -reads the ruleset back. The staging root **moved out of the job user's own tree** in the same -change — it was under `gunbc_fleet_converge_state_dir`, which `runner_host_grants` creates -with `owner=job`, which is precisely what the frontier says does not satisfy it. The job-user -grant roster is **unchanged**: the installs run under a different principal, and re-admitting -them to the job user's sudoers because someone else performs them would grant exactly the -pair the frontier refuses. - -**(4) has executed, and srv1's slot network is converged.** fleet-converge run 36043010087 -(`microvm_network_apply`, srv1, 2026-09-24) staged, installed and read the ruleset back under -the administrator principal (listing digest `da7a00c5…9099`); run 36045264008 -(`microvm_network_observe`, srv1) then minted a `ConvergedSlotNetwork` at generation -36045264008 carrying the same digest, fourteen taps with v6 disabled, and zero helpers. -`microvm_network_host_mutation_principal_frontier`, `host_ruleset_installer_frontier` and -`converged_slot_network_producer_frontier` were deleted on those receipts. - -**And the premise of this whole section is false on srv1 today, which is the finding that -lane produced.** `/etc/sudoers.d/gunbc-deploy` — GENERATED by `gunbc.ci_deploy_sudoers` from a -roster of bare binary paths — grants `ghrunner ALL=(ALL) NOPASSWD:` over `/usr/bin/install`, -`/usr/bin/systemctl`, `/usr/bin/rm`, `/usr/sbin/visudo`, `/usr/sbin/usermod`, -`/usr/sbin/useradd`, `/usr/bin/apt-get` and `/usr/bin/tailscale`, path-unrestricted (verified -live, 2026-09-21). That is the install+systemctl pair the frontier refuses to grant, already -held by the job user, unbounded. The job user is root-equivalent on srv1 **today**, and "CI -jobs cannot become root" was an unrecorded false belief. Rostered as `gunbc.rung_drop` -`the_job_user_holds_a_path_unrestricted_root_grant` and classed as -`gunbc.recurring_failure_mode` -`the_grant_a_frontier_refuses_is_already_held_from_another_authority`. It does not weaken the -frontier — it means satisfying it is **necessary and not sufficient**. **Closing those grants -is a separate, operator-gated item**, and it is what gates untrusted-PR qualification of the -microVM host: legacy jobs on the host consume them today, so each needs a consumer census -before it can be removed, and no lane may narrow them silently. - -### The floor's own job does not fit a cell - -**Update 2026-09-23 — the stall retired.** `runner_microvm_floor_fit_stall` was deleted when its -trigger held: `gunbc.floor_demand`'s standing moved to `gunbc_floor_memory_stat_receipt_2026_09_23` -(a run carrying gunbc#12081 and gunbc#12088), and `gunbc_runner_microvm_shape` now resolves on -production inputs at the unchanged 26 GiB cell row. The executed evidence is -`test.claim.runner_microvm_witness_test` -`the_production_shape_resolves_on_the_memory_remainder_since_the_2026_09_23_receipt` and the -either-state witness beside it. The paragraphs below are the state as it stood before that and are -kept as history; the demand is a single warm run's sampled maximum, so a later receipt can move it -back over the line, and the either-state witness is what would say so. - -Before 2026-09-23: `runner_microvm_floor_fit_stall` remained rostered: the floor's measured held-set -peak exceeded `gunbc.runner_slot_desired` `gunbc_runner_slot_memory_max_bytes` less the -realization reserve. Both sides are derivable rather than transcribed — the cap is that declaration, and -the demand is produced by `gunbc.floor_memory_demand`, whose qualification against readable -limits is what the stall cites. The stall row itself carries the figures it was written -against; read them there, where they sit beside the reading that produced them. - -**Grounding it is an operator decision with a fleet cost**, not an engineering task: raise -per-slot `MemoryMax` to ~28 GiB, which **lowers each host's memory-admitted width**, or -reduce floor demand. The stall explicitly rules out deleting the allowance, reserve or -receipt, and rules out using a fixture cell. - -### The milestone that would settle the cutover question - -**One cell settling `CellReady` once, on one host, after one real job.** Nothing before that -is evidence. If that cannot be reached, the remaining work is unbounded. - -## 3. Dynamic per-VM memory — two compiling, unreconciled implementations - -Design ruled and approved by the side chat; **build parked**. Six items; item 1 is built -**twice, independently, blind to each other**. - -**Neither branch is uncompiled.** The uncompiled attempts are the two *closed* PRs, #11849 -and #11850. - -| branch | sha | evidence | carries uniquely | -|---|---|---|---| -| `session/bright-ram-63` | `125659f70df` | lane-reported: all claims green under `claim_batch`, plus five mutation controls, tree restored green after each | the **executed** authority-token wall | -| `session/royal-moth-544` | `80ec5065822` | lane-reported: all claims green under `claim_batch`, two mutation controls executed | `std.measure round_up_to_grain` (resolves the unowned rounder fork), ceiling-as-parameter, permit-in-provenance | - -The same three file paths exist on both with different contents, so **merging them is a -textual conflict on every file, not a union.** Pick one base and port the other's evidence. - -**Both PRs (#11883, #11885) were found NON-DRAFT on 2026-09-21, although both lanes believed -they had left them draft** — so a parked program was consuming CI and reviewer attention on -every push. Both were converted back to draft at wind-down. Keep them draft while parked: -ready buys coverage that was never approved. *The lesson is the gap itself* — "I left it -draft" is a belief about a past action, and the dashboard opens PRs on a lane's behalf, so -it must be re-read rather than remembered. - -**The defect, stated correctly** (it is a bypass, not an absence). All three verified against -`origin/main`: - -- `CellReserved` holds exactly four fields — offer, slot_key, generation, account. **No - requirement, no grant, no envelope.** -- `admit_executor_capacity` takes a whole `ExecutionRequirements` and reads exactly **one** - field, `requirements.capacity_class`. **Cell admission does not consider memory.** -- `unmet_memory_axis` returns `[]` when `needed.envelope.memory` is `Absent`, so **unstated - memory is admitted** by the supply screen rather than refused. - -So memory *is* screened before reservation and the runner *does* read the requirement; what -is missing is that memory is never converted into a **grant**, never **atomically reserved** -against live host use, and never **carried on `CellReservation`**. - -`MemoryGrant` and `HostMemoryLedger` return **zero matches anywhere on main**, so items 3-6 -have no landed symbols to integrate against. The coordination hazard for item 4 (#11625, -static-vs-dynamic `MemoryHigh`/`MemorySwapMax`) is still **open**, but its head has moved to -`8e75da2aa48` — re-read it before item 4 rather than trusting any recorded sha. - -**Topology precondition, a gate rather than an item:** exact per-Work sizing is valid only -where Work is bound **before** the VM boots. A generic prebooted GitHub runner does not know -its eventual job. Do not let a guest discover its Work after boot and call that dynamic -allocation. - -**Amendment worth not re-litigating:** a per-host **CAS-linearized** ledger is required. -Per-cell records cannot conserve a host-wide sum — A and B each read 60 of 100 free, each -reserve 30, both succeed, 120 committed. One CAS commits cell occupancy and memory claim -together. - -## 4. Owed evidence - -**#11845's discriminating RED was never executed.** The repair is real and approved, and it -lands on a green that is hollow for the two reasons in §1. Branch -`verify/11845-red-mutation` (`2499194f4b2`) is pushed and is exactly one hunk off the merge -head: the `test -e` defect put back inside the new classifier. **Both of its blockers have -since cleared**, so the pair is now runnable — expect green at the merge head, red at -`2499194f4b2` on `an_unread_path_reaches_unobservable_and_never_absent`. **If the mutation -passes, the control does not discriminate and the repair needs a better one.** Delete that -branch afterwards; `verify/11845-red-control` is plain main and can go now. - -**Two unowned repairs**, both one-liners, both real: - -- `v2.lens.reference_derived_residency_reading` has zero occurrences of - `construction_justification`, while its siblings import it from `v2.lens.common`. -- `dag/test/claim/machine_intake/jade_first_contact_model_witness_test.dag:3` imports `Nat` - from `std.types`; `Nat` is declared in `dag/std/nat.dag`. - -**`required_gate_prefixes` has no `dag/test/claim/runner/` row**, so that file is never -resolved by CI unless a diff touches it — a symbol deletion can strand it silently, as -#11762 did. Likewise no row matches `test.claim.fabric.`, so the dynamic-memory witnesses are -discovered and declined. - -## 5. The P0 that opened this session is contained but not closed - -srv2 still carries `ExitType=cgroup` in -`/etc/systemd/system/actions-runner@.service.d/70-fleet-teardown.conf` — the configuration -that left PID 56295 orphaned holding 16 GB for 2.7 days. srv1 and srv3 are converged to -`ExitType=main`. - -**Residual risk, precisely.** srv2-01..05 are **masked**, so the P0 cannot recur on them -today. But the drop-in lives in the **template** directory and the template is *not* masked, -so any **new** instance (`actions-runner@srv2-06`) inherits `ExitType=cgroup`. The remedy is -to run the modelled fleet converge on srv2; it has simply never run there. - -srv4 is off the reach chain (container → srv1 → srv2-lan → srv3) and `gunbc.fleet_reach` -carries no row for it. - -## 6. Method findings worth keeping - -- **A suite of controls that invoke a pure decision with supplied values is blind to - everything after the decision** — the cleanup leg, the receipt the withheld arm writes — - and stays blind however many are added. Two post-sign-off findings on #11902 both lived in - that one gap. **38/38 is not route coverage.** -- **A repair of a fail-open is a prime site for the same class one layer down.** #11902's - own (d) repair discarded the located cause on its withheld arm and wrote a constant. Grep - every declaration your repair *adds* for a call site before pushing, and re-read every new - refusal arm for whether it names which fact failed. -- **At any power-of-two grain, a checked-add rounder and a subtraction rounder refuse the - same set**, because 2^63 is a multiple of the grain. A gibibyte-grain mutation control - stayed green under the mutation. Use a non-power-of-two grain (1000). Two authors asserted - the opposite from reasoning alone; both were wrong. -- **A refusal-arm claim tests that the arm fires, not that the right inputs reach it.** That - is how two lanes and a reviewer converged on bounding by the maximum instead of by - representability. -- **`claim_batch` exits 0 over its own refusal.** A `*Refused` line is a **non-answer** — - neither pass nor fail — and exit status must never be read as the verdict. diff --git a/docs/plans/moa-memory-modeling.md b/docs/plans/moa-memory-modeling.md deleted file mode 100644 index ad9df46b125..00000000000 --- a/docs/plans/moa-memory-modeling.md +++ /dev/null @@ -1,158 +0,0 @@ -# Mechanism-of-allocation memory modeling (owner directive 2026-09-25) - -Amends the serving proof doctrine. Reading: **MoA = mechanism of -action/allocation** — which architectural feature allocates each byte, how that -quantity scales, which ranks hold it, during which phase it exists. The MoA -decomposition is the authority. Measurements serve only three purposes: - -1. validate a derived bound; -2. fill a narrowly named term whose implementation exposes no derivation; -3. falsify the model when observation falls outside its claimed interval. - -## The correction that prompted this - -`floor(1,805,689 / 262,144) = 6` was on its way to `FitProved`. Withdrawn: - -- The engine's printed "GPU KV cache size: N tokens" is a - **full-context-equivalent projection**, not a fungible token inventory. - `extdeps.vllm.kv_layout` already says so: hybrid layouts have - length-dependent groups (full attention scales, windowed groups cap) plus - fixed-per-sequence groups (KDA/Mamba state), and `cold_admission_blocks` - refuses to linearly rescale a full-context figure. -- Six seats is therefore a **CacheCapacityCandidate**, not a proof. The cache - subclaim proves only by direct group fold: - `6 × cold_admission_blocks(exact_layout, 262144) ≤ allocator_blocks_on_limiting_rank` - and independently - `6 × cold_admission_bytes(exact_layout, 262144) ≤ admitted_kv_arena_bytes on EVERY rank`. -- The result vocabulary splits: `CacheFitProved / CacheFitRefused / - CacheFitUnestablished`, and the whole arm stays `FitUnestablished` while any - other subproof (prefill transient, decode transient, load transient, static - residency, supply reserve) is open. - -## GLM-5.3-Flash's four scaling laws (the MoA) - -45-layer hybrid decoder: 34 KDA linear-attention layers + 11 sparse-attention -layers (interleaved ~3:1); 64 KDA heads × dim 128; index_kpool = 4; 512-dim -MLA latent; sparse MoE (~320B total, ~18B active — active-count is a COMPUTE -fact, not a residency fact; all experts are resident unless expert offload or -EP placement says otherwise). - -1. **Weights: static, placement-dependent.** Derive from checkpoint manifest × - TP/EP/PP placement × loader transformation rules. The observed 77.0–81.6 - GiB/rank spread says placement is not an even quarter — build the exact - rank-placement projection; do not enshrine measured constants. -2. **KDA state: ∝ sequences, NOT ∝ length × sequences.** Mamba-style recurrent - state per layer per sequence per rank (illustratively 16 local heads × - 128×128 × 4B = 1 MiB → ~34 MiB over 34 layers, before conv state and - speculative additions). Six seats ≈ hundreds of MiB, not a linear rescale. -3. **Sparse MLA + KPool index: token-dependent.** 11 sparse layers: MLA latent - cache (∝ tokens), KPool compressed index (∝ tokens/4), small per-request - tail state (fixed). Persistent cache law: - `seats × (KdaState + TailState) + seats × SparseMla(length) + seats × KPool(⌈length/4⌉) + allocatorGroupingAndPadding`. -4. **Prefill/decode workspaces: phase-specific transients.** Derivable, not - discoverable: e.g. the upstream sparse-indexer prefill buffer carries 40 - entries per model token × 132 bytes (128-dim FP8 vector + scale) → at - 262,144 tokens ≈ 1.289 GiB per applicable rank — check against the exact - deployed revision. **Read the allocator formula; never learn it from OOMs.** - -## Admissibility of observations - -- A resident-demand component can never be negative. TP1's `−2.54 GiB` graph - figure is preserved raw but admitted as `GraphAttributionUnestablished` - (lower bound 0, upper open) — the profiler interval includes a release. - Never clamped silently, never a credit. A negative observation improving a - verdict violates weaker-evidence-cannot-improve-admission. -- `weights + non-torch` is a COMPOSITE profile term (rank-specific - model/runtime residency at that experiment phase), NOT a checkpoint- - placement receipt. It does not separate checkpoint tensors from - runtime-fixed, allocator, communication, or other non-Torch allocations. -- Measurement standings: `DerivedExact / DerivedConservativeInterval / - MeasuredUpperBound / ObservationOnly / ModelFalsified`. A point sample never - becomes MeasuredUpperBound merely by being the largest seen once. - -## The unified proof subject - -Every term binds to one exact qualified subject (qualified-subject-identity.md): - -```text -MemoryProofSubject { - model: model:zai/GLM-5.3-Flash@checkpoint-digest - runtime: runtime:vllm@source-sha+image-digest - topology: topology:fleet/group-b/tp4@host-census-revision - parallelism: parallelism:tp4/pp1/ep-policy/dcp-policy - allocator: allocator:vllm/hybrid-kv@configuration-digest - workload: workload:glm-serving/262144x6@intent-revision - generation: generation:group-b/ -} -``` - -A 64K×1 receipt discharges only terms invariant across the move to 262K×6. - -Every memory term names its mechanism: - -```text -MemoryTerm { component, placement_law, scaling_law, phase, lifetime, bound, derivation } -``` - -Whole verdict = conjunction of subproofs: `StaticResidencyFit`, -`CacheCapacityFit`, `PrefillTransientFit`, `DecodeTransientFit`, -`LoadTransientFit`, `SupplyReserveFit`. `FitProved` only when every applicable -rank × phase proves. - -## The allocation-plan projection (the durable instrument) - -Patch or wrap the pinned engine to emit its allocation plan — introspection, -not tuning: - -```text -ResolvedKvAllocationPlan { - runtime revision / image digest, cache format, allocator block count, - groups: [ identity, spec kind, layer identities, block size, page size bytes, - tokens per state, max memory usage bytes, - blocks required at the requested sequence length ], - grouping/padding decision -} -``` - -The plan must reproduce the engine's printed capacity as an independent -control. Production admission then evaluates any workload point offline from -the group roster; wet execution becomes falsification + compatibility check, -not the sizing algorithm. (`kv_group_metadata` documented the emission gap: -layer counts and page bytes absent — this closes it at the source.) - -## Discriminating controls (required witnesses) - -```text -same arena bytes + different group roster → different capacity -same printed token figure + changed KDA count → no proof reuse -missing sparse-indexer tail group → CacheFitUnestablished -index_kpool 4 → 8 → derived capacity changes -TP4 receipt applied to TP2 → refused -64K×1 activation receipt applied to 262K×6 → refused -negative component observation → cannot improve headroom -all cache groups prove, prefill open → CacheFitProved + ArmMemoryFitUnestablished -``` - -## What remains legitimately measured - -Foreign/host pool occupancy; driver allocations with no deterministic API; -fragmentation with no conservative construction bound; backend workspaces with -no formula nor sizing function; realized behavior that violates the model -(falsification). Everything else derives. - -## Current honest standing (2026-09-25) - -```text -Per-rank pool supply: observed -Rank-specific composite residency: observed (composite, not placement) -KV arena by rank: observed -Limiting rank: TP3 / srv11 (that launch) -262K × 6 cache cost: CacheCapacityCandidate -262K × 6 prefill transient: unestablished -Whole-arm verdict: FitUnestablished -``` - -262K is entirely plausible on this topology — the hybrid architecture exists -precisely to avoid dense KV growth on all 45 layers — but the durable answer -is an argument from architecture + pinned allocation laws + topology + -workload, not a fitted line through one experiment. diff --git a/docs/plans/model-realization-fork.md b/docs/plans/model-realization-fork.md index e19c01c10a7..70a818080f3 100644 --- a/docs/plans/model-realization-fork.md +++ b/docs/plans/model-realization-fork.md @@ -31,7 +31,7 @@ Every primitive is **modeled** as a `.dag` coproduct and **realized** as a nativ | `eval_binop` Eq/Ne guard | `:1910-1922` | **FAIL-CLOSED** (raises `CrossRepresentationEquality` on numeric straddle) — landed | | `pattern_matches` | `:2276-2563` | RECONCILED (explicit per-variant arms) | | `value_hash` / `CanonKey::new` | `:344` / `:309` | RECONCILED / FAIL-CLOSED (reflexivity check rejects non-reflexive) | -| `parse_table_memo` / `pure_call_memo` / `resolved_graph_cache` | (see lock-down §3) | reconciled-with-caveat → the under-keyed / lossy cache holes are this seam at the cache layer | +| `parse_table_memo` (deleted with the parse memo carrier, operator ruling R2 2026-10-08) / `pure_call_memo` / `resolved_graph_cache` | (see lock-down §3) | reconciled-with-caveat → the under-keyed / lossy cache holes are this seam at the cache layer | `FreeMonoid` is the proof that compositional reconciliation is *possible* (one flatten, all ops). Every other primitive is per-site. diff --git a/docs/plans/mt-collins-dimm-physical-identity.md b/docs/plans/mt-collins-dimm-physical-identity.md deleted file mode 100644 index e5e95368eca..00000000000 --- a/docs/plans/mt-collins-dimm-physical-identity.md +++ /dev/null @@ -1,156 +0,0 @@ -# Mt. Collins DIMM physical identity: documentary findings - -Read 2026-09-11 for `node://adhoc-ca3ce4b5-182`. The upstream findings below use -only publisher documents. No machine was accessed. A later operator report is -carried separately in gunbc, as described below; it supplies no extdeps fact. -The starting citations were `docs/plans/altra-cross-channel-rank-mixing-survey.md`. - -## What can be located without reading silkscreen - -Ampere's **Mt. Collins GSG Issue 1.05 Figure 9 (p.11)** labels all 32 connectors and -places their banks around CPU0 and CPU1. **Figure 10 (p.13)** labels the processors -Socket0 and Socket1 in a chassis photograph, with the NVMe backplane and six fans -at the top and PCIe risers at the bottom. With that orientation, Socket0 is left -and Socket1 is right. They are side by side, not front and back. - -The model carries the Figure 9 bank order in -`extdeps.ampere.mt_collins_getting_started_guide.dimm_layout.dimm_figure_banks`. -The processor grouping and channel identity are resolved by J-number against -`extdeps.ampere.mt_collins_product_brief.memory_population.mt_collins_channel_connector_pairs`; -no second channel table is authored. `gunbc.mt_collins_dimm_physical_identity.resolve_dimm_diagram_identity` -refuses missing or repeated keys and disagreements in processor socket. - -The **document-derived positional reading** is: view from above with the front/backplane -away from you and the rear/riser end toward you; then use Figure 9's order within -each CPU's left and right banks. Joining that order with the existing Table 11 -16-DIMM row selects alternating connectors starting at the **outermost connector -away from each CPU** on both sides. This is a spatial inference from Figures 9 -and 10 plus Table 11, not a vendor sentence instructing an installation order, -a photograph of the operator's board, or a physical colour rule. The model exposes -the source bank order and population join rather than minting this inference as -an independently authored slot list. - -## Colour: three distinct facts - -1. **Drawing legend:** GSG Issue 1.05 Figure 8 (p.10), 1U User Guide Issue 1.00 - Figure 10 (p.39), and 2U User Guide Issue 1.00 Figure 19 (p.52) all use **yellow - for slot#0, used at 1DPC**, and **blue for slot#1, added at 2DPC**. The numbered - J annotations agree with the existing odd-J 1DPC population. The model derives - each connector's legend role from the existing population-role join. -2. **Photographed plastic:** GSG Issue 1.05 Figure 12 (p.36) visibly contains blue - and black connectors/latches. It is a cropped board-revision photograph, not - an annotated complete DIMM colour map. This visual fact is carried at its - actual grain in `motherboard_photo_colours`. -3. **Meaning of physical colour:** **not found in the inspected documents.** - None establishes that physical blue means slot#0, slot#1, or the 1DPC set. - The drawing's blue cannot be translated into the chassis's blue. A readable - photograph identifying actual connector positions, or explicit vendor - documentation of the physical colour scheme, is still needed for that answer. - -This does not establish why a processor failed to train, and does not select a -hypothesis about the running machine. - -## Silkscreen: not established for all connectors - -GSG Figure 9 supplies **diagram annotations** J1–J32, not a silkscreen view. -Figures 10 and 12 show installed DIMMs and small/obscured board text. The 1U guide -Figures 11 (p.40), 24–25 (pp.52–53), and the 2U guide Figures 20 (p.53), 42–43 -(pp.74–75) illustrate replacement without an all-connector readable silkscreen -map. No inspected source establishes the literal text printed beside **each** -DIMM connector. No row therefore claims that a connector's physical silkscreen -reads its J-number. The old table-only comment saying connectors were named -on the board is narrowed to what it actually knew: the guide's identifiers. - -## Mt. Jade is not a physical orientation substitute - -Mt. Jade GSG Issue 1.00 Figure 9 (p.10) explicitly marks chassis front/top and -rear/bottom, with **Socket1 left and Socket0 right**; Figure 10 (p.12) repeats -that grouping in a photograph. Its within-socket numbered bank pattern resembles -Collins, but its processor placement is reversed. No Jade orientation or DIMM -silkscreen spelling is imported into the Collins model. OCP Mt. Jade Rev 1.0 is -already a separate platform authority in `extdeps.ocp.mt_jade.subject`; this -change does not assert that a shared processor or channel topology makes their -physical boards interchangeable. - -## Another figure boundary - -The right-side MCU/channel leader labels in the topology cartoon run 4,5,6,7 -against connector pairs displayed 16/15,14/13,12/11,10/9 (and the corresponding -socket-1 keys). The existing GSG Tables 12–13 authority assigns those pairs -MCU7,6,5,4. This change uses the cartoon only for its slot#0/slot#1 legend, and -Figure 9 for connector positioning. It does **not** replace the established -pair table with the cartoon's MCU labels. Resolving that upstream discrepancy -is outside this physical-identity change. - -## Documents and bytes read - -All four publisher downloads returned PDF bytes without credentials. Full text -was extracted with PyMuPDF; the cited topology, chassis and board-replacement -figures were visually inspected. Figure 12's embedded 1220×1626 photograph was -also inspected at native resolution. No document PDFs or photographs are committed. - -| Document | Revision/date | Location and inspection scope | SHA-256 | -|---|---|---|---| -| Mt. Collins DVT/PVT/MP Getting Started Guide, AMP 2021-00511 | Issue 1.05, 2026-07-20 | [Ampere publisher](https://connect-admin.amperecomputing.com/api/secure-file-download/download-regular/?file=Mt_Collins_DVT_PVT_MP_GSG_v1_05_20260720_52a0348e87.pdf&type=technical-document&documentId=z6zenr250qdpa8eq4irolgds); Figures 7–10 pp.9–13, Figure 11 p.32, Figures 12–13 pp.36–37; Tables 11–13 pp.11–12 | `1c41b4f088fa9c8b760d21d57ae93b0d1a68ee67a740d4eb7ac893753fc580b3` | -| Mt. Collins 1U User Guide and Hardware Maintenance Manual | Issue 1.00, 2022-03-30 | [Ampere publisher](https://connect-admin.amperecomputing.com/api/secure-file-download/download-regular/?file=new-private-files/94/tech/Mt._Collins_1U_Users_Guide_v1.00_20220330.pdf&type=technical-document&doc_id=669); §4.2.7 Figures 10–11/Table 3 pp.39–40; Figures 24–25 pp.52–53 | `7f351cc3bf8a9e2d1e6809a9e5337e527feac61a8ff3c4e4d6514f5e59311e7c` | -| Mt. Collins 2U User Guide and Hardware Maintenance Manual | Issue 1.00, 2022-03-30 | [Ampere publisher](https://connect-admin.amperecomputing.com/api/secure-file-download/download-regular/?file=new-private-files/94/tech/Mt._Collins_2U_Users_Guide_v1.00_20220330.pdf&type=technical-document&doc_id=649); §4.2.7 Figures 19–20/Table 6 pp.52–53; Figures 42–43 pp.74–75 | `a8102a2a661fca508c079ba460da2f701360216517ee090a036e533bbf078bbf` | -| Same 2U guide, operator-supplied **third-party mirror** | Same printed issue/date | [Doslab Electronics mirror](https://repo.doslabelectronics.com/misc/Mt-Collins-2U-Users-Guide-v1-00-20220330.pdf); fetched with curl, HTTP 200, 16,148,131 bytes; byte-identical to Ampere's copy. Initial Python HTTP request returned 403; browser tool timed out. The successful download is the cited read, not those failed attempts. | `a8102a2a661fca508c079ba460da2f701360216517ee090a036e533bbf078bbf` | -| Mt. Jade PVT/DVT (NVMe) Getting Started Guide | Issue 1.00, 2022-04-12 | [Ampere publisher](https://connect-admin.amperecomputing.com/api/secure-file-download/download-regular/?file=new-private-files/94/tech/Altra_Family_Mt._Jade_PVT_DVT_NVMe_GSG_v1.00_20220412.pdf&type=technical-document&doc_id=631); Figures 8–10 pp.8–12 | `f6528f70dec97b8838bd67413566287fc7c465addbf07182e9bdd1d48874c69f` | - -The 2U document has its own `extdeps.ampere.mt_collins_2u_user_guide.subject` -authority, including its publisher URL, separately named mirror URL, issue, -date, digest and Figure 19 citation. The two independent read receipts live in -`gunbc.specification_citation_read_provenance`; the publisher uses the -publisher-fetch arm and the live third-party mirror uses -`DocumentMirrorLocatorFetchedAndTextIngested`. Its observation date is not an -invented archive-capture date. `mt_collins_2u_citation_read_comparison` derives -agreement from subject identity, response status, byte count and digest, and -retains both receipts. Unread or wrong-subject records are incomparable; -different recorded bytes/digests differ. No mirror read is called a publisher -read, and no agreement is an authored boolean beside the DIMM join. - -## Consumption and remaining boundary - -`gunbc.mt_collins_dimm_physical_identity.sixteen_dimm_diagram_identity` is an -executable inspection entry point into `population_diagram_identity`. It carries -ordered banks, canonical channel identity, drawing legend, chassis orientation, -2U guide citation/mirror provenance, and explicit physical identification gaps. -Unknown/duplicate connector references, socket disagreements and invalid population -rows refuse without returning partial slot instructions. The published 32-DIMM -row still has 31 keys; the projection reports the mismatch rather than silently -adding J32. The existing correction remains the authority for that separate inference. - -The later **Mt. Collins rack-facing DIMM population projection** is declared by -`operator_projection_frontier` beside the join, including the capability required -to close it. This change lands the documentary model and executable join, not a -rack UI or a claim that physical identification gaps have been closed. That future -projection must preserve the distinction between figure-based positioning and -verified silkscreen/colour. A clear chassis photograph is the next evidence route -when the operator cannot read the markings. - -## Later operator report: unit 1 only - -Dashboard message `msg_ecd92656-ea35-4746-9e3f-f67d12706ec5`, relayed by -`eager-owl-205` on 2026-09-11, reports the operator's by-eye inspection of -**unit 1, a Foxconn Mt. Collins 1U chassis**: physical blue connectors are the odd-J set used at -1DPC; the silkscreen supplied no useful slot-identification information. -No photograph accompanied the report to this session. We did not independently -inspect the chassis, and the report is not an Ampere statement. - -`gunbc.machine_intake_mtcollins1_dimm_connector_observation.mtcollins1_blue_connector_observation` -records that report, its date, observer, relay message and unit scope. The -reported set references the canonical 16-DIMM population rather than repeating -its J-number list. The upstream documentary findings above remain unchanged. - -`mtcollins1_colour_meaning_divergence` is the third fact: it joins the reported -physical blue set to the documentary roles and finds that blue on this chassis -has a different role from blue in the schematic legend. This is evidence that -the diagram colour does not predict this chassis's colour, not a contradiction -in a vendor physical-colour rule (none was found). - -`colour_evidence_for_unit` returns this evidence only for the existing unit-1 -intake subject. Another unit receives `NoColourObservationForUnit`, and the -returned documentary projection still carries both upstream identification -gaps. The later rack UI must preserve date and by-eye/report provenance; this -receipt does not generalize to other units, the 2U chassis, or the fleet, and it -does not explain or validate processor training. diff --git a/docs/plans/mt-collins-gpu-augmentation-program.md b/docs/plans/mt-collins-gpu-augmentation-program.md deleted file mode 100644 index 546c4dd30ae..00000000000 --- a/docs/plans/mt-collins-gpu-augmentation-program.md +++ /dev/null @@ -1,181 +0,0 @@ -# Mt. Collins GPU augmentation — program - -**Status: contract phase, no geometry authored.** This is a SECOND product program with its own -authority (`product.mt_collins_gpu_augmentation`), deliberately not folded into -`product.printed_chassis`. It pressures different parts of the model than the ALTRAD8UD cassette: -mixed purchased and fabricated structure, a heavy high-value expansion card, PCIe signal integrity -and cable strain, independent power and protective earth, two interacting airflow domains, sustained -thermal evidence, and a prototype-material versus deployment-material distinction. - -The standing rule that **cable routing and grounding apply at every controlled layer** carries over -unchanged. - -## Architecture ruling: companion bay first, hood second - -``` -MtCollinsGpuServingUnit -├── original sealed 1U server (lid, fan wall, CPU/DIMM shrouds untouched) -├── adjacent 1U GPU companion bay -│ retention/load path · airflow · dedicated PSU unless the PDB is proven · PE bond -├── purchased qualified PCIe riser/cable -└── modeled cable, service and thermal interfaces -``` - -The companion bay is the better first experiment because it preserves the stock thermal system, -keeps GPU weight off the riser connector, allows a dedicated PSU and independent intake/exhaust, is -reversible, and separates the server's cooling result from the GPU enclosure's. A modified 2U lid or -hood is a legitimate *comparison* candidate, not the starting point. **Card orientation is not yet -admitted** — exact card dimensions and usable 2U geometry decide it, and a horizontal arrangement may -be the only viable one for a full-height card. - -## Census ANSWERED — this program is experimental, not the critical path - -**~8 of the 28 boxes are at least 2U, some 4U, and take a GPU with no fabrication at all.** So the -1U retrofit is an experiment to run if it is ever wanted, not the route to a served GPU. Everything -below stands as the design if the retrofit is attempted; none of it is on the critical path, and the -program should not consume printer time that the ALTRAD8UD cassette needs. - -The live question moved with it: not "how do we get a GPU into a 1U" but **"what changes in the -ALTRAD8UD stackable chassis when one of its nodes carries a GPU"** — recorded in -[printed-chassis-program.md](printed-chassis-program.md) under the GPU-bearing node, because that is -a variant of the node contract rather than a second product. - -## The census, retained for the reasoning - -The brief's own MTC-GPU-5 lists "purpose-built 2U GPU server" as a comparison candidate. Per the -operator's hardware discussion, **the 2U Gigabyte G242-P31 is the Ampere platform built for exactly -this** — four dual-slot GPUs, front-to-back airflow, PSUs sized for it — while **a 1U Mt. Collins -cannot take a dual-slot full-height card at all**; the 1Us are CI boxes. - -So the cheapest possible first action is an **exact-model inventory census of all 28 boxes**. If any -are the 2U variant, the companion-bay program is not the cheapest route to a served GPU — it is a -workaround for a chassis constraint that a machine already owned does not have. A program that can -be deleted by one inventory query should face that query before any geometry is modeled. - -This is not a reason to discard the brief: the companion bay remains the answer if the fleet is -all-1U, and the ruling's decomposition stands either way. - -## Design for the production process, from the first printed part - -The operator's stated intent is **prototype by printing, produce by manufacturing** — specifically -laser-cut and bent sheet aluminium, which for a colo is worth more than its price: fire-rated by -nature, grounds to the shelf, no tooling, ~$40-60 at 20-100 units. Printing does not scale into -production here; it is the design loop. - -Two consequences bind the geometry **now**, before any part is modeled: - -- **Model bend-friendly, flat-ish panels** — uniform material thickness, bend reliefs, no geometry - that only survives because a printer tolerates it. *A print that only works because printing - tolerates anything is a prototype of nothing.* -- **Freeze before you tool.** The readiness signal is not "it works" but "the last three revisions - were cosmetic." - -This also splits process qualification into **two targets**: the printer–spool process (what the -hole-diameter coupon measures, for the prototype loop) and the sheet-metal vendor process (what the -production parts are actually made by). These are different `ProcessQualificationIdentity` subjects -and must not share one. The coupon program is not invalidated by this — it qualifies the loop that -produces the prototypes — but it does not qualify a production part. - -## Phases - -| Phase | Terminal evidence | -| --- | --- | -| **MTC-GPU-0** contract | Exact GPU SKU, count, bay-vs-hood candidate, rack allocation, prototype-vs-deployment target, power architecture. No generic "Blackwell", "x16 riser" or "2U hood" stands in for a selected product. | -| **MTC-GPU-1** measure | Chassis external/usable envelope; lid, shroud, riser, rear-slot geometry; GPU envelope, mass, bracket, connectors, airflow; PDB connectors and ratings; candidate PSU; rack depth and cable-service envelope — with uncertainty and provenance. | -| **MTC-GPU-2** PLA cold-fit | Rear pass-through/blanking adapter, riser strain relief, bracket-location gauge, cradle prototype, duct prototype. Surrogate mass before the real card sits on printed retention. Fit only — no power or thermal claims. | -| **MTC-GPU-3** powered bay | Independent card load path, admitted PSU, PE, fans/ducting, full cable routing. PCIe link trains at required width and generation, no relevant link errors, GPU idles safely. | -| **MTC-GPU-4** thermal admission | The isolated matrix below. Combined load within exact product limits, no unexplained host regression, no printed-part thermal failure. | -| **MTC-GPU-5** production verdict | Compare custom bay vs integrated hood vs commercial expansion chassis vs purpose-built 2U server on cost, rack occupancy, thermals, reliability, service time, fabrication effort. | - -## Load path — printed parts are never the retention - -The GPU's load terminates in the bay or rack, **never** in the PCIe edge connector, the riser card, -the flexible cable, the printed duct, or the original server lid. - -``` -rack rails / metal bay shell -> metal GPU bracket -> metal or reinforced cradle - -> optional printed alignment pads and duct -``` - -Printed: card-location adapters, anti-sag supports, blower intake ducting, cable strain relief, -connector guards, fan mounts, blanking pieces, geometric identifiers. -Metal: rack-width shell, primary retention, long beams, PSU enclosure, PE path, fire and impact -containment. A rack-width enclosure is not one A1-mini print in any case — this is another -"print the joints and ducts, buy or bend the straight metal" design. - -## Cable and grounding — independent modeled routes - -PCIe data (host riser slot → pass-through → cable → GPU edge) · GPU DC power (pod PSU or admitted PDB -→ auxiliary connectors) · AC (rack supply → bay PSU) · cooling/control (fan power, PWM/tach) · -**protective earth** (AC PE → PSU enclosure → bay metal → rack bond) · functional/chassis reference, -separate from PE. - -Each route carries endpoints, connector orientation, bend volume, moving/fixed classification, strain -relief, hot-surface separation and disconnect order. **The PCIe cable is never retention, never -chassis grounding, never an uncontrolled hinge, and never a service loop with an undefined bend -radius.** The rear pass-through must preserve the host pressure boundary as far as practical — a -large unblanked opening can disturb host airflow even with the lid on. - -## Thermal — two coupled domains - -``` -HostCoolingDomain front fan wall -> CPU/DIMM shrouds -> rear exhaust -GpuCoolingDomain dedicated ambient intake -> GPU cooler/blower -> declared exhaust destination -``` - -Primary law: **`GpuCoolingDomain` must not materially impair `HostCoolingDomain`.** - -Test sequence, each change isolated: stock sealed 1U under CPU/DIMM load (host baseline) → bay -installed, GPU unpowered (passive obstruction, recirculation) → GPU idle (fan/idle-air interaction) → -GPU load only → combined load (terminal case) → declared fan degradation or elevated ambient -(failure margin). - -Observed: CPU temps and throttle state, DIMM temps, BMC-available VRM/board sensors, host fan speeds, -GPU core/hotspot/memory/fan/power/throttle reason, host inlet and exhaust, GPU inlet and exhaust, -printed-part surface temperature at the hottest locations. - -Admission requires: no vendor limit exceeded ∧ no unexplained throttling ∧ host regression within a -declared budget ∧ GPU sustained at target power ∧ printed material within its admitted service -envelope ∧ no recirculation mode left unmeasured. - -## Material policy - -PLA **may** establish dimensional fit, card and bracket location, riser-cable path, interference and -service clearance, room-temperature duct topology, fan and connector placement. - -PLA **may not** establish sustained structural retention near the GPU exhaust, long-term anti-sag, -deployment thermal durability, flame behaviour, or colo suitability. - -A powered PLA prototype is defensible only when the PLA parts are not the sole card support, are not -the mains enclosure or PE path, and are temperature- and deformation-monitored under supervision, -with a secondary metal retention path if the plastic softens. **PETG is not automatically -sufficient** — measure actual part temperatures first. - -## Facts that stay OPEN until measured - -Exact GPU SKU, dimensions, mass, slot width, power, connector locations, airflow direction · rear -riser electrical width and generation · exact qualified riser cable and usable routing length -(signal integrity is not inferable from "Gen4 x16 cable") · PDB GPU-power capability · stock -lid/shroud thermal role · GPU-bay intake/exhaust relationship to server exhaust · available rack -depth and rear service clearance · printed-part operating temperatures · colo/site rules. - -The 60-80 °C blower exhaust figure is a **hypothesis until measured** on the exact card and airflow -arrangement, and "under 80 °C for an hour" is **not** the terminal rule — the exact GPU's vendor -limits, throttle state, hotspot and memory limits, and the server's CPU/DIMM limits govern. - -## Workload model stays separate - -The KV-cache analysis is not mechanical authority. It determines a `SelectedGpuDeployment` -(gpu_sku, gpu_count, target active session population, parked session population, storage tier) -which this program **consumes**. The chassis program must not independently encode "two cards" or -"64 sessions". The load-bearing distinction is that **parked-session capacity is not simultaneous -active decoding capacity** — that may decide whether the augmentation is worth building at all, and -it must not silently decide card count before the active-fraction requirement is selected. - -## Extraction discipline - -This becomes the **second real consumer** that could justify extracting generic facilities currently -inside the ALTRAD8UD program — process qualification, model-to-CAD realization, generated-artifact -materialization, spatial measurement, cable routing, protective earth, thermal observation, -fabrication receipts. Extract when both products ask the **same typed question**, never because the -names look reusable. diff --git a/docs/plans/mtcollins1-boot-observe-publish-capacity.md b/docs/plans/mtcollins1-boot-observe-publish-capacity.md deleted file mode 100644 index 3fbc02da2c9..00000000000 --- a/docs/plans/mtcollins1-boot-observe-publish-capacity.md +++ /dev/null @@ -1,96 +0,0 @@ -# mtcollins1 boot: observe and publish capacity; health separate from the boot verdict - -Status: APPROVED by eager-owl-205 (2026-09-26) with the frontier answer below; work item adhoc-0e07aa33-b20. -Off the #12362 → publish → repin → boot critical path: nothing here lands before #12362. - -## The defect, read as a chain (DESIGN §6b) - -`gunbc.machine_intake_mtcollins1_boot_run` `mtcollins1_boot_qualifications` folds six -qualifications into ONE terminal verdict: `nproc`, `numa`, `boot-media`, `edac-before`, -`workload`, `edac-after`. Two of them (`nproc`, `numa`) compare the observation to an -INTENDED topology (`gunbc.machine_intake_mtcollins1_boot_milestone` -`mtcollins1_boot_topology_expectation`), so an intent mismatch is reported as "did not boot". -The earliest unjustified boundary is not the 160/80 literal; it is that the verdict type answers -four questions with one arm set. Patching the literal (the #12325 milestone + the rung drop -`mtcollins1_boot_accepts_socket1_absent`) was the symptom-link patch: it kept the conflation and -made the intended configuration a boot precondition. - -## The four questions and their homes (DESIGN §3b — inhabit, do not mint) - -| Question | Home (existing) | What changes | -|---|---|---| -| 1. Did it boot | `gunbc.machine_intake_mtcollins1_boot_run` terminal verdict | Verdict narrows to evidence the census image reached its interfaces: envelope closed/sections terminated, `boot-media` (the image we delivered is the one running). `nproc`/`numa`/`edac-*`/`workload` leave the verdict. | -| 2. What is available | `gunbc.compute.host_capacity` (`HostMemoryObservation`, the host `Pool` over `product.capacity.pool`), `product.fabric.envelope` `ResourceEnvelope`, `product.fabric.work` `HardRequirements { threads: HardwareThreadCount }` | A capture-sourced observation reading of CPU set (count + per-NUMA-node CPUs), and MemTotal/MemAvailable and `/dev/shm` size, each field `Observed \| Unknown{cause}` — malformed never becomes zero. Published regardless of (3). No new capacity vocabulary: threads are `HardwareThreadCount`, memory `Kibibyte`, the pool is `product.capacity.pool`. | -| 3. Health | `product.host_health` (`MemoryErrorCounters`, `CounterWindowStanding`, `HostHealthGoal`, assessed via `std.goal_assessment`) | EDAC before/after become a `CounterWindowStanding` over `MemoryErrorCounters`; workload digest stability a health signal; "observed topology vs intended (2 sockets × 80) and vs earlier observations" becomes a `GoalDiverged` deviation, never a boot refusal. Findings carry a consequence (restrict / quarantine), never delete the (2) observation. | -| 4. Eligibility | `product.fabric.supply` `offer_covers_shape` / `unmet_*_axis`, `product.fabric.capacity_admission` | Requirements are judged against the (2) allocatable envelope plus a health policy input from (3). No mtcollins1-specific eligibility code. | - -Open question for the parent on (2): the capture is a BOOT-TIME observation on a machine not yet -enrolled as a fabric host (no `HostDashboardInstance`). I propose the observation lands as a -typed reading in machine_intake that is *shaped* as `ResourceEnvelope` + `HostMemoryObservation` -so enrolment consumes it unchanged; the actual pool publication is a declared frontier (§3c) -triggered by mtcollins1's fabric enrolment. If you want it published to a fabric partition now, -I need the enrolment home named. - -## Memory test sizing (the #12362 residue) - -Sizing follows the observation, never socket count, under an explicit policy row: -`reserve` (bytes held back from the test), `minimum_useful` (below which the test REFUSES as -"not run", not "passed"), and the size attempted = min(MemAvailable − reserve, /dev/shm free). -The receipt records exact bytes attempted and coverage = attempted / MemTotal. A bounded run is -typed `BoundedCoverage { attempted, of }` and can never read as full qualification; full -qualification is a separate arm requiring coverage at the declared threshold. The #12362 -"one-socket 16 GiB profile" dissolves into this policy once #12362 has landed. - -## The cut (DESIGN §3 replacement migration, delete-first) - -The root is `gunbc.machine_intake_mtcollins1_boot_milestone`. Its consumers, enumerated by name -(this root is not in the required gate prefixes' blast radius alone, so named, not inferred): -`gunbc.machine_intake_mtcollins1_boot_run`, `test.claim.machine_intake.mtcollins1_boot_run_witness_test`, -`gunbc.rung_drop.mtcollins1_boot_accepts_socket1_absent` (+ `gunbc.rung_drop` roster, -`docs/design-rung-drops.md` projection). - -One PR, one motion: -1. DELETE `mtcollins1_boot_milestone.dag` and the `nproc`/`numa` qualifications and their causes - (`SocketPlacementMismatch`, `SocketPlacementUnread`, nproc mismatch) from the terminal verdict. -2. RETIRE the rung drop by construction: its subject ("boot qualification accepts socket 1 absent") - no longer exists because the boot verdict no longer asks about sockets. The gate it lowered is - not lost — it moves UP to health, where an unexpected reduction is a `GoalDiverged` finding - against the intended 2-socket configuration. Nothing is weakened, so no new drop. -3. KEEP `mtcollins1_nproc = 160` untouched: a true receipt about the census boot it was read on; - it becomes one of the "earlier observations" health compares against. -4. The numa-line parser (`numa_node_cpus_line`) moves into the capacity observation, not copied. -5. EDAC/workload move from verdict into the health assessment in `product.host_health`. - -Witnesses: an 80-CPU single-socket capture BOOTS, publishes 80 threads, and carries a health -deviation; a malformed nproc section boots (if boot-media held) with threads `Unknown`, never 0; -a capture missing boot-media does not boot; EDAC UE>0 boots but health restricts; the route claim -exercises the real `mtcollins1_boot_terminal_verdict` path (§3 pairing obligation). - -No live hardware is touched. - -## Correction after reading the homes: in-band health is not `product.host_health` - -`product.host_health` is built on a standing rule that its signals are read OUT OF BAND ("a count -the BMC does not expose is an unknown, never a host-OS read"). nproc, numactl and in-band EDAC -from the census image are host-OS reads. So the topology health assessment is a sibling -instance of the shared authority, `std.goal_assessment`, over an in-band subject, in -`gunbc.machine_intake_host_resource_observation`. It is a stated divergence from -`product.host_health`, not a silent fork. In-band EDAC follows the same route at the cut (below): -`product.host_health` `MemoryErrorCounters` / `counter_window_standing` are reused as values, -without the out-of-band epoch claim. - -## Staging (parent ruling: nothing touches the #12362 → publish → repin → boot path until that boot has run) - -- **PR 1 (this change).** Questions 2–4 as new modules: - `gunbc.machine_intake_host_resource_observation` (observation → `host_topology_assessment` → - `topology_health_admission` → `host_offered_shape` → `host_workload_eligibility` over - `product.fabric.supply` `unmet_shape_axes`), and `gunbc.machine_intake_mtcollins1_topology_goal`. - The numa reading has its own strict parser (node and CPU ids, duplicates refused). The milestone module's count-only parser is frozen and deleted at the cut. -- **PR 2 (the cut), after the #12362 boot has run.** Delete the milestone module and the - nproc/numa verdict arms. Retire the rung drop. `mtcollins1_boot_run` feeds its envelope's - sections into `host_resource_observation`, so the boot run becomes the production consumer. - EDAC and workload move out of the verdict. -- **PR 3, after #12362 lands.** Memory-test sizing policy. -- **Declared frontier.** Pool publication into `product.capacity.pool` via - `gunbc.compute.host_capacity`. Trigger: mtcollins1's fabric enrolment (a `HostDashboardInstance` - naming it). No enrolment home is invented here. diff --git a/docs/plans/namecheap-secret-manager.md b/docs/plans/namecheap-secret-manager.md deleted file mode 100644 index d2063a4a638..00000000000 --- a/docs/plans/namecheap-secret-manager.md +++ /dev/null @@ -1,135 +0,0 @@ -# Namecheap credential custody - -The owner reports `projects/582015116396/secrets/namecheap-api-key` provisioned. -The project number matches `fleet_secrets_project_number`; the project ID comes -from `fleet_secrets_gcp_project` (`gunbai-secrets`). No new version is provisioned -by this lane, and no key is requested in chat or in a persistent local file. - -The owner also supplied account `briansrls` and an API allowlist entry named `dev` -for `96.224.201.7`. These are typed operator reports in -`gunbc.namecheap.account`, not evidence of an authenticated API call or a -permanent DNS-controller placement. - -`gunbc.namecheap.credential` binds the container once. Its `latest` reference identifies the -container for IAM and is resolved once by the discovery observer, which records -the returned numeric version. The numeric selector controls apply to future -reviewed mutation consumers, not this discovery path: an approval over a DNS -mutation must not silently switch credential generations through an alias. `gunbc.namecheap.credential_read` uses the existing -`fetch_secret_ref_credential` with `WorkloadIdentityToken`; the shared reader -checks the response resource against the requested project, secret and version. -It does not print or persist the payload. - -The dedicated `namecheap-dns` federation joins `gcp_iam_converge_targets`. Its -trust is pinned by the shared fleet job claim authority to this repository, the -fleet-converge workflow at main, a dispatch on main, and the `namecheap-dns` -environment. Its only secret grant is accessor on `namecheap-api-key`. The -existing IAM workflow plans as iam-observe, files a request in the approval app, -impersonates iam-converge only after approval, rechecks the approved plan, applies -and independently reads back. Enrollment is not evidence of an applied grant. -The new target also changes the resource-local bootstrap bindings needed by the -IAM controller; those must be observed before running its apply. -This preserves the repo's distinction between routine per-secret federated -reads and operation-specific human approvals. For DNS mutations, the consumer -must use the existing `approval_gate` and scoped authorization over the frozen -zone plan; secret possession alone never authorizes a zone replacement. This -increment does not yet connect that DNS mutation consumer. - -Live status: secret creation and the allowlist are operator-reported. No enabled -version or applied IAM binding has been independently observed. This local shell -has no WIF token or gcloud installation. No provider request, IAM write, key -access, or DNS mutation has been executed. The `namecheap_observe` fleet mode now has a dedicated job that supplies WIF -and records the exact secret version plus authenticated getHosts readback. It -still must land on main and its federation must converge before a live dispatch -can pass the provider's main-only claim pins. -The earlier DNS model lives on `work/daily-end-to-end`; this lane is based on the -current repository so it can reuse the landed approval app rather than copying -its newer authorities into that older checkout. The previous local `api_key_file` -onboarding proposal is superseded by this Secret Manager reference. - -Validation on the current main base: a combined full-root run evaluated all -21 IAM convergence controls, five credential controls, 15 XML/provider controls, -and four workflow controls successfully, then regenerated the workflow through -`tools.generated_artifact_gate.main_wet_one`. Its receipt counts 46 because it -also ran one redundant aggregate IAM check, subsequently removed; the retained -45 controls all ran in that invocation. The generated YAML was independently -parsed and checked for the dispatch mode, dedicated identity, event SHA checkout, -SSH exclusion, and success-only receipt upload. The self-contained loopback -transport harness passed and observed the fixture query arriving without the -fixture key in curl's argv. These are local checks, not live GCP or Namecheap -observations. - - -## Read-only verification workflow - -`fleet-converge` mode `namecheap_observe` uses the selected fleet runner and the -`namecheap-dns` environment. The build and observer both check out the event SHA; -`expected_revision` cannot substitute another revision under the trusted WIF -identity. The shared fleet job is excluded, and this job receives no SSH key. - -The entry checks public IPv4 egress against the owner-reported allowlist, fetches -one Secret Manager version through the shared checked reader, and issues only -`namecheap.domains.dns.getHosts`. The encoded query travels to curl via stdin, -not argv or a persistent credential file. There is no setter or configurable -command in this provider interface. Every read has a timeout. Transport and -provider refusals omit raw response bodies, and a response that contains the -credential is withheld before decoding; the decoded retained result and host -attributes are checked again before the observation can reach the receipt. - -The XML subset reader refuses unsupported syntax and ambiguous envelopes rather -than guessing: DTDs and external entities, duplicate attributes, extra documents, -namespace overrides, malformed records, duplicate provider record IDs, and a -response for another domain/command cannot yield an observation. It preserves -all provider result and host attributes, including unknown ones. Unsupported XML features -(including numeric character references) are a located read refusal; this is not -a claim to implement every XML document. - -`target/namecheap-observation.json` records the run ID, attempt, revision, start -time, exact credential version, account, observed public egress, domain and -returned result and host fields. The job uploads it only after success, so failure cannot -publish a previous run's receipt. `mail_mode=unobserved` and -`write_authority=withheld` are deliberate: getHosts does not independently prove -the console's mail mode, and this observer grants no DNS mutation authority. - -The remaining live sequence is: land the reviewed code on main; observe/update -the existing IAM controller's resource-local bootstrap reach for the new target; -run `gcp_iam_converge` and approve its exact plan in the existing app; then run -`namecheap_observe` on a runner whose public egress is allowlisted. Secret -possession, a modeled grant, and a pure witness are not substitutes for those -readbacks. Full-zone DNS mutation, DNS-01 renewal coordination, and dashboard -fleet cutover remain separate unfinished consumers of this observation. - -Provider authority: [Namecheap getHosts](https://www.namecheap.com/support/api/methods/domains-dns/get-hosts/). - -Landing boundary: the active main ruleset requires the `witnesses` status and -merge queue. The IAM authority also requires reviewed main code; the feature -branch cannot impersonate the dedicated main-pinned identity. A read-only query -of the latest 100 fleet workflow dispatches found no `gcp_iam_converge` run. That -is limited history, not proof that bootstrap never happened. The bootstrap -reach and independent readback are still unverified. - -## Review 5331195485 corrections - -The writer, generated console display and artifact uploader now consume -`gunbc.namecheap.observation_artifact.namecheap_observation_receipt_path`. The -workflow shell control reproduces the original unmatched receipt read on the old -generated YAML, then exercises the regenerated success path with only the -declared JSON output. Its planted unmatched read must fail. - -The production client keeps the raw response exclusion and also checks all -decoded retained result and host attribute names and values, including unknown -fields, before returning an observation. Six publication controls cover raw, -URI-encoded, and XML-entity-encoded echoes plus ordinary entity preservation. -Disabling the decoded guard in an isolated test copy makes the three XML echo -controls fail while raw/URI and ordinary-value controls still pass. Refusals -contain no offending value and never claim a partially redacted snapshot. - -The previous head's CI floor identified bare algebra-provider references and an -ambiguous shorthand `domain` binder. List operations now use their native method -forms, and the observer uses an explicit binder. Uppercase `Host` remains unchanged. - -Correction validation: the combined full-root run passed 51 controls and -regenerated the workflow. After the CI-reference corrections, all 15 parser and -six publication controls passed again. The regenerated emitted shell passed the -clean-root success control, left the exact upload artifact, and rejected the -planted unmatched receipt read. `git diff --check` passed. Exact-head CI is the -remaining repository-wide check; no live provider or IAM actuation was used. diff --git a/docs/plans/namespace-cut-replacement-plan.md b/docs/plans/namespace-cut-replacement-plan.md index cbd3d95bd22..d28f4ffe986 100644 --- a/docs/plans/namespace-cut-replacement-plan.md +++ b/docs/plans/namespace-cut-replacement-plan.md @@ -112,7 +112,7 @@ Standing census on the branch (2026-08-15, dated record): **5,531 hard diagnosti ## Step 2 — host and emit machinery - `src/v1/stage0/src/cli_run.rs` (**frozen file — surgical trims, never deletion**): `extract_import_paths` (the starts-with-import text scanner every host import fact builds on), `resolve_virtual_source_with_imports`, `source_declares_import_lines`, `import_module_paths_for_typed_module`, `emit_import_admission_list`; the closure-walk family (`build_import_adjacency`, `import_closure_from_adjacency`/`_from_facts`/`_live_paths`/`_live_paths_with_facts`, `reference_only_direct_import_paths`, `entry_file_touched_via_import_closure`, `import_closure_module_reaches_carrier_home`, `collect_import_closure_module_names_from_facts`, `roster_import_closure_nodes_pre_resolve`, `import_closure_files_from_graph`, `touched_file_in_import_closure`, `import_closure_repo_paths_for_entry`, `augment_closure_modules_from_import_facts`, `import_closure_dag_files`); the `declared_import_closure_*` compile family incl. `compile_entry_on_declared_import_closure_only` and `observe_declared_import_closure_symbol_binding`; the unlisted-import census family (`classify_unlisted_import_binding_source`, `compile_clean_unlisted_import_census`, `compile_clean_unlisted_import_use_blocks_from_policy`/`_cached`); the class-b import-closure gates -- `src/v1/05_emit_rust.dag` (+ twin `v1_compiler_emit_rust.rs`): `emit_imports`, `emit_extract_import_paths_fn` — **this one prints the import scanner into generated stage0; cut it in the same pass or the seed regrows it on regen** — `augment_scoped_data_item_index_with_imports`, `module_imports_std_serialization_coproduct_wire_contract`, `record_lit_resolved_ctor_import_names`, `emit_specific_import_block`, `explicit_import_source_module_for_name`, `import_variant_parent_for_name`, `wildcard_import_pool_surface_names`, `import_module_enum_scope` +- `src/v1/05_emit_rust.dag` (+ twin `v1_compiler_emit_rust.rs`): `emit_imports` — `emit_extract_import_paths_fn` was deleted (generated main already routes compile through `cli_run::compile_emission`; the stamp had no caller) — `augment_scoped_data_item_index_with_imports`, `module_imports_std_serialization_coproduct_wire_contract`, `record_lit_resolved_ctor_import_names`, `emit_specific_import_block`, `explicit_import_source_module_for_name`, `import_variant_parent_for_name`, `wildcard_import_pool_surface_names`, `import_module_enum_scope` - Whole-file deletions: `dag/gunbc/instruments/namespace_import_closure_behavioral_transport.dag` (its own dissolution trigger names this cut) · `dag/gunbc/class_b_import_closure_overlay.dag` · `dag/gunbc/declared_import_closure_binding.dag` · `dag/gunbc/instruments/dag_compile_clean_cli_floor_agreement.dag` · `dag/std/import.dag` (empty stub) · `src/v2/test/claim/long/dag_import_block_lexeme_stamp_test.dag` · `dag/test/claim/module_graph_edge_source_witness_test.dag` - Partial deletions: `dag/gunbc/compile_clean_diagnostic_policy.dag` (only the `UnlistedImportUse` enforcement declarations + dissolve rows) · `dag/gunbc/instruments/diagnostics_witness_transport.dag` (`diagnostics_import_resolution_suite`, `run_diagnostics_import_resolution_witness`) diff --git a/docs/plans/native-route-parallel-realization.md b/docs/plans/native-route-parallel-realization.md deleted file mode 100644 index dba126a842d..00000000000 --- a/docs/plans/native-route-parallel-realization.md +++ /dev/null @@ -1,70 +0,0 @@ -# Native-route parallel realization (Phase 1 model) - -Durable record of the Phase 1 model sent to `eager-raven-113` and accepted with four binding conditions (dashboard GO, 2026-09-14). This document is the artifact to keep. Implementation is not landed under operator wind-down. - -Independent program beside A (#11224, frozen, untouched) and B (#11217, royal-hawk owns B and the producer counts). Nothing here may make B's AFTER measurement harder to interpret. A's frozen subject is never altered or delayed. - -Gatekeeper: eager-raven-113. Host for later builds/experiments: srv1 (`ssh srv1-lan`; `systemd-run --user --scope -p MemoryMax=24G`; `CTRL_BUILD_MODE=local`). srv2 is reserved for A. Receipt class 3/4 (native driver/observer) → pre-landing exact-head receipt when a later change actually lands code. - -## Not credited - -Do not claim later: - -- `3x` -- `five minutes` -- the serial projection -- semantic equivalence of fierce-lark's merged shard surface - -The shard experiment refuted identity volume, memory pressure, allocator return, and host-load tail as causes. The five-shard signature is not yet module-caused (rotation discriminator first; fierce-lark runs it on srv1). - -## Homes (cite, do not fork) - -Homes accepted as written, not forked: - -- `std.realization` — `Placement.LocalChildProcess` (fifth arm). Serial `SourceRootEvalDriver` remains `LocalInProcess`. A prepare worker is a local OS process with its own address space, not a filesystem cache, not a remote host, not an accelerator. Do not nickname Placement as `WorkerKind`. Do not put the arm in `gunbc.compute`. Worker `RealizedStep` = `ExecuteEffect` + `LocalChildProcess` + `Recompute`. Parent merge stays `LocalInProcess` plus `ReadEffect` over worker artifacts (condition 3). Slice 1 does not take a Share cache obligation across siblings. -- `std.realization_width` — envelope is `process_memory_aware_spawn_width` (cores ∩ memory ∩ pids). This is the bound fold, not a second knob. Do not mint `NATIVE_SHARD_COUNT` / `--width`. `NATIVE_SHARD_INDEX` is an observation label only. **Precondition, fail-closed (landing side chat, 2026-09-15):** that helper is not the envelope on its own — it returns `conservative_fallback_width` when the memory or PID observation is zero, and clamps to one worker when the memory or PID budget fits none, so it can answer with a width outside the observed envelope or admit one worker when physical fit is zero. This realization may call it only after a fail-closed contract has established nonzero, readable CPU, memory and PID inputs and capacity for at least one worker; missing inputs or zero fit produce a named refusal (`NoFeasibleRealization`), and the helper's conservative-fallback and minimum-one arms are not admissible answers here. If a stricter existing operation with exactly those semantics is found at implementation time, bind that instead of wrapping this one. -- `std.decision` — width *choice* is `select_realization` with a named `DecisionSubject` and the two funded axes from `width_fold_objective_goals` (wall, peak memory; both LowerIsBetter). A Pareto front is not a winner. Requested N above the envelope is `NoFeasibleRealization` (later named `PrepareWidthRefusedAboveEnvelope` in the prepare-shard contract), never a silent clamp. Width is not computed inside `std.goal_assessment` / `ensure`. -- `gunbc.compute.work_request` / `work_provider_local` — **do not extend** `WorkOperation`. Prepare shards are native-driver work (`NativeTestContext` / `native_test_prepare_module`), not cargo `CompileEntry`. -- Instrument authority = `scratch/deep-cat-655-span-bisect@5b41958d07d9` (`[native-module-residual]`). Reuse; do not re-derive. Do not retouch `[native-prepare-split]`. Named children subtracted from module parent: prepare, eval, row_accumulation, arm_exit, release, meter. Resolve and infer are reported and nested inside prepare; they are not subtracted. Residual uses checked arithmetic and refuses on negative / missing / duplicate / unmatched. Prepare parent = fixed + Σ module parents + residual; if parent is not nested, refuse. - -## Slice 1 construction (approved shape; not landed) - -One immutable context in the parent (`NativeTestContext` via `native_test_context_from_ingest`). Fork after this. Context is read-only for workers. - -Complete module manifest from that context + `universe.modules`. Assignment: **contiguous slices** over stable module-identity order (condition 4). The same function is used for W=1 and W=N. - -Each worker (`LocalChildProcess`) prepares only its assigned modules and writes one artifact. - -Named merge step: `prepare_shard_merge`. Law: bidirectional identity+payload join against serial (arm i). Added ∪ removed ∪ payload-changed ∪ duplicate ⇒ refuse the equivalence claim. Completeness is the join, not a count equality. Parent exclusive rows gain a named merge span; it is not stuffed into prepare residual. - -## Binding conditions (eager-raven-113 GO) - -1. **Acceptance is three arms, not two.** On one pinned quiet-host subject measure: - - (i) in-process serial (today's production `SourceRootEvalDriver`); - - (ii) W=1 via `LocalChildProcess` + `prepare_shard_merge` (placement price: fork, serialization, merge); - - (iii) W=N. - Report all three with total CPU, aggregate memory, process count, parent/worker partitions, merge span named. `(ii)−(i)` is the placement price; `(iii)−(ii)` is the width effect. Merge-law join runs against (i). -2. **Compose on the row-set reshape (#11374).** Add one variant + one roster entry on the keyed exclusive-row collection. Do not add a field to the old literal `NativeDriverExclusiveRows` struct. Build on a branch that includes #11374's head; rebase when it lands on main. -3. **Worker transport:** artifacts under `RUNNER_TEMP`, one file per worker, content-identified by digest; parent `ReadEffect`; missing / malformed / digest mismatch **refuses** (no partial merge). Digest join is part of the merge law. -4. **Assignment:** contiguous slices over stable module-identity order (printable interval; rotation can move whole sets). Same function for W=1 and W=N. - -Everything else as in the model, including: no second width knob; `NATIVE_SHARD_INDEX` is an observation label; slice 2 declarations land with slice 1 but implementation waits on slice 1's green three-arm receipt; receipt class 3/4; srv1 only. - -RED list for a first implementation push (not this wind-down PR): merge-law added/removed/changed; residual refuse arms; width > envelope refuses; missing worker artifact refuses. - -## Slice 2 — modeled with slice 1, implemented after proof - -Context map/reduce: partition ingest reads by source-root file identity. Mappers produce partial fold state; reduce concatenates roots, merges indices fail-closed on duplicate module names, concatenates file_refusals. Placement: same `LocalChildProcess` arm. Width: same envelope + `select_realization`. Merge name: `context_shard_reduce`. The reduce is not a fallback to serial context. Implementation waits on slice 1's merge law + instrument + three-arm receipt existing and green. - -## Honesty vs a later implementation attempt - -A construct attempt existed on `session/crisp-hawk-831` / #11378 and is **not** the landed program. Known gap against this model: children that re-exec and rebuild context are not fork-after-context; `(ii)−(i)` would be contaminated until workers inherit the parent's context (COW fork or a serialized context artifact) without re-ingest. - -## Where bytes would live (when implementation is authorized again) - -- `dag/std/realization.dag` — `LocalChildProcess` -- exhaustive `Placement` matches (accelerator demo and peers) -- `std.compiler_entry` keyed exclusive roster — `ExclusivePrepareShardMerge` / wire name `prepare_shard_merge` -- `gunbc.native_prepare_shard` — contract (envelope, selection, assignment, merge, artifacts, residual law, slice 2 standing) -- `v1.compiler.emit_rust` `SourceRootEvalDriver` — production default remains in-process; child path is opt-in via placement + selected width (selection receipt, not a second knob) -- witnesses: `dag/test/claim/native_prepare_shard_witness_test.dag` and exclusive-key exhaustive matches including `ExclusivePrepareShardMerge => 0` diff --git a/docs/plans/native-scm-cas-fit-and-consumer-cut.md b/docs/plans/native-scm-cas-fit-and-consumer-cut.md deleted file mode 100644 index c825dedd8c9..00000000000 --- a/docs/plans/native-scm-cas-fit-and-consumer-cut.md +++ /dev/null @@ -1,81 +0,0 @@ -# Native SCM: CAS semantic fit and first consumer cut - -Phase 0 ruling, 2026-09-13. This records the dependency contract for Phase 1; it declares no SCM carriers and implements no transition. The consumer is the Phase 1 implementation and its composition evidence. This record remains the semantic boundary for subsequent authoring and landing routes. - -## Shared authority and payload fit - -The leasing/locking home is `std.durable_compare_and_set`, realized by `gunbc.durable_cas_file_store::file_compare_and_set`. `CasOutcome` needs no additional arm for stale generation, absent target, or unexpectedly occupied target: - -- `CasPreconditionFailed { expected: ExpectSlotGeneration { generation }, observed: CasReadableAbsent }` preserves a missing expected slot. -- `CasPreconditionFailed { expected: ExpectSlotAbsent, observed: CasReadablePresent { version } }` preserves an unexpectedly occupied slot. -- `CasPreconditionFailed { expected: ExpectSlotGeneration { generation }, observed: CasReadablePresent { version } }` preserves a generation disagreement. `CasSlotVersion` carries the observed generation, content hash, and value. - -The successful arm retains the committed version. `CasStoreRefused` retains `CasUnreadableSlot`, including malformed state, missing referenced content, and refused reading. `cas_decide` preserves the readable/unreadable distinction before evaluating the expectation. No SCM-local synonym sum may replace this authority. - -Exact native parent identity and slot generation are different facts. A generation expectation alone does not identify the native parent: the consumer must bind the proposed native transition to the parent held by the observed slot version. Git SHA, PR number, and CAS generation are not native commit identity. - -## Realization findings: payload sufficiency is not execution sufficiency - -Inspection of `file_compare_and_set` confirms that its initial eligibility branches preserve all three precondition cases above. `cas_probe_as_readable` supplies generation, recomputed content hash, and value for `ProbedHead`. - -The realization does **not** preserve the full refusal partition on every path: - -1. `cas_probe_from` treats every unsuccessful `Filesystem.Read` as the chain end. Permission or I/O refusal can therefore become absence or an earlier apparent head. Its comment names typed read failure as unavailable, but `extdeps.filesystem.filesystem_io::Filesystem.Read` now exposes `error_kind`, and `filesystem_exact_read` distinguishes absent, unreadable, and unrecognized failure kind. The adoption capability exists; this consumer has not adopted it. -2. `cas_commit_at` maps every unsuccessful `Filesystem.WriteOwnerOnly` to `CasPreconditionFailed`. It does not establish that another writer won. An operational write refusal can therefore be mislabeled as a precondition failure, even when the re-observed state still satisfies the expectation. -3. That same failed-write branch calls `cas_probe_as_readable`, whose `ProbedBeyondBound` arm deliberately returns `CasReadableAbsent` for its documented reporting use. The defect is consuming that projection to construct a decision payload without establishing a lost race. In contrast, the initial `file_compare_and_set` branch and `observe_cas_slot_state` preserve a bound refusal as `CasUnreadableMalformed`. - -These are defects in consumption of the existing authority, not evidence for a fourth CAS outcome. Phase 1 must repair the realization at its existing home and preserve transport refusal through the shared outcome. `WriteOwnerOnly` currently omits the host `error_kind` output that `Read` and `WriteCreateNew` expose; its consumer must obtain typed write classification without parsing error prose. - -No new wet execution was performed for this ruling. The findings above are source-contract findings, not a claimed rung or proof of durable publication. Exclusive creation must not be confused with atomic visibility of complete bytes or crash durability. The Phase 1 composition evidence must establish those publication properties before a mutable slot may name an immutable object. - -The transport distinction is concrete: `v1_compiler.v1_interpreter::write_file_owner_only` opens the final path exclusively and then writes its content. A post-open failure can leave partial bytes at the slot path. The adjacent create-new contract explicitly treats owner-only exclusion as incidental to permissions, whereas `extdeps.filesystem.filesystem_io::Filesystem.WriteCreateNew` promises create-only publication and its realization publishes complete content through a sibling file. Phase 1 must bind the CAS realization to contractual exclusive, complete publication and establish durability; correcting error classification alone is insufficient. The transport realization home is `extdeps.filesystem.rust_realization`, not an SCM-specific filesystem algorithm. - -## Read-back is a separate relation - -Independent read-back consumes a committed receipt and observes the same target. It cannot rewrite `CasCommitted` into a claim that the CAS did not commit. Store refusal, precondition failure, committed-with-read-back-unavailable, and committed-with-read-back-disagreement remain distinguishable. Only the last two require composition over a committed receipt; neither extends or replaces `CasOutcome`. - -A later target generation also cannot erase the receipt: read-back must distinguish subsequent advancement from disagreement about the committed version. A verified transition requires evidence bound to the exact committed subject. A caller-authored expected value is not independent observation. - -## Exact Phase 1 dependency and consumer cut - -Existing producer symbols to consume: - -Before either SCM slot consumer relies on the store, repair these prerequisites in order: - -1. Bind CAS to contractual exclusive, complete, durable publication at the existing homes `extdeps.filesystem.rust_realization` and `gunbc.durable_cas_file_store`. The finding is `gunbc.recurring_failure_mode.consumer_relies_on_incidental_realization_property::consumer_relies_on_incidental_realization_property`. An SCM-local safe-write helper would fork the existing authority. A correctly classified refusal over torn slot bytes is still unsafe, so this prerequisite comes first. -2. Repair refusal-as-contention and unreadable-as-absence at `gunbc.durable_cas_file_store`, consuming the existing typed filesystem observations. The finding is `gunbc.recurring_failure_mode.store_refusal_reported_as_contention::store_refusal_reported_as_contention`. - -These are source-verified readings of contract mismatches, not executed reproductions. The repair lane owes the discriminating REDs. Both defects affect the existing compute control-plane consumer; absence of an SCM caller does not make them prospective. - -- `std.durable_compare_and_set::{CasAttempt, CasExpectation, CasGeneration, CasSlotVersion, CasOutcome, cas_attempt, cas_decide}`: the shared conditional-publication vocabulary and decision. -- `gunbc.durable_cas_file_store::{admit_cas_attempt, VerifiedCasAttempt, file_compare_and_set, observe_cas_slot_state}`: verified attempt admission, store-owned conditional publication, and independent slot observation. `VerifiedCasAttempt` verifies the encoded payload/hash pair; it does not verify the transitive SCM objects named by that payload. -- `extdeps.filesystem.filesystem_io::{filesystem_exact_read, filesystem_create_new}`: existing typed transport-result classification. Upstream filesystem facts remain at that authority, conforming to `extdeps.external_authority::ExternalModelScope`; SCM policy belongs downstream. -- `gunbc.scm.object_store::{ScmObject, CorpusManifestObjectRef}` and `gunbc.scm.ancestry::RepositoryCommitRef`: existing SCM object and commit reference subjects. Their current locator semantics must not be silently promoted into durable cross-party content identity. - -The first consumer carriers to be declared **in Phase 1**, not here, are a repository-target generation slot and a workspace-scoped stage/base slot. These are semantic names for the cut, not claims that declarations already exist. - -The repository-target slot binds one repository and target to its native parent/commit reference and CAS version. Its conditional advance consumes the exact observed generation and a candidate bound to that parent. The workspace slot binds one independently mutable workspace to its selected base and staged manifest, with its own CAS version. Two workspaces share immutable objects and repository targets, but never one mutable stage/base slot. Workspace identity is independent of author identity. - -For each slot, the consuming route is immutable object put-if-absent publication, durable independent verification of the referenced closure, slot payload encoding, `admit_cas_attempt`, `file_compare_and_set`, independent read-back, then composed transition standing. Any unavailable capability refuses at its own boundary. The first two steps cannot be substituted with an in-memory object-table insertion. - -`gunbc.scm.repository_envelope::RepositoryEnvelope` currently carries one document's stage selection; it is not the multi-workspace slot authority. `gunbc.scm.repository_save::save_repository` persists a checked document unconditionally and must not become the conditional publisher. Phase 1 consumes the shared CAS home instead of cloning it or hiding mutable workspace selection inside that document. - -The cut closes only when wet evidence executes the entire object-to-verification-to-CAS-to-read-back composition for both slot subjects, including contention and independent workspaces. Re-proving the isolated linearizer, declaring these carriers, or landing a ticket does not satisfy that trigger. - -## Mandatory Phase 1 provider gate - -2A, publication integrity, precedes 2B, outcome fidelity. Success means one exclusively published final generation containing the complete proposed value. Failure before publication means no final generation path ever became visible. An occupied target leaves its existing complete generation untouched. `WriteCreateNew` is the nearest surviving construction, but fsync of a private staging file does **not** establish durability of the final directory entry: either the contract and implementation cover the publication metadata, or the claimed durability rung stays lower. - -For 2B, real occupancy or a genuine race yields `CasPreconditionFailed` with the re-observed winner. Permission, storage, IO, staging, sync, or publication failure yields `CasStoreRefused`. Error prose is never parsed to choose between them. Finding no readable winner on a second observation cannot widen a store refusal into contention. - -The repair lane owes these wet controls: - -- Inject failure after staging writes begin: no final generation exists. -- Inject failure before publication completes: no committed CAS outcome is returned. -- Race two eligible writers: exactly one commits; the other reports precondition failure naming the observed winner. -- Cause a non-contention store refusal: preserve `CasStoreRefused`, never precondition failure. -- Restart and independently read back: exact generation, content identity, and value agree. -- Mutate the realization back to direct final-path open-then-write: the partial-publication control turns red. -- Mutate the fold to classify every failed publication as contention: the store-refusal control turns red. - -Phase 1 may not declare the CAS realization suitable for native SCM until **both 2A and 2B pass their wet controls**. This gate is a prerequisite to SCM consumption, not an assumption its tests may make. The three discovery records remain source-verified findings, not executed closures. diff --git a/docs/plans/native-scm-census-coverage.md b/docs/plans/native-scm-census-coverage.md deleted file mode 100644 index bb64a61d809..00000000000 --- a/docs/plans/native-scm-census-coverage.md +++ /dev/null @@ -1,45 +0,0 @@ -# Native SCM census: discovery and coverage standing - -This is an incomplete, source-verified census and an unconnected terminal policy. It is not an executing required-lane import wall. No complete Git-use population, successful policy-control run, or Git-free SCM execution guarantee is claimed. - -The appendable use rows live under `gunbc.git_use_census`; `gunbc.git_use_census.roster::git_use_rows` supplies them to `gunbc.git_use_authority::git_import_authority_verdict` in the pure controls. Adding one use does not regenerate DESIGN. These six enrolled uses are all classified: three workspace/source-authority uses, two proposal/review/publication uses, and one history/diff/gate-observation use. There are zero enrolled bootstrap/import, deployment/fleet-identity, compatibility-projection, or unclassified rows. That does not mean those populations are empty. The Rust population currently has three named rows, and the configured-process population one; neither roster is complete. - -## Discovery is unfinished - -Read-only textual searches nominated candidates for source inspection. Those searches are not a closed discovery oracle, do not establish reachability, and do not authorize a clean census verdict. Each enrolled row was checked against its consuming declaration. Unexamined candidates have not been disguised as typed `UnclassifiedGitUse` refusals. That arm is reserved for an observed use whose role cannot be decided, with its reason recorded. - -The remaining population is **unknown for two of the three surfaces and DERIVABLE for the third**, and an earlier blanket "unknown" here was wrong. Rust uses and configured `.dag` process invocations have no producer that enumerates them, so their remainder is genuinely unbounded at this revision. But `ParsedDagGitDependency` — the one surface this policy covers — is derivable *from the same import fact the policy consumes*: the non-fixture `.dag` modules under `dag/` and `src/` carrying a direct module-level `import extdeps.git*` or `import extdeps.github*`, partitioned by whether the module is itself inside `dag/extdeps/` (the authority side) or outside it (the consumer side). That recipe is the instrument, and its output is deliberately not transcribed here: a count copied into prose is unreachable from the thing that owns it and rots without anyone touching either end (DESIGN §6). Standing the recipe up as an entry point that re-derives the consumer-side population, and joining it to this ledger at identity grain, is the first obligation of the census-completion lane — it is what turns this surface's remainder from an assertion into a closed identity join. The two surfaces without a producer keep their existing next-rung triggers. - -One reading worth recording because it is a positive result rather than a gap: **no module under `dag/gunbc/scm/` carries a direct `extdeps.git` or `extdeps.github` import**, by that same derivation. That is the wall's own subject measured clean today — and it is exactly as narrow as it sounds, since it says nothing about the configured-process bypass, which no import fact can see. - -The existing `v1_compiler.cli_run::run_dag_parse_sweep` discovers paths before parsing and refuses unreadable directories, directory entries, source reads, and parse diagnostics. Its existing parsed `DeclarationIndex` is the specified import producer; this ticket must not add a second scan or parse. However, `DagParseSweep` currently returns the index and a count, not the independent pre-parse source-path roster. The proposed policy input requires that roster separately from the parsed module records. `git_import_authority_verdict` joins their path identities in both directions; an omitted expected source or unexpected parsed source refuses, even when counts agree. An unavailable or explicitly incomplete producer also refuses. Copying successful module paths into both inputs would violate the producer contract, not establish completeness. These declarations do not prove that a producer currently supplies the independent input. - -The remaining capability is to carry the existing required walk's source identities through its parsed-index projection, preserving every failure, then execute the same policy on the required acceptance path. Hand-Rust projection/invocation remains subject to the pending external scaffold approval. No host wiring is authored here. The existing line scanner `v2.std.layer_import_scan::layer_import_facts_live` is expressly unsuitable: its unreadable-source omission is recorded separately under `gunbc.recurring_failure_mode`. - -## The wall's actual population - -The policy declaration itself names all three coverage populations: - -- Direct `.dag` Git/GitHub imports: policy coverage only today. After required-lane enrollment and discriminating controls, closure can be reported mechanically preventable; the forbidden import remains writable. The structural next capability is construction of source dependencies that cannot express a forbidden edge. -- Rust Git/GitHub uses: unreached, reported as use-identity rows in the verdict. The next capability is a Node-derived dependency producer whose source scope includes Rust. -- Configured or opaque `.dag` process invocation: a **KNOWN BYPASS** of the direct-import policy. `gunbc.roadmap.roadmap_event_carrier::carrier_exec` executes `layout.git_program` without a use-site Git import. Closing this bypass requires Node-derived process provenance through capability bindings and caller-supplied arguments. No claim is made that merely finding Nodes establishes that provenance analysis already executes. - -In particular, the policy refuses direct Git/GitHub imports in `gunbc.scm.*`; it does not establish absence of Git execution there. Roster growth approval is another subject: review diligence only, mitigatable, until externally originated approval evidence for the exact changed obligation set is verifiable at the gate. A row cannot attest its own approval. - -## Mode and landing construction - -`NativeAuthorityMode` and the row disposition pair are a declared consumption frontier; the selector that read them is NOT in this PR and lands with its consumer. Their planned consumer is `gunbc.git_use_census.report::report_git_uses`, to be supplied by the separate census-completion follow-up: read `git_use_rows`, select each row's disposition, and render it beside the use identity. That entry does not exist today. Its trigger is execution of the report for both modes on the enrolled population. The removed selector test merely compared each projection with its input field; it established no production consumption. The production authority route belongs to ticket 6, not Phase 1's CAS consumption. It does not make the existing `gunbc.native_scm_interaction_contract` Landed presentation unwritable without evidence. The missing capability is an independently observed exact Git merge/read-back and imported native baseline producer, bound to the candidate, with an exclusive bridge-landing mint. No validator is claimed as construction. - -Bridge publication must deterministically project an exact native candidate to one idempotently maintained PR, independently observe and read back the merge, and import the next native baseline before reporting native landing. A legacy GitHub-main advance imports explicit Git provenance and makes proposals on the previous baseline stale for mechanical re-evaluation. Native-authority landing is the native exact-parent advance; Git projection follows it, and an outage records projection repair pending without rollback or Git fallback. Native identities and Git identities are mapped, never equated. These are future route obligations, not implemented routes in this phase. - -## Evidence and remaining acceptance work - -The first recurring-failure row compiled in a targeted remote closure. The remote compiler build for the pure control suite succeeded, but execution refused with `HostBudgetUnreadable`: that worker exposed no enforceable cgroup memory limit. Thus there is no successful pure-control execution receipt, let alone a required-lane receipt. The targeted policy-closure compile completed with two blocking diagnostics: `bridge` and `native` were not found in scope. Source inspection locates the rejection in `v1.compiler.infer::infer_expr_body`: its local-callable branch uses nonempty parameter cardinality to recognize callability, rejecting the declared zero-argument function parameters. This is a substrate blocker, reported to the parent for repair at its authority; no selector spelling workaround is authored. The requested local control invocation also did not execute: the checkout has no built interpreter, and the shim refused to substitute another revision. No local compiler build or budget override was used. The CAS and producer findings are SOURCE-VERIFIED readings of contract mismatches, not executed reproductions. - -The exact-path controls still owed are forbidden-import refusal; legal adapter and rostered-legacy acceptance; refusal after withdrawing or misclassifying either authorization; incomplete/unavailable producer refusal; a host mutation ignoring the policy refusal detected by a control; and a policy mutation admitting the forbidden edge detected by a control. Pure fixture tests do not discharge those required-lane obligations. The full production census, adapter inventory, source identity producer, host connection, and these execution receipts remain unfinished. - -The CAS ruling and ordered Phase 1 producer/consumer cut are in `native-scm-cas-fit-and-consumer-cut.md`. Its provider repairs are prerequisites, not work implemented here. - -The parent amended the Phase 0 exit gate to land these six rows explicitly incomplete. Closing the production census population is a separate follow-up lane owned by the parent, not an implicit addition to Phase 1. Required-lane wall wiring remains a declared frontier; the present identity-join input contract is not a receipt from an independently populated executing producer. - -The data-versus-callable decision is separate from the inference defect. Each constant disposition already lives in a `data GitUseRow`. The generic selector accepts deferred computations so only the selected route is invoked; those parameters are not nullary declarations returning constants. Replacing them with result values would change selection-before-execution into selection between already supplied results. That is not the intended route contract even with a working compiler. The selector specimen remains unchanged and blocked on the parent's separately dispatched nullary-callable inference repair. That lane owns the root-cause fix, its execution evidence, and its failure-mode row. diff --git a/docs/plans/native-succession-consumer-cutover.md b/docs/plans/native-succession-consumer-cutover.md deleted file mode 100644 index 307cb896ed1..00000000000 --- a/docs/plans/native-succession-consumer-cutover.md +++ /dev/null @@ -1,52 +0,0 @@ -# Native succession and operational cutover - -This is the L4 execution design, not an acceptance receipt. `v2.compiler.self_host.promotion_admission` owns promotion; `gunbc.v1_consumer_census` owns consumer relations and projects them into `gunbc.replacement_cut`. No generation has been promoted or consumer switched by this change. The native executable and compile interface are the L2 handoff, not assumed inputs that this document can supply. - -## Consumer trace - -The additional relations are produced by `gunbc.v1_consumer_census` `v1_native_cutover_selector_consumers` and `v1_release_pack_consumers`. Their discovery method and inspected revision/tree are carried by `producer_native_cutover_selectors`. The older producers retain their own pins. The pack relations derive from `gunbc.ci_release_bins` `ci_floor_required_artifact_names`; they are not a copied executable roster. That function enumerates required build/pack dependencies, which does **not** establish that each packaged executable runs. - -| Operational path | Existing selector / dispatch | Dependency that cutover must remove | -| --- | --- | --- | -| Cold CI and fleet builds | `gunbc.repo_self_build` `repo_self_build_package`, read by `repo_self_build_command` and `repo_self_build_all_bins_command` | Builds package `v1-compiler`; installing a native `gunbc` alone leaves this route live. | -| Missing witness executable | `tools.host_prelude` `witness_bin_ensure_built_typed` → `witness_bin_build_args` | Builds the enrolled binary from `v1-compiler` when the release path is missing. A warm run does not exercise this branch. | -| Claim dispatch | `tools.host_prelude` `run_gunbc_claims_ready` → `claim_batch` | The binary links the v1 interpreter. Required witness identities and their positive/refusal contracts must survive replacement. | -| Required build and witness lanes | `gunbc.witness_floor_workflow` `build_lane_run_step`, `witness_floor_run_step` | Both execute `claim_executor`; these relations already existed in the census. | -| Generated-artifact healing | `gunbc.witness_floor_workflow` `heal_regen_step`, `heal_repair_declaration_step`, `heal_repo_local_git_config_step` | Executes `gunbc run` for generation, agreement, pre-mutation repair observation and repository configuration. | -| Ordinary CLI run | `v1_compiler.main` `run_verb` → `v1_compiler.v1_interpreter` `run_in_context_with_args` | Linked interpreter execution remains even if a separate native compiler handles `compile`. | -| Required seed regeneration | `v1_compiler.required_regen_host` `compile_stage0` → `v1.compiler.compile` `compile_sources_selected` | Calls the v1 implementation in-process over the regeneration source population. | -| Rebuilt-seed second pass | `v1_compiler.required_regen_host` `run_built_seed_regen` → rebuilt `claim_executor --required-regen` | The executable digest is checked, but the executable is still the seed. This route is not evidence for seed-independent G1→G2. | -| Release pack, including product transports | `gunbc.ci_release_bins` `ci_floor_required_artifact_names` | Each target receives its own consumer relation. In particular, the Codex stdio transport's product contract cannot be retired as a compiler test. | -| Git hooks | `gunbc.githooks_pre_commit_emit`, `gunbc.githooks_pre_push_emit`, `gunbc.generated_artifact_merge_driver` | Current pre-commit/pre-push scripts configure git and format; the merge driver refuses and prints a repair recipe. Printed regeneration commands are not an automatically executed compiler path. The existing historical recipe relation does not prove hook execution. | - -These are source-body and caller traces, not measurements of executed processes. The census remains `PopulationMeasuredLowerBound`. Three gaps remain explicit: unclassified DAG argv rows outside the selected roots; dependencies internal to the deletion set beyond the traced regeneration and workspace-member relations; and whole-repository reachability. Neither a missing lexical match nor a present pack member settles reachability. Historical `Retire` rows are not proof that the current corresponding path has disappeared. - -The census's lower-bound standing must survive the eventual promotion/cutover composition. An empty blocking set in an incomplete population is not terminal admission. Likewise, a live pack projection observed against a newer tree does not refresh the discovery pin: re-run the manifest and caller joins before using those relations as cutover evidence. The name-to-main mapping in `v1_release_bin_main` was checked against the manifest at the declared discovery pin; future manifest path changes require remeasurement. - -For the next census pass, start from actual entrypoints and follow both the present-artifact and missing-artifact branches. Join workflow argv to its emitting declaration, binary name to the Cargo target/path, target to linked implementation, and runtime dispatch to the invoked operation. Retain the witness or input selecting a conditional branch. Report unresolved dynamic invocations as unmeasured edges, never as absent consumers. Cargo targets outside the release pack also need their own demand/disposition evidence before the population can become complete. The manifest inspection found `carrier_realization_census` and `type_occurrence_binding_census` outside that pack; both link v1 and remain unclassified here. Their absence from the pack is not evidence that their capability obligations can be deleted. - -## Proposed seed-exclusion boundary - -The producer must establish unavailability, not report that a trace happened to contain no seed call. This design is not yet an implemented sandbox or a receipt. - -1. Materialize a disposable execution root from an explicit input manifest: the pinned authoritative source closure S, the exact native G1 artifact, and the declared external toolchain and system dependencies. Do not mount the repository checkout, seed executables, seed source/build workspaces, shared Cargo target outputs, host home, Docker socket, or the oracle's filesystem. Remove inherited descriptors and credentials, prohibit network access, and constrain filesystem/process access to this root. An empty PATH alone is insufficient: an absolute path, inherited descriptor or helper could still reach the seed. -2. Build in a new isolated workspace/target directory using the emitted implementation. Join the complete Cargo dependency graph and every build-script/source input against the admitted build manifest. Refuse unknown dependencies and any retired implementation dependency. Package-name bans and `ldd` output alone cannot establish this: renamed packages, static linkage, or copied seed bodies evade them. The L1/L2 declaration-to-produced-source coverage and producer provenance are prerequisites for the claim that G1 itself contains the replacement implementation. -3. Launch G1 on S inside that boundary to produce G2 source; build G2 under the same input restrictions. The seed oracle runs separately and supplies comparison observations only. It cannot be a build dependency, subprocess, service, or fallback available to G1/G2. -4. Bind an enforceable memory cgroup for the launched process and its descendants and read the effective limit back before execution. A declared planning number is insufficient. The executor may be BuildBuddy or a CI runner; the capability, source pin and artifact provenance decide admission, not the venue. A sized BuildBuddy runner with a writable delegated child cgroup is an available realization to investigate, so no new required CI job is implied. -5. Exercise the boundary with deliberate attempts to execute a seed path, open a seed artifact, resolve a seed Cargo dependency, and invoke a fallback. Require the expected located denial from each attempt beside a permitted native/toolchain control. A failing process without the denial observation is unavailable evidence, not proof that exclusion worked. - -The correctly hashed rebuilt-seed specimen is filed in `gunbc.recurring_failure_mode.authority_substitution`. Its distinguishing control is a candidate wrapper whose self-launch succeeds while its build still links the retired compiler; replacement admission must refuse that wrapper. This control complements the external-access denials, since an in-process seed makes no external call to deny. - -The general host isolation and launch operations must be modeled at their owning `std`/`extdeps` layer before implementing a transport. L4 must not put an unmodeled sandbox into hand-Rust and then bless it with a Bool. - -## Binding evidence to the launched artifact - -`BehavioralComparisonObservation` currently carries outcomes and a corpus, and its receipt projections accept a caller-supplied `GenerationIdentity`. That API alone cannot prove which executable ran. For native promotion, the execution observer must obtain identity from the immutable artifact it actually launches and carry the observed launch binding through receipt assembly. - -A candidate realization is a sealed executable object or an immutable content-store object held by the launcher. The same object is hashed using the existing observed artifact digest carrier and executed; hashing a pathname and later reopening it leaves a replacement race. The launcher owns this binding, captures process completion and case output, and rejects any mismatch with the declared artifact before producing receipts. A read-only mount is only sufficient if its backing object cannot be replaced or modified for the run's duration. Dynamic loaders and shared libraries belong in the admitted dependency manifest too. This is a proposed transport contract, not a claim that current `WitnessBin.Run` enforces it. - -The remaining generation axes come from the pinned production inputs: producer artifact, source closure, target model, toolchain and build configuration, including build location or a verified path-remapping policy. G2 provenance joins the exact G1 launch, exact S input, emitted population and resulting G2 artifact. The caller cannot turn an unrelated successful observation into evidence for this chain merely by naming G1 or G2 at assembly time. - -Behavioral evidence must join required **case identities and outcomes**, including positive cases and intentional refusals. Equal counts are insufficient, as the census's same-size substitution control demonstrates for consumer identities. Planted faults need a reported disagreement at the expected case; process failure alone is not detection. The corpus used for G1, G2 and the fault control must be the same admitted population. Consume `gunbc.explicit_witness_admission` `known_red_probe_execution_roster` through its existing self-host projection; expected-red quarantine for a missing required capability remains open. Consume `seed_retention_frontier_roster` for retained obligations. Discharge each through execution or an explicit changed contract; do not copy either population into an L4 backlog. - -Only after those observations exist can they feed `admit_promotion`, and only after named consumer executions select its admitted artifact and old operational routes are retired can the terminal cutover claim hold. Required generated-file byte agreement remains a separate live obligation throughout; behavioral promotion does not waive it. diff --git a/docs/plans/nominal-type-declaration-plan.md b/docs/plans/nominal-type-declaration-plan.md deleted file mode 100644 index 6a41a0fb660..00000000000 --- a/docs/plans/nominal-type-declaration-plan.md +++ /dev/null @@ -1,260 +0,0 @@ -# PLAN — nominal types as the first consumer of the coercion frontier; delete refinements - -**Status: PLAN, revision 5 — design only. Do not re-enqueue.** Revision 4's O3, O4 and O5 are RULED (2026-10-02); O1, O2 and C5 are recut here on the operator's ruling; O6/C4 is a separate lane. Earlier history: Revision 3 (one declaration modifier -per point in the visibility plane, plus a `derives` list) was approved and then withdrawn by the -operator on 2026-10-02: too many keywords, and the nominal question is really a coercion question. -This revision recasts it as the **first consumer of the coercion frontier** that -[v2 compiler architecture](v2-compiler-architecture.md) §1.4 and its frontier line ("Coercion and -named conversions") name. §8 places revision 3 (option A) side by side with this model. - -Governing sections: DESIGN §4 (operations from inhabitance; the coercion ruling, `bool_indicator`), -§3 (replacement migrations), §4b, §5; architecture plan §1 (completion law, residual addressees), -§1.4. - -Every count is **provisional** — line greps at main `dd0614551b5`, not the census — and is not a size -(DESIGN §6). M0, the resolved census (`gunbc.instruments.type_declaration_use_census`, being built by -tidy-koi-264), is the only admissible size. - -> **PARKED (operator ruling, 2026-10-03).** M0 landed as an instrument -> (`//gunbc/instruments:type-declaration-use-census`, gunbc#13093) with **no M2 sizing**. Its receipt, -> instrument-dispatch run [37119024146](https://github.com/gunb-ai/gunbc/actions/runs/37119024146), is -> no observation (exit 2): native resolve reaches only part of the corpus and decides no casts, so the -> migration cannot be sized. The program resumes when a re-run of the instrument shows enough native -> resolve and infer coverage. S1 and P1–P3 are moot until then. Re-run the instrument for any figure; -> none is carried here. -> -> **Open items from M0's census** (posted on gunbc#13024 by tidy-koi-264; to be settled when this -> resumes): -> -> 1. **Bodyless declarations that are not `nominal_opaque`** (`type MachineWidth` and similar) -> have no row in the declaration table, and the census reports them as `BodylessUnclassified`. The -> plan must say whether they are phantom/index types, abstractions with no view, or a further class. -> Under rev 5's model they are the bodyless form, but whether every one of them needs modeled -> operations (§3) is not decided. -> 2. **Existing one-field records.** The census classes them as `UnconstrainedNominal`. Under rev 5 a -> one-field record *is* a nominal (records are nominal), so that reading agrees with §2. Whether they -> should still be reported as a separate row, so that authored brands and pre-existing records stay -> distinguishable in the migration, is open. -> 3. **Opaque's second disjunct** ("a nominal whose view no consumer outside its module reads") is not -> computed. Only `nominal_opaque` decides `Opaque`. Computing it needs view-read evidence, a -> representation read outside the declaring module. Rev 5's §3 relies on exactly that evidence -> (`EffectivePosixPrincipalName` → record), so the generated cutover needs it. -> 4. **`OperationWorkaround` for encoding** cannot be classed while encoding's capability home (S2) is -> unnamed. Until then, encode-shaped strips go to the exception list with the callee named in their -> evidence. - -## 1. What stays as ruled - -- `type X = Y` is a transparent alias: X and Y are the same type. -- Refinements (`where `) are deleted. Every invariant becomes a checked construction one layer - down. -- No typecase, and no conditional design for one. Matching is on constructors and closed sums. -- M0, the resolved semantic census, sizes the migration. -- The cutover is one atomic, generated PR that deletes `where`, `brand`, `nominal_opaque`, the v1 - spelling matches and hand tables, `gunbc.where_refinement_predicate_vocabulary` with - `WherePredicateGrounding`, and #12506's parked row together (the full consumer list is revision 3 §6, commit 97dea7c55fd, and stands unchanged). -- Checked construction returns `Result` residue plus a typed cause (`std.error_primitives` `Result`). - -## 2. The model: four forms, no new words - -> alias = equality · record = visible constructed data · bodyless = abstraction · functions = explicit progress - -| form | meaning | in | out | operations | -|---|---|---|---|---| -| `type X = Y` | alias; X and Y are the same type | — | — | Y's | -| `type X { value: Y }` | a new type with visible data | its constructor (confinable by `sole_constructor` + `admit_callers`) | field projection | product inhabitance (§4) | -| `type X` (bodyless) | an abstraction; the declaration is all a client sees | only by the module's modeled operations | only by the module's modeled operations | only modeled operations | -| `fn` | explicit progress between any two types | — | — | — | - -The brand (`where brand("…")`), the refinement and `nominal_opaque` stop being declaration facts: - -| old mode | becomes | -|---|---| -| brand | one-field record, public constructor | -| refinement | one-field record, `sole_constructor`, plus a public checked function `Y -> Result` | -| `nominal_opaque` | bodyless `type X` whose operations its module models (§3), or a record when the census shows nothing needed hiding (as for `EffectivePosixPrincipalName`, §3) | - -No crossing between a nominal and its representation is implicit (§5). A crossing is a residual -addressed to the call site (O4, ruled: the addressee home is `v2.std.residual`), and the developer -answers it with an ordinary function call (O5, ruled). - -**Routes are proven, never inferred from a signature (O1 as ruled).** A signature proves neither -exactness nor injectivity: `id.value`, `concat("roadmap:", id.value)` and `sha256(id.value)` all have -type `RoadmapNodeId -> String`. So every function remains a valid **explicit** conversion when it is -called. A function becomes a **compiler-discoverable route** (one a residual may name as its answer, -and one operations may be derived along) only when the compiler derives a structural homomorphism -witness for its body: - -- `X -> Y` whose body projects the sole field; -- `Y -> X` whose body applies the sole constructor; -- `Y -> Result` whose success arms construct `X` from the input by the sole constructor. - -Two proven routes for one crossing refuse as `AmbiguousTargetCandidate`. There is no keyword and no -route table: the witness is the authority. - -## 3. Bodyless types, proved against every current `nominal_opaque` site - -The bodyless form already exists and is already the abstraction/target-realized form: `std.types` -declares `type Unit`, `type Json` and `type Bytes` with no body, and `v2.std.type_binder` wraps a -bodyless declaration (`type_opaque_wrapper`). There are two `nominal_opaque` declarations today. - -**`std.types` `Secret` (`nominal_opaque = String`).** Today it is opaque in name only. The corpus has -`as Secret` casts in (M0 `NominalIntroduction` rows targeting `Secret`; most in `test.claim.*` fixtures; production mints in -`extdeps.cloud.gcp.adc_document`, `extdeps.cloud.gcp.secret_manager`, `gunbc.spark.glm_canary_converge`), -and `as String` casts out, e.g. `extdeps.cloud.gcp.secret_manager` -`encode_sm_access_version_payload_wire` and `extdeps.cloud.gcp.secret_ref` (M0 counts -them). Both directions are unconfined, so DESIGN §4b's "cosmetic until construction enforces it" is -literally its state. As `type Secret` (bodyless) it needs: - -- **trusted construction**: the mints are upstream reads (secret-manager payload decode, the ADC - document, credential reads) plus test fixtures. Each becomes a call to a modeled operation of - `Secret`, e.g. `secret_of_upstream_text(s: String) -> Secret`, confined by `admit_callers` to the - named upstream decoders and the named fixture modules (precedent: `extdeps.apple.app_attest` - `attestation_verification_from_implementation` admits named test fixtures); -- **trusted elimination**: the wire encoders and transports call a modeled `secret_wire_text(s: Secret) - -> String`, likewise confined; -- **its target realization**: `gunbc.rust_source_type_bindings` already has a row binding `Secret` to - `std::string::String`. It is `StillBareNameDebt`, waiting on "the same threading capability as Bytes". - -**`extdeps.access.posix_effective_principal` `EffectivePosixPrincipalName` (`nominal_opaque = NonEmptyStr`).** -It has one mint (`effective_posix_principal_observation`) and no read outside its module. Nothing is -hidden from anyone, so it is a **record** with `sole_constructor`, not a bodyless type. M0 confirms -there is no outside read before the cutover generates it. - -**The capability that does not exist yet: realized operations of an abstract type.** Today a -bodyless type's operations are host seams: `std.bytes` `utf8_encode_bytes` and `bytes_octets` are -self-calling bodies that the interpreter intercepts by **name**, and the Rust emitter realizes them from -a name-keyed bridge table (`extdeps.languages.rust.emit`). `std.bytes` marks that arrangement itself as -a `Scaffold` (`bytes_seam_host_realization_marker`). So `type Secret` has no trusted -construction or operation support that is not a name-keyed seam. Per the ruling, that capability is -what this program names and builds, rather than a word: - -> **AbstractOperationRealization:** an operation of a bodyless type is a function whose body is a -> target-realization row keyed by the function's **declaration** (not its spelling), whose -> mint and elimination are confined by `admit_callers`, and whose realization per required target -> (interpreter and Rust today) is declared beside the type's own declaration-keyed binding row. - -It dissolves the `Bytes` seam scaffold and the `Secret` `StillBareNameDebt` row with one mechanism. -It is also the "threading capability" that the row's restoration trigger already names. Its home is -the existing realization-binding authority (`gunbc.rust_source_type_bindings` and its interpreter -counterpart), extended by declaration key. It is not a new vocabulary. - -## 4. Operations: equality and hash from product inhabitance - -- A **record** has equality and hash when every field does. That is product inhabitance, so a - one-field record over `String` is comparable and hashable because `String` is - (`std.algebra` `algebra_profile_equality_extensional`; `std.content_hash` `HashFamily`). - `RoadmapNodeId == RoadmapNodeId` works, and `RoadmapNodeId` where `String` is declared still refuses: - the operation is derived, and the value does not cross. -- A **proven embedding** (a route with a derived witness, §2) may also carry equality and hash. A - signature never does. -- **Ordering never by default** (O3, ruled). It requires an operation the module writes. -- A **bodyless** type gets only its modeled operations. `Secret` has no display, JSON or equality unless - its module models them. Constant-time comparison is a modeled operation, realized per target. - -No `derives` list exists in any form. - -## 5. What `v2.std.coercion` and `v2.std.inhabitance` must gain (model-first) - -What exists today: `CoercionResult { target, quality: Identity | Exact | Widened, witness }`; -`CoercionMismatchKind = NoTargetCandidate | AmbiguousTargetCandidate | StructuralMismatch | -WouldLoseInformation`; `find_witness` under `TargetSelectionPolicy`; `declared_type_inhabitance` over -`DeclaredTypePosition`. `v2.std.refinement_widening_predicate` is representation value-set widening -(`rust i32 → python int`). It is not the `where` machinery and is kept. - -1. **C1: residual addressee** in `v2.std.residual` (O4, ruled). It is minted once and consumed by - `v2.std.coercion` first, then by completion (architecture §1.5). Each `CoercionMismatchKind` maps - totally to its addressee. -2. **C2: `ChangesInterpretation`**, a mismatch kind for an exact, lossless crossing that drops or adds - meaning (`RoadmapNodeId → String`, `true as Int`). Its addressee is the call site. -3. **C3: proven route discovery.** It derives the §2 homomorphism witness from a function body - (sole-field projection, sole-constructor application, success-arm construction), reusing - `v2.std.witness` `HomomorphismWitness`. A residual names the proven routes as its answer. Two proven - routes give `AmbiguousTargetCandidate`. -4. **C4: native `sole_constructor` minting and enforcement plus `admit_callers`.** This is **a separate - lane**, dispatched by sharp-raven-357 on 2026-10-03, closing both native drops - (`gunbc.rung_drop.admit_callers_discarded_on_the_native_route`, and `sole_constructor` neither minted - nor enforced natively). It is referenced, not built here. -5. **C5: product inhabitance for equality and hash** in `v2.std.inhabitance`: a record inhabits - equality or hash when its fields do. A proven-embedding arm is second. There is no signature arm. -6. **C6: AbstractOperationRealization** (§3), with `Secret` and `Bytes` as its first two consumers. -7. **C7: the first instrument** named by the architecture frontier: one core module emitted to the - Rust and TypeScript targets, with the per-target mismatch list checked against M0's rows. - -The migration is then generated from M0: brand → one-field record; refinement → `sole_constructor` -record plus checked function; `Secret` → bodyless with modeled operations; `EffectivePosixPrincipalName` -→ `sole_constructor` record; aliases untouched; introduction casts → constructor or modeled-mint calls; -workaround casts (strip to compare or hash) → the derived operation; true raw consumers → explicit -projection or modeled elimination. - -## 6. Controls (expecting red first) - -The six arms of `test.claim.brand_nominal_identity_witness_test` are kept, re-spelled over records: -COLLISION and MISMATCH refuse; CONSTRUCT, STRIP and DUAL accept; UNDECLARED_SITE refuses at parse. -These controls are added: - -| control | expected after | today | -|---|---|---| -| `RoadmapNodeId` at a declared `String`, no call | residual `ChangesInterpretation`, call site, names the proven projection | no addressee: **red** | -| `concat("roadmap:", id.value)` as a function of type `RoadmapNodeId -> String` | valid when called; **not** named as a route | **red** until C3 exists | -| two proven `X -> Y` routes | `AmbiguousTargetCandidate` | **red** | -| `Port { value: 0 }` outside the module | refuse on the v2 route | **red** (C4 lane's control) | -| `port_of(n: 0)` / `port_of(n: 80)` | `Err` with residue and cause / advances, by execution | positive control | -| `RoadmapNodeId == RoadmapNodeId` | accept by product inhabitance | M0 measures | -| `s as Secret` for a String | refuse; only `secret_of_upstream_text` from an admitted caller | **red** (cast admitted today) | -| `secret_of_upstream_text` from an unadmitted module | refuse | **red** | -| `Secret` displayed, JSON-encoded or compared, nothing modeled | refuse | **red** | -| `Secret` on the Rust target through the declaration-keyed row | emits and runs | **red** (`StillBareNameDebt`) | - -None of these retires (DESIGN §4b(4)). - -## 7. Rungs (DESIGN §4b) and §4's coercion law - -- **Identity:** structurally guaranteed. Records are nominal, and a bodyless declaration is its own type. -- **Invariants and confinement:** **no invariant is called structural until both native drops in the - C4 lane are closed.** Until then the v2 route is mechanically preventable at best, stated per type. -- **Secret:** structural only once C4 and C6 both hold. Before that it is cosmetic, as it is today. -- **Coercion law:** unchanged. The only silent route is the alias's identity. Every nominal crossing is - an explicit call, and a missing one is a residual with an addressee. - -## 8. Revision 3 (option A) against this model - -Populations are M0's rows, named by class; none is transcribed here (DESIGN §6). - -| | option A (rev 3) | rev 5 | -|---|---|---| -| **new words** | `nominal`, `sealed`, `opaque`, `derives(…)` = 4 | 0. Bodyless, `sole_constructor` and `admit_callers` already exist | -| **TrueAlias** | untouched | untouched | -| **UnconstrainedNominal** (old brand) | `type T nominal = Y` | `type T { value: Y }` (constructor in, projection out) | -| **CheckedConstructionStage** (old refinement) | `sealed` + checked ctor | `sole_constructor` record + checked function | -| **Opaque** | `opaque` | bodyless `type X` + modeled operations (needs C6) | -| **introduction sites** (M0 `NominalIntroduction` rows) | rewritten to a constructor call | same rewrite, generated; identical burden | -| **projection sites** (M0 `NominalProjection` and `OperationWorkaround` rows) | rewritten to `.value` | rewritten to explicit projection; **workaround sites** (compare, hash) **vanish** under C5 product inhabitance instead of being rewritten, so this model's burden is lower by exactly M0's `OperationWorkaround` count | -| **capabilities** | `derives(…)`: author-listed, a second vocabulary to keep in step with `std.algebra` | derived by product inhabitance; nothing listed; a bodyless type has only modeled operations | -| **invariant rung** | structural once `sealed` is enforced | structural once the C4 lane closes both native drops: **the same precondition**, already modeled and with a seed implementation | -| **`RoadmapNodeId` as `String`** | refuses; author projects `.value`, with no stated reason | residual to the call site naming the proven projection: the acknowledgment the operator asked for | -| **needs that do not exist** | 3 modifiers + derives parser/infer, visibility semantics, capability derivation | C1 addressee, C2 kind, C3 proven routes, C5 product inhabitance, C6 AbstractOperationRealization (C4 is its own lane) | -| **reusable beyond nominals** | no | yes: C1–C3 and C6 are frontiers the architecture plan already owes (`bool_indicator`, target emission mismatches) | - -The deciding row is the last one. Option A built a nominal-only mechanism next to an unfinished -coercion model. Revisions 4–5 finish the coercion model, and nominals fall out of it. That is §2's -test that net concepts must not grow by re-invention: `sealed` was a re-invention of -`sole_constructor`, which the revision 3 DFS missed. - -## 9. Decisions - -Ruled (2026-10-02/03): O3 (no ordering by default), O4 (`v2.std.residual`), O5 (answer by function -call); O1 recut to proven routes (§2); O2 recut to the bodyless form (§3); C5 recut to product -inhabitance (§4); C4 a separate lane. Left for the operator: - -- **P1: AbstractOperationRealization's home.** Recommend: extend the existing declaration-keyed - realization binding (`gunbc.rust_source_type_bindings` and its interpreter counterpart) from types to - a bodyless type's operations. Do not open a new module. -- **P2: Secret's fixture mints.** Most `as Secret` sites are `test.claim.*` fixtures. Either each test - module is listed in `secret_of_upstream_text`'s `admit_callers` (explicit and long), or a separate - confined `secret_fixture_of` operation admits a fixture prefix. Recommend: **per-module - `admit_callers`**, generated by the cutover from M0. A prefix is a policy hole any new test module - falls into silently. -- **P3: sequencing.** C1 → C2 → C3 → C5 land model-first, beside the C4 lane. C6 lands after C4 (its - confinement needs C4). The generated cutover lands after all of them. M0 runs independently. - Recommend: as listed. diff --git a/docs/plans/one-namespace-hierarchy.md b/docs/plans/one-namespace-hierarchy.md deleted file mode 100644 index 503531d453d..00000000000 --- a/docs/plans/one-namespace-hierarchy.md +++ /dev/null @@ -1,67 +0,0 @@ -# One namespace hierarchy (owner direction 2026-09-25) - -## The ruling - -There is ONE hierarchy, and every context uses it. - -A node's position is its path in the namespace graph `x.y.z`. Walking that path -from a node to the root yields every alignment level: the issue's own node, -each named ancestor, the implied nodes (the repo level — "gunbc" — and every -folder/namespace segment in between), up to the root. - -- **Alignment is the namespace walk.** Working an issue nested in the - hierarchy means aligning to each parent in turn, up from your own node. The - alignment ladder's chain (task → projects → root) becomes a derivation over - the namespace path, not a separately maintained relation. -- **Implied nodes are first-class levels.** "gunbc" (the repo) and every - intermediate namespace segment exist as alignment levels even when no issue - or project row names them. A level's existence does not depend on a row; - rows attach to levels, levels come from the namespace. -- **A project is an instantiation of a time-bounded namespace chunk of work** — - usually one touching a namespaced program (e.g. `gunbc.spark.*`). A project - is not a parallel tree; it is the namespace slice - `(subtree root, time window, work intent)`. -- **Issue hierarchy is the same graph.** Upstream/downstream (parent/child) - relations between issues are contiguity in the namespace; a child issue's - default parent is its namespace parent. Blockers remain the SEPARATE - must-complete-before-start dimension (dependency edges, not hierarchy). - -## What this unifies (the three graphs we currently carry) - -1. The module/file namespace (`gunbc.roadmap.roadmap_page`, folder paths) — - today implicit. -2. The alignment ladder's hand-maintained task→project→root rows - (`roadmap_alignment.dag`) — today a parallel relation. -3. The issue tracker's parent/child fields (`extdeps.google.issue_tracker`) — - today a third relation. - -Target: one namespace graph; (2) and (3) become projections/annotations of it. - -## Consequences queued - -- The alignment chain derivation becomes: walk the node's namespace path, - materialize implied levels, overlay declared rows (projects/issues) at their - levels. `AlignmentChainRowUndeclared` then means "a row references a - namespace level that does not exist," and ambiguity means "two declared - owners for one level." -- The issue hierarchy (nested tickets, tracker lane) derives its nesting from - the same walk — the nested-menu rendering is the namespace tree with issues - attached. -- Dispatch alignment (the pending authority consolidation) consumes this: - the brief renders every level from the node to the root, each with its - alignment standing. -- Witness discipline: one derivation, three projections — a change to the - namespace graph must move all three projections together; a projection that - can move independently is a derivation defect. - -## Open questions (owner, when convenient) - -- Namespace identity for implied levels: is `gunbc.spark` a first-class node - with its own standing/state, or only a waypoint that aggregates its - descendants' standing? (Affects the cadence math and the dashboard's tree.) -- Do filesystem folders and dag module segments share one namespace by law, - or are they two namespaces with a declared mapping? (They are currently - close but not identical: `dag/gunbc/roadmap/` ↔ `gunbc.roadmap.*`.) -- Project time-bounds: does a project's window attach to the namespace slice - itself, or to one instantiation row, allowing sequential instantiations of - the same slice? diff --git a/docs/plans/org-admin-credential-acquisition.md b/docs/plans/org-admin-credential-acquisition.md deleted file mode 100644 index 4576c11c741..00000000000 --- a/docs/plans/org-admin-credential-acquisition.md +++ /dev/null @@ -1,129 +0,0 @@ -# Org-admin credential acquisition, custody, and read path - -Status: modeled; no credential has been obtained, stored, or read. The storage-class and freshness -ruling is from the parent relay of the 2026-08-30 pricing-study memo. Upstream capability and token -facts are owned by `extdeps.github.org_admin_auth` and cite GitHub's documentation there. - -## Decision - -The terminal credential is a GitHub App installation access token. The durable secret is the app -private key; the actuator mints a narrowly scoped installation token for each invocation and lets -that token expire after one hour. This makes the broadly useful bearer token short-lived and -programmatically renewable. - -The acceptable interim is a human-created fine-grained personal access token whose resource owner -is the organization and whose permissions are organization `Self-hosted runners: write` and -`Administration: write`. Store it with an explicit expiry and rotate-before date. A classic PAT -with `admin:org` works but is broader than the fine-grained PAT. An OAuth App token also works after -interactive web or device authorization, but is user-bound and long-lived by default; it adds an -application and refresh lifecycle without improving this interim. Neither is selected. - -## Acquisition census - -| Shape | Converge access | Acquisition | Lifetime and rotation | Revocation | -|---|---|---|---|---| -| Fine-grained PAT | `organization_self_hosted_runners` read/write covers runner groups, selected repositories, and organization runner registration tokens; `organization_administration` read/write covers the organization Actions policy | Human creates it in GitHub's web UI; organization approval may be required | Configurable expiry; rotate before expiry and revalidate after replacement | Token settings, organization policy/approval, user removal, leak detection | -| Classic PAT | `admin:org` covers the same organization endpoints (and more) | Human creates it in GitHub's web UI | User-selected expiry; GitHub may revoke on expiry, exposure, or prolonged non-use | User credential settings, OAuth authorization API, organization/enterprise incident controls | -| GitHub App installation token | Installation with organization `Self-hosted runners: write` and `Administration: write` covers the converge surface | App registration/key and installation require an owner decision; each access token is then minted programmatically from the app key | Installation token expires after one hour; mint per invocation, never rotate a stored installation token | Suspend/uninstall the installation, reduce permissions/repositories, or revoke/rotate the app private key | -| OAuth App user token | `admin:org` covers the organization endpoints while the authorizing user remains an org admin | Interactive authorization-code web flow or device flow | Long-lived by default; optional eight-hour access token plus refresh token | User revokes authorization, app owner revokes token/authorization, org/enterprise policy, user loses admin standing | - -The GitHub endpoint authorities are `extdeps.github.org_admin_auth.runner_groups_citation`, -`actions_permissions_citation`, `installation_token_citation`, and `oauth_flow_citation`. The model -does not bind any of them to `gh`: CLI, REST, and SDK are interchangeable realizations. - -## Custody and read path - -`gunbc.auth.org_admin_credential.SecretMaterial` is a custody descriptor, not secret bytes. It -records one store, an allow-list of readers, rotation, credential standing, and the validation -receipt. Actual bytes remain behind the existing -`gunbc.auth.materialized_secret.with_materialized_secret` bracket; this plan does not create a -parallel reader. - -For CI, use a GitHub Actions organization secret restricted to the repository and workflow lane -that performs org observation/apply. A repository secret is an acceptable narrower placement when -only one repository runs the actuator. For an operator session, use a named local credential-store -profile and expose it to only the validation or converge process. Do not put a token in argv, a -worktree file, committed configuration, a receipt, or this plan. - -Bytes-present and working-credential are intentionally different facts. `SecretMaterial.store` and -`CredentialCustodyStanding` answer custody. `CredentialOperationStanding` answers whether an -executed probe proves one exact capability on one target through the required horizon. A runner- -groups read receipt never authorizes mutation. `admit_org_admin_credential` refuses absent or -wrong-principal material, incomplete genealogy, a due probe, insufficient exact capability, and an -expired operation horizon. No arm turns absence into a skip. - -## Acquisition genealogy and runtime dependency - -`CredentialAcquisitionTransaction` is multi-input over a closed `AcquisitionRequirement` sum: -credential proofs, human authorization ceremonies, authority resources, grants, approvals, -authenticator bindings, attestations, and custody use. `CredentialGenealogyStanding` is immutable -audit history and is complete only with explicit roots, including the human ceremony and external -account-recovery boundary for privileged GitHub bootstrap. A ceremony receipt records the -authenticator classes GitHub accepted; it never records passwords/OTPs and never becomes current -authentication merely because it is recent. - -Present use is a separate `CurrentUseRequires` graph with a typed -`DependencyInvalidationEffect`; historical edges are `HistoricalOnly`, app-key loss can -`BlocksFutureMinting`, and only provider-evidenced relationships `InvalidatesCurrentUse`. -`RenewalRequires`, `CredentialRenewalCandidate`, and `CredentialContinuityStanding` form a third -graph, so an operator renewal obligation is not confused with an outage. This prevents an expired -browser session or revoked creation-time PAT from poisoning an otherwise usable app credential. - -## Fleet-converge consumer - -The existing `gunbc.fleet_converge_workflow.fleet_converge_job` owns the first consumer; there is no -parallel workflow. Its `org_actions_observe` mode invokes -`gunbc.fleet.org_actions_inspection.org_actions_inspect_wet`. The entry refuses a missing secret, -then composes the read through `std.goal_assessment.inspect_goal`: the goal-blind observer -`observe_org_actions` executes the read-only runner-groups call, strictly decodes the response and -reads each group's selected repositories through `extdeps.github.org_actions.CliOrgRunnerGroups`; -`gunbc.fleet.org_actions_standing.assess_org_actions` then compares the observed groups with the -desired goal. A refused or malformed read, or a repository-read failure, is a -typed observation refusal, and any desired-state divergence is a located `GoalDiverged`; each is a failing -typed outcome. It never silently skips and it has no write/fix-divergence arm. - -The validation receipt is uploaded as a workflow artifact only after the probe and joined reads -succeed, and it records the assessment verdict. The artifact contains identity, endpoint, organization, -verdict and time—not a token, header, -response body, or token fingerprint. Until the operator creates `GUNBC_ORG_ADMIN_TOKEN`, dispatching -the mode witnesses the intended missing-credential refusal. Once the secret exists, the same route -performs the live read without a code-path switch. - -## Interim human handoff - -**Superseded 2026-09-05, never taken.** The `org_actions_observe` step now mints a one-hour installation -token in-run from the existing `gunbai-ci` App key (Secret Manager `ci-github-app-private-key`, read -through the same WIF path as the fleet key), so no personal token is created and no Actions secret -holds a credential. The steps below are kept as the record of the design that was replaced. What -remains of the terminal migration is narrowing to a dedicated least-privilege App; the read path and -custody contract are already the terminal ones. - -1. In GitHub's fine-grained token UI, the operator selects `gunb-ai` as resource owner, grants - organization `Self-hosted runners: write` and `Administration: write`, and chooses a bounded - expiry. No repository content permission is needed for the org-settings probe itself. -2. The operator pastes the value directly into the chosen store: the restricted Actions secret for - CI, or the named local credential profile for a supervised session. The value is never pasted - into an issue, PR, shell history, command argument, or receipt. -3. Before any converge depends on it, the validation lane materializes it as an environment-bound - secret and executes the read-only `GET /orgs/{org}/actions/runner-groups` probe (the - `ListOrganizationRunnerGroups` endpoint). A 2xx response proves organization subject and - self-hosted-runner read scope without changing the subject. The later actuator must separately - require the write grants declared by the credential shape; a successful read is not fabricated - proof of write access. -4. The observer writes a `CredentialCapabilityProbeReceipt` containing only material identity, - credential kind, exact read capability, endpoint, organization, producer, effects, and - observation time. `CredentialOperationUsable` binds it to the target and validity horizon. It - contains no token, response body, headers, or recoverable token fingerprint. -5. Expiry, revocation, owner change, scope change, a missing receipt, or a receipt outside its - horizon changes standing away from `CredentialOperationUsable`; observe and apply both refuse before - mutation. Rotation repeats steps 2–4 and then deletes/revokes the prior material according to - the selected store's deletion semantics. - -## Terminal migration - -Register and install a dedicated GitHub App with only organization `Self-hosted runners: write` and -`Administration: write`. Store its private key using the same SecretMaterial custody contract and -existing materialization bracket. Mint an installation token per invocation, record its returned -expiry, run the same read-only probe, and discard it after use. Once that path has an executing -positive receipt and absent/revoked negative controls, delete the interim PAT row and revoke the -PAT; do not retain both as fallback authorities. diff --git a/docs/plans/parse-grammar-choice-overlap-residue-finding.md b/docs/plans/parse-grammar-choice-overlap-residue-finding.md deleted file mode 100644 index c7bc0af321d..00000000000 --- a/docs/plans/parse-grammar-choice-overlap-residue-finding.md +++ /dev/null @@ -1,90 +0,0 @@ -# `parse_grammar_choice_overlap_residue` — a named grammar deficiency nothing executes - -Status: finding, filed independently of the census that surfaced it. It outlives that census. - -**The unexecuted law and the unmeasured deficiency are the same fact seen twice.** - -Subject: `a6ca6882d18114b52532c0804dc89f97b441f493`. - ---- - -## The finding - -`v2.compiler.source_authority` `parse_dag_source_ast` refuses a large fraction of authored `.dag` -source, always with one reason: `parse_grammar_choice_overlap_residue`. - -Measured with a positive control in the same run, so a harness fault could not masquerade as a -finding: - -```text -CONTROL_3LINE the tree's own "module m / fn add" fixture => ACCEPTED -REAL_SMALL dag/gunbc/bash_materialized_transport.dag => ACCEPTED -REAL dag/extdeps/shell/exec.dag => REJECTED - reason = parse_grammar_choice_overlap_residue -``` - -Then a random 10-file sample of real corpus files: - -```text -A src/v2/std/constraint_satisfaction_predicate.dag -R dag/test/claim/filesystem_read_hermetic_witness.dag parse_grammar_choice_overlap_residue -R dag/test/claim/wet_hermetic_equivalence_witness_test.dag parse_grammar_choice_overlap_residue -R src/v2/test/claim/.../cargo_fmt_dead_param_test.dag parse_grammar_choice_overlap_residue -A dag/test/fixture/sole_constructor_sealed/admitted_caller.dag -A dag/extdeps/transports/file.dag -A src/v2/workflow/host_discovered_owned_data_manifest.dag -A src/v2/lens/structural_similarity.dag -R src/v2/test/claim/round_trip/source_authority_contract_test.dag parse_grammar_choice_overlap_residue -A dag/extdeps/cache/catalog_placement.dag - -6 accepted, 4 refused — all four the same reason -``` - -**The grouping is the finding, not the rate.** Ten files support no corpus refusal rate, and none is -claimed. They do support that five refusals across two source roots — four sampled at random, plus -`extdeps/shell/exec.dag` found independently — share one named cause: one grammar deficiency with -many victims, not scattered file-specific problems. Group by *why*, not by where the fix would be typed. - -## Why it is invisible - -`parse_dag_source_ast` has **no consumer**. Its only two call sites are inside -`canonical_dag_source_parse_print_law`, whose name occurs exactly once in the tree — its own -definition. It has no callers. - -So the deficiency is not hiding; it is never asked, because the only path that would surface it is -a law nothing executes — DESIGN §5's **specification-without-execution** class, in the compiler's -own source authority: a parse/print round-trip law, written, typed, never run. - -The two halves are one fact: a law nobody runs cannot report the deficiency it would catch, and an -unmeasured deficiency leaves the law looking healthy. Executing the law turned an unknown into a -named reason with a file list. - -## Why this matters beyond the census that found it - -1. **The v2 self-host program depends on this path.** `parse_dag_source_ast_with_model` sits under - `semantic_ir_from_source_with_model` and the normalize/resolve chain — the same - tokenize→parse→normalize→resolve pipeline the `wave1_gate1` body-producer witnesses exercise on - fixtures. Those witnesses pass on hand-authored three-line modules. The refusal population lives - in *authored corpus source*, where self-host has to work. -2. **Fixture-grain evidence is not corpus-grain evidence.** The gap between "parses `module m / fn - add`" and "parses `extdeps/shell/exec.dag`" is the gap between a green witness and a working - compiler; only the first is measured today. -3. **It is a wall on any future consumer of the parse route**, not just this census — including the - corpus-parse alternative considered in the - [projection increment spec](parsed-body-projection-increment-spec.md), which this finding is - part of the reason to reject. - -## What is not claimed - -- No corpus refusal rate. Ten files is a sample that establishes a shared cause, not a proportion. -- No root cause in the grammar. The reason symbol names a choice-overlap residue; which - productions overlap, and whether the fix is one rule or many, is not established here. -- No claim that repairing it makes the parse route viable for corpus-grain work — the separate - memory measurement in the increment spec says it would not. - -## Next step - -Measure the refusal population properly — every file under both production roots, verdict and -reason per file, as a structured artifact — and reduce the refusals to the grammar production(s) at -fault: a bounded measurement on an existing route, no seed change. Repairing the grammar is -downstream of knowing which productions overlap. diff --git a/docs/plans/parsed-body-projection-increment-spec.md b/docs/plans/parsed-body-projection-increment-spec.md deleted file mode 100644 index 0d614310555..00000000000 --- a/docs/plans/parsed-body-projection-increment-spec.md +++ /dev/null @@ -1,156 +0,0 @@ -# Parsed-body projection — increment spec (admission decision input) - -Status: **specification for an admission decision, not an implementation plan.** No work has -started or may start until the operator rules, because part of the increment lands in the frozen -v1 seed (§5). - -Audience: whoever decides admission. States what is broken, what would fix it, the evidence grade -per claim, and — separately, as different admission questions — which parts are v1 seed changes -and which are v2 `.dag` changes. - -Subject measured: `a6ca6882d18114b52532c0804dc89f97b441f493`. -Evidence: → [projection blocker and derived shell seed](shell-dag-census-0a-projection-blocker.md). - ---- - -## 1. What breaks without it - -SHELL-DAG-CENSUS-0A must derive, from parsed bodies, every authored path that can attempt -POSIX/Bash program interpretation, with a merge bar of **zero production parse refusals** and -**zero unexplained unknown routes**. That bar is unreachable — not difficult — on today's fact -surface, and the failure is silent. - -The sharpest statement is not the coverage percentage but this: - -> **A file that was never loaded reads identically to a file that carries no shell route.** - -No per-file `ParseRefused` row counts the difference, because non-import is not an event the -surface reports. So a census here produces not an incomplete answer that announces itself but a -clean, confident population that is silently wrong — DESIGN's **empty-observation narrow**, the -exact artifact this cut was dispatched to prevent. Everything below is downstream of that. - -## 2. The six axes, with evidence grade - -Evidence grade is stated per axis: a code reading and an executed discriminating pair are not the -same strength. - -| # | Axis | Remedy | Evidence grade | -|---|---|---|---| -| 1 | Service operations and transports are absent entirely — `ItemKind::ServiceItem` has no accessor | a `ServiceItem` accessor carrying operations, transport argv, and stdin channels | **Structural, verified independently.** The enum has six variants and exactly three have accessors; confirmed by the dispatching lane | -| 2 | `FnArrowDecl.output` is a wiring-liveness skeleton — a bound-but-unused `let` RHS is dropped | a body projection total over statements: effect positions retained independently of return reachability | **Execution-proven** (discriminating fixture pair, §3) | -| 3 | No named-argument edges — labels erased, literals hoisted to the call node | argument edges labelled with the authored argument name, positional index preserved | **Execution-proven** (same run, §3) | -| 4 | No arm or occurrence identity | stable occurrence identity on body nodes; arm identity on match arms | Read from the marshal (`marshal_generic` emits undifferentiated positional children) | -| 5 | Callee is an authored lexeme, not a resolved identity — a string literal and a constructor are the same atom | resolved declaration identity on callees; a node-kind discriminator separating literal / callee / constructor / variant | Read from the marshal (`atom_identity_node` is the single atom kind) | -| 6 | The registry is the entry's import closure, not the corpus | a corpus-grain denominator that is an enumerated file set, not an import closure | **Execution-measured** (§4) | - -### 3. The execution proof for axes 2 and 3 - -Two functions of identical shape, folded through `fn_arrow_decl_facts_live`, atom identities -collected from each `output`: - -```text -fn fixture_dead_let_shell() -> String { - let unused_result = fixture_sink(program: "echo DEADLETMARKER") - "returned-without-using-unused_result" -} -fn fixture_live_named_args() -> String { - fixture_sink2(program: "echo LIVEMARKER", args: "echo ARGSMARKER") -} - -DECL probe.fixture.fixture_dead_let_shell ATOMS: (empty) -DECL probe.fixture.fixture_live_named_args ATOMS: fixture_sink2 | echo LIVEMARKER | echo ARGSMARKER -``` - -The dead-let arm yields **nothing** — callee identity and program literal both absent; the live -arm yields both. Same construct, opposite verdict, so the loss is the projection's, not the -probe's. The live arm also demonstrates axis 3: three atoms in authored order with **no labels**, -so nothing records which literal was `program:` and which `args:`. - -### 4. The measurement for axis 6, with its counting method stated - -A fold over `fn_arrow_decl_facts_live()` under `--source-root dag --source-root src/v2` reported -**1,698** declarations. - -The denominator depends on the counting method, so it is stated: - -| method | count | note | -|---|---|---| -| line-start `fn` / `func` / `test fn` | **41,965** | the accessor's filter is `ItemKind::FnItem \|\| FuncItem`, and `ItemKind` has no separate test variant, so a `test fn` **is** an `FnItem` — this is the matching denominator | -| line-start `fn` / `func`, excluding `test fn` | 30,851 | the same count with the 11,114 test declarations removed | - -The conclusion is invariant: **1,698 of 41,965 is 4.0%; of 30,851 it is 5.5%.** Either way the -surface is a small single-digit fraction of the tree, and its content is whatever the entry -happened to import. - -This is a declared frontier, already guarded: `v2.lens.affected_set.corpus_dependency_view` calls -`fn_arrow_decl_substrate_is_whole_tree` and, when false, routes to a host refusal reading -`corpus_dependency_view per-PR execution refused … (blocked-on-#6239)`. Fail-closed, correctly. -**A census inherits that refusal**, which is why axis 6 is plausibly not new work — see §5. - -## 5. Which parts are v1 seed changes and which are v2 `.dag` changes - -Separated because they are different admission questions. - -| axis | v1 seed (`src/v1/stage0/src/coproduct_reflection.rs`) | v2 `.dag` | notes | -|---|---|---|---| -| 1 | **yes** — a new `eval_service_decl_facts_live` accessor | yes — the carrier type and its `.dag` surface | additive | -| 2–5 | **yes** — a new non-lossy body marshal | yes — the carrier types | **additive, not a modification** (below) | -| 6 | **probably none** | none | likely already-sanctioned pending work (below) | - -Two properties matter for admission: - -**Axes 2–5 are an additive second accessor, not an edit to the existing one.** The existing -marshal's lossiness is not a defect — dropping a dead `let` RHS is exactly what -`v2.lens.wiring_liveness` needs, and its comments say so; changing it would break that lens. The -increment adds a second projection beside it, keeping the blast radius off every current consumer, -so the freeze question is "may the seed grow a new accessor", not "may its existing semantics -change". - -**Axis 6 is probably not this increment's to fix.** The whole-tree registry path exists, gated -on #6239 with a typed refusal in place; if that lands, axis 6 closes without anything here. -Listed for completeness, not as a request. - -## 6. The cost argument for doing it at ingestion rather than corpus-wide - -The alternative — a corpus-wide fold parsing source in `.dag` via `v2.compiler.source_authority` -`parse_dag_source_ast` — was measured, and fails on **both** axes: - -- **Correctness.** Random 10-file corpus sample: 6 accepted, 4 refused, all four with the same - reason as the earlier `dag/extdeps/shell/exec.dag` refusal — `parse_grammar_choice_overlap_residue`. - One grammar deficiency, many victims. The refusal set contains the census's own seed file, and the - merge bar is zero production parse refusals. Filed separately: → [parse grammar choice overlap - residue](parse-grammar-choice-overlap-residue-finding.md). -- **Cost.** 1 file / 63.1 s; 10 files / 67.5 s; 40 files / **`EXIT=137`, OOM-killed after 34 files**. - Marginal cost ≈0.49 s per file against ≈62.6 s fixed world-acquisition overhead, so time is not - the wall; memory is — and not from large files: the kill came at file ~34 of 40 with only - **94 KB** of source consumed, on ~7.8 KB files, the 212 KB outlier sorted last and never reached. - The mechanism (parse-tree retention vs interpreter heap growth) is **not** established or claimed. - What is established: the process cannot hold the result of parsing 34 small files, against a - subject of 3,733 files and 31.3 MiB. - -DESIGN §6 already rejected this shape: the corpus-wide censuses were deleted in #8140 because -*"the unit of computation was the world, the unit of fact was one module's authorship, and the -price was paid by every consumer that wanted a witness roster and nothing else."* Its declared -next-rung trigger is exactly this increment's shape: a fact *"belongs on the module's own -declaration, checked at ingestion where the module is parsed anyway — one module's facts from one -module's source — rather than reconstructed corpus-wide by a consumer that wanted something -else."* - -## 7. The admission question, stated without advocacy - -The v1 seed is frozen with an admission test of **purpose**: a change is admitted when it serves -the v2 self-host program. This increment does not obviously pass, and the decision is not the -requesting lane's. Both readings are recorded: - -**For admission.** A non-lossy body projection is the substrate capability *any* consumer needs to -read real bodies rather than a wiring skeleton. The existing surface is why `v2.lens.effect_reach` -classifies host-effect sinks by name equality against a remembered callee list — not a chosen weak -principle, but a surface that cannot express a stronger one. Every future lens wanting real bodies -inherits that ceiling. - -**Against admission.** The requesting program is a shell-migration census, not v2 self-host. Seed -growth justified by a neighbouring program is how a freeze with an active-maintenance arm becomes -absorbing, the standing danger `gunbc.v1_maintenance_standing` names. - -If admission is refused, the census stays blocked, honestly. Worse for this lane, correct for the -repository, and preferable to a census whose population is confident and silently wrong. diff --git a/docs/plans/planning-work-contract.md b/docs/plans/planning-work-contract.md deleted file mode 100644 index 974ca2df457..00000000000 --- a/docs/plans/planning-work-contract.md +++ /dev/null @@ -1,77 +0,0 @@ -# The planning work contract (owner directive 2026-09-25) - -Supersedes the planning-ticket-type and Fabric·planner-role sketches from -conversation (never landed). Do NOT add Planning to the issue-type vocabulary; -do NOT introduce a second workflow role or principal. - -## The model - -**Planning is one first-class work contract**: a bounded effect envelope whose -result is an `IssueRevisionCandidate`, an investigation report, or a comment -response. It is the same generic work request assignment mints, with a -different envelope — not a different kind of issue, agent, or principal. - -The durable invariants stay put: - -- the issue remains the durable problem/outcome; -- component remains domain placement; -- assignment remains accountability; -- parent/child and blockers remain separate graph dimensions. - -## Issue changes are versioned candidates with CAS application - -Fabric may: comment, propose field changes, propose relation changes, propose -draft children. Fabric may NOT: directly rewrite the target issue, or edit -code, under a planning contract. - -```text -IssueRevisionCandidate { - issue - base_issue_revision -- the version it was derived against - proposed_field_changes - proposed_relation_changes - proposed_draft_children - author / work-request identity -} - -apply(candidate, expected_revision = candidate.base_issue_revision) - → Applied { new_revision } - | RevisionConflict { current_revision } -- CAS refusal - | CandidateRefused { cause } -``` - -A proposal never mutates the issue until accepted through the CAS route. The -acceptor is the issue's accountable human (or a later explicit automation -ruling) — the candidate mechanism itself grants no write path. - -## Comments are durable narrative events, never implicit commands - -Ordinary prose has no workflow effect. The explicit operation is: - -```text -AskFabric { issue, referencing_comment, asker } - → mints the same generic work request assignment mints - (bounded planning envelope) - → the answer lands as a comment response in the thread -``` - -No mention-parsing, no prose-triggered turns: if it isn't an `AskFabric` -event, nothing runs. - -## The ban - -No operational meaning may be encoded in: planning-flavored components, -Planning issue types, labels, title prefixes, or role-encoded assignee aliases -(e.g. "fabric-planner@…"). Structure carries meaning; names don't. - -Future recurring patterns are promoted only when they carry a DISTINCT effect -envelope, deliverable, admission rule, verification, or application route — -all five named, or it stays an ordinary work request. - -## Queue position - -Durable comments are already in flight (agent-52's vertical). This contract is -the next vertical behind it: the versioned IssueRevisionCandidate + CAS -application route, the AskFabric operation minting the generic work request, -and the bounded planning envelope (comment/report/candidate results only) as -the envelope vocabulary sibling of the execution envelope. diff --git a/docs/plans/post-engine-pr-roadmap.md b/docs/plans/post-engine-pr-roadmap.md index 111408b5c6e..272764fc58c 100644 --- a/docs/plans/post-engine-pr-roadmap.md +++ b/docs/plans/post-engine-pr-roadmap.md @@ -53,7 +53,7 @@ Eight parallel stage surveys, all reported; raw catalogs with full per-pass tabl | P5 | infer-core | `build_imported_variants` + `owner_of_exported_arm`/`exported_coproduct_item` (04_infer.dag:6079,6033,6056) | Full Disj-scan + re-export chain walk of the exporting module **per imported name per importer** — fires for every name incl. non-variants (common case scans and finds nothing). Part of the machinery this PR built; already ledgered as §A.8's sibling | **O(N·K·D·M_target)** — core imported by all N modules is re-scanned N·K times | Per-exporting-module `Map` + `Map` computed once (re-exports baked in), stored on `TypedModule`; binder does O(1) `map_get` | | P6 | interpreter | `Env::extend` chain (v1_interpreter.rs:538-576, 1328, 5988) | `call_function` extends the **caller's** env, not a fixed base → chain depth = dynamic call depth d; every var/global read walks O(d) | **O(d²)** per depth-d recursion; multiplies every interpreted workload incl. all v2 stages (~100× constant on top) | Invoke top-level fn bodies against a fixed global base env (true lexical scoping) — chain depth becomes lexical nesting | | P7 | v2 translate/serialize | `06_translate.dag:3555→1270→397` facts lookup; `:814,931,1076` serialization | `InferredTree.facts.lookup` = linear scan over all entries, called per node (O(M²)); output built with `list_append(partial, …)` (O(L²)) | Two independent quadratics on the v2 emit path, ×100 interpreted | Index facts by `occurrence_id`; accumulate output reversed / join once | -| P8 | v2 parse | packrat memo table (src/v2/compiler/02_parse.dag:744,774) | **Dead memo**: `let _memo = parse_table_insert(...)` — result discarded, table never threaded out, lookup always sees the empty table; position key recomputed via `length` O(T) per attempt | Exponential worst-case backtracking; every production re-parses at every position. The performance twin of the "inert lens" (coverage by illusion — the machinery exists, nothing flows through it) | Thread the updated table through `ParseExprResult` (state-monad); carry an integer cursor | +| P8 | v2 parse | packrat memo table (src/v2/compiler/02_parse.dag:744,774; the memo was later deleted with the parse memo carrier, operator ruling R2 2026-10-08) | **Dead memo**: `let _memo = parse_table_insert(...)` — result discarded, table never threaded out, lookup always sees the empty table; position key recomputed via `length` O(T) per attempt | Exponential worst-case backtracking; every production re-parses at every position. The performance twin of the "inert lens" (coverage by illusion — the machinery exists, nothing flows through it) | Thread the updated table through `ParseExprResult` (state-monad); carry an integer cursor | | P9 | v2 resolve + infer | `03_resolve.dag:200,261` namespace closure-chain; `04_infer.dag:298→cardinality.dag:260` nested catamorphism | `Map` primitive is a closure chain → O(E) per atom lookup × A atoms = O(M²)/module (the resolver lesson verbatim, interpreted); termination-proof witness re-runs a full `fold_node` over each node's subtree = O(M²) | O(M²) per module, ×100 interpreted | Indexed map for `Namespace.bindings`; compute descent proofs compositionally inside the existing bottom-up fold | ### SUSPICIOUS tier (fix-worthy, lower share) diff --git a/docs/plans/power-on-sequence-model.md b/docs/plans/power-on-sequence-model.md deleted file mode 100644 index 62c83258e63..00000000000 --- a/docs/plans/power-on-sequence-model.md +++ /dev/null @@ -1,416 +0,0 @@ -# The Mt. Collins power-on account (power-on sequence model, Mt. Collins first) - -Status: PLAN. It encodes the side-chat rulings R1–R3 and Q1–Q2 and the corrections from the reviews of `e4e8a4a4d4` and `75f0bd113c`, all before any implementation. Owner: calm-lynx-884, under eager-gull-22 (hw-boot). - -## 0. The requirement - -Operator, 2026-10-03, verbatim: "take ALL of the information we've gotten today (and in our existing models) - and construct a monolithic 'this is what actually happens when you turn on the computer' workflow - meaning, our diagnostic process will be 'this is our best understanding of what happened' - basically, the end of the workflow (error, no error, any information we have) - and then, as we add more platforms (mt jade, x86), we will abstract the workflow and plug in the differing pieces as we need - this will give us an idea of how to onboard new platforms". - -The product is one **power-on account** per attempt: `gunbc.machine_intake_mtcollins1_power_on_account`. It covers success, degraded success, stated failure, repeated restarts, transport failure, partial observation and unresolved cause. It is never a pass/fail bit, and it never asserts past the evidence (DESIGN §4d, §5). It is monolithic for Mt. Collins. §9 records the seams for Mt. Jade and x86, and abstraction follows the second platform. - -## 1. Rulings this plan encodes - -- **R1 (side chat, decided): #13041 lands first, and slice A replaces it atomically.** Slice A deletes `gunbc.machine_intake_boot_outcome` `HostBootObservation` and its authored datum `mtcollins1_post_firmware_set_boot_2026_10_03`. In the same change, it makes firmware convergence (`gunbc.fleet.mtcollins_firmware_converge` `boot_verdict`) consume an operation-specific readback projected from the **whole** session (§6). Reaching UEFI is a milestone, not an ending, so the projection never reads the ending alone. The former slice E is folded into A. -- **R2 (decided): the opaque `CP:` reader is public, and it establishes only an opaque sequence relation.** It may say "cycle C of capture S ended after token X; reference capture R (digest, firmware and configuration scope) contains X followed by Y". It may not establish `DdrTraining` or any other `AmpereBootStage`, may not call Y a stage success, and may not locate an ending "at DdrTraining @ X". Semantic attribution belongs to the gunbc-private refinement slice (§8). -- **R3 (decided): the refused SMpro pair belongs to the secondary-join subject and is not decoded there.** Socket 1's `CUR_BOOTSTAGE 0x0001` / `BOOTSTAGE 0x03ff` is `extdeps.ampere.smpro_register` `SmproBootProgressRefused`. It licenses no "stuck at PMpro" claim, no DDR claim, and no other last-stage claim. It is carried inside `gunbc.machine_intake_mtcollins1_socket1_investigation_observation` `MtCollins1SecondaryJoinOutcome` (#13025, on main), which this plan reuses and does not re-derive. It also gets an open question on the join subject. -- **Q1 (decided): a component-owned console line establishes only that component's console milestone.** Examples: AMI `Press or ` gives `UefiConsolePromptObserved`; `UEFI Interactive Shell v2.2` gives the stronger `UefiShellObserved`; `GNU GRUB`, `EFI stub:` and `Linux version` give their exact component milestones. A milestone never establishes an `AmpereBootStage`, stage completion, health or a successful handoff. It carries the exact line and its parser, the cycle, the capture digest and its ordering evidence. -- **Q2 (decided): the account consumes one frozen, attempt-bound `AttemptConfigurationReceipt` taken before power-on** (§3). Historical rows supply topology only, never current occupancy: `mtcollins1_slots` is the 2026-09-10/11 16-DIMM census, and the `mtcollins1_physical_orientation` CPU rows are a 2026-09-23 operator report. -- **Fact correction.** **Socket 0's** CCIX record is GPI-pending (gated): in `smpro-err-20261003T005609Z`, slave 0x9E (socket 0) reads `GPI_RAS_ERR 0x7E = 0x0002` beside the ERR_CCIX_RCA_LINKUP_FAIL record (`0xA6 = 0x0074`, `0xA7 = 0x2244`). **Socket 1's** record is ungated per the retained hand read the side chat cites. In that capture, socket 1's (0x9C) error registers mostly refused (`rsp=0xff`), so the ungated reading rests on the hand read. The first draft had this reversed. - -## 2. What already exists and is reused (§3, §3b) - -| Concern | Owning authority (reused, never forked) | -|---|---| -| Firmware boot stages 0–9 | `extdeps.ampere.scp_diagnostic` `AmpereBootStage`, `AmpereBootStatus` | -| SMpro register pair and its refusal rule | `extdeps.ampere.smpro_register` `smpro_boot_progress_of` → `SmproBootProgress` (`SmproBootProgressRead` / `SmproBootProgressRefused`) | -| Per-socket SMpro reading | `gunbc.machine_intake_ampere_smpro_observation` `SmproSocketStage`, `SmproProbeOutcome`, read through `SmproPassObservation` | -| Second socket's join | `gunbc.machine_intake_mtcollins1_socket1_investigation_observation` `MtCollins1SecondaryJoinReading`, `secondary_join_outcome` → `MtCollins1SecondaryJoinOutcome` | -| SMpro/PMpro error records and GPI gate (#13058) | `extdeps.ampere.smpro_internal_error` `SmproInternalRecord`, `SmproRecordGate` | -| DRAM console block and roster | `gunbc.machine_intake_ampere_dram_console_observation` `AmpereDramConsoleObservation` | -| Firmware summary, inter-socket lines, CPER | `gunbc.machine_intake_ampere_socket_console_observation` `AmpereFirmwareSummaryObservation` | -| NVPARAM dump (#13059) | `gunbc.machine_intake_ampere_nvparam_console_observation` | -| Firmware console statements, SEL cycles | `gunbc.machine_intake_pre_os_bringup_verdict` `FirmwareConsoleReport`, `boot_cycles` | -| SEL vocabulary, Ampere OEM SEL | `extdeps.bmc.ipmi_sel`, `extdeps.ampere.oem_sel` | -| GRUB | `extdeps.bootloader.grub` `GrubConsoleReport` | -| Kernel module refusals | `gunbc.machine_intake_kernel_module_decompression_observation` | -| SOL collector, KVM, virtual media, handoff | `gunbc.machine_intake_sol_collector_observation`, `gunbc.machine_intake_mtcollins1_kvm_still`, the bundle's `MtCollins1MediaAttachRecord` / `MtCollins1HandoffMedia`, `extdeps.bmc.megarac` | -| Harness phases | `gunbc.machine_intake_mtcollins1_boot_phase_timing` `MtCollins1BootPhase`. These are what our harness did, never firmware stages. | -| Slot topology (J-label to socket/MC/slot), never current occupancy | `gunbc.machine_intake_mtcollins1_memory_census_observation` `mtcollins1_slots` (the 2026-09-10/11 census) | -| Firmware versions | #13041 `gunbc.fleet.mtcollins_firmware_baseline` | -| Authored outcome being replaced | #13041 `gunbc.machine_intake_boot_outcome` `HostBootObservation` | -| The host | #13055 `gunbc.managed_host` `ManagedHost` | - -## 3. Shape: one attempt, three orthogonal standings, cycles of subjects - -These three things are independent, and none may stand in for another: - -1. **What the host did** (`HostAccount`): its cycles, and per cycle the standings of each subject. -2. **What we observed** (`ObservationCoverage`): one standing per carrier, saying whether it was read, refused, empty, partial or not taken, and why. An empty SOL capture is a coverage gap, never SoC silence. A BMC authentication refusal is a coverage gap on the BMC carrier, never a failed "BMC came up" stage. -3. **Whether our workflow worked** (`CollectionOutcome`): media attach and handoff, override consumption, power-after, bundle write, credential residue. A media-read failure or a bundle-write failure is a collection outcome, never a host ending. - -Beside these sits the **attempt configuration receipt** (`AttemptConfigurationReceipt`). It is frozen and bound to one attempt, it is taken before power-on, and the account consumes only it. The fold never queries mutable or historical global rows. The receipt separates: - -- **expected configuration and policy**: the expected topology this attempt is judged against; -- **the requested stimulus** (for example "remove the socket-1 CPU", "J1 only", "firmware set 2026-10-02") **from the applied-stimulus receipt**. For manual CPU or DIMM work, the applied receipt is a human completion/inspection receipt the workflow consumes. A workflow-authored request is not evidence that the change happened; -- **the current physical population** per socket and per slot, each field with its standing: `LiveObserved { source }`, `OperatorAttested { receipt }`, `Conflicted { readings }` or `NotRecorded { reason }`; -- **live firmware readbacks** (BMC, SCP, UEFI, CPLD), taken after the physical configuration is set and before actuation; -- **provenance**: subject (the `ManagedHost` row), attempt, timestamp, artifact and digest. - -Static slot and orientation modules may map a label such as `J1` to topology. They may never supply current occupancy. A question that needs a field whose standing is `NotRecorded` or `Conflicted` becomes an open question, never a default. - -### Cycles - -A power-on attempt is a **session of cycles**. The old firmware stopped on cycle 1 and reached UEFI on cycle 2 (run 37062170720). The new firmware repeats one stopped cycle 6 times (steps 1, 2 and 2b). One cycle can have socket 0 reaching Linux while socket 1 never joins. So: - -- **Cycle segmentation does not depend on reaching DRAM.** - - Cycle 1 opens on the admitted chassis-power-on effect. - - Each later cycle opens on typed restart/reset boundary evidence: SEL `System Boot Initiated` / `S5` records, power or reset observation, and console evidence. - - A `DRAM FW version` banner binds a console span to a cycle. It is not the sole authority that a cycle exists, because a host can stop in SMpro, PMpro or ATF before the banner, and an empty SOL can sit beside SEL and SMpro evidence of a boot. - - Each cycle carries its identity (index, boundary evidence, console span if any) and the capture digests. -- **Within a cycle, standings are keyed by subject**, not by one total chain: - - `Platform`: the BMC and chassis power; - - `Socket { k }`: that socket's SMpro progress via `SmproBootProgress`, and its error records via #13058; - - `SecondaryJoin`: `MtCollins1SecondaryJoinOutcome`; - - `DramFirmware`: the console block, roster, and untrained sockets established only by a closed roster; - - `Uefi`: summary, NVPARAM, POST; - - `BootLoader`, `KernelStub`, `Kernel` and `Census`. - -### Stage attribution rule (R2 applied) - -A **firmware stage** (`AmpereBootStage`) is established only by a public `AmpereBootStage` reading, which is an SMpro register pair decoded by `smpro_boot_progress_of` (`SmproBootProgressRead`). Console output establishes **console milestones** (Q1): exact component-owned lines, such as the DRAM firmware block, the firmware summary, `UefiConsolePromptObserved`, `UefiShellObserved`, the GRUB banner, an EFI stub line, the kernel banner and the census marker. A milestone never implies an `AmpereBootStage`, completion, health or handoff. An opaque checkpoint token implies neither. This is structural: only the `Socket{k}` progress reading can carry `SmproBootProgressRead` and so an `AmpereBootStage`, while console subjects carry milestone readings that have no stage field (§5). - -## 4. Mt. Collins: subjects, carriers, and what success, failure and silence look like - -| Subject | Runs on | Carriers | Success | Stated failure | Silence or refusal | -|---|---|---|---|---|---| -| Platform: BMC | MegaRAC on standby | BMC reads, `mc info` | Answers. The reflash resets users and SEL (self-observed, 0.45.3). | Not applicable: a refused read is a coverage gap | Coverage gap | -| Platform: chassis power | BMC → sequencer | Chassis read, SDR rails, SEL `Power Unit` | On, rails ok | Rail not ok (`mtcollins1_sensor_anomalies`) | Power never reads on | -| Socket k: SMpro/PMpro progress | SMpro, PMpro | SMpro registers per pass | `SmproBootProgressRead` advancing | Reported status `Failed` | `SmproBootProgressRefused` is an open question, never a stage | -| Socket k: error records | SMpro, PMpro | 0x7E, 0xA0–0xAD (#13058) | No pending record | Only a `PendingAndDecoded` record. On 2026-10-03, socket 0's `ERR_CCIX_RCA_LINKUP_FAIL` (loc 68, code 116) is the one current stated error | A matching payload under `GpiUnavailable` or ungated (socket 1 on 2026-10-03) is an anomalous raw record plus an open question. It is never a second `DecodedBootError` or `PendingErrorRecord` | -| SecondaryJoin | PMpro, both sockets | Summary active sockets and inter-socket lines, socket-1 SMpro pair | `SecondaryJoined` | `SecondaryJoinTimeout` (a liveness finding, not a fault record) | `SecondaryJoinUnclassified { reason }` | -| DramFirmware | DRAM firmware | Console block, roster, opaque `CP:` tokens | Roster closed with every populated slot listed (against the `AttemptConfigurationReceipt` population) | `FirmwareConsoleReport` training refusal | Last token followed by restart or silence (the ending rule, §5) | -| Uefi | AMI Aptio | Summary, NVPARAM, POST, KVM | Summary printed, active sockets = expected | CPER/BERT records, summary mismatch | — | -| BootLoader | GRUB from virtual media | Console | Kernel loaded | `GrubBootFailed` | `GrubStillTrying` | -| KernelStub | Linux EFI stub | Console (`extdeps.linux.efi_stub`, v6.8) | Initrd loaded | `Failed to load initrd: 0x8000000000000001` (EFI_LOAD_ERROR, run 37069907299) | — | -| Kernel | Linux | Console (`extdeps.linux.boot_console`, v6.8) | Banner, no panic | `Kernel panic - not syncing: …`, module refusal | — | -| Census | Our workload | Host-capture markers | END marker | `TerminalRefused` | `TerminalPending` | - -### Self-observed facts the fixtures carry (public) - -- **The new-firmware loop.** Steps 1, 2 and 2b each ran 6 identical cycles. In each: last token `00001a0a`, only `SK0 MC0` in the roster, socket 1's SMpro B0 stuck at `0002`, zero console error lines (no `ERR:`, `fail`, `timeout` or window lines), SMpro unresponsive about 1.1 s after the last token while chassis power stays on, then a restart. The next cycle's SEL shows `S5/G2 soft-off` + `System Restart` plus Ampere OEM records (`c0`, manufacturer `00cd3a`). The observer-clock period was ~25.8 s. The mechanism is not public. -- **Step 3, positive control.** With the socket-1 CPU removed (J1 only), the new firmware reaches the firmware summary (1 active socket) and the AMI UEFI shell, and stays stable for 8+ minutes. One earlier run restarted once after the summary, and that did not recur. -- **Old firmware.** Run 37062170720: cycle 1 stops after `00001a0a`, cycle 2 continues to UEFI. Run 37069907299: reaches UEFI with 1 socket, GRUB, then initrd EFI_LOAD_ERROR and a VFS panic. -- **The UEFI summary prints `Failsafe status : N` and `Reset status : N`.** They are carried as observed, undecoded summary fields (§7). - -## 5. The account's types - -``` -PowerOnAccount { - attempt: AttemptIdentity, // run id, capture digests - host: HostIdentity, // from ManagedHost (#13055); mtcollins1 is a row, not a constant - configuration: AttemptConfigurationReceipt, // frozen before power-on; the only configuration the fold reads - account: HostAccount, - coverage: ObservationCoverage, - collection: CollectionOutcome, - open: List, -} - -HostAccount { - cycles: List, - milestones: List, // e.g. UEFI reached in cycle 2 -- a milestone, never an ending - ending: HostEnding, - degradations: List, // orthogonal to the ending - errors: List, // every stated failure, with its cycle and subject -} - -BootCycleAccount { index: Int, span: CaptureSpan, standings: List, checkpoints: CheckpointObservation } - -SubjectStanding // the reading's type is fixed by its subject, so no subject can carry another's evidence - = PlatformStanding { part: PlatformPart, reading: PlatformReading } - | SocketProgress { socket: Int, reading: SocketProgressReading } // the ONLY carrier of an AmpereBootStage - | SocketErrorRecords { socket: Int, records: List } - | SecondaryJoinStanding { outcome: MtCollins1SecondaryJoinOutcome } - | ConsoleSubject { subject: ConsoleSubjectKind, reading: MilestoneReading } // DramFirmware | Uefi | BootLoader | KernelStub | Kernel - | CensusStanding { reading: CensusReading } - -SocketProgressReading = ProgressRead { progress: SmproBootProgressRead, pass } | ProgressRefused { pair: SmproBootProgressRefused, pass } - | ProgressStatedFailure { progress: SmproBootProgressRead } | ProgressNotRead { coverage_ref } -SocketRecordReading = PendingDecoded { record: SmproInternalRecord } // a stated error - | UngatedRaw { raw, gate: SmproRecordGate } // anomaly + open question, never an error -MilestoneReading = MilestoneObserved { milestone, line, parser, capture_sha256, order } | MilestoneStatedFailure { error, line } - | MilestoneAnomalous { anomalies } | MilestoneNotObserved - -HostEnding // what the HOST did last; says nothing about errors on the way - = ReachedCensus // may coexist with errors and degradations - | StoppedWithStatedFailure { cycle, subject, error } - | RestartedRepeatedly { cycles: Int, last_cycle_ended_after: CheckpointRelation?, cadence: RestartCadence, - console_errors: Nat, reset_cause: ResetCauseObservation } - | StoppedWithoutRestart { last_cycle_ended_after: CheckpointRelation?, console_errors: Nat } - | HostEndingNotEstablished // only when the remaining HOST evidence establishes no ending - -Degradation = SocketNotJoined{outcome: MtCollins1SecondaryJoinOutcome} | ActiveSocketsBelowExpected{active, expected} - | DimmNotTrained{slot} // only against a receipt field LiveObserved/OperatorAttested - | PendingErrorRecord{socket, record: SmproInternalRecord} // PendingAndDecoded only - -CheckpointObservation { capture_sha256, tokens: List, malformed: List } -CheckpointRelation { // R2: an opaque relation, nothing more - cycle: Int, capture_sha256, last: OpaqueToken, - reference: ReferenceCapture { capture_sha256, firmware: FirmwareSnapshot, configuration: ConfigurationSnapshot }, - continuation: ReferenceContinues{next: OpaqueToken, remaining: Nat} | ReferenceEndsThere | NotInReference | ReferenceUnread{detail} -} - -ResetCauseObservation = ResetCauseUnobserved { searched } | ResetCauseObserved { raw: Word } // decode is private -RestartCadence = CadenceTimed { period: Second, cycles } | CadenceCounted { cycles } // production SOL is not timestamped - -ObservationCoverage { carriers: List } -CarrierCoverage { carrier: Carrier, standing: Read | ReadEmpty | Partial{detail} | Refused{cause} | NotTaken{reason} } -Carrier = Sol | Kvm | SmproRegisters | SmproErrorRecords | Sel | Sdr | ChassisPower | RedfishInventory | VirtualMedia | HostCapture - -CollectionOutcome { media, handoff, override, power_after, bundle_write, credential, phases: MtCollins1BootPhaseTiming } - -OpenQuestion { subject: PowerOnSubject, question: OpenQuestionKind } -OpenQuestionKind = ResetCauseNotPubliclyReadable | RetryStatePersistence | CheckpointMeaningNotPubliclyModeled - | SmproPairRefused{socket} | RestartCadenceNotTimed | ConfigurationNotRecorded{field} - | CarrierNotRead{carrier} | UngatedRecordUnexplained{socket} | ConfigurationConflicted{field} -``` - -Laws, each with a discriminating RED in slice A: - -1. **`ReachedCensus` is orthogonal to errors.** A GPI-pending CCIX record plus a one-socket census yields `ReachedCensus` with `degradations` (`SocketNotJoined`, `PendingErrorRecord`). It is never `StoppedWithStatedFailure`. -2. **Cycles are preserved.** Run 37062170720's console yields two cycles: cycle 1 ended after `00001a0a`, cycle 2 has the milestone "UEFI reached". The ending reflects the last cycle, and the milestone survives. -3. **Coverage and collection never choose the host ending.** An empty SOL, a BMC authentication refusal or a bundle-write failure is recorded in `coverage` or `collection` and nothing else. If another carrier establishes `ReachedCensus`, the UEFI shell or a reset, that host result stands beside the coverage defect. `HostEndingNotEstablished` applies only when the remaining host evidence establishes no ending. Converse RED: `SOL = ReadEmpty` with a valid host-capture END marker yields `ReachedCensus` plus an SOL coverage gap. -4. **No stage from a token.** No law or arm takes an `AmpereBootStage` from a `CP:` token. Mutating the last token changes only `CheckpointRelation`. -5. **The refused pair is not a stage.** `SmproBootProgressRefused` on socket 1 produces `SecondaryJoin` evidence and an `SmproPairRefused` open question, and no `Socket{1}` stage. -6. **Configuration-dependent questions need the receipt.** If the receipt's DIMM population is `NotRecorded` or `Conflicted`, "every populated DIMM trained" is an open question, never assumed. A historical row never stands in: a step-3 attempt whose receipt says "socket 1 CPU absent, J1 only" is judged against that receipt, never against `mtcollins1_slots` or the 2026-09-23 orientation report. -7. **A cycle exists without DRAM.** A chassis power-on plus SMpro progress or SEL boundary evidence, with no `DRAM FW version` banner, yields one partial cycle whose console span is absent. It never yields zero cycles or `HostEndingNotEstablished` on that ground alone. -8. **Ungated is not stated.** A `GpiUnavailable` or ungated record with the same payload as a pending one is `UngatedRaw` plus `UngatedRecordUnexplained`. It adds nothing to `errors` or `degradations`. - -## 6. Consumption and the replacement migration (§3, §3c) - -**Firmware convergence (R1).** `gunbc.fleet.mtcollins_firmware_converge` `boot_verdict` stops taking `HostBootObservation?`. It takes `FirmwareSetBootReadback`, projected from the whole account by `firmware_set_boot_readback(account)`: -- `BootReachedUefiAfterSet { cycle, milestone, ending: HostEnding, degradations, account_ref }`, when any cycle reached a UEFI milestone. The session ending and a reference to the whole account travel with it, so a UEFI milestone cannot erase a later `RestartedRepeatedly` ending. Convergence decides with both in hand. -- `BootProgressLostAfterSet { relation: CheckpointRelation, cadence, reset_cause }`, when no cycle reached UEFI and the ending is `RestartedRepeatedly` or `StoppedWithoutRestart`. -- `BootReadbackUnobservable { coverage }`, otherwise. - -`BootProgressLostAfterFirmwareSet` keeps its role as the convergence refusal and takes the relation instead of an authored checkpoint integer. `HostBootObservation`, `mtcollins1_post_firmware_set_boot_2026_10_03` and the `gunbc.machine_intake_boot_outcome` module are deleted in the same change. The authored 2026-10-03 datum is replaced by an account derived from the committed capture excerpt (step 1), which is that witness's fixture. - -**Consumer and semantic census for every deleted producer.** Each finding gets a typed destination or an explicit retirement: - -| Deleted | Consumers today | Destination | -|---|---|---| -| `…bundle` `mtcollins1_boot_bundle_findings` | bundle `mtcollins1_boot_bundle_text`, `mtcollins1_boot_outcome_after_bundle`; `…boot_run` `mtcollins1_boot_conclude` (`findings=` receipt line); bundle witness | The `PowerOnAccount` rendered through `std.observation` presentation. Receipt `findings=` becomes `account=` (one summary line) plus `coverage=` and `collection=` | -| `…bundle` `mtcollins1_boot_outcome_with_findings` | `mtcollins1_boot_outcome_after_bundle`; bundle witness | Same role. It appends the account summary plus its errors and degradations to a refusal, and never flips the verdict (kept REDs) | -| `bmc_all_failed_text` aggregate (the "unreachable" finding) | bundle findings | `coverage`: BMC carriers `Refused { cause }`. Retired as a host statement | -| `processor_findings`, `memory_findings`, `socket_absent_finding` | bundle findings | `coverage` plus `Degradation` against the `AttemptConfigurationReceipt` (BMC's inventory view vs the expected topology) | -| `sel_findings` | bundle findings | Per-cycle SEL events: memory/processor events go to `DramFirmware` / `Socket` anomalies, and restart records go to cycle segmentation | -| `sensor_findings` → `…bmc_sensor_observation` `mtcollins1_sensor_findings` | bundle; sensor witness | `mtcollins1_sensor_anomalies` (typed, in the sensor module) on `Platform{ChassisPower}` | -| `smpro_findings` → `gunbc.machine_intake_ampere_smpro_observation` `smpro_pass_findings` | bundle (3 passes); SMpro witness | `Socket{k}` standings via `smpro_boot_progress_of`, and `SecondaryJoin` via `secondary_join_outcome`. The "sockets differ" string is retired: the difference is now the two typed standings | -| `console_findings`: `…dram_console_observation` `ampere_dram_findings` | bundle; DRAM witness | `ampere_dram_untrained_sockets` (closed-roster rule, in the DRAM module) feeds `DegradationDimmNotTrained` / `DramFirmware` anomalies; malformed rows become `DramFirmware` anomalies | -| `console_findings`: `socket_summary_findings` | bundle | `SecondaryJoin` via `secondary_join_outcome`, and `ActiveSocketsBelowExpected` against the receipt's expected configuration | -| `console_findings`: `…kernel_module_decompression_observation` `kernel_module_decompression_findings` | bundle; kernel-module witness | `Kernel` `StatedFailure { KernelModuleRefused }` | -| `media_attach_findings`, `handoff_media_findings`, `override_findings`, `power_after_findings` | bundle; `megarac_media_convergence_witness_test` | `CollectionOutcome` (typed records, rendered once) | -| credential-shred string | bundle findings | `CollectionOutcome.credential` | -| `HostBootObservation` and its datum (#13041) | `mtcollins_firmware_converge` `boot_verdict`; its witness | `FirmwareSetBootReadback` (above) | - -**Consumers outside the required gate.** The production consumers are all in this closure: `mtcollins1_boot_run` (its fleet-converge entry) and the bundle. The test consumers are the witnesses of `mtcollins1_boot_diagnostic_bundle`, `ampere_smpro_observation`, `ampere_dram_console_observation`, `mtcollins1_bmc_sensor_observation`, `kernel_module_decompression_observation`, `megarac_media_convergence` and `mtcollins_firmware_converge`. Each one is migrated in slice A to the typed destination above, with each of its discriminating REDs kept. None is deleted without its claim moving. - -## 7. Reset cause, retry state and the summary's status lines - -The SCP keeps a reset-cause record, and no public register window reaches it: the full 0x00–0xFF SMpro sweep (`sweep-20261003T014255Z`) found none. So `ResetCauseObservation` is publicly `ResetCauseUnobserved { searched }`. `RetryStatePersistence` asks whether any failsafe or retry state survives the reset, which decides bounded retries versus an infinite loop. It is open on every `RestartedRepeatedly` ending. On cycles that reach UEFI, the summary's `Failsafe status` and `Reset status` fields are carried as raw observed values and cited as evidence on those two questions, never decoded publicly. - -## 8. The gunbc-private refinement slice (named) - -The private slice is **P: the Mt. Collins power-on account refinement**, in gunbc-private. It consumes the public `PowerOnAccount` and the private evidence (gunbc-private #202/#203: disassembly and SCP notes). It returns a private refinement that: -- attributes `CP:` tokens to stages and sub-steps (e.g. the DDR training eye sweep); -- names the restart mechanism (the SCP boot-complete wait and the watchdog); -- decodes `ResetCauseObserved` and the summary's status fields; -- answers `RetryStatePersistence` where the private evidence can. - -It imports the public account, the public repo never imports it, and nothing private flows back into public. - -## 9. Seams (noted, not abstracted) - -| Seam | Mt. Collins | Mt. Jade (#13065) | x86 | -|---|---|---|---| -| BMC family | AMI MegaRAC 0.45.3 | AMI MegaRAC, Ampere JADE LTS SPX12 | Varies | -| Stage authority | `AmpereBootStage` via SMpro | Same silicon. To verify that the SMpro I2C route is exposed. | No SMpro: port-80/IPMI POST codes, a different authority | -| Second socket | CCIX/2P, PMpro, `MtCollins1SecondaryJoinOutcome` | Same, needs its own join row | UPI/xGMI, MCA | -| DRAM evidence | Ampere DRAM console block, opaque `CP:` | Likely the same, with references per platform | Vendor MRC messages | -| Summary and NVPARAM | AMI Aptio on Ampere | Likely the same | None | -| Console route | MegaRAC SOL (`DiesOnReset`) | MegaRAC SOL | Varies | - -The differences found when Mt. Jade lands become the onboarding checklist. - -## 10. Implementation slices - -1. **A (#13117): the power-on account, replacing every string finding and `HostBootObservation`, after #13041 lands.** Former slice B (#13058's error records) is folded in, and §10a records the decisions. It includes: - - the public readers: opaque `CP:` (`gunbc.machine_intake_ampere_checkpoint_console_observation`), and the EFI stub and kernel lines (`extdeps.linux.efi_stub`, `extdeps.linux.boot_console`, read by `gunbc.machine_intake_linux_boot_console_observation`); - - the `AttemptConfigurationReceipt` (frozen before power-on, including the human completion/inspection receipt for manual changes), and coverage; - - the account; - - the convergence readback; - - the bundle and receipt cutover; - - every census row in §6, plus the REDs of §5. - - Fixtures are committed excerpts from the `mtcollins1-captures-2026-10-03` evidence branch, including step 3 as the new-firmware positive control, and the run 37062170720 / 37069907299 artifacts. A pre-review draft of the readers and a fold exists on `session/calm-lynx-884-slice-a-wip`. It predates these rulings (it uses a scalar ending, stage attribution from the DRAM banner, and its own SMpro pair rule) and will be reworked, not landed as-is. -2. **B: the attempt-configuration receipt's live inputs**: the applied stimulus, current population and pre-power-on firmware readback (decided Q4). The error records originally planned as B landed in A. -3. **C: NVPARAM** as UEFI-subject context, gated on #13059. -4. **D: `ManagedHost`**, gated on #13055. If #13055 lands before A, it is folded into A. -5. **P: the gunbc-private refinement slice** (§8). - -## 10a. Decisions taken while implementing slice A (#13117) - -These were settled while building slice A and through its side-chat reviews. Each one is enforced by a witness in #13117 and recorded here so this plan stays the authority. - -- **Cycles open only on typed boundary evidence.** Each SEL boot record gets a `SelBootBoundaryStanding` from an ordered fold with no look-ahead: - - the first record corroborates the confirmed power-on when it is a power-up; - - the first record is `BoundaryAmbiguous` when it is a restart after a confirmed power-on. It opens no cycle and becomes an open question, because this MegaRAC can log the power-on's own first boot as "System Restart" (step 1); - - every later record, including a second power-up, is a distinct boundary. - - Console banners only bind spans to cycles, and only when the counts agree; otherwise the binding is `ConsoleSpansUnbound`. The BMC clock is never joined to ours. -- **Stated failures are counted per occurrence.** - - Panics and initrd failures are read per printed line. - - Firmware statements, GRUB reports and module refusals are read by their owning readers over each console span's own lines. - - Each error carries its cycle. Only a failure in the last cycle can choose the ending, and the furthest one there wins. Earlier and unbound errors stay as history. -- **A loop is distinguished from progress after a restart.** `RestartedRepeatedly` applies only when the last cycle got no further than an earlier one. Otherwise the ending is `StoppedWithoutRestart { restarts_before }`. -- **Topology, population and absence are judged only against the receipt.** - - The expected topology is `ExpectedSockets`, or `ExpectedTopologyNotRecorded` (production today). - - Processors are checked once per expected socket: missing, duplicated, or unreadable socket ID. - - Memory is checked per socket, over the union of expected-populated and observed sockets, against `SlotRow { label, socket, populated }`. - - Sensor absence counts only for an expected socket. - - Health states reported by the BMC stay unconditional. -- **SMpro.** - - Passes are reported per pass, not placed into cycles. - - #13058's records are placed by their gate (pending → stated error plus degradation; ungated → anomaly plus open question). - - Pending warnings are kept. - - The error-record registers have their own coverage carrier, separate from the boot-stage pair. -- **Each milestone carries provenance.** It has its capture digest and a `DeclarationRef` to its reader. A one-variant nullary sum does not resolve in the substrate, so a declaration citation is used instead. -- **Convergence refuses UEFI followed by a loop.** It reads the whole-account readback. A UEFI milestone followed by a `RestartedRepeatedly` ending refuses as `BootReachedUefiThenRestartedRepeatedly`. -- **#13025's `secondary_checkpoints` is deleted (Q3).** The per-socket CP classification belongs to the private refinement slice. -- **The receipt's live inputs are slice B (Q4).** Slice B adds the boot-workflow input for the applied stimulus (with a human inspection receipt through the operator-attested route), the current population, and the pre-power-on firmware readback. Until it lands, those fields are `NotRecorded` and every question needing them is open. - -## 11. Questions - -Q1 and Q2 were decided by the side chat on `75f0bd113c` and are encoded in §1, §3 and §5. Q3 (delete `secondary_checkpoints`) and Q4 (add the receipt inputs, as slice B) were decided during slice A (§10a). Q5 and Q6 are decided; none is open: -- **Q5 (decided by eager-gull-22, 2026-10-03):** the plan and the inspection receipt are committed JSON under `artifacts/receipts/`, read from the checkout by a fail-closed typed reader that records the file's digest. One dispatch input names the file. Inline JSON in a dispatch input is refused. -- **Q6 (decided, 2026-10-04):** add the `attempt_receipt` input to the fleet-converge `mtcollins1_boot` mode. It was operator escalation msg_1b57e749, approved by default after 15 minutes with no answer, and relayed by eager-gull-22. - -## 12. Slice B: the attempt receipt's live inputs (plan, for review before code) - -Slice B fills the `AttemptConfigurationReceipt` fields that slice A records as `NotRecorded` (decided Q4). The boot run takes its host as a `ManagedHost` / `ManagedHostBinding` (`gunbc.managed_host`), not as mtcollins1 constants; cut 4d of the managed-host untangle will re-root the rest of the run. - -**Two records, each minted only by its checks.** - -``` -AttemptConfigurationPlan (sole constructor) { - subject: ManagedHost, attempt: AttemptPlanId, - expected: ExpectedTopology, requested: StimulusRequest, // expected and requested are stated ONCE, here - fixed_at: OperatorAttestedTime, -} -AttemptInspectionReceipt (sole constructor) { - plan: AttemptConfigurationPlan, - applied: StimulusApplication, cpus: PopulationReading, dimms: PopulationReading, - completed_at: OperatorAttestedTime, evidence: ReceiptEvidence, witnessed_by: NonEmptyStr, -} -OperatorAttestedTime { rendering: NonEmptyStr, attested_by: NonEmptyStr } // a time a person wrote, never an observer clock reading -ReceiptEvidence { path, digest: Sha256FileDigest, commit } // the committed file the run read -``` - -**The attempt identity comes from the dispatch, not from the artifact.** The current run's attempt identity is minted outside the receipt: it is the fleet-converge dispatch's existing `transaction_nonce`, which the operator mints and which the run name echoes. Admission runs before any pre-power read or actuation and joins `receipt.plan.attempt` to that identity. It then consumes the identity in a **create-once slot**: -- The slot is keyed by (managed subject, `transaction_nonce`), in its own attempt-admission namespace, separate from the unit hold. -- `Absent -> Consumed` is the only successful transition, done by compare-and-set against `Absent` (`std.durable_compare_and_set`). -- No hold release or hold cleanup deletes or rewrites a slot. - -So a used identity stays used: it is not blocked by a later one, and it does not block one. A valid receipt authored for attempt A cannot be selected by attempt B, and A cannot be replayed after B. - -Controls, each an implementation RED: -- the first A succeeds; -- a concurrent or repeated A refuses; -- B then succeeds; -- A still refuses after B; -- releasing the unit hold does not erase A's consumed standing. - -Implementation cut obligations: -- The workflow's `transaction_nonce` description changes from "not consumed by any step" to its admission role. -- A named receipt with an empty or absent `transaction_nonce` refuses. - -The run mints an `AttemptInspectionReceipt` only after every check passes, and each failed check refuses as its own typed cause: -- the file's digest matches what was read; -- the subject is the host this run's `ManagedHostBinding` bound; -- the plan's attempt equals this dispatch's attempt identity; -- the attempt identity was not already consumed; -- the plan's identity is the one the receipt carries; -- the attested ordering holds: `fixed_at` is at or before `completed_at`, as attested. - -**Times.** The plan's and receipt's times are what a person wrote, so they are `OperatorAttestedTime`, never `ObserverTimestamp`. They are ordered only among themselves. "Before the power write" is established STRUCTURALLY: admission is a step this run completes before it actuates. It is never established by comparing an attested time with an observer clock. - -**The absent-receipt rule (decided by eager-gull-22).** -- **No receipt named:** the dispatch input is empty. The boot proceeds with the configuration `NotRecorded` and no topology judgement, as in slice A. -- **A receipt named but refused:** a missing file, a digest or parse failure, an empty `transaction_nonce`, a subject, host, attempt or plan mismatch, a consumed identity, or an attested ordering violation. The boot REFUSES before any pre-power read or actuation, with that typed cause. -- REDs, one per arm: - - an empty input reaches actuation with the configuration `NotRecorded`; - - a valid receipt for attempt A dispatched as attempt B refuses before actuation; - - a replayed identity refuses; - - a malformed file refuses. - -The pre-power-on firmware readback is bound to the same plan (`FirmwareReadBeforeActuation { plan, rows }`), so a readback from another attempt cannot fill this one. A receipt that is absent or refused leaves the fields `NotRecorded` or refused with their cause. The boot never proceeds on a guessed configuration. - -**The operator-attested route** follows `std.human_intervention`: -- The inspection is a `HumanIntervention` step with `HumanSurfaceOnly`, because a physical change has no API. -- Its `DischargedAt { evidence }` cites the evidence PRODUCER: the fail-closed reader that mints `AttemptInspectionReceipt` from the committed file, a `DeclarationRef` to that function. It does not cite a flag or a boolean standing. - -**The pre-power-on Redfish population read is not live by default.** A controller reading taken before power-on may be the BMC's cache from the last POST, so it is `PopulationControllerReading { freshness: FreshnessEstablished | CachePossible, rows }`: -- It can corroborate the inspection or conflict with it. A conflict is `PopulationConflicted`, an open question, never resolved by preference. -- Only `FreshnessEstablished` counts as a live reading. Nothing on main establishes that freshness today, so today's reading is `CachePossible`. - -**Field routes.** - -| Field | Route | -|---|---| -| expected topology and requested stimulus | the plan, stated once | -| applied stimulus and population | the inspection receipt; the controller reading only corroborates or conflicts | -| firmware | a new pre-power-on `hpm check` read through `extdeps.bmc.ipmi` (none exists on main; `gunbc.fleet.mtcollins_firmware_converge` only renders a dry argv), plus the SMpro version word where it answers, both bound to the plan | - -**Delivery (Q5, decided by eager-gull-22 on 2026-10-03).** The plan and the inspection receipt are committed JSON under `artifacts/receipts/`, reviewed and versioned like any change. eager-gull-22 authors them from what the operator reports about the physical change. One fleet-converge dispatch input names the receipt file, and the boot run reads it from the checkout with the fail-closed typed reader above. Inline JSON in a dispatch input is refused because it is not reviewable. Adding that input is Q6, decided. - -### 12a. Slice B1 as built - -Slice B1 is `gunbc.host_boot_attempt_admission`. It holds the plan and inspection records, the reader, the create-once attempt slot, and the projection into `AttemptConfigurationReceipt`. It is host-generic: mtcollins1 appears only as its route row (the receipt variables and the slot roster), in the `gunbc.host_maintenance_hold_reason` pattern. It returns one sealed `BootAttemptClearance`. - -**Placement** (agreed with warm-crane-577): the module sits beside the boot authorization. `mtcollins1_boot_under_live_unit_hold` calls `admit_boot_attempt(proof, revision)` right after `UnitHeld`, so admission runs under the hold and **before any controller read**. -- **Baseline:** the SDR cache and SEL baseline (`mtcollins1_boot_baseline`) are taken only after clearance. -- **Refused receipt:** the boot reads nothing from the controller, releases the hold, writes nothing, and fails with the typed cause. The matrix control is `a_refused_named_receipt_reads_no_baseline_and_writes_nothing`. -- **Configuration record:** the frozen configuration travels on the attempt record into the bundle. Slice A's always-`NotRecorded` placeholder is deleted. -- **Untangle cuts:** cuts 4a and 4d move the call site, and O2 carries its refusals. - -**The receipt is a tracked blob, digested.** -- **Revision binding:** admission reads the receipt at the boot's bound revision, not from the worktree. `extdeps.git.inspect` `ListTreeEntryAtPath` finds the entry, decoded by the existing `gunbc.namespace_step0_subject_collector` ls-tree reader. `git show :` reads the content. -- **Refusals:** no entry is `ReceiptNotTracked`. A symlink, gitlink or directory is `ReceiptNotRegularFile`. -- **Evidence:** `ReceiptEvidence { path, digest: Sha256FileDigest, commit }` carries the SHA-256 of exactly those bytes, from `extdeps.tools.sha256sum`. -- **Follow-up:** the step0 decoder belongs in `extdeps.git`. Extracting it is left to the namespace lane rather than forked here. - -**Times** are admitted only as canonical UTC instants, using `gunbc.auth.approval_capability` `utc_instant_is_canonical`, which checks calendar-valid fields. They are ordered with `utc_instant_before`, and equal instants are admitted. - -**Populations join exactly, or the receipt refuses.** -- **CPUs:** the CPU rows name exactly the plan's expected sockets. -- **DIMMs:** the DIMM rows name exactly the host's slot roster. For Mt. Collins that is the Getting Started Guide's 32 connectors (`dimm_figure_banks`), labelled as the guide labels them (its `J` prefix and the connector number) and placed on their bank's socket. Every label must be known and on its roster socket, and every slot must appear, populated or not. -- **Refusal causes:** a missing socket, an unknown label, a label on another socket, and an omitted slot each refuse with their own cause. - -**The slot store** is `/var/lib/gunbc/boot-attempts`, provisioned by `gunbc.runner_host_grants` `unit_hold_store_operations` beside the unit-hold store: same hosts, owner and mode. It is a separate directory, so a hold's release or recovery cannot reach it. Its executed control on the real store host is the first grant convergence followed by a receipt-carrying boot on srv1. - -Where the build differs from the plan above, with reasons: -- **Slot key.** The key is `attempt---`, with the host and nonce admitted only over `[A-Za-z0-9_-]`. It is injective by construction and is not a hash: the corpus's `content_hash_of_value` is a 64-bit structural hash, which does not meet "collision-safe" against a chosen nonce. -- **The plan's identity.** The inspection names its plan by `plan_subject` and `plan_attempt`. Because an attempt identity admits one boot, that pair identifies the plan. - -### 12b. Slice B2 as built (firmware), and B3 (controller population) - -Slice B2 adds one pre-power read, `mtcollins1_boot_pre_power`. It is taken after admission and before `mtcollins1_boot_actuate_held`, and `gunbc.host_boot_attempt_admission` `attempt_configuration_with_pre_power` joins it to the frozen configuration. -- **The read.** `ipmitool hpm check` is a new `extdeps.bmc.ipmi` `HpmCheck` operation. It is parsed by `extdeps.bmc.ipmitool_hpm_check`, which landed with the CPLD route fix (#13254). -- **The rows.** Each component's active cell is kept as rendered, because the auxiliary bytes have no public decode. -- **Binding.** The readback is `FirmwareReadBeforeActuation { attempt, source, rows }`, where `attempt` is the attempt admission bound. An unread or refused table leaves firmware `NotRecorded` with its cause. -- **The acceptance matrix.** The dry BMC (`gunbc.bmc_dry_realization`) answers `HpmCheck` with the retained BMC 0.32 capture, as a layout fixture. - -**B3: `PopulationControllerReading` (declared frontier).** B3 is the pre-power Redfish population read. It is labelled `CachePossible`, and is joined to the inspection as corroborated or conflicted per socket. -- **Why it is not in B2:** its route dispatches `shell.Mktemp.Dir` and `shell.Remove.FileForce` (for the netrc) and `redfish.Http.GetResourceByPath`. The boot dry world does not model these, and no mtcollins1 Redfish response is retained to model them from. -- **Trigger:** a retained mtcollins1 Redfish capture of the service root, Systems, the system, Processors and Memory. eager-gull-22's read-only probe takes it when the BMC is reachable. -- **Caveat:** after the 2026-10-02 reflash, gunbc gets 401 on Redfish because its role is missing. If the capture is 401 bodies, the trigger also needs the login convergence to restore that role. -- **Already written:** the join and folds are in WIP commit `16a67dfb99d` on `session/calm-lynx-884-slice-b2`. \ No newline at end of file diff --git a/docs/plans/precedence-climbing-tree-contract.md b/docs/plans/precedence-climbing-tree-contract.md new file mode 100644 index 00000000000..fa8ab2c971c --- /dev/null +++ b/docs/plans/precedence-climbing-tree-contract.md @@ -0,0 +1,500 @@ +# Precedence-climbing tree contract for v2 dag binary expressions — design plan + +Status: DESIGN ONLY (work item adhoc-2680a784-274, grammar program, manager stern-bear-500; +reviewer sharp-raven-357). No code lands until this plan is approved. This file sits in a +docs-only PR that will not be merged. + +## 0. The defect, re-derived (§6b) + +The slice is `v2.extdeps.languages.dag dag_grammar_binary_expr_expr` → `v2.compiler.parse` +(`parse_expr_sequence`, `parse_expr_repeat`, `parse_expr_optional`) → the parse tree → +`v2.compiler.body_lowering_fold` (the only reader of the tower's internal shape). + +- **What the grammar says.** `dag_grammar_binary_expr_expr` = `dag_grammar_pipe_expr_helper`. + It is seven inlined helpers (pipe → or → and → equality → comparison → additive → + multiplicative). Each is `Sequence(next_level, Repeat|Optional(Sequence(op_choice, next_level)))` + and bottoms out at `^dag_production_unary_expr`. +- **What the parse establishes.** Every operand, even a bare `a`, passes through all seven levels. + At each level the parse tries the continuation, fails on the operator choice, and records the + failure in the tree. An empty Repeat goes through `parse_stamp_derived_repeat`, which mints an + occurrence id. An empty Optional becomes `grammar_empty_node`. Every level is also a + `parse_stamp_derived_conj` Conj with its own minted `DerivedBy` id. +- **The result.** The tree has one node per precedence level per operand. Precedence (which + operator binds which operands) is encoded in how deep a node sits, not in any node of its own. +- **What the only consumer actually needs.** It needs the operator applications, folded + left-associatively. `body_lowering_fold` throws the level structure away again: + - `body_lower_strip_operator_free_levels` peels `sequence(level, empty)`. + - `body_lower_fold_operator_tail` re-folds the Repeat spine. + - `body_lower_is_pipe_tower_root` decides at the root that `|>` is the outermost level. + +**The earliest unjustified boundary is the grammar's tree contract.** It states precedence as +tree nesting, which no admitted consumer demands. The parse then does that work for every operand +and stamps it as tree content, and lowering undoes it. This is §2's "work no admitted consumer +demands", so the fix is to delete it. Caching, memoizing or batching the levels would only make +it cheaper. #13244 already took the per-parse fixed cost (grammar preparation). What it left is +exactly this per-operand slope. + +## 1. Proposal: one node per operator APPLICATION + +Add a precedence-climbing operator-table combinator to the grammar algebra, the one authority +both directions read (DESIGN §4: one grammar, read in both directions). + +``` +// v2.std.grammar +type InfixAssociativity = InfixLeft | InfixNonAssociative +type InfixOperatorRow { + token_class: Symbol // exactly one terminal class per row + binding_power: Int // higher binds tighter + associativity: InfixAssociativity + guard: Optional // a zero-width predicate run before the operator (§3 below) +} +GrammarExpr += InfixTable { operand: GrammarExpr, operators: List } +``` + +**Forward (parse).** A new `v2.compiler.parse` `parse_expr_infix_table`, using precedence +climbing (Pratt / binding-power): + +1. Parse one operand. +2. Peek the next token's class and look it up in the table, keyed by class. The lookup is + prepared once in `prepare_grammar_expr` as `PreparedInfixTable` and indexed, like the + prepared choice plan. +3. If no row matches, or the row's binding power is below the current minimum, return the operand + **unwrapped**. No continuation node is attempted or stamped. +4. Otherwise run the row's guard, consume the operator, and recurse at + `binding_power + 1` (left associative). +5. Stamp **one** application node per operator consumed: + `Conj { grammar_infix_left, grammar_infix_operator, grammar_infix_right }`, minting one + `DerivedBy` id from its three children. +6. A `InfixNonAssociative` row refuses a second operator of the same binding power with a typed + `ParseRefusalReason` (proposed name `NonAssociativeOperatorChain`). The source stays + describable, but no accepted tree contains the chain: rung 3. + +**The production shell stays one per expression occurrence** (`parse_wrap_production_captured` +for `dag_production_binary_expr`). Inside it, the captured child is either the bare unary operand +or the root application node. + +**Backward (emit).** A new `v2.std.grammar` `grammar_emit_infix_table`: + +- An application node emits its left side, the operator terminal, then its right side. +- A non-application node emits as the operand. +- A tree that violates the table refuses: for example, an application whose left child is an + application of lower binding power, or an unknown operator class. Such a tree would print text + that reparses differently, so this refusal keeps the round trip honest rather than adding + parentheses. Parse trees never need parentheses inserted, because grouping is a primary of + their own. + +**Fold.** Add `GrammarExprFold.infix_table`. This is what keeps +`body_lower_grammar_terminals` reading the operator set off the grammar rather than from a +hand-written list (review 70545). + +**The dag rows.** `dag_grammar_binary_expr_expr` becomes +`InfixTable { operand: Nonterminal(dag_production_unary_expr), operators: dag_infix_operators }`. +The rows carry **v2's current precedence, unchanged**: + +| binding power | operators | associativity | +|---|---|---| +| 1 | `\|>` | left | +| 2 | `\|\|` | left | +| 3 | `&&` | left | +| 4 | `==`, `!=` | non-associative (today an `Optional`) | +| 5 | `<`, `>`, `<=`, `>=` | non-associative (today an `Optional`) | +| 6 | `+`, `-` (guarded) | left | +| 7 | `*`, `/`, `%` | left | + +**Why a grammar variant, and not a flat `unary (op unary)*` with precedence re-folded in +lowering.** That flat alternative ("single-level operator-table production") was considered and +is rejected: + +- It moves precedence out of the grammar into a lowering-side table, so the grammar would no + longer be the single authority on surface syntax. +- It would accept `a < b < c` grammatically, so that refusal would happen later, in lowering. +- The backward emitter could no longer tell a well-formed tree from a mis-associated one. + +The price of the variant is one new arm in every `GrammarExpr` consumer. Section 4 enumerates +them. + +## 2. Readers, by symbol (git grep of every production and emitted identity) + +### The grammar (deleted, replaced by one table) + +All in `v2.extdeps.languages.dag`: + +- Deleted: + - the seven `dag_grammar_{pipe,or,and,equality,comparison,additive,multiplicative}_expr_helper` + - `dag_grammar_{multiplicative,additive,comparison,equality}_op_choice` + - `dag_grammar_additive_newline_minus_guard`, which becomes the `-` row's guard +- Re-derived: + - `dag_grammar_binary_expr_expr` + - the explanatory block above `dag_grammar_arrow_lambda_name_head_expr`, whose + "pipe → or → … → multiplicative" derivation argument is restated over the table +- Unchanged: + - `dag_grammar_unary_expr_expr` (already a single level) + - `dag_grammar_postfix_expr_expr` + - the `dag_grammar_root` production rows for binary, unary and postfix + - `dag_grammar_match_expr_expr`'s scrutinee nonterminal + - `dag_surface_operator_canonicalization_lookup` / `_member`, which read token classes, not + tree shape + +### Grammar algebra and parse (std / compiler): one new arm each + +- In `v2.std.grammar`: + - `GrammarExpr`, `GrammarExprFold` and `fold_grammar_expr` + - backward emit: `grammar_emit_expr` gains `grammar_emit_infix_table` + - `formal_grammar_symbol_to_grammar_expr` does not change, because there is no formal-symbol + form +- In `v2.compiler.parse` (`02_parse.dag`): + - `prepare_grammar_expr`, which builds `PreparedInfixTable` + - `parse_expr_with_first` dispatch + - FIRST analysis: `compute_grammar_first_analysis_given` + - FIRST(table) = FIRST(operand) + - nullable(table) = nullable(operand) + - `compute_nullable_set` + - left recursion: `grammar_has_left_recursion_given`, where the table is left-recursive iff its + operand is + - `authored_choice_site_residue` / `flatten_choice_exprs`: no Choice site, so no roster row + - lookahead: `lookahead_site_overlaps` / `lookahead_alternatives_overlap` treat the table as + its operand followed by an optional infix suffix + - every exhaustive `match` over `PreparedGrammarExpr`. The listed `PreparedRepeat` arm sites are + the census, and the compiler refuses any that is missed. +- Grammar validation (`grammar_validate_and_analyze`) gains a structural refusal: duplicate + `token_class` within one table (§3). + +### Lowering: the only reader of the tower's internal shape + +In `v2.compiler.body_lowering_fold`. + +Deleted (they exist only to undo the level encoding): + +- `body_lower_strip_operator_free_levels` +- `body_lower_tail_has_binary_operator` +- `body_lower_fold_operator_tail` +- `body_lower_tail_element_pipes_into_fold` +- `body_lower_tail_pipes_into_fold` +- `body_lower_tail_starts_with_pipe` +- `body_lower_node_is_pipe_token` +- `body_lower_is_pipe_tower_root` +- `body_lower_tower_pipes_into_fold` + +Re-derived over the application node: + +- `body_lower_read_operator_expression`: an application node lowers to + `lower_binary_infix(left, op, right)`, and a prefix operator still goes to + `lower_unary_prefix`. +- `body_lower_operator_operand` +- `body_lower_binary_apply` and `body_lower_piped_fold_optional`: "pipe into fold" becomes "an + application node whose operator is `|>` and whose right operand is a fold-family call". This + is decided at the node itself, so the root-detection apparatus and its census (#12550) go away. +- `body_lower_try_infix_transform` +- `body_lower_grammar_terminals`: one new algebra arm, collecting the table's token classes. +- `body_lower_binary_operator_tokens` and the `data` row `body_lower_binary_operators`. + +Shell readers, unchanged because the production shells are unchanged: + +- `body_lower_is_pass_through_emitted` +- `body_lower_find_core_substrate_first_hit_child` +- `body_lower_reduce_emitted` +- `body_lower_body_subtree_lower_from_binary` / `_from_postfix` +- `body_lower_arm_operand_resolved_read` +- `body_lower_postfix_expr` +- `body_lower_primary_expr` +- `body_lower_production_emitted` + +Two more, which only receive the lowered shape: + +- `v2.std.compilers.body_lowering` `lower_binary_infix` / `lower_unary_prefix` build the output. + Their input is unchanged. +- `v2.lens.vacuity` `vacuity_bool_witness_fn_body_evidence` calls `body_lower_try_infix_transform` + and receives the same lowered result. + +### Normalize, resolve, infer, eval, emit: none read the parse shape + +- Normalize and resolve: no reader found. +- `v2.compiler.occurrence_role` `dag_name_production_rows`: keyed on `dag_production_postfix_expr`. + That production is untouched, so names under postfix keep `NamesAreReferences`. +- Infer: + - `v2.compiler.infer` (`04_infer.dag`): `infer_operator_is_plus_token`, + `infer_transform_binary_infix`, `infer_transform_is_binary_infix_algebra_shape` + - `v2.extdeps.runtimes.v2_evaluator` `v2_eval_transform_is_int_add` + - All read lowered operator atoms. +- Emit: the Rust, TypeScript and dag-target emitters read `v2.std.compilers.target_model` + `InfixToken` over lowered nodes. The dag target's token emission (#13099 `TargetBindLetShape`, + #13113 `bound_tokens_source_text_with_transforms`) produces token lists from lowered core, not + parse trees. + +### Backward grammar emit and round trip (#12878, #13099, #13113) + +These re-run as controls; none needs re-derivation beyond the new arm: + +- `v2.std.grammar`: `grammar_emit_parse_tree`, `grammar_emit_sequence`, `grammar_emit_repeat`, + `grammar_emit_optional`, `grammar_emit_choice` +- `v2.extdeps.languages.dag` `dag_emit_parse_tree` / `_with_root`, which read `dag_grammar_root` + and so pick up the table automatically +- `gunbc.instruments.dag_emit_real_grammar_round_trips` +- `v2.test.execution.dag_grammar_backward_round_trip` +- `v2.test.execution.dag_emit_interface` +- `v2.test.emit.dag_target_text_round_trip` + +### Lenses + +`v2.lens.text_string_importer_census` reads fixed-depth ancestor walks of identity strings: + +- `primary_is_argument` expects postfix → unary → binary → `dag_surface_expr`. +- `value_prefix`, `value_is_argument`, `value_initializes_bare_string_annotation`, + `value_is_statement_or_arm`, `value_is_field_init`, `value_initializes_annotation` and + `value_is_right_plus_operand` / `site_kinds_at` (`dag_token_plus`) read positions w4 to w8. + +**What changes:** + +- Operands that stand alone keep the same walk, because the shells are unchanged and no level + nodes are named. +- Operands *under* an application gain the application node in their walk. +- The `+` operand reading must be re-derived from the application node's operator edge, not from + a sibling position. + +**The control** is the census output compared before and after **at identity grain**: same sites +and same kinds, not the same count. The test `v2.test.text_string_importer_census` hard-codes +about 108 walk strings (`tsil_*`). Those are re-derived; the expected *census result* is +unchanged. + +Other lenses: + +- `v2.lens.complexity_accumulator_copy.analyze` `postfix_expr_capture` and the + `collect_value_identifiers` comment: re-read; postfix is unchanged. +- `v2.workflow.floor_cost_debt_edit` (`removal_safe_follower`, `removal_operand_opener`, + `removal_walk_*`): token stream only, unaffected. + +### Tests that name the shape + +- `v2.test.manual.body_lowering_normalize_add`: `body_lowering_binary_infix_holds`, + `body_lowering_expr_infix_holds`. +- `v2.test.body_lowering.reify_operand_refusal`: `ror_first_binary`. +- `v2.test.parse.newline_dual_role_operator_parse`: + - `n_is_unary`, the unary-count route control from #13034. This survives, because unary is + unchanged. + - The comments citing the additive helper are re-pointed at the `-` row guard. +- `v2.test.parse.block_expr_as_binary_operand_parse` and + `v2.test.parse.block_headed_operand_class_parse`: the derivation prose is re-pointed. Their + arms are part of the control set (§6). +- `v2.test.manual.grammar_choice_ambiguity_audit`: the comment about "binary_expr's precedence + levels" yielding identical rows. Those rows are gone. +- `dag/test/claim/long/dag_arrow_lambda_witness_test` `pre_repair_expr_expr`: a copy of the expr + grammar. It references the production, not the helpers, so it is unaffected; to be confirmed + by compiling it. +- `layout_line_break_test`: a copy of the minus guard, re-read against the row guard. +- Fixtures that only use operator token atoms are unaffected: + - `enclosing_expression_structure`, `fold_operand_structure` + - `rust_binop_emit`, `add_body_*`, `body_lowering_infix` + - `dag_bind_let_block_scoped`, `closure_parse_batch_two`, `variant_field_lowering` + - `dag_line_comment_annotation_channel` + +### Prose-only mentions (no consumer; re-pointed so they don't cite deleted symbols) + +- `gunbc.recurring_failure_mode` + `v2_lexer_carries_no_layout_so_a_newline_before_a_dual_role_operator_merges_silently` +- `gunbc.recurring_failure_mode` + `data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering` +- `gunbc.compiler_frontend_program_status` +- `v2.test.long.accumulator_copy_fold_analysis` + +### Other languages + +- `v2.extdeps.languages.kotlin` has its own named-production tower (`kotlin_grammar_binary_expr` + → equality → comparison → additive). It shares no symbol with the dag tower, so it is **not** a + consumer and is not changed in this transition. Once `InfixTable` exists it is the obvious next + inhabitant. That is a separate, later change, named here so it does not read as forgotten. +- `swift.dag` uses one generic operator token and no levels. `rust.dag` and `java.dag` have no + tower. +- No generated `*.rs` mirror names any of these identities. + +## 3. Overlap roster stays 0, by construction + +**The table contains no `Choice`.** The roster (`authored_choice_site_residue` over +`flatten_choice_exprs`) has no site to inspect inside it. Operator selection is an *index lookup +keyed by token class*, not an ordered alternative. + +**Disjointness is structural.** `grammar_validate_and_analyze` refuses a table whose rows repeat +a `token_class`: + +- A duplicate row is writable in source, but no validated grammar contains one: rung 3. +- Each row is exactly one terminal class, so "disjoint FIRST sets" is "distinct class symbols": + decidable and total. +- There is no order among rows. The zero-ambiguity ruling (2026-10-02) holds because nothing is + left to order. + +**Prefix vs infix `-` are disjoint by position.** Infix `-` is consulted only *after* a complete +operand. Prefix `-` lives only in `dag_grammar_unary_expr_expr`, at operand start. This is the +same split #13034 relied on. + +**The newline-minus guard survives as the `-` row's `guard`.** The guard is +`NotPredicate(RefuseOnMatch(AfterLineBreak(Terminal(minus))))`, exactly the expression #13034 +authored, moved from the additive helper's sequence onto the row. `parse_expr_infix_table` runs +the guard before consuming the operator: + +- A line-break `-` raises `NewlineBeforeDualRoleOperator`. The refusal survives the predicate, as + in `parse_expr_not_predicate`. +- A same-line `-` is subtraction. + +Its controls in `v2.test.parse.newline_dual_role_operator_parse`, both the refusal arm and the +same-line subtraction route that counts unary layers, must stay green *unchanged*. + +**Relation to #13126 (still OPEN).** Whether or not it lands first, the table adds no row. If +#13126 lands first, its required zero-count claim +(`test.claim.parse_test_grammar_choice_overlap`) is part of this change's control set. + +## 4. Replacement migration order (DESIGN §3: delete-first, one authority transition) + +The parse tree is consumed by lowering inside the same compilation. That makes it a +**gap-intolerant boundary**: no intermediate state can have the grammar emitting one contract +while lowering reads the other. So the transition is **one PR**. + +1. **Delete the root.** Remove the seven helpers, the four `_op_choice` functions and the + standalone guard. `dag_grammar_binary_expr_expr` becomes the `InfixTable` row. This breaks + every reader loudly, and what breaks is the census in §2. +2. **Add the algebra arm in `v2.std.grammar` and `v2.compiler.parse`.** The `GrammarExpr` / + `PreparedGrammarExpr` matches are exhaustive, so the compiler enumerates every site, and each + one gets its honest arm (§2). This includes the duplicate-class refusal and the + `NonAssociativeOperatorChain` reason in `v2.std.parse_refusal_reason`. +3. **Re-derive lowering over the application node, and delete the tower readers outright.** No + reader understands both shapes, and nothing falls back to the old tower. The old tree may be + used only offline, from history, as the differential oracle in §6. +4. **Re-derive the census lens and its walk strings**, the test fixtures, and the prose citations. +5. **Re-measure the declared cost drops (§5).** Retire a drop only where its trigger is actually + met, never by assertion. + +**Root inside the required gate?** The parse/lowering route is exercised by required floor +claims, so the deletion census is loud there. The lens (`text_string_importer_census`) and +`kotlin.dag` are named explicitly above because a product-layer reader could stay quietly broken +(§3: enumerate before deleting). + +## 5. Expected saving, retired drops, headroom + +**What wise-ant-549 measured (#13244)** for one operand `a` entering at each level: binary layer +3,080 steps (about 440 per inlined level). Of that, an empty repeat stamp is 14 and memo +bookkeeping about 110. For comparison: primary 1,348, postfix 860, unary 493, expr guard 1,665, +arg guard 1,263. + +**Structural claims.** These must hold, and each falsifies the design if it doesn't: + +- **S1.** No Repeat or Optional continuation node is attempted or stamped for an operand with no + operator. Per operand, the binary layer does one peek plus one indexed lookup. +- **S2.** The number of application nodes equals the number of operator tokens. Level nodes: 0. +- **S3.** The lowered output (after `body_lowering_fold`) is node-identical for every control + expression, ignoring occurrence ids. + +**Magnitude estimate.** This is labelled as an estimate: missing it falsifies the estimate, not +the diagnosis. + +- The binary layer drops from about 3,080 to about 150 steps per operand (one prepared-table + lookup at the cost of an indexed lookup, about 6.5, plus frame overhead). That saves **about + 2.9k steps per operand position**. +- An operator token adds one application stamp, about the cost of one sequence stamp, instead of + re-entering seven levels for its right operand. +- Per *token*, the saving is about 2.9k × (operand positions / token). That fraction is around + 0.4–0.5 in typical bodies, so **about 1.2–1.5k of today's 2.6–3k per-token slope**. +- The rest of the slope is the expr and arg guards (1,665 + 1,263) and postfix/primary. Those are + out of scope here and are the next target. + +**Which drops retire.** Each is predicted, and each is retired only on measurement: + +- **`gunbc.rung_drop.dag_text_round_trip_nested_bind_new_witness_eval_step_cost`**, with + `floor_eval_step_cost_drop_dag_text_round_trip_rows` (Bind body 108,685; Bind value 106,590; + budget 72,300 = 100 ms × 723 steps/ms, from `required_floor_new_witness_envelope_ms` and + `floor_eval_step_calibration`). + - Its trigger names exactly this capability. + - **Honest prediction: this change alone probably does NOT bring them under budget.** The + two-Bind text has about 4–6 operand positions, so the expected saving is about 12–17k + against the 34–36k needed. + - The drop therefore stands until the measurement says otherwise. Its trigger already admits + "or parse claims evaluated on the native route" as the alternative capability. + - The plan will **not** reword the trigger to declare it met. +- **`gunbc.rung_drop.dag_emit_round_trip_new_witness_eval_step_cost`**, with + `floor_eval_step_cost_drop_dag_emit_round_trip_rows`. A candidate; re-measured against its + "two parses of a one-line module" trigger. +- **Headroom (not drops).** The four #13233 claims sitting just under budget gain about 3–6k + each: bare name 67,569, empty brace 67,713, minus-led 68,299, mixed conditional 46,473. So does + every gated parse claim, roughly in proportion to its operand count. + +The measured per-claim figures are re-derived by the floor run. Section 6 names the instrument +rather than transcribing future numbers. + +## 6. Discriminating controls + +1. **Tree controls (before/after).** A representative set, parsed on the real route + (`dag_prepared_grammar`), with the tree printed both ways: + - `a`, `a + b`, `a + b * c`, `a * b + c`, `a - b - c` (left associativity) + - `!a && b`, `-a * b` + - `a && b || c`, `a == b`, `a < b` + - `xs |> fold(0, f)`, `a + b |> f` + - `match x { … } && b`, `if c then 1 else 2 + 3` (block-headed operands, #12817) + - `(a + b) * c` + - `x` then a line `-1` (refusal) + + **Red arms:** + - `a < b < c` and `a == b != c` refuse with `NonAssociativeOperatorChain`. Today they fail + with the generic leftover-token reason. The change in reason is part of the diff and is + stated, not hidden. + - A grammar with a duplicated operator row refuses validation. + - A hand-built tree whose application violates binding power refuses backward emit. +2. **Semantic equivalence after lowering.** For every expression above, and over the full corpus + on the native route, the `body_lowering_fold` output equals the pre-change output (S3). + - The oracle is the old grammar run from history as an offline differential: the pre-change + binary run on the same inputs, with no fallback in production. + - Corpus scope: `gunbc test //gunbc/instruments:self-host` plus the required floor. +3. **Round trip.** `gunbc.instruments.dag_emit_real_grammar_round_trips`, + `v2.test.execution.dag_grammar_backward_round_trip` and `v2.test.emit.dag_target_text_round_trip` + stay green. Text → tree → text → tree must be a fixed point over the control set. +4. **Overlap.** The roster count stays 0 (`v2.test.manual.grammar_choice_ambiguity_audit`, and + #13126's zero-count claim once it lands). The newline-minus controls of #13034 stay green + unchanged. +5. **Step saving, measured.** wise-ant-549's #13244 per-level probe is re-run on the same + fragments (`x: 1`, `x: a + b + c`, the 15-token fragment), together with the floor run's + per-claim eval steps for the rows in §5. + - S1 and S2 must hold exactly. The magnitude estimate is reported against them with any miss + named. + - This needs the probe to be an entry point rather than a PR-body transcript. If it is not one + today, landing it is part of the change (§6, name the instrument). + +## 7. The v1 seed + +**v1 does not need to change.** `src/v1/02_parse.dag` already precedence-climbs: + +- `parse_expr_bp`, `infix_bp`, `find_operator_bp` / `find_operator_binop` over the + `OperatorSpec` rows of `dag/extdeps/languages/dag/syntax.dag` `dag_operators` +- the newline refusal in `is_ambiguous_prefix_infix_newline_boundary` + +Nothing in v2 reads that table, and nothing in this change serves the self-host program through +v1. So the v1-maintenance purpose test (`gunbc.v1_maintenance_standing v1_seed_standing`) admits +no edit, and none is proposed. + +**This design does surface three seed/v2 divergences in the operator facts.** Per DESIGN §7, each +is either a v2 regression or a seed defect. This plan preserves v2's current behaviour and does +not decide them: + +- **`|>` precedence.** + - v1: binding power 17/18, the *tightest*, so `a + b |> f` = `a + (b |> f)`. + - v2: the *loosest* level, so `(a + b) |> f`. + + This is a real semantic split on the same source text. **It needs a ruling** before the table's + row order is treated as settled. +- **Chained comparison/equality.** v1 accepts `a == b == c` left-associatively; v2 refuses it. + This plan keeps v2's refusal, now typed, as the stronger rung. It should be confirmed as a seed + defect. +- **`??` (`NullCoalesce`).** It is in the v1 table and absent from v2. That is out of scope here, + noted only so the table is not read as complete. + +**Two operator tables for one language.** After this change, the seed's `dag_operators` and v2's +`dag_infix_operators` both describe dag infix precedence. That is not a new nickname. The seed +table is the frozen X of the seed carve-out, and v2's row is the authority going forward. No v2 +code may read or derive from the seed table. + +## 8. Open questions for the reviewer + +1. **The `|>` precedence ruling (§7).** Recommendation: keep v2's loosest-pipe. It is what the + lowering's pipe-into-fold reading and the corpus are written against. Record the seed's + reading as the defect. +2. **`InfixTable` as a new `GrammarExpr` variant** versus the flat single-level production with + precedence in lowering (§1). Recommendation: the variant. +3. **The application node's representation.** Proposed: a `Conj` with three named projections + (`grammar_infix_left`, `grammar_infix_operator`, `grammar_infix_right`). The alternative is a + dedicated `TypeNode` connective. The `Conj` form reuses the parse's existing stamping and + emit's projection reading and adds no connective. Recommendation: `Conj`. diff --git a/docs/plans/prepared-effect-input-carry.md b/docs/plans/prepared-effect-input-carry.md deleted file mode 100644 index 55a41aa5f0c..00000000000 --- a/docs/plans/prepared-effect-input-carry.md +++ /dev/null @@ -1,205 +0,0 @@ -# Prepared effect input carried into nullary warm rows (PR two of the #10994 chain) - -ADJUDICATED AND IMPLEMENTED. The model below was sent to eager-raven-113, fierce-lark-661 and -bright-boar-435 before any code was written; all three approved both decisions (2026-09-14), each -with one condition, and every condition is discharged in the landed change: - -- the carried value's DISPOSITION is printed on the floor's phase line beside its content digest, - so a run whose carrier refused is diagnosable rather than silent (bright-boar's condition on - Decision 1); -- `std.materialization_ladder` is cited IN SOURCE, not only here: the four obligations are typed - values in `v2.workflow.floor_prepared_effect_input_ladder`, decided by the ladder's own fold and - exercised by `v2.test.floor_prepared_effect_input_ladder` (the condition both fierce-lark and - eager-raven attached to Decision 2); -- the binding's absence after the tier is cleared has its own control (eager-raven's condition b). - -Two things the evidence found that this document's first cut did not, recorded because both -changed the implementation rather than the prose: - -1. `run_in_context` calls `call_function` DIRECTLY, reaching neither the cross-claim tier nor the - prepared-input binding. A control that evaluated the producer that way asserted a value - RECOMPUTE also produces — the decoration DESIGN section 4b names. The controls now evaluate - through an ordinary call site and assert an OBSERVED SERVE from the tier's own hit observer. -2. With that fixed, the changed-carrier RED went red on the REAL implementation: the - producer-to-carry map held a CLONE of the carry, so re-binding the acquisition left the key - unchanged and the producer was served a value derived from the previous carrier. It now holds - the acquisition NODE and reads its current binding at every key derivation. - -## The gap, stated at the floor's own vocabulary - -`v2.workflow.floor_pure_producer_share` admits two fill dispositions and only two: - -- a WARM row is NULLARY and preparation-forceable — `install_pure_producer_share` evaluates it - once in a Hermetic frame over its own module scope and the fill is billed to preparation; -- a CLAIM-FORCED row has claim-shaped arguments, so it fills inside the fold on first touch and - the paying claim carries the fill. - -A producer whose value depends on one effectful read of a committed carrier fits neither. It -cannot be warmed as a nullary pure row, because `store_cross_claim_pure_memo` is keyed on -(resolved fn node, portable argument row) and an effect-dispatching nullary call contributes -NOTHING to that key — the read's content is invisible to the key, which is the ctx-blind homonym -class in its content form: two different carrier contents would share one entry. And as a -claim-forced row it re-derives per claim, which is the rostered -`shared_precondition_re_derived_once_per_claim_frame`. - -The live instance: `gunbc.roadmap_authority.roadmap_authority_projection` — one -`Filesystem.Read` of `dag/gunbc/roadmap/roadmap_acceptance_event_history.jsonl` (16 lines, -~10 KB) followed by a pure ~186 ms fold, demanded by seven required-lane claims and by #10994, -whose current blocker is an enrolment margin RED (306 ms then 279 ms against a 302 ms margin) -sitting on top of exactly this shared precondition. - -## One fact that makes the carry legitimate rather than a memo of an effect - -Hermetic mode already classifies this read as INPUT ACCESS, not a host effect: the -checkout-input carve-out in `v1_interpreter::eval_service_call` dispatches a readonly -`Filesystem.Read`/`List` WET when `hermetic_checkout_input_disposition` confirms the path under -the checkout root. The carrier is such a path. So the value is a function of the commit the run -prepared — constant for the whole prepared subject — and carrying it once is not caching an -effect, it is binding an input the run already holds fixed. Anything the disposition cannot -confirm (out-of-root, `.git`/`target`, unresolvable) is refused by that existing wall and is -therefore never carryable here either. - -## The model - -Two declarations in `v2.workflow.floor_pure_producer_share`, beside the two existing rosters. - - type PreparedEffectInput { - // The nullary declaration whose evaluation ACQUIRES the input. Resolved to a fn node at - // preparation, exactly like every rostered producer — never admitted by bare name. - acquisition: String - // Why the acquisition is a run-constant checkout input rather than host state, and the - // consumers the carry serves. Evidence fields, same discipline as RefusedShareCandidate: - // name the instrument, never transcribe its output. - ground: String - measurement: String - } - - type CarriedInputWarmRow { - producer: String // the pure fold, resolved to its fn node - parameter: String // the named parameter the carried value fills - input: String // which PreparedEffectInput.acquisition supplies it - measurement: String - } - - data floor_cross_claim_prepared_effect_inputs: List - data floor_cross_claim_carried_input_warm_rows: List - -### What preparation does - -`install_pure_producer_share` gains one phase, ordered BEFORE the existing warm loop: - -1. Resolve each `acquisition` to its fn node in a frame over the prepared subject (the existing - `floor_authority_frame`, unchanged — Hermetic). An unresolved spelling or a module outside the - subject is a stale row and STOPS THE LINE, exactly as a stale warm row does today. -2. Refuse at install any acquisition whose declaration is not nullary: a carried input with - arguments is a different concept and must not be smuggled in under this one. -3. Evaluate it ONCE, under `observe_shared_build`, so the acquisition is billed to preparation - and adjudicated by the same preparation refusal as every other shared build. -4. Reify the value totally to `PortableValue` — the SAME total walk publication already performs - — and take its content identity as the structural digest over that form (`std.content_hash` - `Fnv1a64Structural`). Printed on the `[floor-phase]` line, so the receipt says WHICH content - was carried, not merely that something was. -5. Bind (fn node → carried `Value`) for the run's prepared subject, cleared with the tier by - `clear_cross_claim_pure_memos`. - -Then, for each `CarriedInputWarmRow`, warm the producer with a ONE-ELEMENT argument row -`[(parameter, carried value)]` instead of `[]`. Everything downstream is the existing mechanism -untouched: the store keys on (producer fn node, portable argument row) and serves only after the -full portable argument row verifies structurally equal. - -### What a claim does - -- A claim's call of the ACQUISITION declaration is served the carried value instead of - re-dispatching the read. This is the only genuinely new serve path, and it is deliberately NOT - the pure memo: the pure memo refuses any call that dispatched an effect (`effect_dispatch_count` - guard), and that refusal is correct and stays. Admission here is a separate, roster-declared - binding keyed on resolved fn-node identity, installed for one prepared subject. -- A claim's call of the PRODUCER passes the same value it just received, so the portable argument - row equals the one preparation stored, and the existing cross-claim tier serves it — no new - keying, no new retention, no new lookup rule. - -### Why content identity is not a new mechanism - -The carried value IS the key material: a changed carrier content produces a different portable -argument row, `cross_claim_portable_args_match` fails, and the claim recomputes. Stale serve is -therefore unwritable rather than guarded — the §5 preference for construction over validation. -Roster identity is the resolved fn node on both halves (acquisition and producer), so the -bare-name homonym class stays closed exactly as review 57446 closed it. - -## Refusal arms (never an empty default) - -| state | disposition | -|---|---| -| acquisition module outside the prepared subject | `PreparedEffectInputModuleOutsideSubject` — line stop | -| acquisition spelling resolves to no declaration | `PreparedEffectInputUnresolved` — line stop | -| acquisition declaration is not nullary | `PreparedEffectInputNotNullary` — line stop | -| acquisition evaluation errors (read refused, hermetic refusal, parse error in the fold) | `PreparedEffectInputAcquisitionFailed` — line stop, carrying the interpreter's own located error | -| acquired value fails total reification | `PreparedEffectInputNotPortable` — line stop, with the located path/kind from the refusal itself | -| carried-input warm row naming an unknown input | `CarriedInputWarmRowInputUnknown` — line stop | -| warm evaluates but the store refuses it | the existing `PureProducerShareWarmNotStored` — line stop | - -One arm is deliberately NOT a floor refusal, and it is the one worth arguing about: when the -acquisition SUCCEEDS and returns its domain type's own refusal arm (here -`RoadmapAcceptanceEventHistoryLoadRefused`), the floor carries that value faithfully and every -claim receives the identical typed refusal it would have computed for itself. The floor does not -read domain types and must not decide that one variant of a caller's coproduct means "stop". The -alternative — the floor stopping the line on a refusal arm — would require the floor to know -`gunbc.roadmap_authority`'s vocabulary, which is the layer inversion §3 forbids. Stated here as a -deliberate divergence so it is decided rather than discovered. - -## Home in the materialization domain (§3b), and one stated divergence - -This inhabits the materialization / carried-value home: a computation identity (the resolved -declaration) joined to declared inputs (the carried value's content digest), a provider whose -scope reaches the demands' least common visible ancestor (preparation, which is the LCA of the -seven claims), a key representing every declared input the result depends on, and retention -spanning exactly the obligated lifetime (one prepared floor run). §2's ordering is respected in -the direction it demands: the demand graph is minimized FIRST — the repetition is authored -duplication whose shared-state ancestor is preparation, so the repair is the CARRY, and the -existing cross-claim tier is merely where the carried value is held. - -The divergence, stated: this does not mint a new `std.materialization_provider.ArtifactRequest` -variant. `ArtifactRequest` keys durable, cross-run artifacts by identity digest with a realization -receipt naming the provider; the carried input is an in-process value bound for the lifetime of -one prepared subject, with no store, no path, and no cross-run reuse. Minting a variant there -would put a run-scoped binding into the durable-artifact algebra. If the reviewers read that the -other way, the alternative is a real one and this is the decision to take before code. - -## Evidence - -### Executed at fixture grain, through the real `install_pure_producer_share` path - -`cargo test --release -p v1-compiler --lib pure_producer_share` — 17 passed, 0 failed. Five of -them are this change's: - -- the input is acquired ONCE and its producer warmed over it, with the producer then SERVED - (asserted from the tier's hit observer, not from the value); -- **the discriminating RED**: a changed carrier content is not served the value derived from the - old one. Discrimination receipt — with `cross_claim_key_args` returning `None` (the - empty-argument-row implementation this control targets), this test FAILS; with the real - implementation all 17 pass; -- the prepared-input binding is absent after the tier is cleared, and the producer answers for - itself again; -- a carried row naming an input nobody prepares stops the line; -- a non-nullary acquisition is refused. - -### Still owed at live grain (stated as owed, not as done) - -## Evidence plan (the live arm) - -1. **Identical verdict.** The seven required-lane claims plus #10994's forecast witness produce - the same verdicts with the rows enrolled as without. A carry that changes a verdict is a - defect, not a speedup. -2. **Present-vs-absent, per row, on one tree.** Two `workflow_dispatch` runs on a branch ref - pinned to an exact sha (the corrected instrument recorded in the roster header: a pushed side - branch triggers nothing, and a PR pair measures two different merge refs), reading - `required_floor_claim_cost.tsv` and the `[floor-shared-fill]` ledger. Admission is - serve-below-recompute at identity grain; a row that does not clear it is withdrawn into - `floor_cross_claim_refused_candidates` rather than defended by a family aggregate. -3. **No stale serve (the discriminating RED).** A control that mutates the carrier's content and - shows the previously carried value is NOT served: the portable argument row differs, the - producer recomputes, and the verdict follows the new content. Red goes green only by the key - actually carrying content identity — an entry keyed on the empty argument row passes the happy - path and fails this control. -4. **Acquisition is dispatched once.** The effect-dispatch count over a floor run is 1 for the - carrier read with the rows enrolled, N without. diff --git a/docs/plans/printed-chassis-program.md b/docs/plans/printed-chassis-program.md deleted file mode 100644 index a26aead8415..00000000000 --- a/docs/plans/printed-chassis-program.md +++ /dev/null @@ -1,1363 +0,0 @@ -**Current execution queue:** [CAS-1–CAS-7](cassette-completion-tasks.md) tracks shared profiles, height-related rear cooling, integrated stacking, power/harness placement, qualification and print release. - -**Unified multi-platform design, 4 October 2026:** [cassette profiles and consolidation](cassette-platforms.md) defines one shared design for ALTRAD8UD, GH200 P4261, Mt. Collins and Mt. Jade. New platforms retain their native power/management assemblies where appropriate; no copied board-specific tray generators. - -# Printed node chassis — program plan - -**Current review: 4 October 2026.** The operator requested fewer screws and faster assembly. -Revision 06 uses 17 printed pieces, four tray clamps, six fan/guard bolts and five broad-grip -retaining pins. See [cassette joinery review](cassette-joinery-review.md). Earlier counts below -are dated history. No further gauge sequence or cassette print is scheduled by this review. - -**Pinned plan. Progress reconciled 2026-10-03.** Printers are in use; the operator has the -printed standoff gauge in hand. The operator reports that the existing gauge aligns with numbered holes 1 and 3. - -## Current progress — 2026-10-03 - -Evidence: merged PRINT-N commit `b242faf7655` (#10680), current printed-chassis modules, -and the operator's 2026-10-03 report. Historical execution results are not tests rerun today. -This dated status supersedes the pre-arrival implementation narrative below. - -- **CAD bootstrap and measured export profile landed.** PRINT-N records CadQuery 2.8.0 running - through user-owned micromamba/conda-forge and measured tessellation observations in - `product.printed_chassis.export_profile`. The old kernel blocker is resolved. Full STEP/3MF - solid-readback acceptance is not established by that fact and remains to be audited. -- **Physical printing reached.** PRINT-N records printer-02 reporting `RUNNING` on the gauge - project after FTPS delivery and MQTT start through the modeled path. The operator now confirms - the gauge is in hand. No authenticated printer operation has been performed in this session. -- **Coupon feedback recorded.** `hole_fit_measurement.operator_ladder_reading_2026_09_04` - records M3 interference / tight / free at 3.00 / 3.10 / 3.20 mm nominal rungs. Printer attribution - remains absent; this is not universal hole compensation or qualification of both machines. -- **Initial pair fit observed, 2026-10-03.** The operator reports that both gauge holes align - perfectly with numbered holes **1 and 3**, with the gauge underneath the motherboard and no - screws installed. The supplied annotated image identifies these as **F and C**. This is an - operator-reported alignment pass on that pair; it does not establish loaded support, exact - tolerances, the other holes or the full FIT milestone. One board is sufficient for this check. - -### Numbered reference and observed pair - -The repo already cited the operator's Drive image in -`extdeps.boards.asrock_rack.asrock_altrad8ud_standoff_figure_authority`. A byte-for-byte local copy -is now [ALTRAD8UD_standoff_grid.png](../extdeps/asrock_rack/ALTRAD8UD_standoff_grid.png), retrieved -2026-10-03 from [the original Drive file](https://drive.google.com/file/d/1z36tmUWAwOwXWy6ASmaPWJNzKoZggBs8/view). -Use the image's numbers in operator instructions. Its annotated decimal coordinates are a visual -reference; they do not replace the standard's exact coordinate authority. - -| Image hole | Model location | -|---|---| -| 1 | F | -| 2 | M | -| 3 | C | -| 4 | H | -| 5 | L | -| 6 | B | -| 7 | R | - -**Observed:** 1–3 (F–C) aligns simultaneously underneath the board, without fasteners, per operator. -**Next candidate:** 3–4 (C–H). Other prepared checks are 4–5, 5–2, 3–6 and 4–7. -No new photo is required to identify the pair already reported against this reference. - -### Prepared follow-on experiments — 2026-10-03 - -`product.printed_chassis.fit_gauges` selects five additional gauges using the existing standard -location authority, gauge admission and CadQuery emitter. They are spacing experiments, not confirmed -mounting locations or qualified structural parts. Start with C–H after the existing F–C check passes; -the others extend coverage to the remaining inferred holes. Each is a separate part so it can be -identified by its file and labeled before leaving the bed. - -| Pair | Question answered | -|---|---| -| C–H | Does the rear-to-middle spacing agree on the C/H/L column? | -| H–L | Does the middle-to-front spacing agree on that column? | -| L–M | Does the front-row spacing agree? | -| C–B | Does the additional rear hole agree? | -| H–R | Does the outboard middle hole agree? | - -J remains excluded and S remains unobserved. A connected set of pair distances does not fix every -relative angle; the assembled adjustable fixture still must confirm the full map. Do not install -extra standoffs merely because the standard has a named location. - -**Measurements to gather before a mounting fixture:** - -- Identify the seven candidate holes (F, C, B, H, R, L, M) on one physical board; record any absent - or unexpected hole and pair mismatch. No need to disassemble several boards for this step. -- Actual standoff thread, height from support to board underside, and contact-pad/shoulder diameter; - identify the screws being used and their usable engagement. Do not infer these from the M3 coupon. -- Highest underside protrusion and clearance around each support; note solder joints/components - near the front overhang where a possible support would touch. -- Board edge dimensions and access around rear I/O, memory latches and power/storage connectors. - -**Measurements that can follow, before cassette geometry:** cooler model and total installed height; -PSU model, actual envelope and mounting points; fan model and intended fasteners; intended drives and -expansion cards; loaded node mass; cable connector bodies and the space needed to plug/unplug them; -available deployment width/depth/height. Missing values stay open instead of becoming defaults. The current fleet inventory identifies Dynatron W1 coolers on srv3 -and srv4; that does not identify the cooler on the board being checked, and its catalog row supplies -no measured installed envelope. - -**Recovered operator setup reports:** Creality PLA, white on printer-01 and black on printer-02, -reported September 4–5; on October 3 the operator says both setups are identical. These reports are -retained in `gunbc.fleet.printer_setup_reports`, with source timestamps and a read-only `main` -projection. The historical AMS report did not name a printer. The operator explicitly confirmed AMS physical -slot 1 on both machines on October 3; that now governs the print command. -Nozzle/plate settings still need reconciliation against retained execution evidence. Recovered files disagree: `print-n/work/gauge_petg.3mf` specifies PETG with -textured PEI; `printn-slice/gauge_container.3mf` specifies PLA with a cool plate. Preset metadata does not supersede the operator’s filament reports. Do not reuse their machine G-code blindly. Final slicing must also check -placement including brim/skirt: the longest narrow gauge nearly spans the 180 mm bed. Keep model -scale at 100%; resolve any placement conflict through explicit orientation/adhesion settings, not -by shrinking a measuring instrument. The existing fan-mount experiment -is optional after the actual fan and fasteners are identified; it does not gate motherboard FIT. - -### Preparation results — 2026-10-03 - -Generated **C–B, C–H, H–L, L–M and H–R** as individual STL and STEP files under -`/home/briansrls/print-prep-2026-10-03`. Generation ran through `fit_gauges.batch` with the -compiler built from this worktree; the generated Python contains the modeled geometry and export -settings. `geometry-readback.json` records hashes and executed results for all five parts: -valid single solids, expected dimensions and cylindrical hole locations/radii, STEP round-trip -agreement, closed manifold meshes, mesh bounds and volume agreement. These checks establish file -conformance for these gauges only, not physical fit or printer/material qualification. - -The three `test.claim.fit_gauges_witness_test` checks executed and passed: every candidate fits -the actual modeled printer envelope; none requests J/S; an unknown pair cannot emit a substitute. -`witnesses.log` retains the run. No physical observation has been inferred from those software tests; the 1–3 fit is separately -recorded from the operator above. - -**Ready for final slicing, not yet machine-ready G-code:** use C–H as the next candidate after the -existing F–C result. Use the recovered PLA reports; reconcile nozzle, plate and adhesion settings before -slicing. No upload or printer start was performed. Authenticated delivery/start was not revalidated; -the old `printer_credential_migration_run` includes destructive secret migration and is not an -appropriate routine readiness probe. A subsequent print must use an admitted credential path rather -than rerun that migration merely because it previously started a job. - -### Requested two-printer batch — 2026-10-03 - -The operator requests concurrent printing on both printers. Prepare **printer-01: holes 3–4 -(C–H)** and **printer-02: holes 4–5 (H–L)**. This checks two additional board spans; because the -parts differ, it is not a printer-to-printer calibration comparison. Source-identical STL copies and -hashes are staged under `/home/briansrls/print-prep-2026-10-03/two-printer-batch/`, one directory per -printer. Material/colour reports are now modeled: Creality PLA, white on printer-01 and black on -printer-02. Physical spool identity and current bed-clear state are not established by those reports. -The selected slicing profile is the A1 mini 0.4 mm nozzle, Generic PLA and Textured PEI plate; -this is a recorded process choice, not a claim of current printer telemetry. -The routine workload-identity entry point is implemented in the migration below; its deployment -remains pending. The operator-token batch route and live progress are recorded separately below. -No printer start is implied by staging the files. Label the finished gauges 3–4 and 4–5 before removing them. - -### Printer authentication migration to the ntfy workflow - -The operator requested migration on 2026-10-03. `gunbc.fleet.printer_federation_provision` -adds a dedicated `printer-lan` reader to the existing ntfy-approved GCP IAM estate. It requests -access only to the two rostered printer secrets; the print worker cannot create, rotate or delete -credentials or change IAM policy. The provider pins the repository, main workflow/ref, dispatch -and `printer-lan` environment through the shared OIDC claim authority. - -`fleet-converge` mode `printer` has two operations: - -1. `observe` resolves a printer's existing credential once and writes its exact version identity - to a receipt. The receipt contains no access code. It performs no printer action. -2. `print` requires the selected printer, staged project path on srv1, expected SHA-256, numeric - credential version and an explicit clear-bed report. It hashes a private copy before upload, - uses the same version for FTPS and MQTT, and refuses a workflow rerun. A successful receipt - means **start published**, not a claim that the machine reported RUNNING. Unknown outcomes - require observation, never automatic replay. - -The dedicated job runs on srv1, checks out the dispatch event SHA even if `expected_revision` is -supplied, and serializes starts per printer without cancelling an in-flight operation. It uses -`WorkloadIdentityToken`; no operator-token fallback and no destructive credential migration. -`gunbc.fleet.printer_host_tools.converge` is administrator preparation for the MQTT tool, using -the existing apt ensure and the tool's declared package. The worker checks the actual client -version before any upload. Mosquitto identifies its version through the documented `--help` -interface ([upstream manual](https://mosquitto.org/man/mosquitto_pub-1.html)). - -**Deployment standing:** source changes are not evidence of installed IAM grants or a dispatched -print. The main-pinned workflow must land before it can federate. The existing IAM bootstrap must -cover the new target resources so its observer can plan and its approved executor can apply those -resource-local bindings. Then run the shared `gcp_iam_converge` workflow and approve its concrete -diff through ntfy, prepare the srv1 MQTT tool, and run credential observation. Final sliced projects -and live printer-state verification remain required before dispatching these gauges. The operator -reported both beds clear during this session; do not carry that report to an unrelated later print. - -Validation: all seven printer workflow binding, admission, tool-version, OIDC, IAM approval and -start-replay regression tests passed. The canonical artifact gate regenerated the workflow; YAML -structure assertions and shell syntax checks passed. These are source checks, not live deployment -or printer observations. - -### Operator-token batch execution — 2026-10-03 - -The operator supplied a one-hour GCP token and asked to print before the migration lands. For this -batch, `printer_operator_approval.run` uses the existing `OperatorSuppliedToken` capability and the -shared signed ntfy approval gate. It does not relabel that token as WIF, invent an Actions run, or -change IAM/secret versions. The permanent main-pinned GitHub route remains separate. - -The approval binds printer, project SHA-256, exact credential version and batch attempt. After the -live approval it subscribes to a fresh report and requires IDLE/FINISH with zero print error, then -creates a persistent per-printer start marker before upload/start. A refused or ambiguous start is -never automatically replayed. `printer_report` reads telemetry through the same fleet identity, -exact credential and pinned CA; its report is current-state evidence, not causal acknowledgement. - -Printer-01 credential version 1 was read successfully through the modeled client. Holes 3–4 -were sliced successfully by Orca 2.4.2, with ZIP and embedded G-code checksum checks, one plate, -identity scaling, 15 layers/3 mm height, and bounds including the brim within 180 mm. Selected -settings: PLA, 220 °C nozzle, 65 °C textured bed, 0.20 mm layers, 2 mm outer brim. Estimated -19m22s and 6.99 g. Project SHA-256: -`28a8700489cf908edb9ed59c0aa871fe6903b4d558c3b64de6a7636af701580e`. - -The isolated srv1 execution directory is `/home/briansrls/print-run-2026-10-03`; credentials live -only in owner-only private staging files and are not repository artifacts. Holes 4–5 also sliced -and passed the same checks: 15 layers, 13m48s, 3.67 g, SHA-256 -`a8f434e80014b6a316d30665b372bd0df3e858a68a2ff04a5fe4f50031d68535`. - -The shared approval client now follows the declared writer cutover to the approval broker on -8085; the old roadmap endpoint refused writes as designed. Both requests were filed and reached -approval polling. Four operator-route witnesses passed, including all three writer postures. -Fresh read-only reports showed printer-01 IDLE/error 0 and printer-02 IDLE/error 83902511. -Both reported PLA in AMS tray 0 (physical slot 1), while the existing start command selects the -external spool. The initial executions were stopped before upload to resolve that mismatch. -No start marker, physical start, or RUNNING observation was claimed at that point. - -### Local preparation environment - -The isolated worktree is `/home/briansrls/.worktrees/gunbc/3d-printing-2026-10-03`. -Local preparation artifacts are in `/home/briansrls/print-prep-2026-10-03`: - -- `venv/`: CadQuery 2.8.0; resolved dependency versions in `requirements.lock`. -- `target/debug/gunbc`: compiler built from this worktree. Invocation requires an enforced cgroup - memory budget; the preparation run uses a systemd scope with `MemoryMax=24G` and no swap. -- `mounting-check-map.svg`: schematic from the checked-out standard-location rows; all physical - correspondences remain subject to the bench check. -- `verify-generated-gauges.py`: reads the emitted literal geometry instructions, runs them in - CadQuery, checks the resulting solid and STEP round-trip dimensions/hole axes/volume, and checks - STL bounds, closed manifold edges and volume. Its result only concerns generated-file conformance. -- `slicer-probe/DO_NOT_PRINT_old_settings.3mf`: tool-recovery probe using the old project settings; - deliberately excluded from the next-print candidates. - -The preserved OrcaSlicer 2.4.2 AppImage at `/tmp/orca.AppImage` matches the digest recorded in -`extdeps.printing.orca_slicer`. Its extracted CLI at `/tmp/orcax/squashfs-root/AppRun` now launches -after installing its missing OpenGL/WebKitGTK runtime dependencies. `--help` and model inspection -work. The modeled probe now uses the supported `--help` interface. `with_verified_orca` copies -and hashes the pinned AppImage in a private realization, runs probe and slice inside its callback, -and removes the realization before returning. `slice_run.run` uses it to produce the gauge project; -its success establishes slicing, not an observed physical print. - -### Gauge observations to collect today - -Record each board separately: stable label, model/revision, selected holes and gauge orientation. -The gauge checks **LocF–LocC**, modeled as offsets of **157.48 mm along the rear edge and -22.86 mm in depth**. These are model values, not measured dimensions of the printed part. -Establish the physical hole correspondence before interpreting a result; say whether checking board -holes directly or installed standoffs, since the latter also tests standoff placement. - -Report whether both locations align simultaneously with the gauge resting naturally; if not, -which end aligns and the direction of the miss. Note obstructions or warp. Do not count alignment -obtained by bending the gauge or drawing it into place with screws as a pass. Record measured -values only when actually measured, with the instrument used. A photo can supplement the result. -An F–C pass supports that pair on that board, not the full mounting map or underside clearance. - -### Next steps and dependencies - -| Order | Work | Dependency / completion evidence | -|---|---|---| -| 1 | Record each board's F–C result | Operator observations; retain disagreements and distinguish board-hole alignment from standoff fit | -| 2 | Resolve misses; extend successful checks to remaining mounting locations | For a miss, distinguish orientation, printed-gauge dimensions and board/standoff placement before changing geometry. A pass permits additional pair gauges or an adjustable fixture, not freezing the entire map | -| 3 | Audit CAD, slicing and print evidence | Can proceed during board checks: locate generated artifacts, profile/toolchain identity, printer/material/nozzle binding and outstanding solid-readback controls; close PRINT-7/8 only against their full criteria | -| 4 | Complete process observations | Attribute the existing coupon only from retained evidence; otherwise print labeled replacements. Observe both printer/spool combinations independently and repeat attempts before claiming repeatability | -| 5 | Build an unpowered adjustable fit fixture | Confirm remaining mounting positions, standoff thread/height, underside keep-outs and connector access. PLA can serve this experiment without qualifying structural or powered use | -| 6 | Measure cassette inputs and choose joints/material | Can start now: actual cooler and PSU envelopes, node mass, drive requirement, fasteners, connector access and cable service space. Joint family and structural material precede their qualification prints | -| 7 | Prototype one removable cassette and one fixed bay | Requires fit and structural-process evidence; then PSU/cables, airflow and powered thermal validation under standing requirements, followed by 2x2 service demonstration | - -The immediate milestone is a verified mounting map and an unpowered fit fixture. Full cassette -geometry depends on measured hardware envelopes and structural-process qualification. - - -Governing statement: - -> Build a low-cost, space-efficient, thermally intentional system of independently removable -> ALTRAD8UD-1L2T server cartridges, using small-format printers for the custom geometry and -> conventional materials only where they are structurally superior. - -The experiment succeeds when the model produces real printer artifacts, a board fits, a node runs, a -middle node is removable without disturbing neighbours, and the cost/space/thermal receipts decide -whether more printers are worth buying. - -## Original motivations (the spine — every decision answers to these) - -1. Cheaper than commodity chassis. -2. More space-efficient — this is not a storage chassis. -3. Deliberately directed airflow, not a case shaped for a different machine. -4. Forcing pressure on the spatial/fabrication model. This is a first-class goal, not a side effect. - -## Fixed facts - -| Fact | Value | Authority | -|---|---|---| -| Node subject | ASRock Rack ALTRAD8UD-1L2T | `extdeps.boards.asrock_rack` | -| Board outline | 267 x 244 mm | vendor manual §1.4, read first-party | -| Intended nodes | 8 (srv1-4 + 4 inbound) | operator | -| Printers | 2 x Bambu Lab A1 mini, scaling to N | operator order relay | -| Build volume | 180 x 180 x 180 mm | A1 mini spec table, operator relay | -| Fan form factor | 80 mm; **count derived**, not assumed | operator + derivation | -| Rack unit | 2x2 block, composable | side-chat ruling | -| Operator effort | Research component masses online; use explicit model/BOM ranges where needed. Do not request weighing or hardware disassembly for mass inputs. | operator direction, 4 October 2026; [mass assumptions](milk-crate-stack-design.md#mass-is-a-fold-over-components) | -| Service law | Integrated corner-column stack; proposed temporary supports permit target-only removal, otherwise LIFO (4 October 2026, superseding R9) | operator ruling, [milk-crate stack design](milk-crate-stack-design.md) | -| PSU placement | independent power cassettes assigned to node groups by usable wattage, port count and interface compatibility; mains stays in power cassettes (4 October 2026; supersedes one-per-column allocation) | operator ruling, [milk-crate stack design](milk-crate-stack-design.md) | -| External bundle | 2 x Ethernet + 1 x protected, isolatable DC feed per node; AC at shared supply | operator acceptance, 4 October 2026 | - -## Standing laws - -- **Missing physical input policy: refuse.** No default, no nominal catalogue figure, no nearby standard. -- **Hidden CAD geometry authority: refuse.** The model owns dimensions *and* geometry-selection laws. -- **Printed polymer is never the mains enclosure and never a conductor in either grounding network.** Protective earth is bolted to metal and survives every removal the design invites; the board-to-chassis bond is functional only. -- **MVP thermal fixture choice carries no authority over deployment layout.** Using the 4U cooler - first to reduce thermal uncertainty must not silently become the rack pitch. -- **Bay pitch derives from measured millimetres.** "2U" and "4U" are catalogue labels, not lengths. - - -## The numbered spine — PRINT-0 .. PRINT-15 - -The six projects are the working-memory units; these are the executable steps. Zero-indexed per the -RLM-N / MEMORY-0 convention already used in this directory. Each step names its terminal evidence, -because a step without one cannot be said to be done. - -| Step | Project | Terminal evidence | State | -|---|---|---|---| -| **PRINT-0** | MEASURE | Board outline typed from the vendor manual (243.840 x 266.700 mm exact, axes corrected); micro-ATX lettered grid under its own standard authority; standoff placement derived from per-location evidence standings | **done** | -| **PRINT-1** | TOOLCHAIN | Build envelope, filament classes, slicer platform claims as separate authorities, **and a concrete A1 mini product row that the coupon fit witness consumes** | **done** (binding mutation-verified) | -| **PRINT-2** | MEASURE | Measurement authority: absent refuses, duplicate-key conflict refuses, precision budget enforced | **done** | -| **PRINT-3** | CAL | Coupon authority: ladder as modeled experimental design, rungs and plate derived | **done** | -| **PRINT-4** | TOOLCHAIN | Realization contract v0: contract derives its own rungs; a wrong-step handler is caught and located | **done** | -| **PRINT-5** | TOOLCHAIN | Raw transport schema admission: unknown version, missing field and unknown operation all refuse before any geometry call | **done** | -| **PRINT-6** | TOOLCHAIN | CadQuery handler + authority wall; sealed instruction plan; walls 1-3 execute, wall 4 (solid readback) is a declared boundary | handler/export profile landed; full readback closure unverified | -| **PRINT-7** | TOOLCHAIN | STEP/3MF emitted and re-inspected; output conformance re-establishes envelope, holes, walls | kernel blocker resolved; full acceptance to verify | -| **PRINT-8** | TOOLCHAIN | Slicer profile bound or refused; per-printer manifests; ProcessQualificationIdentity minted, not branded | live printing exercised; qualification closure to verify | -| **PRINT-9** | CAL | Coupons printed on BOTH printers; each printer+spool admitted or refused **independently** | coupon feedback recorded; independent qualification and repeatability pending | -| **PRINT-10** | FIT | Adjustable-standoff fixture; real board mounted unpowered; hole map measured back and frozen | gauge in hand; 1–3 (F–C) alignment pass reported; full fixture/map open | -| **PRINT-11** | CASSETTE | Structural cassette: rails, tray, handle, latch; carries node mass; no seam in layer-separation tension | needs PETG decision | -| **PRINT-12** | CASSETTE | Shared fixed-frame PSU/distribution and node harnesses; every connector insertable; nothing side-loaded; **PE continuous with board, standoffs and cassette all removed** | needs PSU envelope | -| **PRINT-13** | CASSETTE | 80 mm fan carrier + replaceable duct; powered thermal admitted against a bench baseline | needs cooler choice | -| **PRINT-14** | RACK | One fixed bay; cassette retained in service, removable after disconnect; empty bay structurally complete | | -| **PRINT-15** | RACK | Management-node mount (Raspberry-Pi class) on the rack, carried as a bay peer rather than an accessory | | -| **PRINT-16** | RACK | 2x2 block; an enclosed node extracted with neighbours untouched | | -| **PRINT-17** | VERDICT | Cost, volume, print hours, labour, reprint rate, thermal, service time -> buy-more-printers decision | | - -**THE SEQUENCE IS INTEGERS, AND AN EARLIER REVISION BROKE THAT.** It carried PRINT-11b and PRINT-13b -while still calling itself N = 15, so the spine had two numbering authorities at once and the count -was wrong. Letter suffixes were how cable routing and the management mount got appended as -afterthoughts instead of being placed. Renumbered; new work takes the next integer. - -**CABLE ROUTING IS NOT A STEP.** The operator's requirement is routing at EVERY controlled layer, so -it is a property each cassette and rack step must satisfy to be called done, not one step of its own. -It was PRINT-11b, which was exactly the bolt-on that requirement forbids. Every step from PRINT-11 -onward now carries the cable obligation in its own terminal evidence: endpoints, bend and service -volume, clip positions, strain relief, moving-versus-fixed classification, and declared separation -from blades and hot surfaces. - -**N = 17.** PRINT-0 through PRINT-6 are landed and executing. PRINT-1's reopening is CLOSED: the -build envelope is owned by `extdeps.printing.bambu_lab_a1_mini` and the coupon fit witness reads -`a1_mini_build_envelope` rather than an inline literal. That was verified by MUTATION rather than by -reading the import — shrinking the product row's x from 180 mm to 50 mm flips -`w_the_hole_ladder_coupon_fits_the_a1_mini` from green to red, so the binding is live and not -nominal. The check was run because the module's own annotation asserted "changing this row is what -moves that witness", and an annotation asserting a property the code does not have is exactly what -#10119 had to repair four times. - -PRINT-5 and PRINT-6 landed with #10119. PRINT-N (#10680) subsequently resolved CAD bootstrap, -added measured export-profile work, and exercised physical printing. PRINT-7/8 still require an -evidence audit against their full criteria; job-start evidence alone does not close them. - -### What blocks what - -- **CAD bootstrap is resolved.** Full output-conformance and process-qualification evidence - remain to be reconciled. -- **Joint family** (dovetail / tongue-and-groove / keyed slide / pin) gates the - ProductionInterfaceCoupon only — a later half of PRINT-8, not the MachineProcessCoupon. -- **Structural material qualification** gates durable/powered cassette work. PLA can carry CAL - and unpowered FIT; no PLA-to-PETG receipt carry. -- **Deployment envelope** gates PRINT-13..14 layout admission, nothing earlier. -- **Drive count** gates PRINT-11 completion only. - -## The six projects - -| Project | Scope | Terminal evidence | State | -|---|---|---|---| -| **CONTRACT** | Goals, non-goals, authority graph, refusal law, MVP boundary | No assumption represented as a default | deferred — no consumer yet | -| **MEASURE** | Board, cooler, PSU, fan, DIMM, cable, **deployment envelope**, uncertainty | Pack sufficient to generate CAL+FIT | **partial — coupon readings recorded; board-fit observations pending** | -| **TOOLCHAIN** | Realization contract, CadQuery handler, authority wall, STEP/3MF, slicer env, printer nodes | Deterministic generation + executed no-parallel-authority controls | partial — CAD/export and live printing exercised; full conformance open | -| **CAL+FIT** | Coupons, qualified tolerances, adjustable standoff fixture, real-board fit, hole-map feedback | One unpowered board fits; hole authority admitted | in progress — coupon feedback and gauge in hand | -| **CASSETTE** | Structure -> PSU/cables -> airflow -> powered thermal | Removable node operates and is serviceable | not started | -| **RACK+VERDICT** | One bay -> 2x2 block -> scale/economics | Middle-node service proven; buy-more-printers verdict | not started | - -Consolidation from eleven stages must not collapse the internal gates. CASSETTE keeps -`structural -> PSU/cable -> airflow -> powered thermal`; RACK+VERDICT keeps -`single bay -> populated 2x2 -> economic verdict`. - -## The CAD authority wall (TOOLCHAIN's load-bearing control) - -CadQuery is a **realization handler**. It may map modeled operations to kernel APIs, reorder -provably-equivalent operations, heal representations, triangulate a determined solid, and implement -a *modeled* derivation whose identity and inputs the model already fixed. - -It may **not** choose dimensions, offsets, clearances, wall thicknesses, radii, chamfers, feature -presence, hole patterns, joint topology, rib placement, segmentation boundaries, print orientation, -load-path geometry, duct path or cross-section, fallback values, or one construction algorithm over -another where the physical result differs. - -A numeric-literal scan is **necessary but insufficient** — `fillet()` vs `chamfer()` carries no -literal. The wall is four-part: - -- **A. Geometry-taint dataflow.** Any expression reaching a geometry-affecting call derives only - from the typed contract, a modeled operation, or a proved non-semantic toolchain constant. -- **B. No-default completeness.** Unknown feature kinds, missing fields, unsupported derivations and - schema skew all refuse. No Python defaults for geometry-affecting fields. -- **C. Realization trace.** Every realized feature maps to one modeled source; no unmodeled feature. -- **D. Output conformance.** Re-inspect the emitted STEP/3MF for envelope, hole centres, wall - thickness, mating dimensions, clearances, build-envelope fit, collision/extraction envelopes. - -**Negative control:** hard-code a plausible wall thickness in the handler and prove the wall rejects -it while the part still looks valid. A wall that never flips is a decoration. - -Duct curvature is the boundary case: inlet + outlet + envelope + airflow obligation do **not** -determine a duct. The model must fix the family and the selection law (centerline derivation, -minimum bend radius, wall thickness, transition rule); the handler may implement that law and may -not choose it. - -## Landed so far - -- `extdeps.boards.asrock_rack` — typed 243840 x 266700 um outline, first-party manual read. The - extents are NOMINAL: `asrock_altrad8ud_board_depth_nominal_exact` is the exact ARITHMETIC - conversion of the vendor's published 9.6 x 10.5 in, not the as-built extent of any board here, and - the rename exists to stop "exact" being read as zero physical tolerance by a clearance derivation. - Mounting holes are a standard-location correspondence with a per-location standing, so an inferred - hole and a physically confirmed one are different facts; `asrock_altrad8ud_open_mounting_unknowns` - carries the five unmeasured ones with their discharge instrument. -- `extdeps.standards.micro_atx` — the nine microATX mounting locations own their own authority - rather than sitting inside the ATX 2.2 module, which now carries only ATX 2.2 facts. Coordinate - provenance is `OperatorRelayedDocument` from the shared `DimensionEvidence` carrier, not a prose - string: a citation STATUS is a typed fact, and the string form was a nickname nothing could consume. - **One structural over-claim is recorded and deliberately not half-fixed** — `extdeps_model_scope` - cites the archived ATX 2.2 PDF as `first_citation` for the microATX locations, a machine-readable - claim the provenance row denies, because this session's extraction recovered that PDF's text layer - and not its mounting-location artwork. No microATX document is in hand, so the repair needs a - first-party read or an `ExternalAuthority` arm that can carry a relayed-with-no-document chain; - both are named as triggers rather than guessed at. -- `extdeps.printing.bambu_lab_a1_mini` — the printer as a PRODUCT ROW: build envelope, nozzle - diameters, filament diameter, temperature ceilings, input voltage and frequency range, machine - extents, and a ten-row material suitability roster. The fit witness reads the envelope from here - instead of a literal it wrote itself, so it goes red if the printer authority vanishes. -- `extdeps.vendor.bambu_lab`, `extdeps.printing.fdm` — agnostic FDM shapes; build-envelope fit that - reports every exceeded axis and never searches orientations. -- `extdeps.printing.bambu_studio` — spec-sheet and release-artifact platform claims carried as two - authorities; the Linux disagreement modeled, not resolved. -- `product.printed_chassis.measurement` — SPATIAL-1 binding; absent refuses, duplicate-key conflict - refuses with no precision or recency tie-break. Six witnesses green, two proven to flip. -- Compile-clean: 0 blocking errors in these files; the 57 remaining are pre-existing on main. -- `product.printed_chassis.coupon` — **TOOLCHAIN v0's first consumer.** The hole-diameter ladder as a - modeled experimental design: base, step and rung count are authored, every rung and the plate width - are DERIVED. `PlateDimensions` is its own type so a malformed plate has no zero-extent fallback to - return. Micrometre-to-millimetre conversion rounds UP, because truncation reports a 180.5 mm part - as fitting a 180 mm machine. Eight witnesses green; the conversion proven to flip in both the - arithmetic and the fit path. **V0 is now closed at one operation** (`OpThroughHole`) — see the v0 - population section for why the second one was deleted rather than kept. -- `product.printed_chassis.realization_contract` — the transport wall. Raw JSON from the CAD handler - is admitted through `decode_envelope`, which refuses an unsupported schema version, an unknown - operation kind, and an envelope carrying no operations, each with a typed cause naming what it - received. Conformance reports the EARLIEST divergence rather than the last, and a count mismatch is - its own outcome rather than a silent zip truncation. - - **It compares WHOLE GEOMETRY, not the whole specimen, and the distinction is load-bearing for - PRINT-5.** Compared: plate on three axes, operation count, then every operation field. NOT compared: - specimen identity (carried, single-armed), revision (not a compared subject at all), and feature - identity — operation *i* is "the *i*-th rung" by LIST POSITION, not by an identity the operation - carries. A permutation is refused because position is compared, which is not the same as features - having identities, and the difference reappears the moment two features share a geometry. PRINT-5 - must not read a conforming verdict as adjudicating identity or canonical ordering. - - **The admitted value is unforgeable, and an earlier revision only CLAIMED it was.** That revision - put the geometry on the accepted arm (`EnvelopeDecodedV0 { plate, features }`) and argued admission - was unavoidable "because the refusal arms have no geometry to hand on" — a correct argument for an - insufficient conclusion, since it rules out the raw envelope bypassing the judge and says nothing - about a caller bypassing the raw envelope. A probe compiled the forgery from a foreign module. - `DecodedRealizationV0` is now `sole_constructor`, so only `decode_envelope` can mint one; a foreign - module may name the arm and cannot obtain a value to put in it. - - **That wall now has an executing witness, and the claim that it could not have one was false.** - An earlier revision of this plan said the RED was a compile failure and so could not be enrolled in - a corpus that must compile, and named an expect-compile-refusal harness as the missing capability. - That was DESIGN §4b's own distinction misapplied: a state unauthorable in the ACCEPTED CORPUS may - still be perfectly authorable as SOURCE HANDED TO THE COMPILER BY A FIXTURE, and only the second - boundary decides whether a check's RED exists. The harness was already here — - `gunbc.compile_diagnostic_census`, whose `compile_dag_diagnostic_census` takes source as data and - returns either an observed diagnostic population or a typed `CensusNotRunnable`; the seven-form - precedent against a sealed fixture type is `test.claim.sole_constructor_completeness_audit_probe_test`. - The finding was routed by the side chat, which cited the precedent rather than the claim. - - The witness is `test.claim.printed_chassis_admitted_realization_seal_witness_test`, built on the two - forms that module's own annotation names as exact: a count scoped to diagnostic class AND - `subject_name`, and a differential between two sources differing on one axis. It carries a red - source writing the `DecodedRealizationV0` record literal from a foreign module, a green source with - identical imports and no literal, and the red-minus-green subtraction as a third witness so the - shared imported closure cancels rather than being asserted away. The `CensusNotRunnable` arm answers - `-1`, so every assertion compares against a non-negative quantity and a harness that never ran - satisfies nothing — which is exactly the vacancy that produced the near-false-finding below. - - **The refusal payload could carry a success, and the witnesses were where it showed.** - `RealizationVerdict`'s refusal arm was typed `RealizationRefusedAtWire { admission: EnvelopeAdmission }`, - and `EnvelopeAdmission` includes `EnvelopeDecodedV0`. No route produces that state — - `judge_realization` builds the refusal arm only on the three refusing branches — but reachability is - not occupancy, and the state was writable. The tell was not in the contract at all: four witnesses - carried a dead `EnvelopeDecodedV0 { admitted: _ } => false` arm purely to satisfy exhaustiveness over - a case the route cannot produce, which is what a coproduct too wide for its position looks like from - the consumer side. The refusal reasons are now their own closed coproduct `EnvelopeRefusal`, and - `EnvelopeAdmission = EnvelopeDecodedV0 | EnvelopeRefused { refusal: EnvelopeRefusal }`. The class - climbs from mechanically preventable to structurally impossible, and the four dead arms are deleted - with it — §4b(4) retires the redundant production handling while every discriminating witness stays - enrolled. - - **The trusted-type boundary sat one step early, and the fix turned out to be the name.** The routed - review's objection was that the admitted carrier is minted before conformance runs. I pushed back - that decode-admitted and contract-conformant are two genuinely different facts and so the *stage* - was not wrong. That was true and beside the point: the defect was that an unqualified capability - name — "admitted" — was attached to the earlier fact, while it was also the only geometry-bearing - value a handler could obtain. Renamed `DecodedRealizationV0`. The stage stayed. - - **What the pushback did surface is a second, real defect, deferred deliberately.** `judge_realization` - returns a verdict and no geometry, so a handler must obtain geometry elsewhere and associate it with - the verdict itself. That association is unbound: judge envelope A, retain envelope B's decoded - geometry, hand B to the handler under A's conforming verdict. Every honest caller passes the same - envelope twice and the invalid pairing stays writable — the same class as the refusal payload above, - one level out. The close is a second sealed type returned *from* the judgment that established - conformance, minted from the contract's own canonical specimen rather than from the transported - values that happened to compare equal, so what a handler cuts is the model's geometry and the wire is - reduced to an assertion that was checked. `AdmittedRealizationV0` is reserved for it and unspent. - - It is not built in this revision because nothing actuates geometry yet: with no handler the pairing - has no site at which to go wrong, and minting a sealed carrier with zero consumers to hold a symbol - is what §2 prices as redundant. So it is declared as `realization_v0_open_obligations` — a typed - carrier and not an annotation, because PRINT-5 must not be able to reach a handler without answering - it, and no `Accepted` program can read a comment. - - **The coupon could not be identified from its own geometry, and the fix is an authority change, not - a handler change.** The ladder steps 0.10 mm across eleven rungs, so end to end the holes differ by - 1 mm and are visually interchangeable; rotated 180° the coupon reads as a valid coupon with its - ordinals reversed, and every measurement is attributed to the wrong rung. Routed review found it, - and also corrected its own first proposal — one obligation was doing two jobs: - - - **orientation** — which end of *this* coupon is the 3.00 mm end. A property of the canonical - geometry, and now discharged by making that geometry asymmetric. - - **print-instance attribution** — which printer and spool made *this* piece of plastic. Two - correctly-oriented R1 coupons remain interchangeable. No geometry closes this, because the datum - must be *identical* on both coupons or the two processes stop sharing a subject. It stays open, - routed to the manufacturing manifest and a physical handling route. - - Holding them as one obligation is why the earlier route fold was near-vacuous — a single arm whose - RED could not be authored. Splitting is what let one of them close. `HoleDiameterLadderR1` is the - coupon *design* identity and never the identity of a printed instance. - - **The datum is its own field, not another entry in the feature list.** Appending one more - `OpThroughHole` and remembering the last one is the datum makes "which hole is the datum" a - positional convention — the class this module already deleted once — and it is worse here, because - the datum exists precisely to remove an ambiguous reading. `HoleDiameterCouponGeometry` carries - `plate`, a scalar `orientation_datum`, and `ladder_holes`, which makes four things structural: one - datum exactly (zero and two are unwritable, not refused), the datum cannot become a twelfth rung, - canonical geometry with no datum has no representation, and every consumer must name which - population it means. The wire splits the same way — two *different* populations, not two parallel - lists of one. - - **The near-miss worth recording is inside the derivation.** A first cut read rung zero's x back off - the built feature list to make the correspondence structural. `.first()` returns an option, so it - forced an `Absent` arm for a ladder that cannot be empty — and the arm answered with a fabricated - datum at the margin. An absorbing fallback in miniature, inside the one function the whole - orientation guarantee rests on. It now reads `hole_ladder_margin`, the same row the ladder fold's - `i = 0` term reads: one row, two readers, no unreachable branch to fabricate in. The correspondence - is asserted by `w_the_datum_is_at_rung_zero` rather than constructed, which is honestly one rung - lower — mechanically preventable, not structurally impossible — with the trigger named. - - **Evidence, and it discriminates.** Six controls: canonical datum conforms; datum at the far end - refuses naming `FieldCenterX` (the 180° reading itself); datum on the ladder centre-line refuses - naming `FieldCenterY` (right x, so it passes only if the y is compared); the datum stays out of the - measured population; it clears the ladder band and the plate edge; and it sits at rung zero's x. - A mutation that compares the modeled datum against itself turns the two refusal controls **RED** - while the positive control stays green — so they discriminate the wall rather than merely - exercising it. - - **Probe discipline learned here, recorded because it nearly produced a false finding.** The first - forgery probe returned exactly the baseline error count — consistent with "forgery permitted" — from - a file the compiler had never read, because an added source root was silently ignored. Confirmation - and vacancy produce the same number. Only a deliberate must-fail control in the same file - distinguished them. - -### PRINT-5 — the handler, and the one thing it does not yet do - -The pairing discharge and the CadQuery handler both land: `AdmittedRealizationV0` is sealed and -minted only inside `judge_realization` from the **canonical** specimen, and `realize_admitted` takes -that capability and nothing else. Five witnesses green by execution — per-feature trace with the -datum as a distinct subject, uncentred plate, whole-program equality against a model-rebuilt -expectation, exact millimetre rendering on odd micrometres, and overtravel moving no measured -dimension. - -**Actuation is registered but NOT materialized, and that is the honest state.** -`CouponCadQueryProgramArtifact` is a `gunbc.generated_artifact` variant generated through -`artifact_generate`, and it is `NotCommitted`. Since `main_wet` selects only committed artifacts, -**nothing writes the `.py` to disk today.** The remaining work is a typed workspace materializer and -a pinned CadQuery environment; using `main_wet` to keep an actuation claim alive would have been the -claim outrunning the execution. - -The commit-policy question resolved against my first answer. I had registered it -`CommitRequired { consumer: FabricationToolchain }`, reasoning that `RepoConsumer` already spanned -more than git and CI. The correction, from review 5095012465: `RepoConsumer` answers the narrower -question *why must these bytes persist in a git checkout*, and a CAD kernel reading a materialized -workspace establishes no such requirement — one `FabricationToolchain` arm would have conflated the -program consumer, the solid consumer, the mesh consumer, the slicer and the printer. The arm becomes -justified only when a workflow requires an operator to obtain *this file* from a checkout, and then -it is narrow (`FabricationWorkstationCheckout`) with a receipt. - -Committing it had already produced its own evidence: `.gitignore`'s blanket `*.py` swallowed the -path, so an artifact declared `CommitRequired` could never be committed and the drift gate would -have read an absent file indefinitely. A derived negation section in `gunbc.gitignore_emit` fixed -that and has been reverted with the policy — with no such artifact it is machinery whose -non-vacuity control names a path that no longer exists. **The latent class is real and now -unwalled**: a `CommitRequired` artifact whose path matches a developer ignore pattern is silently -uncommittable. Its trigger is the first such artifact; walling it before one exists would be a check -whose RED is unauthorable. - -**Historical pre-PRINT-N state; the measured export profile subsequently landed.** Export was removed from the emitted program rather than profiled. The -`cq.exporters.export(result, "…stl")` line chose five things the model never stated — STL over STEP -or 3MF, a filename, a working-directory-relative destination, format-by-extension, and CadQuery's -default tessellation policy. The last is not neutral: STL is a mesh format, so every modeled circle -becomes a polygon whose fidelity is set by linear and angular tolerances, and the emitter was -choosing how round the holes this coupon exists to *measure* come out. The program now builds -`result` and stops. `CadQueryExportProfile` is a real obligation and is deliberately unauthored: -with no kernel here its tolerance fields would be literals with no oracle. - -**Four defects the review found, three of which my own annotations had asserted away.** `zip_map` -truncates (`Empty` on either side) while the comment claimed it refuses a length mismatch — the -third appearance of the positional-parallel-list defect here, and the first where a comment supplied -the false guarantee; replaced by one row per substitution, which deletes the correspondence. The -capability wall leaked through `plate_line`/`cut_line`, callable on bare geometry, directly beneath a -header claiming there was "not even a private one" — the emitters now destructure geometry inside -the arms of a sealed `CadQueryInstructionV0` that only `cadquery_emission_plan` can mint. -`AdmittedRealizationV0` had no seal evidence at all while a witness annotation asserted its seal -held; its red/green/differential battery is now enrolled and green. And the overtravel annotation -described a doubled-overtravel diff that no body ever performed — the claim is narrowed to what -executes, with the solid readback recorded as an open obligation. - -The discharged `RealizationV0Obligation` vocabulary is deleted. Keeping an empty roster was argued -on the grounds that it preserves a typed place for the next obligation, but nothing read it — no -gate required emptiness — so the forcing did not exist, and the discharged property is carried where -it executes, in the type of the accepting arm. - -**Historical bootstrap probe, superseded by PRINT-N conda-forge bootstrap.** Wall 4 — re-reading the produced *solid* -for plate dimensions, datum location and the eleven ladder holes — is a §4b boundary obligation, not -a rung. Its trigger was written as "a runner with cadquery importable", which was too vague to act -on, so it was probed to termination: - -- CadQuery **does** install from PyPI on this arm64 container (wheels exist for aarch64); the - earlier note that this box has "no CAD kernel" was a missing-bootstrap observation, not an - architecture wall. `pip` bootstraps into a venv via `get-pip.py` (the system interpreter is - PEP-668 externally-managed, so `--user` refuses). -- `import cadquery` then fails on **`libGL.so.1`**, which is satisfiable by extracting - `libgl1`/`libglx0`/`libglvnd0` plus the X client libs. -- Past that, `casadi` requires **`GLIBCXX_3.4.32`**, and a libstdc++ carrying it requires - **`GLIBC_2.38`**. This container is **glibc 2.36**. - -The probe then incorrectly concluded the trigger was **a base image with glibc ≥ 2.38 (or an x86_64 runner), not a pip install** — -and assembling one by hand-extracting `.deb`s onto `LD_LIBRARY_PATH` is where a probe turns into the -workaround §5 names, so it was stopped there rather than pushed through. The environment wall 4 needs -is a *pinned, identified* toolchain — Python version, CadQuery version, OCP closure, image digest, -install method — which is the same identity the qualification receipt has to carry anyway. - -**What this costs tomorrow, stated plainly.** Without wall 4 the physical deviation measured on a -printed coupon is the sum of model→source, kernel, tessellation, slicer and printer–spool terms, and -the coupon exists to isolate the last. A print made before wall 4 closes therefore yields operational -evidence (adhesion, gross scale, obvious defects) and a valid **A-vs-B differential** between the two -printer–spool pairs — the upstream terms are common-mode across two prints of the same artifact and -cancel in the comparison — but it **cannot issue an absolute process-qualification receipt** against -the modeled nominal. Relative comparison survives; absolute qualification does not. - -## The two-day cut (printers arrive 2026-09-03) - -**Ruling: a CAL-driven vertical slice, not a general framework and not hand-authored coupon CAD.** - -``` -CalibrationCouponAuthority -> RealizationContract v0 -> four-part wall - -> CadQuery handler -> STEP/3MF conformance -> bound slicer profile - -> per-printer-node manifests -> physical coupon observations -``` - -The coupon is TOOLCHAIN's first real consumer. Governing rule: - -> **Generalize TOOLCHAIN only one consumer ahead.** CAL determines v0; FIT determines the next -> expansion; CASSETTE the next. A new geometric operation arrives only with taint coverage, -> refusal behaviour, trace coverage and output conformance. - -A wall with no handler is only a rule definition — the geometry-taint arm is not commissioned until -it observes real geometry calls, passes the lawful ones and rejects mutations. - -### v0 operation population (closed; a feature belongs only if a chosen coupon consumes it) - -box/prism · cylindrical through-hole · slot · linear or grid repetition · male/female clearance pair · -wall or rib · rigid transforms · boolean union and subtraction - -**`part/revision datum marking` was in this list and has been REMOVED from it, deliberately.** As -`OpDatumMark { text, at_x, at_y }` it fixed text and position and left FONT, GLYPH METRICS, STROKE -WIDTH, ALIGNMENT, ORIENTATION, DEPTH and ENGRAVED-VERSUS-EMBOSSED to the handler — the §3 tell in its -exact form, the handler choosing geometry the model had not fixed. It had already produced a silent -falsehood: `specimen_extent` assumed the mark was engraved and so assumed it could not enlarge the -plate, an assumption stated nowhere and enforced by nothing, and a handler that embossed would make -the envelope-fit answer wrong in the unsafe direction. Determining it fully means modelling fonts, -which is a domain rather than a field, imported to label a calibration coupon. - -The need behind it is real and is tracked, not dropped: several coupons will exist physically and -must be told apart by hand. `coupon_v1_physical_identification_obligation` carries it as a V1 -obligation with the route that looks right — identify the coupon by GEOMETRY, a coded through-hole or -notch pattern, which `OpThroughHole` already determines completely, checked by the same contract the -holes already pass through. - -The ladder itself is a modeled experimental design. "It is only a calibration coupon" is not -permission for Python to choose test dimensions. - -### CAL splits into two coupon classes - -- **`MachineProcessCoupon`** — X/Y deviation, hole and slot deviation, sliding clearance, thin wall - and rib, orientation anisotropy, first-layer behaviour, repeated-feature consistency. Depends on - **no** server or site measurement. This is the first print. -- **`ProductionInterfaceCoupon`** — the exact joint family, insert/captive-nut geometry, fastener - clearance, rail fit. Needs no server dimensions but **does** need the joint family and purchased - hardware identities chosen. Do not invent an M3 pocket or dovetail angle to populate it. - -### The slicer is a SECOND authority boundary - -Scaling, dimensional compensation, line width, first-layer compensation, wall construction, layer -height and orientation all change the physical result. A `.3mf` carrying hidden hand-edited -compensation is as much a parallel authority as a Python literal. Qualification identity: - -``` -printer_node x firmware x material_product x material_spool x installed_nozzle - x slicer_identity x slicer_profile x orientation x support_policy - x coupon_revision x calibration_epoch -``` - -### Wall commissioning needs four negative controls, not one - -1. **Numeric/dataflow** — a plausible Python-derived wall thickness. Taint must reject. -2. **Nonnumeric topology** — a fillet, chamfer or extra rib with no modeled feature identity. Trace - or taint must reject. -3. **Default** — omit a required contract field and let a helper default take over. Completeness - must reject. -4. **Output** — alter an exported hole or envelope after lawful realization. Conformance must reject. - -### Material: PLA does not carry to PETG - -No PLA-to-PETG receipt carry. Distinct admissions: -`ToolchainSmokeAdmitted` · `FitPrototypeProcessQualified` · `StructuralProcessQualified` · -`ThermalServiceQualified`. - -So the PETG decision blocks the **durable/powered CASSETTE path**, not the first CAL print or the -unpowered FIT fixture. Qualify each printer-spool combination independently; never infer that an -observed difference is the printer alone. - -## Open decisions (operator) — structural and rack work - -- **Drive count** — a CASSETTE completion obligation. Does not block gauge checks or the unpowered fixture. -- **PETG** — blocks structural/powered parts, not CAL or unpowered FIT. -- **Deployment envelope** — **not on the pre-arrival critical path.** Its absence must refuse - rack-layout admission later, not block calibration now. - -## Next - -Follow the dated next-step table above. Board-specific gauge observations and remaining mounting -checks lead to the unpowered fixture; toolchain-evidence reconciliation and cassette measurements -can proceed independently. The former list predated the landed PRINT-N implementation. - -**Deferred as definition-only residue until each has a consumer:** `CONTRACT` (needs the generator), -`DeploymentEnvelope` (needs the layout-comparison consumer). The design of both is pinned above. - -## Supply identity — RULED, and it unblocks the filament slice - -The open question was whether a spool is an inventory **lot** or a **PhysicalAsset instance**. Ruled: -**one supply instance per physical spool, allocated at durable physical individuation, each retaining -its source procurement lot.** The dependency runs one way only — - -``` -identity makes a later divergence attributable -NOT: a later divergence earns the object an identity -``` - -so the two day-one spools are separate instances *before* either print begins, even though they came -from one order, name one catalog product, and have no known difference. Waiting for measured -divergence would leave the first divergence with nowhere truthful to live. - -Grains stay distinct rather than collapsing onto the lot: catalog product (what was sold), -manufacturer batch (what the maker says), procurement lot (what was bought), supply instance (which -spool fed this print), condition observation (instance x time). **One order is not one material -batch** — `ProcurementLotIdentity` and manufacturer batch standing stay independent, since one order -may span two production batches and two orders may share one. Moisture at print time is an -instance-and-time observation; a drying cycle mints a new condition observation, never a new spool -identity. - -RFID and operator label are both **evidence routes, not identity kinds**. They may coexist; an RFID -read failure does not create a different spool, replacing a label does not create a different spool, -and disagreement must refuse the attribution rather than letting either route silently win. The RFID -UID is a machine-read manufacturer identifier, not a guaranteed globally unique spool serial, until -something establishes that. - -The lot-to-spool relation must not be authored twice. The existing inventory event already relates -installed assets to the lot whose stock moved, so day one is one install event per spool with -`quantity: 1`. The generic rule refuses only when assets *exceed* quantity, which would admit -`quantity 2, assets [spool_a]`; an attribution-critical supply needs the stronger local law -`asset count == installed quantity AND the requested asset occurs exactly once`. The admitted supply -may carry the derived source lot, and must never accept an independently authored `source_lot` -without comparing it against the event. - -Catalog boundary: `PhysicalAsset` requires a catalog `DeclarationRef` while `ProcurementLot` may -carry an unread catalog standing. Instance-per-spool must not force a fabricated exact filament -product — where only a package description is known, keep the unresolved standing and limit the -receipt to an end-to-end observation. Do not point a spool at the `Pla` polymer class as though a -class were a particular commercial product. - -### Correction landed to the printer slice - -The manifest annotation said the roster is filled by **reading the serial off each machine**. That -conflated two states the carrier already distinguishes: `PhysicalAsset.physical_serial` is optional, -so a printer present on the bench with an unread serial and a printer that has not arrived are -different facts, and collapsing them would report a machine the operator is standing in front of as -absent. A row is allocated at receipt and durable individuation — an operator-applied label suffices -— and the serial **corroborates** that binding rather than establishing it. If a serial is ever -required before attribution, that is an admission refusal arm, visible and countable, never silence. - -### Two coupons per machine is two attempts, not two objects on one plate - -`A1/B1` then `A2/B2`, each pair consuming the same machine artifact, supplies and nozzle unchanged -and no intervening calibration unless recorded. Two copies printed together answer **within-job / -bed-position** variation; two executions answer **print-to-print** variation. Both are useful and -they must not share one receipt name. - -## On arrival (2026-09-03) - -``` -printer setup and identity observation - -> same MachineProcessCoupon on each printer - -> measure without silently averaging contradictions - -> admit or refuse each process identity independently - -> derive qualified FIT clearances - -> generate adjustable FIT fixture - -> mount the real board unpowered -``` - -The duplicate-measurement repair already has the right semantics for this: lookup refuses -contradiction. A future adjudication relation may supersede a measurement, but selection must never -be smuggled into ordinary resolution. - -### The bench procedure, in the order it must physically happen - -Written out because the ordering is the whole content: two steps here capture facts that **no later -measurement can reconstruct**, and both are cheap only while the parts are still on the machines. - -1. **Label both printers before either is powered on.** A physical label, applied by hand — `A` and - `B` is sufficient. This is the durable individuation the roster needs; the identity is allocated - here, not derived from anything read later. Record the machine each label went on. -2. **Label both spools before either is loaded.** Same rule, `PLA-A` / `PLA-B`. Do this while they - are still sealed and distinguishable as objects rather than as "the one in the left printer". -3. **Read the serials, if convenient — and do not wait on them.** The serial *corroborates* the - binding made in step 1; it does not establish it. If a serial is unreadable, the printer is still - registered. If an RFID tag reads, record it as evidence beside the label, never instead of it. - Label and tag disagreeing is a refusal to resolve later, not a coin flip. -4. **Load spool A into printer A, spool B into printer B, and write down which went where.** This - pairing is the treatment variable of the entire experiment. It is not recoverable from the plastic. -5. **Print the same coupon on both machines, concurrently.** Same generated artifact, same profile, - same nozzle. Concurrency matters: it holds ambient conditions roughly fixed across the pair. -6. **Bind each coupon to its printer and spool BEFORE it leaves the bed.** Write on it, bag it and - label the bag, or photograph it in place — any durable mark. **This is the irreversible step.** - The two coupons are geometrically identical *by design*, so once both are off their beds and on - the same table, nothing measured afterwards can tell them apart. Getting this wrong does not - degrade the experiment; it voids it. -7. **Then repeat as a second attempt: A2 and B2**, same supplies, same nozzle, same profile, no - intervening calibration unless it is recorded. A1/B1 versus A2/B2 answers *print-to-print* - variation. Two copies on one plate would instead answer *bed-position* variation — a different - question with a different receipt name, and conflating them is how a machine difference gets - attributed to a corner of a build plate. - -What is deliberately **not** on this list: any dimension read off a coupon, any compensation constant, -any judgement about which machine is better. Those are measurements, and they are all still available -tomorrow. Steps 1, 2, 4 and 6 are the only ones that expire. - - -## The floor-budget cluster, and why this program did not take the coverage loss - -Adding this program's witnesses tipped a rotating subset of ten whole-corpus-reflection claims past -the floor's 500ms per-claim ceiling — claims that already sat at 415-436ms on main and were already -`[over-cost]` flagged. Two runs tipped DISJOINT subsets, so there was never a single slow witness to -chase. - -The interim move was to lift those ten off the required floor into `test.claim.long.`, declared as a -§4b(3) rung drop. That was reverted before it was pushed, and the reason is worth keeping: a child -lane investigating the cause found `deduplicate_identities` building a COPIED ACCUMULATOR inside a -quadratic fold, over a population that is the corpus — roughly 16s of the 19s that one reflection -costs. Rewritten as a set-membership fold it drops the standing to ~4.8s. - -So the ceiling was never the problem and the ten witnesses were never really the subject. Taking -them off the floor would have spent ten executing checks — including the one that EXECUTES the -compile-phase ratchet — to work around a cost-shape defect that §6 says is always fixed regardless of -realized n. The fix lands on main ahead of this program, and this program takes no drop. - -The transferable rule: when a budget refusal names your change as the trigger, the trigger and the -cause are different questions. A rotating victim set is the tell that you have found neither. - -## Grounding — R14, and why it is TWO networks rather than one path - -**Corrected.** An earlier revision of this section said the earth connection is "part of the docking -interface" and that the earth path runs through the standoffs. That is wrong, and wrong in the -dangerous direction: it makes the safety path depend on a mechanism whose whole purpose is to be -disconnected by hand, routinely, by design. The requirement below replaces it. - -There are two networks. They serve different purposes, they have different failure consequences, and -conflating them is what produced the earlier error. - -**1. Protective earth (PE) — a safety network, and never load-bearing on anything removable.** -From the AC inlet's earth pin, through the PSU's own vendor enclosure, to a dedicated bonding point -on the rack's metal member, and from there to every accessible conductive part. It is bolted, not -docked. The witness is stated as three simultaneous conditions, because any one of them alone is a -state the rack will really be in: - -> PE continuity holds with the motherboard removed, AND with every motherboard standoff removed, AND -> with the removable cassette undocked. - -If pulling a node can open the earth network, the design is wrong regardless of how good the contact -is when it is seated. - -**2. Board-to-chassis bond — a functional network, for EMI and reference, not for safety.** -Board mounting hole → metal standoff → cassette metal reference. This one legitimately breaks when -the board is removed, because that is what it is for. It may be a return and reference path; it may -never be the reason a chassis surface is safe to touch. - -The consequences for the build: - -- **Metal standoffs into a metal member.** The hybrid load path already buys aluminium extrusion or - threaded rod — that member is the natural bonding conductor and is present for structural reasons - anyway. It serves network 2, and is a *bonded branch of* network 1, not a segment of it. -- **Printed polymer is never a conductor in either network, never the mains enclosure, and never the - sole earth path.** The PSU stays in its vendor enclosure. -- **The docking interface carries no PE obligation.** A dock is a connector; PE is a bolt. - -The obligation this creates for MEASURE: the PSU's earth-stud or bonding-screw location, the rack -member's bonding point, and the standoff material and thread. None is measured yet. - -The witness set, written now so the design is falsifiable before anything is printed: - -| # | Condition | Required outcome | -|---|---|---| -| G1 | Motherboard absent | All accessible rack metal remains PE-bonded | -| G2 | Every motherboard standoff absent | PE continuity holds | -| G3 | One cassette extracted | PE continuity holds for the remaining rack | -| G4 | Board-hole bonding unknown | **No** board-to-chassis bond is claimed | -| G5 | Any printed polymer segment | Never carries PE continuity | - -G4 is the one that is easy to skip: not knowing whether a mounting hole is bonded to board ground is -a reason to make no claim, not a reason to assume the convenient answer. A dedicated bonding stud, -conductor, terminal and tested connection are part of the rack design. Incidental contact through -rails or mounting screws is never the bond. - -### The witnesses above are not sufficient, and the gap is a sequencing one - -G1-G3 prove that the REMAINING rack stays bonded after something is removed. They say nothing about -whether the thing that was removed was correctly bonded while it was powered — a cassette can be -live, unbonded, and still leave a perfectly continuous rack behind it. Two ordering relations close -that, and both are about the service protocol rather than about geometry: - -- **Power admission requires the bond.** `NodePowerAdmitted -> EveryRequiredAccessibleMetalPartPeBonded`. - A node may not be energised unless every accessible conductive part it brings is already bonded. -- **Bond removal requires the power to be gone.** `PeBondMayBeRemoved -> AcDisconnected AND HazardousEnergyAbsent`. - The earth connection is the last thing disconnected and the first thing connected. - -This is why the bond is a captive bolt in the connect/disconnect sequence even though the docking -interface carries no PE obligation. "A dock is a connector, PE is a bolt" settles what the MECHANISM -is; it does not settle WHEN the bolt is made and broken, and the second question is the one that -decides whether a person servicing a live rack is safe. - -### Front-edge support — admitted only under all five conjuncts - -The 29.21 mm of board hanging past the last mounting row is a cassette input, not a description. A -support there is a *candidate*, and it is admitted only when all of the following hold, because each -one of them independently turns a helpful support into a defect: - -1. The underside keep-out at the contact region is known. -2. The contact region is non-conductive. -3. The support does not obstruct the extraction path (R-middle-node-removal). -4. The support actually reacts connector insertion load — otherwise it is decoration. -5. The support does not become an **unintended electrical bond**, which is where this requirement - meets the grounding correction above: a support that quietly bonds the board underside to the - cassette creates a path nobody modelled and nobody tests. - -## Cable routing — R12, at every layer - -Every run gets endpoints, an access envelope, a minimum bend and service allowance, fixed clip -positions, strain relief, a moving-versus-fixed segment classification, and declared separation from -fan blades and hot surfaces. The service witness stays structural: after disconnecting the declared -external bundle and releasing the latch, the extraction path is collision-free and no neighbouring -cable or node has to move. - -**PST changes the fan harness.** The ARCTIC P8 PWM PST carries a 4-pin connector AND a 4-pin socket, -so fans daisy-chain and fan count is decoupled from the board's five headers. That is a cardinality -fact only: a chain still owes admission against header continuous and startup current, connector and -wire rating, maximum chain length, failure isolation, and tach semantics. At the published 0.09 A a -three-fan chain draws 0.27 A and a five-fan chain 0.45 A steady — neither figure proves a chain safe, -because the header rating and startup behaviour are unmeasured. Do not assume every chained fan is -independently observable just because a downstream socket exists; tach forwarding needs manufacturer -authority or an executed electrical observation. - -## The GPU-bearing node — how a card changes the cassette's structure - -A node carrying a full-height dual-slot card is a **variant of the node contract**, not a second -product. What it changes is not "make it bigger": it converts two of the cassette's founding -assumptions. - -**1. Distributed load becomes a concentrated cantilever.** The board is ~1 kg spread over standoffs; -a dual-slot card is ~1.5-2.5 kg hanging off one slot at one end. The existing standoff model cannot -carry it, and neither can the PCIe connector. The node needs a **second load termination** — a card -support spanning to the cassette frame, with the printed part as an alignment pad rather than the -retention. This is the same law the GPU augmentation program states, arriving inside the chassis: -*printed parts locate, metal retains.* - -**2. The stack load path is sized by the heaviest node, not the average one.** "Remove a node from -the middle without lifting the others" means every shell carries the stack above it through its own -sidewalls. A GPU node is the heaviest member, so it sets the sidewall section for **every** node in a -mixed stack — or the model must carry a per-node mass and refuse a stacking order whose lower -members cannot carry what is above them. The second is the honest version and it is a real -refusal the current model cannot express. - -**3. One airflow domain becomes two, and the second one exhausts sideways.** The cassette channels -80 mm fan air across the DIMMs. A blower card pulls its own air and exhausts out the bracket — which -in an OPEN, stacked chassis discharges into whatever is adjacent, including the intake of the -neighbouring node. **Inter-node recirculation is a failure mode the single-node airflow model does -not have.** It forces a declared exhaust destination per node and the rule that one node's exhaust is -never another's intake — which in practice means a consistent front-intake/rear-exhaust convention -and a duct from the blower to the rear face, even though the chassis is otherwise open. - -**4. Grounding gets materially harder, not incrementally.** The standing requirement is a ground for -each board. A card bracket normally grounds through a metal chassis; in a printed, insulating -cassette there is **no such path** unless one is modeled. The GPU does not create this gap, it -exposes it — the bracket is simply the first component that assumes a conductive chassis and finds -none. - -**5. Service becomes node-scoped.** A mid-stack node must extract with its card, cables and service -loops intact, so disconnect order and bend volumes become properties of the node, not the rack. - -### The modeling consequence, and it is the operator's ruling applied - -The shared authority is the **logical** structure — where load goes, where air goes, where fasteners -and interfaces sit. The **process** is an input to realization, not a fork of the design: PLA wants -thicker walls and rewards ribs; sheet metal is one folded surface that forbids enclosed ribs and -bosses and requires a bend radius and relief at every fold; molding wants uniform walls, draft and no -undercuts. So the process-specific part is **more than thickness**, and the difference must live in a -realization bound to a `FabricationProcess`, never in a hand-edited second model. - -That is the same shape `gunbc.product.printed_chassis.realization_contract` already has for CadQuery, -and it says the emitter must not be forked per process: one logical model, N bound handlers, STL for -the printer and DXF for the laser shop derived from **one** source. Forking the emitter per process -is §3's fused-transport tell exactly. - -### Open facts this cannot proceed without - -Exact card envelope, mass, slot width and bracket geometry · blower exhaust direction and volume · -node pitch (which decides upright versus flat-on-riser, and the riser makes it a cable problem) · -aux power connector type and sustained draw against the node supply and stack distribution · whether -a mixed stack is allowed at all, since a uniform GPU stack and a mixed stack have different sidewall -answers. - -### Batch follow-up: AMS and printer-02 storage - -On 2026-10-03 the operator confirmed AMS physical slot 1 on both printers and identified -printer-02 error 83902511 (0x0500402f) as a microSD error. The operator was unsure whether -that card was in use. This LAN route uploads to SD storage and starts a `file:///sdcard/` -project, so the error remains a start blocker; it is not suppressed. No formatting or deletion -of printer storage is authorized or performed. The start payload now selects AMS and maps -the single project filament to tray 0; the signed operator request includes this feed choice. - -The operator subsequently reported formatting printer-02’s card locally. A fresh no-error -report is still required before its start. Remote formatting was requested as a future capability; -the current modeled workflow has no format operation, and support on these printers is unverified. - -At 22:01 UTC printer-02's AMS request was approved. Its fresh report admitted IDLE/error 0, -but FTPS upload returned curl exit 25 / server 550 before MQTT start. Its durable batch marker -is retained; there is no automatic replay. Printer-01's approved attempt received a partial -temperature-only report and correctly refused before upload. The observer now allows up to -eight fresh messages to obtain state and error together, without inventing missing fields. - -The operator confirmed that printer-02 recovered and appeared functional after formatting. -A separate after-format attempt retains the original failed upload marker and requests new -approval. No MQTT start had been sent in the failed attempt. Status observation now subscribes -before publishing a read-only `pushing.pushall` snapshot request with Mosquitto request/response; -it still requires an explicit complete state/error report and cleans the private credential files. - -Correction at 22:12 UTC: the operator reports printer-02 still shows SD card “error” in -settings despite clearing print_error. Stop its after-format attempt before actuation and -prioritize printer-01. A zero print_error is not proof of usable storage. - -### Printer-01 start observed by operator - -At approximately 22:15 UTC the full-snapshot gate admitted printer-01 and its holes 3–4 -project uploaded successfully. MQTT publication did not return PUBACK. The operator then -confirmed the machine was heating or calibrating. The publisher had already exited with -connection-lost code 7 and cleaned its credential/CA directories before termination was attempted. -The transport outcome is UNKNOWN; the batch marker is retained and no start replay is permitted. -This is operator-observed preparation, not yet a telemetry-confirmed extrusion or completed print. -The selected feed is AMS physical slot 1; slice estimate 19m22s. Printer-02 remains held for -SD-card settings error, despite its zero print_error report. - -At 22:19 UTC a fresh authenticated snapshot confirmed printer-01 `RUNNING`, `print_error=0`, -and subtask name `gunbc-28a8700489cf908edb9ed59c0aa871fe6903b4d558c3b64de6a7636af701580e.3mf`. -It reported 4%, 18 minutes remaining, layer 0/15 (preparation), nozzle 176 °C and bed 62 °C. -This reconciles the unknown MQTT transport result with observed execution of the intended gauge; -it does not establish completed extrusion or a finished part. No duplicate start was sent. - -### Printer-02 recovery retry - -The operator subsequently reported printer-02's card working and requested another attempt, -supplying a fresh one-hour token. The previously stopped after-format attempt had no start claim -and had sent no printer command. At approximately 22:31 UTC its new signed approval was observed, -the fresh full-state check admitted the printer, and the holes 4–5 file uploaded successfully. -AMS physical slot 1 remains selected. MQTT publication is awaiting reconciliation; it is not -replayed. The earlier FTP-550 attempt and its claim remain separate and retained. - -At approximately 22:36 UTC, fresh telemetry confirmed printer-02 RUNNING/error 0 with the -expected `gunbc-a8f434e80014b6a316d30665b372bd0df3e858a68a2ff04a5fe4f50031d68535.3mf` -subtask, 38%, layer 0/15, and 8 minutes remaining. The upload/start route now has physical -execution evidence after the card recovery; this is not a long-term card-health qualification. - -The operator reported printer-01 finished the holes 3–4 gauge, both holes line up well, -and the part was removed / bed cleared. Record C–H as an operator-reported alignment pass, -without inferring load-bearing qualification. Prepare holes 3–6 (C–B) as printer-01's next gauge. - -The operator authorized the next printer-01 print after confirming C–H fit and a clear bed. -Holes 3–6 (C–B) sliced through the verified Orca entry with the same selected profiles. -Readback: SHA-256 `fb6d4bd1fcb23c07087b5575e6f5b20619f7ab8da03217828148d84cfcefc09f`, -15 layers / 3 mm, identity scaling, one plate, valid embedded G-code MD5 and ZIP integrity, -brim bounds [57.211684, 80.071684, 122.788316, 99.928316] mm, 12m10s / 2.59 g PLA. -The slicer metadata carries `bed_temperature_too_high_than_filament` at the upstream Generic PLA -textured-plate setting of 65 °C; this setting matches the successfully printed/aligned prior gauge. -It is retained as an observed warning, not described as warning-free. The new request binds this -project digest and a separate holes-3-6 attempt; printer-02's running job is not touched. - -The other remaining candidates are also sliced and validated under -`/home/briansrls/print-prep-2026-10-03/next-batch/` (no start requested for either yet): - -| Gauge | SHA-256 | Estimate | PLA | -|---|---|---|---| -| 5–2 (L–M) | `607601f18602ae273472654c01b9d34ca1674b7f6f5356c7136a382af41f545b` | 19m34s | 7.10 g | -| 4–7 (H–R) | `972ec6278c3334e070d4bd31884ce781211454c1c32296652e5f9a6ae4269252` | 13m26s | 3.42 g | - -Both passed ZIP/G-code checksum, one-plate, 15-layer/3-mm, identity-transform, A1 mini/PLA/0.4-mm -profile, and 180-mm bed-bound checks. L–M including brim spans X=1.331684–178.668316 mm; no -scaling was applied. Both retain the same 65 °C bed-temperature warning described above. - -### Operator direction: stop the pair-gauge sequence - -The operator requested stopping further gauge printing and moving toward chassis/rack parts. -Do not enqueue the prepared 5–2 or 4–7 gauges. A fresh snapshot already confirmed the newly -started printer-01 3–6 job RUNNING/error 0 with its exact digest filename and 12 minutes remaining; -the operator explicitly chose to let that active print finish. No cancellation is requested. -The operator expects the remaining positions to be accurate; retain that as an expectation, not -a measured fit result. 1–3 and 3–4 have reported alignment passes. - -Repository inspection found no printable full chassis/cassette/rack-bay geometry. Existing concrete -geometry covers coupons, standoff gauges and a fan-mount plate; rack_mount models occupancy of -catalog hardware, not printable rack construction. The proposed next geometry is a segmented, -adjustable motherboard carrier for unpowered assembly/fit, forming the first chassis prototype. -Remaining mounting positions can stay adjustable rather than requiring more pair gauges. Actual -standoff thread/height and underside clearance still determine the board-support interface; -PSU/cooler envelopes, structural material and load/joint checks remain for the powered cassette. - -### Hosted concept workbench - -The operator accepted the requirements and requested a hosted site for iterating on the chassis, -including independent 3D rotation of each piece. Published privately: -[Altra Chassis Workbench](https://altra-chassis-workbench.briansrls448156.chatgpt.site). -It contains an explicitly provisional four-panel carrier, segmented rails, handle and retaining -tabs, with a 2×2 fixed-frame study. Individual isolation/orbit, explosion, withdrawal and design -controls are available. There are no print controls or manufacturing exports. -The missing hardware dimensions remain unknown; the geometry is a visual proposal, not an -admitted fabrication result. Source project details are recorded in `sites/README.md`. -Desktop/mobile rendering and the principal interactions were exercised successfully. -All temporary copies of the operator's GCP token were removed after printing work ended. - -### Cassette hardware and airflow study — 4 October 2026 - -Operator clarification: the target cooler is Dynatron (W1 in the preceding question), and PSUs -must be interchangeable rather than tied to one model. Roughly 400–600 W was an example range, -not a finalized electrical qualification. PSU compatibility must include accepted physical envelope, -mounting adapters, connector requirements, intake/exhaust clearance and service access; wattage -alone is insufficient. The inventory's unread PSU identities remain unread. - -Concept 02 adds a rear cassette-resident adjustable metal PSU cradle, a variable PSU envelope, -Dynatron W1 geometry, DIMM clearance zones, one to three front 80 mm fans with individual carrier -feet, side airflow guides, a schematic internal harness corridor and front-to-rear direction arrows. -The operator proposed cassette-mounted fans. All these components travel with the cassette in the -2×2 frame view. Three fans is a layout default, not an established cooling requirement. - -The [Dynatron manufacturer page](https://www.dynatron.co/product-page/w1), checked 4 October, -publishes active dimensions 141.1 × 84 × 72 mm and passive dimensions 116 × 84 × 72 mm. -The study depicts the active envelope with simplified fins/fan; active versus passive operation, -actual socket position/orientation and installed offset are not established by that depiction. -The P8 PWM PST uses its catalog 80 × 80 × 25 mm envelope; the existing 71.5 mm hole pitch and -4.4 mm clearance remain convention-unverified. Fan count, ducting and header/PST-chain electrical -admission still require evidence. Airflow arrows are not simulation results. - -PSU slider defaults (150 × 85 × 150 mm), DIMM zones, the 50 mm rear service gap, adjustable cradle -interfaces and harness route are design proposals. They are not measured hardware or a claim that -all 400–600 W PSUs fit. No drive or expansion-card configuration has been invented. A height check -flags simple study-pitch conflicts; it does not prove full assembly clearance or powered suitability. - -Source commit: `8c944389c0dbcb25639d4c6069587863ae0d0fd5` in the separate Sites repository. -Browser checks passed for selectable parts, PSU isolation, fan-count changes, height-conflict -feedback, airflow visibility, orbit, rack withdrawal, requirements and mobile overflow. -No additional gauge or chassis print was started. - -### Shared power accepted — supersedes cassette-resident PSU - -On 4 October 2026 the operator accepted shared power per four-node block. This replaces the earlier -cassette-resident PSU requirement and its per-node AC bundle. Eight nodes comprise two independently -powered four-node blocks. Each block has a fixed-frame 12 V supply system and four separately -protected, isolatable and disconnectable node feeds. Cassettes retain their board, cooler and fans. -The earlier roughly 400–600 W example concerned individual supplies and is not a shared-block rating. - -[ASRock manual §2.8](https://download.asrock.com/Manual/ALTRAD8UD-1L2T.pdf) documents direct +12 V -DC input and distinguishes it from the ATX signal-adapter connection. This establishes a supported -input mode, not a qualified multi-node distribution system. Required power connections, independent -startup/shutdown behaviour, aggregate load/startup demand, branch fault protection, wire/connector -ratings, voltage drop, protective-earth continuity and service isolation still require engineering. -No connector pinout, fuse size, PSU wattage, redundancy or live hot-swap capability is selected here. -A nonredundant shared supply is a four-node failure domain; redundancy remains an open decision. - -Concept 03 depicts one fixed top power shelf, one adjustable shared PSU space reservation, one -four-branch distribution placeholder and stationary branch harnesses. A DC disconnect placeholder -travels with each cassette. Withdrawal illustrates the node shut down and its branch isolated and -disconnected first. Top-shelf placement and all distribution dimensions are visual proposals. Shared -supply height is additional to the two node rows; cassette pitch is governed by cassette hardware. -Older Concept 02 notes above are retained as history and are superseded for power topology. - -Concept 03 source: `7f77b6884f23af034fd1632e5918c3a21e988e5a` in the Sites repository. -Browser checks passed: shared PSU isolation, no PSU on the cassette-only view, fan-count controls, -height feedback, airflow visibility, rack withdrawal, requirements and mobile layout. - -### Rear exhaust and first fan-mount print — 4 October 2026 - -The operator requested moving fans to the rear to keep hands at the front handle away from them, -and authorized modeling/printing a first part. Rear cassette-mounted exhaust is the new layout; -front-to-rear airflow remains. Rear I/O/cable access and a finger guard remain unresolved. Both -beds were reported clear; printer-01 is selected with the previously confirmed PLA in AMS slot 1. - -The first real part reuses `fan_mount.arctic_p8_fan_mount`: 92 × 92 × 4 mm plate, 76 mm aperture, -71.5 mm hole pitch and 4.4 mm mounting holes. This is a useful fan mounting plate for unpowered -assembly fit, not another motherboard spacing gauge and not a complete cassette. Cassette -attachment and guarding have not been fabricated. Hole-pattern evidence remains convention-unverified. -`fan_mount_print.main` carries the existing admitted geometry through the existing CadQuery emitter -and exercised STL export settings; there are no independently authored CAD coordinates. - -Generation and verification artifacts are in `/home/briansrls/print-prep-2026-10-04/rear-fan-mount`. -CadQuery 2.8.0 checks passed for one valid solid, dimensions, all five hole axes/radii, analytic volume, -STEP round-trip and a closed manifold STL. STL SHA-256: -`2b0c7df7e8cf79dc772a33ee6f62a6ccf9e5568ef07e0309545313480e35b4f8`. -Orca 2.4.2 sliced through `slice_run.run` at 0.20 mm, 20 layers, PLA/0.4 mm nozzle, 220 °C nozzle, -65 °C textured bed and 2 mm outer brim. Estimate 28m41s, 11.40 g. ZIP/G-code checksum, one plate, -identity scaling and 180 mm bed bounds passed; bounds including brim are 42.071684–137.928316 mm -on both axes. The retained bed-temperature warning matches the previously exercised 65 °C setting. -The slice_info first-layer-time metadata is malformed; G-code estimates 6m4s and that metadata is -not an admission input. Project SHA-256: -`71da943ff2ed73a6b47c54759ba288e82af7d084c269bf9a413d4d901cf5199c`. - -Concept 04 is published with rear fans and downloadable generated STL/STEP, source -`c2a49273da70f20a23e2e044fe552dceb74b773f`. Browser interaction checks passed. -The operator-token/ntfy route is being used because PR #13149 remains unmerged. The request binds -printer-01, credential version 1, exact project digest and attempt `rear-fan-mount-20261004-01`. -A start result must be observed separately; preparation is not evidence of printing. - -The first launch refused before submission because its submission-key environment variable was -missing; it filed no approval and created no start marker. Corrected operator launches on srv1 -require these existing key paths (never their contents) plus the private token-file path: - -- `GUNBC_APPROVAL_SUBMISSION_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-submission-mac-key` -- `GUNBC_APPROVAL_CAPABILITY_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-mac-key` -- `GUNBC_APPROVAL_STORE_RECEIPT_MAC_KEY_FILE=/etc/gunbc-roadmap/approval-store-receipt-mac-key` -- `RUNNER_TEMP` pointing to an owner-only staging directory. - -The refusal log is retained as `print-01-preflight-refusal.log` under the srv1 run directory. -Retrying the same attempt before any request/start marker exists does not replay a printer start. - -The corrected run passed the signed approval gate, observed the printer ready, claimed its durable -start marker, uploaded the exact project and sent one AMS-slot-1 start. MQTT publication lost its -connection without acknowledgement after two minutes, so the effect was classified UNKNOWN and -was not replayed. A separate modeled `printer_report.run` observation then returned RUNNING, -print_error 0, exact digest filename `gunbc-71da943ff2ed73a6b47c54759ba288e82af7d084c269bf9a413d4d901cf5199c.3mf`, -19% and 23 minutes remaining. This confirms the requested job active; layer_num was 0, so it is -not yet evidence of deposited layers or completed fit. The snapshot is retained locally and on srv1. -The operator independently reported probably heating. Printer-02 was not started. Temporary copies -of the operator token were deleted from both hosts. - -### Complete logical cassette review — 4 October 2026 - -The operator reports that the printed rear fan plate fits. Record this as a physical fit -observation, not a caliper measurement or a promotion of the upstream conventional hole pattern. -The operator requests the whole cassette model now, review of all pieces together, then the first -cassette print. Stop the individual gauge sequence. No printer start is authorized by this review -artifact generation alone; the next print follows assembly review and the existing print workflow. - -Review 05 replaces browser-authored mechanical geometry with `product.printed_chassis.cassette` -and `rack_assembly`. `assembly_geometry` carries typed box/cylinder CSG, micrometre coordinates, -part kind, fixed/moving scope, print orientation and mating-interface records. `cassette_review` -emits the full manifest. `review_realization` emits the generic CadQuery host adapter; it chooses -no mechanical dimensions and grants no fabrication admission. The old Site `concept.js` is removed. -Vendor board outline, fan plate/pattern, W1 envelope and P8 depth resolve from their existing owners. - -One cassette has 49 printed instances / 29 distinct authored geometries: four vented tray panels, -four panel splices, four rails, two rail splices, a handle, two retaining stops, two rear extension -arms, two fan crossmember halves and a splice, three fitted-pattern fan plates, three feet, three -guards, two side airflow guides, four guide spacers and twelve guard spacers. The tray is -271.840 × 294.700 mm. Each piece fits the 180 mm A1 mini envelope in its declared orientation. -The two-level block retains a 140 mm bay pitch; this is a custom frame, not a claim of standard -2U/4U rack compliance. The fixed metal frame includes runner proposals. Shared 12 V supply, -four protected/isolatable feeds and distribution space remain fixed when a cassette is extracted. -Supply dimensions are explicitly space reservations; no supply wattage or circuit is selected. - -The guides initially intersected the rail flanges and outer fan feet. The corrected model raises -the guide foot to 13 mm, adds front 8 mm / rear 4 mm spacers, and drills the outer foot for the shared -rear bolt stack. The front rests on the panel; the rear rests on the outer fan foot. This clears -the rail flange by 1 mm. Guard pads enclose the four fan fastener holes; upper 7 mm / lower 2 mm -spacers account for the 5 mm fan foot. Guard protection, fan bolt length and engagement still need -assembly review. All 56 distinct modeled fastener axes have a coaxial mate in another printed part. -This is geometric alignment evidence, not a fastener strength or engagement test. - -Reproduction (from this worktree; use a memory-limited scope for the compiler): - -```sh -systemd-run --user --scope -p MemoryMax=24G -p MemorySwapMax=0 gunbc run \ - --source-root dag --source-root src/v2 \ - --entry dag/gunbc/product/printed_chassis/cassette_review.dag --function main > model.json -systemd-run --user --scope -p MemoryMax=24G -p MemorySwapMax=0 gunbc run \ - --source-root dag --source-root src/v2 \ - --entry dag/gunbc/product/printed_chassis/review_realization.dag --function main > realize-review.py -python realize-review.py model.json fresh-output-directory -``` - -Use the exercised CadQuery 2.8.0 environment. Output includes bed-oriented STL for each printed -instance, local-coordinate STEP, cassette assembly STEP, viewer meshes, a manifest with digests, -parts/print-quantity/interface CSVs and one review ZIP. Generated artifacts and run logs are retained -under `/home/briansrls/print-prep-2026-10-04/cassette-r05`; the Site carries the review package. -The adapter refuses invalid/disconnected printed solids, cuts missing material, out-of-bed parts, -nonmanifold STL, STEP/STL bounds or volume divergence, unmatched fastener bores, orphan interface -members and intersections between printed parts. Kernel bounding boxes explicitly ignore cached -triangulation, while STL bounds allow the modeled tessellation deflection. - -Remaining interfaces are deliberately represented as unresolved: board support thread/height and -underside keep-outs, loaded rail/joint/material behavior, real cooler pose and rear cable access, -fastener engagement, selected power/distribution hardware, thermal performance and unspecified -storage/expansion. No invented standoffs are provided. The first cassette is an unpowered assembly -fit, not a populated operational chassis. Review the complete parts and these interfaces together; -do not restart a gauge-by-gauge process. - -Final exercised generation passed for all 49 printed pieces, 29 unique geometries, 56 matched -fastener axes and zero printed-part intersections. Negative controls separately refused an oversized -part, a disconnected solid, an unknown primitive, a displaced mating bore and an orphan interface. -The emitted adapter was the code actually executed. Local browser checks passed with the final -package; the Site shows the same generated mesh geometry as the downloadable CAD. No slicing, -approval request or printer start was performed in this review step. Reuse the already fitted fan -plate when scheduling quantities after review. - -Review 05 published successfully at the existing private workbench, Site source `a9658fd05d27e83220c611b5fb00ac4397b74739`. diff --git a/docs/plans/qualified-subject-identity.md b/docs/plans/qualified-subject-identity.md deleted file mode 100644 index 3aefce5d84a..00000000000 --- a/docs/plans/qualified-subject-identity.md +++ /dev/null @@ -1,122 +0,0 @@ -# Qualified subject identity (owner direction 2026-09-25) - -Sits under `one-namespace-hierarchy.md`: that ruling says there is one -hierarchy; this one says every object in every context names itself with one -construction. Together: one algebra, one graph, three+ projections. - -## The algebra - -```text -QualifiedSubjectName - = authority - + namespace path - + local identity - -SubjectIncarnation - = QualifiedSubjectName - + revision / generation / invocation -``` - -This is NOT one flat namespace for every object. Every object uses the same -construction while preserving its owning authority: - -```text -buganizer:gunbc/issue/ -github:gunb-ai/gunbc/pr/12210 -github:gunb-ai/gunbc/pr/12210@52c56a8 -dag:gunb-ai/gunbc/test/:: -dag:gunb-ai/gunbc/test/::@52c56a8 -run:srv1/ -compiler:gunbc-v2/diagnostic/match_arm_navigation_refused -``` - -At the .dag level: reuse the existing qualified-name/namespace carrier. Do NOT -mint stringly per-kind identities (`BuganizerNodeName`, `PrNumber`, -`TestLabel`, `RunName`). Kind-specific types may wrap one qualified identity; -they must not reinvent its namespace semantics. - -The `SubjectIncarnation` half is load-bearing: it prevents the UI from -attaching a result from main, an older PR head, or a stale run to the stable -subject as though the subject were timeless. Every displayed runtime claim is -bound to an exact incarnation. - -## The relation vocabulary (two issue-to-issue dimensions, never collapsed) - -| Relation | Meaning | Affects scheduling? | -| ----------------------------- | ------------------------------ | ------------------: | -| `UpstreamOf` / `DownstreamOf` | Issue decomposition or nesting | Not inherently | -| `Blocks` / `BlockedBy` | Must complete first | Yes | -| `Implements` | Issue → PR | No issue dependency | -| `ValidatedBy` | Issue → test/claim | No | -| `ObservedBy` | Test/claim → exact run | No | -| `ReportedDiagnostic` | Exact run → diagnostic | No | -| `BasedOn` | PR head → another PR head | Not automatically | - -Laws: - -- A stacked PR is `BasedOn`, not automatically `Blocks`. It becomes `Blocks` - only when the associated issue genuinely cannot complete independently. -- A test is not automatically a child issue; a run is not a blocker node; a - diagnostic is not an issue dependency; a merge-queue state is not an issue - status; a numbered landing-order list is not a dependency graph. -- Never use PR numbers as issue node IDs. Each pairing is an explicit - `BuganizerIssue --Implements--> GitHubPullRequest` link. -- Common causes collapse: N affected tests with one shared refusal render as - ONE blocker plus an N-row impact population, never N independent blockers. - -## Execution-milestone state machine (acceptance tracking) - -```text -ContextRefused → ContextResolved → BodyReached → BodyExecuted - → AssertionVerdictProduced → verdict -``` - -A population that has not reached its bodies is `BLOCKED`, not `FAIL`: - -```text -Passed / Failed in body / Blocked before body / Not yet adjudicated -``` - -Milestones (e.g. `BodyReached`) are issue-local acceptance milestones, not -synthetic dependency nodes and not new issues. Do not pre-create "unknown -blocker" issues for failures not yet observed. - -## Presentation rules (enforced on the tracker surfaces) - -1. Never display a result without its incarnation (head sha / invocation id / - exact run). -2. Distinct states: `PASS / FAIL / BLOCKED / NOT RUN / RUNNING / UNREADABLE` — - no red "failed" styling for pre-body refusal. -3. Render a DAG, not an ordered prose list (independent branches proceed - independently). -4. `Upstream / Downstream` and `Blocked by / Blocks` render in separate - sections even when one issue pair carries both. -5. PRs, tests, runs, diagnostics attach to issue nodes through typed - relationships as evidence (Implementation / Verification sections); they - never masquerade as issue nodes. -6. Collapse common causes (above). -7. Never infer issue completion from PR state — "approved" / "merge queue" / - "checks running" are properties of the implementing PR incarnation; the - issue closes only by its own completion rule. Verification with unresolved - attribution reads `pending attribution`, never a blamed subject. - -## The invariant - -> Every displayed object has one authority-owned qualified subject name; every -> runtime claim is bound to an exact incarnation; upstream/downstream expresses -> issue nesting; blockers express must-complete-first ordering; PRs, tests, -> runs, and diagnostics attach as evidence rather than silently becoming issue -> nodes. - -## Where this binds current lanes - -- Tracker frontend (agent-48 + follow-ups): assignee/issue semantics, the - detail page's sections (Upstream/Downstream vs Blocked by/Blocks, - Implementation, Verification), state vocabulary, DAG rendering. -- Issue-tracker extdeps model: the relation vocabulary above becomes the - typed relation set; the roster gains incarnation fields on any rendered - result. -- Alignment consolidation: alignment walks the namespace path of the - QualifiedSubjectName; ambiguity/refusal uses the same carrier. -- SCM/belt surfaces: captures, receipts, and verdicts already carry exact - heads — the display rule (1) makes that binding uniform on every surface. diff --git a/docs/plans/realization-measurement-loop.md b/docs/plans/realization-measurement-loop.md index 83af37d8e04..05999fac4e9 100644 --- a/docs/plans/realization-measurement-loop.md +++ b/docs/plans/realization-measurement-loop.md @@ -159,7 +159,7 @@ The work-item view of the phases above, ordered by dependency toward the accepta - **F1** scheduler gives heavy nodes the budgeted width — *#5421 merged* (= Phase 1 width landing). - **F2/F3** key-completeness lens (`ToolchainChange ∈ triggers ⟹ input ∈ inputs_considered`) + `resolved_graph` keyed by construction — *#5423, in review* (= the Phase 2 key-derivation axis, spec-level). -- **P1 — honest keys, verified by execution** *(deps F2,F3)*: realizer-key lens (executed digest ⊇ declared `inputs_considered`, RED otherwise — would have caught the `from_utf8_lossy` collision); stable transform-id (reproducible source/commit/tree hash, not binary bytes); census parity for `parse_table_memo`. (= Phase 2 "converge key derivations".) +- **P1 — honest keys, verified by execution** *(deps F2,F3)*: realizer-key lens (executed digest ⊇ declared `inputs_considered`, RED otherwise — would have caught the `from_utf8_lossy` collision); stable transform-id (reproducible source/commit/tree hash, not binary bytes); census parity for `parse_table_memo` (since deleted with the parse memo carrier, operator ruling R2 2026-10-08). (= Phase 2 "converge key derivations".) - **P2 — one door: caching by inhabitance** *(deps P1)*: `realize(subject)` as the sole API; eliminate raw bypass (`build_multi_entry_index`, hand-rolled `ParseTable` HashMap → inhabited via `realize`, DESIGN §6's own example). (= Phase 2 one-kernel/N-backends.) - **P3 — reach → layer + completeness fail-closed** *(deps P2)* — **the core ask / the acceptance gate**: reach analysis lens (consumer set + boundary → minimal sufficient `Placement`); completeness gate (subject realized below reach → ERROR; non-redundant transform with no supplier at reach → ERROR — **land the redundancy-only cut first**, cost half waits on §4); supplier provisioning + typed request. - **P4 — economic tier** *(deps Phase 0 measurement)*: per-transform measured cost (`PerformanceReceipt` → cost-aware reach); `Materialization` = `Recompute | Memoize | Share` chosen by measured cost. diff --git a/docs/plans/receipts/allocation-state-custody-2026-09-29/README.md b/docs/plans/receipts/allocation-state-custody-2026-09-29/README.md deleted file mode 100644 index 767c8ad35ed..00000000000 --- a/docs/plans/receipts/allocation-state-custody-2026-09-29/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Protected-state credential custody prerequisite - -This cut extracts only the storage credential row from allocation PR #12505 onto -main `889312b928ae19593bb78e918bd2687c86e3db11`. It does not install the protected -storage server, designate a workspace slot, or deploy the allocation stack. - -The existing host custody convergence resolves the pinned Secret Manager version, -checks the administrator SSH subject, writes through the existing typed remote -file authority, and reads back content, permissions, ancestors and staging cleanup. -The state credential uses root:operator 0640 inside root:operator 0750, outside job -and guest trees. Permission-bit readback is not an ACL or effective-open proof. -The cloud accessor remains a row in the existing secret-accessor roster. - -After review and qualification, the bounded custody sequence is the existing -`fleet-converge` mode `host_credential_custody_converge`, `credential=fabric_state_writer_key`, -once for srv1 (storage/controller) and once for srv2 (the website). Use the merged -reviewed source revision and the workflow's own WIF identity. No operator token, -manual SCP, or fallback approval key is required. Read each run's custody receipt -before advancing to storage service commissioning. Those runs have not occurred. - -The branch-local release build passed under 6 GiB/no swap (15m10s). The initial -older-binary test and both branch-local full-root test/emission runs exited 137. -Byte-identical dependency-only runs also exited 137: 1746 modules for custody -controls, 2110 for the standard registry emitter. The latter reached some -transitive typechecking but neither produced a qualification result or workflow -output. No memory limit was increased. - -PR #12580 remains draft and unqualified for installation. The superseded PR run -36510800728 was cancelled and supplies no qualification. The explicitly dispatched -integrated run 36512302394 is pinned to 7768a9597efb03bd6729cda76beb85b8e9485544 -and remains outstanding at this checkpoint. The generated workflow option -must be emitted and checked before landing. These failures are a significant -local qualification blocker, not proof that the custody behavior passed or a -claim that any specific source defect caused the process kills. - -Further bounded checks used the existing typed-module cache cap of one and -`MALLOC_ARENA_MAX=2`, matching the CI allocator setting. Both custody controls -(1746 original modules) and a direct call to the canonical fleet workflow -generator (1822 original modules plus a harness) ended with systemd -`Result=oom-kill`, before results. These were still 6 GiB/no swap; no further -identical local retry is planned. The integrated CI run uses its existing budget. - -CI run 36512302394 passed floor, compiler, clippy and emit-build; generated -projection drift remained. PR run 36513776136 regenerated and verified the -workflow successfully. Its repair artifact intentionally excludes workflow files. -The emitted YAML was therefore read from that exact unchanged CI source checkout -over the modeled srv3 access path, checking HEAD before and after and requiring -an empty dag/src/v2 diff. Only the credential dropdown changes. The emitted bytes -and their provenance are now retained; the updated branch still needs its checks. -No credential installation was performed. diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/README.md b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/README.md deleted file mode 100644 index 2f40d3fcd70..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# IAM bootstrap qualification and partial commissioning - -Source: `a23a0f99cbfe3f39da52cd40638eb44c85e76c6c`, based on main -`2886b9e396d5ca6026c54212f27c52ea4113fba4`. Independent draft: #12565. - -The branch's own release build succeeded. Its binary passed all three focused -controls, printed the exact bootstrap plan, and refused the missing-credential -control with the expected reason before creating a started receipt. All commands -ran under `MemoryMax=6G`, `MemorySwapMax=0`. `receipt.json` records binary and -closure-manifest hashes. The runbook reproduces the byte-identical closure; this -is not a full landing-suite result. - -## Cloud result: stopped before permissions were granted - -Attempt `20260928-a23a0f99c-1` submitted creation of the IAM job's pool, provider, -and observer account, but immediate readback did not establish their readiness. -It refused without reaching role or permission stages. - -Attempt `20260928-a23a0f99c-2` reread the same named resources, created the apply -account and missing bare Namecheap pool/account, and completed the identity -stage. Existing target resources were read, not replaced. It then created all -seven exact role definitions and independently reread them. - -GCP refused the next stage's deny-policy creation with HTTP 403: -`Permission iam.googleapis.com/denypolicies.create denied on resource -cloudresourcemanager.googleapis.com/projects/582015116396`. - -No capability bindings or workflow impersonation bindings were applied by this -bootstrap. No target provider was created. The privilege probes and real -OIDC/approval workflow acceptance have not run. Existing unrelated identities and -access were not removed. Roles are definitions, not grants to the new accounts. - -The operator-supplied token was read through a private temporary file, never a -command argument. The launcher removed that file after each attempt. Receipt -copies redact the temporary pathname and contain no token. The refusal is a -missing permission, not an observed token-expiry error. - -## Resume - -An existing administrator credential or approved administrative workflow must -supply the deny-policy authority on `gunbai-secrets`. The bootstrap never grants -that authority to itself. Run the same modeled entry with a new receipt identity; -completed named subjects and role definitions are reread before further effects. -Later capability and impersonation checks may expose additional missing -permissions; they have not been qualified by this partial run. - -Successful deny readback, capability reconciliation, observer/apply checks, and -final workload trust must precede the real approval-workflow acceptance run. - -## Fresh-token retry - -Attempt `20260928-a23a0f99c-3` used a newly supplied operator token and reread -the existing identities and seven role definitions. GCP again refused deny-policy -creation with HTTP 403 for `iam.denypolicies.create`. Capability and workflow -trust stages were not reached. Temporary credential removal was independently -checked. Refreshing the token did not supply the missing IAM permission. diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/build.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/build.log deleted file mode 100644 index 41e6e0db3b0..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/build.log +++ /dev/null @@ -1,114 +0,0 @@ - Compiling proc-macro2 v1.0.106 - Compiling quote v1.0.45 - Compiling unicode-ident v1.0.24 - Compiling syn v2.0.117 - Compiling shlex v1.3.0 - Compiling find-msvc-tools v0.1.9 - Compiling cc v1.2.62 - Compiling synstructure v0.13.2 - Compiling version_check v0.9.5 - Compiling serde_core v1.0.228 - Compiling libc v0.2.186 - Compiling zerofrom-derive v0.1.7 - Compiling typenum v1.20.1 - Compiling cfg-if v1.0.4 - Compiling zerofrom v0.1.8 - Compiling yoke-derive v0.8.2 - Compiling serde v1.0.228 - Compiling stable_deref_trait v1.2.1 - Compiling yoke v0.8.2 - Compiling serde_derive v1.0.228 - Compiling object v0.37.3 - Compiling zerovec-derive v0.11.3 - Compiling memchr v2.8.0 - Compiling zerovec v0.11.6 - Compiling displaydoc v0.2.5 - Compiling ar_archive_writer v0.5.1 - Compiling zmij v1.0.21 - Compiling psm v0.1.31 - Compiling serde_json v1.0.149 - Compiling tinystr v0.8.3 - Compiling bitmaps v2.1.0 - Compiling im v15.1.0 - Compiling stacker v0.1.24 - Compiling rand_core v0.6.4 - Compiling litemap v0.8.2 - Compiling writeable v0.6.3 - Compiling itoa v1.0.18 - Compiling icu_locale_core v2.2.0 - Compiling rand_xoshiro v0.6.0 - Compiling sized-chunks v0.6.5 - Compiling zerotrie v0.2.4 - Compiling potential_utf v0.1.5 - Compiling generic-array v0.14.7 - Compiling icu_properties_data v2.2.0 - Compiling icu_normalizer_data v2.2.0 - Compiling utf8_iter v1.0.4 - Compiling icu_collections v2.2.0 - Compiling icu_provider v2.2.0 - Compiling ring v0.17.14 - Compiling unicode-properties v0.1.4 - Compiling zeroize v1.8.2 - Compiling v1-stage0-runtime v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_runtime) - Compiling rustls-pki-types v1.14.1 - Compiling subtle v2.6.1 - Compiling smallvec v1.15.1 - Compiling icu_normalizer v2.2.0 - Compiling icu_properties v2.2.0 - Compiling v1-stage0-std-core v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_std_core) - Compiling getrandom v0.2.17 - Compiling untrusted v0.9.0 - Compiling utf8parse v0.2.2 - Compiling crc32fast v1.5.0 - Compiling anstyle-parse v1.0.0 - Compiling v1-stage0-std-surface v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_std_surface) - Compiling idna_adapter v1.2.2 - Compiling crypto-common v0.1.7 - Compiling block-buffer v0.10.4 - Compiling adler2 v2.0.1 - Compiling colorchoice v1.0.5 - Compiling equivalent v1.0.2 - Compiling percent-encoding v2.3.2 - Compiling simd-adler32 v0.3.9 - Compiling rustls v0.23.40 - Compiling anstyle-query v1.1.5 - Compiling hashbrown v0.17.1 - Compiling is_terminal_polyfill v1.70.2 - Compiling anstyle v1.0.14 - Compiling anstream v1.0.0 - Compiling indexmap v2.14.0 - Compiling miniz_oxide v0.8.9 - Compiling form_urlencoded v1.2.2 - Compiling digest v0.10.7 - Compiling idna v1.1.0 - Compiling v1-stage0-extdeps-languages v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_extdeps_languages) - Compiling rustls-webpki v0.103.13 - Compiling webpki-roots v1.0.7 - Compiling toml_datetime v0.6.11 - Compiling serde_spanned v0.6.9 - Compiling strsim v0.11.1 - Compiling winnow v0.7.15 - Compiling heck v0.5.0 - Compiling log v0.4.29 - Compiling clap_lex v1.1.0 - Compiling toml_write v0.1.2 - Compiling once_cell v1.21.4 - Compiling toml_edit v0.22.27 - Compiling clap_builder v4.6.0 - Compiling clap_derive v4.6.1 - Compiling webpki-roots v0.26.11 - Compiling flate2 v1.1.9 - Compiling v1-stage0-v1-artifact v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_v1_artifact) - Compiling url v2.5.8 - Compiling cpufeatures v0.2.17 - Compiling base64 v0.22.1 - Compiling v1-compiler v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0) - Compiling ureq v2.12.1 - Compiling sha2 v0.10.9 - Compiling v1-stage0-v1-infer v0.1.0 (/home/briansrls/scratch-wt/gcp-iam-bootstrap/src/v1/stage0_v1_infer) - Compiling clap v4.6.1 - Compiling toml v0.8.23 - Compiling hmac v0.12.1 - Compiling lazy_static v1.5.0 - Compiling hex v0.4.3 - Finished `release` profile [optimized] target(s) in 15m 29s diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-1.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-1.log deleted file mode 100644 index 820189fcf42..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-1.log +++ /dev/null @@ -1,17 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 20 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-1-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-1-identities.txt (114 bytes) -gcp iam converge observe federation: named creation is not yet established by readback; no rights granted diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-2.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-2.log deleted file mode 100644 index dc9ecc69f38..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-2.log +++ /dev/null @@ -1,91 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-2-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-2-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-2-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[rest] POST https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-2-deny.txt (879 bytes) -status 403: { - "error": { - "code": 403, - "message": "Permission iam.googleapis.com/denypolicies.create denied on resource cloudresourcemanager.googleapis.com/projects/582015116396", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "permission": "iam.googleapis.com/denypolicies.create", - "resource": "projects/582015116396", - "error_info_id": "CiQwMWEwZWEwOS1lY2U1LTdmYTgtOGUyNi03NGQyNmM0MjRkODQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng==", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWEwOS1lY2U1LTdmYTgtOGUyNi03NGQyNmM0MjRkODQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng%3D%3D" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-3.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-3.log deleted file mode 100644 index 0d7ec5449d0..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/cloud-attempt-3.log +++ /dev/null @@ -1,74 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 20 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-3-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-3-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-3-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[rest] POST https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies -[file] write_create_new target/gcp-iam-bootstrap-20260928-a23a0f99c-3-deny.txt (879 bytes) -status 403: { - "error": { - "code": 403, - "message": "Permission iam.googleapis.com/denypolicies.create denied on resource cloudresourcemanager.googleapis.com/projects/582015116396", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "permission": "iam.googleapis.com/denypolicies.create", - "error_info_id": "CiQwMWEwZWExMi1mMzBkLTdkMjUtOWYwZC1mMjNhNGJmYmFhNmQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng==", - "resource": "projects/582015116396", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWExMi1mMzBkLTdkMjUtOWYwZC1mMjNhNGJmYmFhNmQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng%3D%3D" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/controls.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/controls.log deleted file mode 100644 index 7bff137e61b..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/controls.log +++ /dev/null @@ -1,7 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 20 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-identities.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-identities.txt deleted file mode 100644 index 2522b49ad07..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-identities.txt +++ /dev/null @@ -1 +0,0 @@ -refused: gcp iam converge observe federation: named creation is not yet established by readback; no rights granted \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-started.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-1-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-deny.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-deny.txt deleted file mode 100644 index bb00c5c4118..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-deny.txt +++ /dev/null @@ -1,20 +0,0 @@ -refused: status 403: { - "error": { - "code": 403, - "message": "Permission iam.googleapis.com/denypolicies.create denied on resource cloudresourcemanager.googleapis.com/projects/582015116396", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "permission": "iam.googleapis.com/denypolicies.create", - "resource": "projects/582015116396", - "error_info_id": "CiQwMWEwZWEwOS1lY2U1LTdmYTgtOGUyNi03NGQyNmM0MjRkODQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng==", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWEwOS1lY2U1LTdmYTgtOGUyNi03NGQyNmM0MjRkODQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng%3D%3D" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-identities.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-roles.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-started.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-2-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-deny.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-deny.txt deleted file mode 100644 index 02019ed4fe0..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-deny.txt +++ /dev/null @@ -1,20 +0,0 @@ -refused: status 403: { - "error": { - "code": 403, - "message": "Permission iam.googleapis.com/denypolicies.create denied on resource cloudresourcemanager.googleapis.com/projects/582015116396", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "permission": "iam.googleapis.com/denypolicies.create", - "error_info_id": "CiQwMWEwZWExMi1mMzBkLTdkMjUtOWYwZC1mMjNhNGJmYmFhNmQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng==", - "resource": "projects/582015116396", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWExMi1mMzBkLTdkMjUtOWYwZC1mMjNhNGJmYmFhNmQSFXByb2plY3RzLzU4MjAxNTExNjM5Ng%3D%3D" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-identities.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-roles.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-started.txt b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/gcp-iam-bootstrap-20260928-a23a0f99c-3-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/no-credential.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/no-credential.log deleted file mode 100644 index 9a75d5f411d..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/no-credential.log +++ /dev/null @@ -1,5 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 24 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -no GUNBC_GCP_ACCESS_TOKEN_FILE in the environment: this entry needs a path to a file holding an access token diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/plan.log b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/plan.log deleted file mode 100644 index 35135a13db7..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/plan.log +++ /dev/null @@ -1,52 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -Bootstrap stages: identities -> roles -> deny -> capabilities -> privilege-readback -> workflow-trust -project gunbai-secrets -workflow provider github-gcp-iam-converge-oidc -workflow condition assertion.repository_id == '1143718943' && assertion.repository_owner_id == '244123794' && assertion.workflow_ref == 'gunb-ai/gunbc/.github/workflows/fleet-converge.yml@refs/heads/main' && assertion.ref == 'refs/heads/main' && assertion.event_name == 'workflow_dispatch' && assertion.environment == 'gcp-iam-converge' -workflow principal set principalSet://iam.googleapis.com/projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge/* -deny policy gunbc-iam-converge-boundary -deny principal serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com -deny permissions secretmanager.googleapis.com/versions.access,secretmanager.googleapis.com/versions.add,iam.googleapis.com/serviceAccounts.actAs,iam.googleapis.com/serviceAccounts.getAccessToken,iam.googleapis.com/serviceAccounts.getOpenIdToken,iam.googleapis.com/serviceAccounts.implicitDelegation,iam.googleapis.com/serviceAccounts.signBlob,iam.googleapis.com/serviceAccounts.signJwt -Target bootstrap creates only bare pools/accounts; target providers and workload trust remain approval-gated. -role projects/gunbai-secrets/roles/gunbcIamConvergeCreate permissions=iam.workloadIdentityPools.create,iam.workloadIdentityPools.get,iam.serviceAccounts.create,iam.serviceAccounts.get -role projects/gunbai-secrets/roles/gunbcIamConvergeProvider permissions=iam.workloadIdentityPoolProviders.create,iam.workloadIdentityPoolProviders.get,iam.workloadIdentityPoolProviders.list -role projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy permissions=iam.serviceAccounts.get,iam.serviceAccounts.getIamPolicy,iam.serviceAccounts.setIamPolicy -role projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy permissions=secretmanager.secrets.get,secretmanager.secrets.getIamPolicy,secretmanager.secrets.setIamPolicy -role projects/gunbai-secrets/roles/gunbcIamObservePool permissions=iam.workloadIdentityPools.get,iam.workloadIdentityPoolProviders.get,iam.workloadIdentityPoolProviders.list -role projects/gunbai-secrets/roles/gunbcIamObserveAccount permissions=iam.serviceAccounts.get,iam.serviceAccounts.getIamPolicy -role projects/gunbai-secrets/roles/gunbcIamObserveSecret permissions=secretmanager.secrets.get,secretmanager.secrets.getIamPolicy -binding projects/gunbai-secrets role:52:projects/gunbai-secrets/roles/gunbcIamConvergeCreate;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com role:59:projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/agent-github-app-private-key role:58:projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher-private role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com role:59:projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-mtcollins1-boot role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com role:59:projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/bmc-mtcollins1-gunbc role:58:projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/fleet-automation-ssh-key role:58:projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-namecheap-dns role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com role:59:projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/namecheap-api-key role:58:projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/agent-github-app-private-key role:51:projects/gunbai-secrets/roles/gunbcIamObserveSecret;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher-private role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-mtcollins1-boot role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/bmc-mtcollins1-gunbc role:51:projects/gunbai-secrets/roles/gunbcIamObserveSecret;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/fleet-automation-ssh-key role:51:projects/gunbai-secrets/roles/gunbcIamObserveSecret;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-namecheap-dns role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/namecheap-api-key role:51:projects/gunbai-secrets/roles/gunbcIamObserveSecret;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/approval-submission-mac-key role:51:projects/gunbai-secrets/roles/gunbcIamObserveSecret;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com role:52:projects/gunbai-secrets/roles/gunbcIamObserveAccount;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -binding projects/582015116396/secrets/approval-submission-mac-key role:34:roles/secretmanager.secretAccessor;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition-title:54:gcp-iam-converge pinned approval-submission-mac-key v1;condition-expression:87:resource.name == "projects/582015116396/secrets/approval-submission-mac-key/versions/1";condition-description:96:gunbc.auth.gcp_iam_converge: the one version the gcp iam converge job reads, to sign its request; -binding projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com role:30:roles/iam.workloadIdentityUser;member:120:principalSet://iam.googleapis.com/projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge/*;condition:4:none; -binding projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com role:30:roles/iam.workloadIdentityUser;member:120:principalSet://iam.googleapis.com/projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge/*;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/receipt.json b/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/receipt.json deleted file mode 100644 index 192c58d5a66..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-2026-09-28/receipt.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "source_revision": "a23a0f99cbfe3f39da52cd40638eb44c85e76c6c", - "base_revision": "2886b9e396d5ca6026c54212f27c52ea4113fba4", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "closure_modules": 837, - "closure_manifest_sha256": "8b62fc4c64f5d76a879ae2c2fb7802e3a2aa5d53684431d7fa3276cd70d8d298", - "memory_max": "6G", - "memory_swap_max": 0, - "release_build_exit": 0, - "focused_controls": { - "passed": 3, - "failed": 0 - }, - "plan_exit": 0, - "missing_credential_exit": 1, - "missing_credential_exact_reason_verified": true, - "missing_credential_created_started_receipt": false, - "cloud_bootstrap": { - "status": "refused", - "attempts": 3, - "completed_stages": [ - "identities", - "roles" - ], - "refused_stage": "deny", - "http_status": 403, - "missing_permission": "iam.denypolicies.create", - "capability_bindings_applied": false, - "workflow_trust_applied": false, - "temporary_credentials_removed": true, - "fresh_token_retry": "same HTTP 403 at deny-policy creation" - }, - "full_landing_suite": "not run", - "oidc_approval_workflow_acceptance": "not run" -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/README.md b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/README.md deleted file mode 100644 index 10cf532a6c9..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Temporary bootstrap authority convergence receipt - -Source: `70009ad9d2c8f184153976576f5a1ec2211da1b3` on draft PR #12565. -The branch binary passed 12 focused controls over 843 byte-identical modules, -within 6 GiB with swap disabled. The full landing suite was not run. - -The organization-scoped run verified the operator subject and primary IAM email, -verified project ancestry, granted the time-bounded Deny Admin cell, installed -and read back the project deny policy, then removed the exact temporary grant -and verified its absence. The retirement journal and sanitized execution log -are included. Existing unrelated grants were outside the owned cell. - -The capabilities stage subsequently applied the project create-role binding, -then stopped on a successful pool getIamPolicy response containing `{}` with -neither a body etag nor an HTTP ETag. No pool policy write followed. Account -privilege probes and workflow trust were not reached. The bootstrap as a whole -is incomplete; no VM or fleet deployment acceptance is claimed. - -The earlier project-scoped attempt was rejected by GCP with HTTP 400: Deny Admin -is grantable at organization scope. Its log and initial seven-control receipt -are historical, not qualification of the final source. The new run also binds -the verified stable subject to the actual primary email `brian@gunb.ai`. - -## Remaining dependency - -The pool's initial IAM policy needs an established safe publication contract. -A successful empty read does not supply an optimistic-concurrency token. Do not -replace this with an unconditional setIamPolicy or fabricate an etag. Google's -[Policy contract](https://docs.cloud.google.com/iam/docs/reference/rest/v1/Policy) -describes etag as the concurrency protection; the current required-etag wire -shape reports this boundary as a decode refusal. A follow-up should model the -missing publication prerequisite explicitly, including provider-supported -initialization or established exclusive ownership, before enabling the write. - -The temporary credential file was removed by the launcher. No token is included. diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/controls.log b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/controls.log deleted file mode 100644 index afefd6cdcf9..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/controls.log +++ /dev/null @@ -1,11 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback -PASS authority_deadline_is_bounded_and_retry_does_not_renew_it -PASS uncertain_publication_cannot_be_settled_from_an_absent_cell -PASS unknown_policy_and_unowned_grants_never_authorize_changes -PASS authority_retirement_preserves_existing_human_and_foreign_access diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt deleted file mode 100644 index 96c40eedfff..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt +++ /dev/null @@ -1 +0,0 @@ -target-project:gunbai-secrets;authority-resource:organizations/266638272282;role:19:roles/iam.denyAdmin;member:18:user:brian@gunb.ai;condition-title:50:gunbc-bootstrap-deny-deny-bootstrap-org-20260928-1;condition-expression:48:request.time < timestamp('2026-09-28T23:15:00Z');condition-description:95:Temporary dependency for gunbc IAM deny-policy installation; cleanup remains owed after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt deleted file mode 100644 index 96c40eedfff..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -target-project:gunbai-secrets;authority-resource:organizations/266638272282;role:19:roles/iam.denyAdmin;member:18:user:brian@gunb.ai;condition-title:50:gunbc-bootstrap-deny-deny-bootstrap-org-20260928-1;condition-expression:48:request.time < timestamp('2026-09-28T23:15:00Z');condition-description:95:Temporary dependency for gunbc IAM deny-policy installation; cleanup remains owed after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt deleted file mode 100644 index 96c40eedfff..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -target-project:gunbai-secrets;authority-resource:organizations/266638272282;role:19:roles/iam.denyAdmin;member:18:user:brian@gunb.ai;condition-title:50:gunbc-bootstrap-deny-deny-bootstrap-org-20260928-1;condition-expression:48:request.time < timestamp('2026-09-28T23:15:00Z');condition-description:95:Temporary dependency for gunbc IAM deny-policy installation; cleanup remains owed after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt deleted file mode 100644 index 96c40eedfff..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt +++ /dev/null @@ -1 +0,0 @@ -target-project:gunbai-secrets;authority-resource:organizations/266638272282;role:19:roles/iam.denyAdmin;member:18:user:brian@gunb.ai;condition-title:50:gunbc-bootstrap-deny-deny-bootstrap-org-20260928-1;condition-expression:48:request.time < timestamp('2026-09-28T23:15:00Z');condition-description:95:Temporary dependency for gunbc IAM deny-policy installation; cleanup remains owed after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-authority-cleanup.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-authority-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-authority-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-capabilities.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-capabilities.txt deleted file mode 100644 index c897ef1e0af..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-capabilities.txt +++ /dev/null @@ -1 +0,0 @@ -refused: status 200 undecodable: HTTP 200 body did not inhabit the declared output (null at etag) \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-deny.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-deny.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-deny.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-identities.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-roles.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-started.txt b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/gcp-iam-bootstrap-20260928-organization-authority-1-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/operator-userinfo-readback.json b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/operator-userinfo-readback.json deleted file mode 100644 index 7d0a1f9a653..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/operator-userinfo-readback.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "endpoint": "https://openidconnect.googleapis.com/v1/userinfo", - "observation": "read-only authenticated API research; native convergence verifies before publication", - "sub": "116084671989231734979", - "email": "brian@gunb.ai", - "email_verified": true -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-apply.log b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-apply.log deleted file mode 100644 index 6b28875b530..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-apply.log +++ /dev/null @@ -1,117 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[rest] GET https://cloudresourcemanager.googleapis.com/v3/projects/gunbai-secrets -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt (397 bytes) -◐ started Clock.Now -✓ Clock.Now done in 1ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt (397 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:setIamPolicy -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt (397 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -◐ started Clock.Now -✓ Clock.Now done in 2ms -[rest] POST https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-deny.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:setIamPolicy -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-authority-cleanup.txt (78 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:setIamPolicy -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260928-organization-authority-1-capabilities.txt (97 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -status 200 undecodable: HTTP 200 body did not inhabit the declared output (null at etag) diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-identity-controls.log b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-identity-controls.log deleted file mode 100644 index 29fdcef5d65..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-identity-controls.log +++ /dev/null @@ -1,16 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback -PASS authority_deadline_is_bounded_and_retry_does_not_renew_it -PASS uncertain_publication_cannot_be_settled_from_an_absent_cell -PASS unknown_policy_and_unowned_grants_never_authorize_changes -PASS authority_retirement_preserves_existing_human_and_foreign_access -PASS bootstrap_authority_requires_an_organization_resource -PASS definite_publication_rejection_is_distinct_from_unknown_commit -PASS deny_propagation_retry_requires_live_authority_and_a_remaining_budget -PASS cleanup_dependency_does_not_wait_for_successful_deny_installation -PASS authority_beneficiary_requires_the_verified_stable_google_subject diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-receipt.json b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-receipt.json deleted file mode 100644 index 68ca89d3178..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/organization-receipt.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "source_revision": "70009ad9d2c8f184153976576f5a1ec2211da1b3", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "closure_modules": 843, - "closure_manifest_sha256": "73b2373809d4b41d22cf8d00db62dc996afd06042a28b676409e735b6b3653ca", - "memory_max": "6G", - "memory_swap_max": 0, - "focused_controls_passed": 12, - "authority_operation": "deny-bootstrap-org-20260928-1", - "authority_expires_at": "2026-09-28T23:15:00Z", - "organization": "organizations/266638272282", - "operator_subject": "116084671989231734979", - "operator_member": "user:brian@gunb.ai", - "cloud_status": "temporary authority granted and read back; deny policy installed and read back; temporary authority removed and absence verified; capabilities stopped at missing pool policy etag", - "full_landing_suite": "not run", - "workflow_trust": "not enabled", - "credential_file_removed": true, - "pool_policy_readback": { - "http": 200, - "etag_header": null, - "policy": {} - }, - "remaining_dependency": "safe initial pool IAM policy publication with concurrency protection" -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-ancestry-readback.json b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-ancestry-readback.json deleted file mode 100644 index a8c927d63d3..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-ancestry-readback.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "endpoint": "https://cloudresourcemanager.googleapis.com/v3/projects/gunbai-secrets", - "observation": "read-only authenticated API research; native convergence rereads this before publication", - "name": "projects/582015116396", - "projectId": "gunbai-secrets", - "parent": "organizations/266638272282", - "state": "ACTIVE" -} diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-scope-rejected.log b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-scope-rejected.log deleted file mode 100644 index 68eee46624a..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/project-scope-rejected.log +++ /dev/null @@ -1,86 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260928-authority-1-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260928-authority-1-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[file] write_create_new target/gcp-iam-bootstrap-20260928-authority-1-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-20260928-1-intent.txt (344 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-20260928-1-publication-elected.txt (344 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:setIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260928-authority-1-deny.txt (230 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -authority mutation not established; recovery remains owed: status 400: { - "error": { - "code": 400, - "message": "Role roles/iam.denyAdmin is not supported for this resource.", - "status": "INVALID_ARGUMENT" - } -} -; authority cleanup outstanding: authority dependency unresolved: expired, unowned, or publication may still commit; no grant retry or renewal diff --git a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/receipt.json b/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/receipt.json deleted file mode 100644 index ae232277a9b..00000000000 --- a/docs/plans/receipts/gcp-iam-bootstrap-authority-2026-09-28/receipt.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "source_revision": "e413906c2a71e3268ddf4b93d0327f3d1becf2d4", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "closure_modules": 842, - "closure_manifest_sha256": "6befe7f9b00c41861cdccb76e1a7c25868bde8a876ccbd083b52c31ea905a73d", - "memory_max": "6G", - "memory_swap_max": 0, - "focused_controls_passed": 7, - "authority_operation": "deny-bootstrap-20260928-1", - "authority_expires_at": "2026-09-28T23:00:00Z", - "cloud_status": "HTTP 400: Deny Admin is not grantable on a project; no grant applied", - "full_landing_suite": "not run", - "temporary_credentials_removed": true -} diff --git a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/README.md b/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/README.md deleted file mode 100644 index 1e751548a20..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Exclusive pool initialization source qualification - -The operator confirmed on 2026-09-29 that no other administrator will update the -pool policies during this bootstrap. The change models a one-off initial policy -publication under that exclusivity assumption, then requires an observed etag for -all normal updates. This is not provider-enforced CAS during initialization. - -All 15 focused controls passed under 6 GiB with no swap, using the branch release -binary and 843 byte-identical dependency modules. Controls distinguish empty -wire policies from update authority and reject nonempty unfenced policies, -unsupported policy versions, existing audit configuration, undeclared pools, -and incomplete or revisionless initialization readback. - -The complete bootstrap module typechecked in this closure. The new cloud-write -path and interrupted-publication recovery have not been exercised against GCP. -No live pool initialization or workflow credential-minting acceptance is claimed. -The create-only publication journal stays in the same recovery workspace; an -uncertain publication cannot be retried from a fresh workspace. - -An existing administrator credential is still needed for initial bootstrap. The -normal approval workflow first authenticates as iam-observe through its dedicated -pool, then requests approval and obtains iam-converge credentials. Its initial -trust is not yet installed, so it cannot create that trust for itself. There is -no usable gcloud installation, ADC file, or configured token file in this shell. diff --git a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/controls.log b/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/controls.log deleted file mode 100644 index c0cc6807ad4..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/controls.log +++ /dev/null @@ -1,19 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 20 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback -PASS authority_deadline_is_bounded_and_retry_does_not_renew_it -PASS uncertain_publication_cannot_be_settled_from_an_absent_cell -PASS unknown_policy_and_unowned_grants_never_authorize_changes -PASS authority_retirement_preserves_existing_human_and_foreign_access -PASS bootstrap_authority_requires_an_organization_resource -PASS definite_publication_rejection_is_distinct_from_unknown_commit -PASS deny_propagation_retry_requires_live_authority_and_a_remaining_budget -PASS cleanup_dependency_does_not_wait_for_successful_deny_installation -PASS authority_beneficiary_requires_the_verified_stable_google_subject -PASS empty_pool_policy_does_not_authorize_unconditional_publication -PASS pool_initialization_rejects_existing_unfenced_bindings -PASS pool_initialization_readback_requires_exact_cells_and_a_revision diff --git a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/receipt.json b/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/receipt.json deleted file mode 100644 index a4206b1529a..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-initialization-2026-09-29/receipt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "source_revision": "07c17deb9eba5a1b545d68e56fddbfacac1c94e4", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "closure_modules": 843, - "memory_max": "6G", - "memory_swap_max": 0, - "focused_controls_passed": 15, - "live_pool_initialization": "not executed", - "full_landing_suite": "not run", - "credential_dependency": "existing administrator credential; normal workflow initial federation still awaits bootstrap" -} diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/apply.log b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/apply.log deleted file mode 100644 index 320d988a0d7..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/apply.log +++ /dev/null @@ -1,244 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-deny.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-authority-cleanup.txt (78 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] write_create_new target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt (438 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[file] write_create_new target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt (438 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] write_create_new target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt (439 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[file] write_create_new target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt (439 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] write_create_new target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt (437 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[file] write_create_new target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt (437 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] write_create_new target/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt (282 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[file] write_create_new target/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt (282 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:setIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-capabilities.txt (78 bytes) -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -[file] write_create_new target/gcp-iam-bootstrap-20260929-pool-initialization-1-privilege-readback.txt (1028 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -apply-account readback credential unavailable; workflow trust withheld: status 403: { - "error": { - "code": 403, - "message": "Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist). Remediate access with this Troubleshooter URL or share it with your administrator - https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA%3D%3D .", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "error_info_id": "CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA==", - "permission": "iam.serviceAccounts.getAccessToken", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA%3D%3D" - } - } - ] - } -} - diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-authority-cleanup.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-authority-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-authority-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-capabilities.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-capabilities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-capabilities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-deny.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-deny.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-deny.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-identities.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-privilege-readback.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-privilege-readback.txt deleted file mode 100644 index 7b6068988cd..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-privilege-readback.txt +++ /dev/null @@ -1,19 +0,0 @@ -refused: apply-account readback credential unavailable; workflow trust withheld: status 403: { - "error": { - "code": 403, - "message": "Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist). Remediate access with this Troubleshooter URL or share it with your administrator - https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA%3D%3D .", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "error_info_id": "CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA==", - "permission": "iam.serviceAccounts.getAccessToken", - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5My04Mzg0LTcwNzItYmI4OC04MjJiYWNlNzVkMGISAA%3D%3D" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-roles.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-started.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-bootstrap-20260929-pool-initialization-1-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt deleted file mode 100644 index d87ad974c40..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt +++ /dev/null @@ -1,3 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt deleted file mode 100644 index d87ad974c40..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt +++ /dev/null @@ -1,3 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-gcp-iam-converge -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-closed.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-closed.txt deleted file mode 100644 index b195bdceb73..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-closed.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-elected.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-elected.txt deleted file mode 100644 index b195bdceb73..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-heal-publisher-elected.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-heal-publisher -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt deleted file mode 100644 index f4d0845f19d..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-mtcollins1-boot -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt deleted file mode 100644 index f4d0845f19d..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-mtcollins1-boot -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt deleted file mode 100644 index ece3b479b8e..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-namecheap-dns -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt deleted file mode 100644 index ece3b479b8e..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt +++ /dev/null @@ -1,4 +0,0 @@ -exclusive-pool-policy-initialization-20260929 -projects/582015116396/locations/global/workloadIdentityPools/github-namecheap-dns -role:54:projects/gunbai-secrets/roles/gunbcIamConvergeProvider;member:66:serviceAccount:iam-converge@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; -role:49:projects/gunbai-secrets/roles/gunbcIamObservePool;member:65:serviceAccount:iam-observe@gunbai-secrets.iam.gserviceaccount.com;condition:4:none; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/receipt.json b/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/receipt.json deleted file mode 100644 index b2159f7436f..00000000000 --- a/docs/plans/receipts/gcp-iam-pool-live-2026-09-29/receipt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "source_revision": "be068166e93def4ab273b3d4cd11c5476a182e57", - "pool_initialization": "completed with readbacks and revision tokens", - "capabilities": "completed", - "privilege_readback": "refused: operator lacks iam.serviceAccounts.getAccessToken on iam-converge", - "workflow_trust": "not granted", - "temporary_deny_authority": "absence reverified", - "temporary_token_file": "removed", - "memory_max": "6G", - "memory_swap_max": 0 -} diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/apply.log b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/apply.log deleted file mode 100644 index dcf9fd23b54..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/apply.log +++ /dev/null @@ -1,404 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 20 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/roles -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-deny.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-authority-cleanup.txt (78 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-capabilities.txt (78 bytes) -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt (427 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt (426 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-probe-authority.txt (78 bytes) -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 2 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-1-privilege-readback.txt (1028 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -apply-account readback credential unavailable; workflow trust withheld: status 403: { - "error": { - "code": 403, - "message": "Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist). Remediate access with this Troubleshooter URL or share it with your administrator - https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA%3D%3D .", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA%3D%3D", - "permission": "iam.serviceAccounts.getAccessToken", - "error_info_id": "CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA==" - } - } - ] - } -} - diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/controls.log b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/controls.log deleted file mode 100644 index 6fdb2b3d97e..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/controls.log +++ /dev/null @@ -1,21 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback -PASS authority_deadline_is_bounded_and_retry_does_not_renew_it -PASS uncertain_publication_cannot_be_settled_from_an_absent_cell -PASS unknown_policy_and_unowned_grants_never_authorize_changes -PASS authority_retirement_preserves_existing_human_and_foreign_access -PASS bootstrap_authority_requires_an_organization_resource -PASS definite_publication_rejection_is_distinct_from_unknown_commit -PASS deny_propagation_retry_requires_live_authority_and_a_remaining_budget -PASS cleanup_dependency_does_not_wait_for_successful_deny_installation -PASS authority_beneficiary_requires_the_verified_stable_google_subject -PASS empty_pool_policy_does_not_authorize_unconditional_publication -PASS pool_initialization_rejects_existing_unfenced_bindings -PASS pool_initialization_readback_requires_exact_cells_and_a_revision -PASS probe_authority_has_no_signing_or_policy_permission -PASS probe_cleanup_is_required_for_trust_but_not_successful_probes diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-authority-cleanup.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-authority-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-authority-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-capabilities.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-capabilities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-capabilities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-deny.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-deny.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-deny.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-identities.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-privilege-readback.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-privilege-readback.txt deleted file mode 100644 index 46c242cbff9..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-privilege-readback.txt +++ /dev/null @@ -1,19 +0,0 @@ -refused: apply-account readback credential unavailable; workflow trust withheld: status 403: { - "error": { - "code": 403, - "message": "Permission 'iam.serviceAccounts.getAccessToken' denied on resource (or it may not exist). Remediate access with this Troubleshooter URL or share it with your administrator - https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA%3D%3D .", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "IAM_PERMISSION_DENIED", - "domain": "iam.googleapis.com", - "metadata": { - "troubleshooter_url": "https://console.cloud.google.com/iam-admin/troubleshooter/summary;errorId=CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA%3D%3D", - "permission": "iam.serviceAccounts.getAccessToken", - "error_info_id": "CiQwMWEwZWE5ZS05ODlkLTcwZjYtOTRiZS1hOTZmNWI2NjYwNjMSAA==" - } - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-probe-authority.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-probe-authority.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-probe-authority.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-roles.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-started.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-1-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index ed615588743..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index ed615588743..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index ed615588743..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index ed615588743..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index 10de7ae96f7..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index 10de7ae96f7..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index 10de7ae96f7..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index 10de7ae96f7..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/gcp-iam-probe-probe-20260929-1-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-1;condition-expression:48:request.time < timestamp('2026-09-29T01:05:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/receipt.json b/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/receipt.json deleted file mode 100644 index 5eec823bba3..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-authority-2026-09-29/receipt.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "source_revision": "da6f77d4bd3929d41c6ea4a58093420a6a4cb7d4", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "focused_controls_passed": 17, - "closure_modules": 843, - "memory_max": "6G", - "memory_swap_max": 0, - "probe_operation": "probe-20260929-1", - "probe_deadline": "2026-09-29T01:05:00Z", - "cloud_status": "both temporary probe grants installed and read back; token mint refused through bounded retry; both grants removed and absence verified; workflow trust withheld", - "full_landing_suite": "not run", - "temporary_token_file": "removed", - "mint_attempts": 30 -} diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/apply.log b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/apply.log deleted file mode 100644 index f3988596a10..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/apply.log +++ /dev/null @@ -1,352 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-started.txt (54 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-deny.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 1ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-authority-cleanup.txt (78 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-capabilities.txt (78 bytes) -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt (427 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt (426 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-probe-authority.txt (78 bytes) -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:testIamPermissions -[file] write_create_new target/gcp-iam-bootstrap-20260929-probe-authority-2-privilege-readback.txt (1828 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -status 403: { - "error": { - "code": 403, - "message": "Cloud Resource Manager API has not been used in project 582015116396 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "SERVICE_DISABLED", - "domain": "googleapis.com", - "metadata": { - "containerInfo": "582015116396", - "service": "cloudresourcemanager.googleapis.com", - "consumer": "projects/582015116396", - "serviceTitle": "Cloud Resource Manager API", - "activationUrl": "https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396" - } - }, - { - "@type": "type.googleapis.com/google.rpc.LocalizedMessage", - "locale": "en-US", - "message": "Cloud Resource Manager API has not been used in project 582015116396 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." - }, - { - "@type": "type.googleapis.com/google.rpc.Help", - "links": [ - { - "description": "Google developers console API activation", - "url": "https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396" - } - ] - } - ] - } -} - diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-authority-cleanup.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-authority-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-authority-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-capabilities.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-capabilities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-capabilities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-deny.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-deny.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-deny.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-identities.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-privilege-readback.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-privilege-readback.txt deleted file mode 100644 index ebc3b097d9f..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-privilege-readback.txt +++ /dev/null @@ -1,35 +0,0 @@ -refused: status 403: { - "error": { - "code": 403, - "message": "Cloud Resource Manager API has not been used in project 582015116396 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.", - "status": "PERMISSION_DENIED", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "SERVICE_DISABLED", - "domain": "googleapis.com", - "metadata": { - "containerInfo": "582015116396", - "service": "cloudresourcemanager.googleapis.com", - "consumer": "projects/582015116396", - "serviceTitle": "Cloud Resource Manager API", - "activationUrl": "https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396" - } - }, - { - "@type": "type.googleapis.com/google.rpc.LocalizedMessage", - "locale": "en-US", - "message": "Cloud Resource Manager API has not been used in project 582015116396 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." - }, - { - "@type": "type.googleapis.com/google.rpc.Help", - "links": [ - { - "description": "Google developers console API activation", - "url": "https://console.developers.google.com/apis/api/cloudresourcemanager.googleapis.com/overview?project=582015116396" - } - ] - } - ] - } -} diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-probe-authority.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-probe-authority.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-probe-authority.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-roles.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-started.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-bootstrap-20260929-probe-authority-2-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index 8157e104a58..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index 8157e104a58..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index 8157e104a58..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index 8157e104a58..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index d91803eb262..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index d91803eb262..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index d91803eb262..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index d91803eb262..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/gcp-iam-probe-probe-20260929-2-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-2;condition-expression:48:request.time < timestamp('2026-09-29T01:15:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/receipt.json b/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/receipt.json deleted file mode 100644 index 3583953c60c..00000000000 --- a/docs/plans/receipts/gcp-iam-probe-propagation-2026-09-29/receipt.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "source_revision": "1630eddc7", - "mint_attempts": 17, - "token_mint": "succeeded", - "positive_probe": "refused SERVICE_DISABLED cloudresourcemanager.googleapis.com for project 582015116396", - "temporary_grants": "both removed and absence read back", - "workflow_trust": "withheld", - "credential_file": "removed" -} diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/README.md b/docs/plans/receipts/gcp-iam-services-2026-09-29/README.md deleted file mode 100644 index 4bcaa3f7825..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Successful GCP IAM bootstrap, 2026-09-29 - -At source `85bdb4e19`, all ten modeled bootstrap stages completed with exit 0. -Cloud Resource Manager was enabled and independently read back on the exact -project; the other required APIs were already enabled. Resource-specific grants, -positive and negative permission probes, and the observer estate read succeeded. -The observer could read the pinned approval-submission credential; the writer -received exact HTTP 403 reading that same known-readable credential. - -Both temporary exact-account probe grants were removed and absence verified -before the final observer/apply workload-trust bindings were installed/read back. -The earlier temporary organization Deny Admin grant was also verified absent. -Issued probe tokens retain their original 600-second lifetime; grant removal is -not revocation of already issued tokens. The operator credential file was removed. - -18 focused controls passed over 844 byte-identical modules under 6 GiB/no swap. -The full landing suite was not run. This is bootstrap commissioning, not a VM -allocation or a completed approval-workflow acceptance run. - -Workflow acceptance was separately dispatched on main as run `36505895733`, -source `15b977d417c1a7de52eae96f48bcd3955fb1f94c`, mode `gcp_iam_converge`, host -`srv1`. That run uses its own GitHub OIDC credentials, not the supplied operator -token. Its success and approval delivery must be established independently. - -The acceptance run subsequently completed its release build and its observer -GitHub OIDC authentication step successfully. The plan/file/wait step remains -in progress; approval delivery, apply authentication, and apply/readback have -not yet been established. - -Attempt 1 subsequently passed request approval and writer GitHub OIDC -impersonation. Apply accepted a Namecheap provider creation, then refused because -the created subject was not yet visible on immediate readback. No later effects -were applied. This is a partial cloud change, not a successful estate convergence. - -Attempt 2 reruns the failed job on the same source, with a fresh run-attempt -identity and a freshly observed plan. It has passed approval and writer OIDC -impersonation and is executing apply/readback. No administrator token is involved. - -Attempt 2 completed successfully. The existing approval app admitted the fresh -request; the apply identity was minted only afterward, and the existing workflow -completed its approved effects and readback. Run: https://github.com/gunb-ai/gunbc/actions/runs/36505895733/attempts/2 . -This closes the ordinary GCP IAM workflow acceptance case, not host credential -custody, protected storage commissioning, or VM acceptance. diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/apply.log b/docs/plans/receipts/gcp-iam-services-2026-09-29/apply.log deleted file mode 100644 index 2bd54d5da0f..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/apply.log +++ /dev/null @@ -1,410 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -[file] read -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-started.txt (54 bytes) -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/iam.googleapis.com -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/iamcredentials.googleapis.com -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/cloudresourcemanager.googleapis.com -[rest] POST https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services:batchEnable -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/cloudresourcemanager.googleapis.com -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/secretmanager.googleapis.com -[rest] GET https://serviceusage.googleapis.com/v1/projects/gunbai-secrets/services/sts.googleapis.com -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-services.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-identities.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeCreate -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeProvider -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeAccountPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamConvergeSecretPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObservePool -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveAccount -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamObserveSecret -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-roles.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-deny.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-authority-cleanup.txt (78 bytes) -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-heal-publisher-private-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-mtcollins1-boot-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-namecheap-dns-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-elected.txt -[file] list target -[file] read target/gcp-iam-pool-initialization-github-gcp-iam-converge-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-capabilities.txt (78 bytes) -[rest] GET https://openidconnect.googleapis.com/v1/userinfo -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt (427 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt (426 bytes) -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-authority.txt (78 bytes) -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 1ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -◐ started Clock.Now -✓ Clock.Now done in 2ms -◐ started sleep.Delay.Seconds -✓ sleep.Delay.Seconds done in 5 seconds -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -[rest] POST https://cloudresourcemanager.googleapis.com/v1/projects/gunbai-secrets:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:testIamPermissions -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:testIamPermissions -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-gcp-iam-converge:testIamPermissions -[rest] POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:generateAccessToken -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-heal-publisher-private/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/heal-publisher-private@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/agent-github-app-private-key:getIamPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-mtcollins1-boot/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/mtcollins1-boot@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/bmc-mtcollins1-gunbc:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/fleet-automation-ssh-key:getIamPolicy -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/locations/global/workloadIdentityPools/github-namecheap-dns/providers -[rest] GET https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/namecheap-dns@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/namecheap-api-key:getIamPolicy -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key/versions/1:access -[rest] GET https://secretmanager.googleapis.com/v1/projects/gunbai-secrets/secrets/approval-submission-mac-key/versions/1:access -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-privilege-readback.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt (426 bytes) -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-cleanup.txt (78 bytes) -[rest] GET https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Fprojects%2F582015116396/denypolicies/gunbc-iam-converge-boundary -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:setIamPolicy -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -[file] write_create_new target/gcp-iam-bootstrap-20260929-services-and-probes-1-workflow-trust.txt (78 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt (427 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[rest] POST https://iam.googleapis.com/v1/projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt (426 bytes) -[file] list target -[file] read target/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-intent.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-elected.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-publication-closed.txt -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt -[rest] POST https://cloudresourcemanager.googleapis.com/v3/organizations/266638272282:getIamPolicy -◐ started Clock.Now -✓ Clock.Now done in 2ms -[file] write_create_new target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt (397 bytes) -[file] list target -[file] read target/gcp-iam-authority-deny-bootstrap-org-20260928-1-retired.txt diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/controls.log b/docs/plans/receipts/gcp-iam-services-2026-09-29/controls.log deleted file mode 100644 index bcdab55c990..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/controls.log +++ /dev/null @@ -1,22 +0,0 @@ -[workspace-root] discovered /home/briansrls/scratch-wt/gcp-iam-bootstrap -✓ typecheck v2.std.compilers.target_model done in 23 seconds -✓ typecheck v2.compiler.target_serialize done in 2 seconds -✓ typecheck v2.compiler.translate done in 3 seconds -PASS bootstrap_role_drift_never_authorizes_adoption -PASS deny_exceptions_and_incomplete_readback_withhold_trust -PASS workflow_trust_requires_completed_deny_and_privilege_readback -PASS authority_deadline_is_bounded_and_retry_does_not_renew_it -PASS uncertain_publication_cannot_be_settled_from_an_absent_cell -PASS unknown_policy_and_unowned_grants_never_authorize_changes -PASS authority_retirement_preserves_existing_human_and_foreign_access -PASS bootstrap_authority_requires_an_organization_resource -PASS definite_publication_rejection_is_distinct_from_unknown_commit -PASS deny_propagation_retry_requires_live_authority_and_a_remaining_budget -PASS cleanup_dependency_does_not_wait_for_successful_deny_installation -PASS authority_beneficiary_requires_the_verified_stable_google_subject -PASS empty_pool_policy_does_not_authorize_unconditional_publication -PASS pool_initialization_rejects_existing_unfenced_bindings -PASS pool_initialization_readback_requires_exact_cells_and_a_revision -PASS probe_authority_has_no_signing_or_policy_permission -PASS probe_cleanup_is_required_for_trust_but_not_successful_probes -PASS service_enablement_requires_enabled_state_on_the_exact_project diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-authority-cleanup.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-authority-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-authority-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-capabilities.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-capabilities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-capabilities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-deny.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-deny.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-deny.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-identities.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-identities.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-identities.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-privilege-readback.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-privilege-readback.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-privilege-readback.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-authority.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-authority.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-authority.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-cleanup.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-cleanup.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-probe-cleanup.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-roles.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-roles.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-roles.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-services.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-services.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-services.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-started.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-started.txt deleted file mode 100644 index b286fc75b3e..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-started.txt +++ /dev/null @@ -1 +0,0 @@ -operator bootstrap started; not a convergence receipt diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-workflow-trust.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-workflow-trust.txt deleted file mode 100644 index 480da8f3ea6..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-bootstrap-20260929-services-and-probes-1-workflow-trust.txt +++ /dev/null @@ -1 +0,0 @@ -stage checks completed; see the stage authority for configuration/probe scope diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index dc3646c20d9..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index dc3646c20d9..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index dc3646c20d9..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index dc3646c20d9..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-converge@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-converge@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt deleted file mode 100644 index 384a398af82..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-intent.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt deleted file mode 100644 index 384a398af82..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-closed.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt deleted file mode 100644 index 384a398af82..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-publication-elected.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt b/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt deleted file mode 100644 index 384a398af82..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/gcp-iam-probe-probe-20260929-3-iam-observe@gunbai-secrets.iam.gserviceaccount.com-retired.txt +++ /dev/null @@ -1 +0,0 @@ -projects/gunbai-secrets/serviceAccounts/iam-observe@gunbai-secrets.iam.gserviceaccount.com;role:57:projects/gunbai-secrets/roles/gunbcIamBootstrapProbeToken;member:18:user:brian@gunb.ai;condition-title:38:gunbc-bootstrap-probe-probe-20260929-3;condition-expression:48:request.time < timestamp('2026-09-29T01:20:00Z');condition-description:83:Temporary exact-account bootstrap readback; physical cleanup required after expiry.; \ No newline at end of file diff --git a/docs/plans/receipts/gcp-iam-services-2026-09-29/receipt.json b/docs/plans/receipts/gcp-iam-services-2026-09-29/receipt.json deleted file mode 100644 index 6e985b5b2a8..00000000000 --- a/docs/plans/receipts/gcp-iam-services-2026-09-29/receipt.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "source_revision": "85bdb4e19", - "binary_sha256": "179882133a4c03958a1bed8a8b444ea06e48c63a6c42263ff83694508825b415", - "focused_controls_passed": 18, - "closure_modules": 844, - "memory_max": "6G", - "memory_swap_max": 0, - "cloud_status": "all ten bootstrap stages completed; exit 0", - "probe_operation": "probe-20260929-3", - "probe_deadline": "2026-09-29T01:20:00Z", - "full_landing_suite": "not run", - "temporary_grants": "probe grants and prior deny grant absent on final independent readback", - "credential_file": "removed", - "newly_enabled_api": "cloudresourcemanager.googleapis.com", - "workflow_acceptance": { - "run_id": 36505895733, - "source_revision": "15b977d417c1a7de52eae96f48bcd3955fb1f94c", - "status": "dispatched; no success claimed" - } -} diff --git a/docs/plans/receipts/google-workspace-admin-api-2026-09-28.md b/docs/plans/receipts/google-workspace-admin-api-2026-09-28.md deleted file mode 100644 index 089aa5a569c..00000000000 --- a/docs/plans/receipts/google-workspace-admin-api-2026-09-28.md +++ /dev/null @@ -1,41 +0,0 @@ -# Workspace administration API qualification — 2026-09-28 - -This is documentation research, not a live organization readback. No credentials were -provisioned and no Google-side setting was changed. - -| Control | Supported observation found | Mutation | Evidence | -|---|---|---|---| -| Auth Platform web client | Console readback | Manual console action | [Create clients](https://support.google.com/cloud/answer/15549257) | -| Internal audience | Console readback | Manual console action | [Configure consent](https://developers.google.com/workspace/guides/configure-oauth-consent) | -| This client's Specific Google data access | Console readback | Manual console action | [Control app access](https://support.google.com/a/answer/7281227) | -| External directory sharing | Cloud Identity Policy API, `directory.external_directory_sharing` | API mutation unsupported; manual console action | [Supported settings](https://docs.cloud.google.com/identity/docs/concepts/supported-policy-api-settings), [admin control](https://knowledge.workspace.google.com/admin/users/let-third-party-apps-access-directory-data) | -| Profile photo editing policy | Console readback | Manual console action | [Profile editing policy](https://knowledge.workspace.google.com/admin/users/allow-directory-users-to-change-their-profile-and-photo) | - -“No supported read found” is a dated qualification of these documented surfaces, not a claim -that Google can never add an API. The Policy API inventory lists several global API-controls -settings, but those are not the specific-client access control in row 3. The Directory API's -user/photo operations are not an administration API for row 5. IAM `oauthClients` applies to -[Workforce Identity Federation](https://docs.cloud.google.com/iam/docs/workforce-oauth-app), -not the Auth Platform client in row 1. - -Row 4 values are `REQUESTER_BASIC_PROFILE_ONLY` and `ORGANIZATION_DIRECTORY_DATA`. -Unspecified/unknown values are not evidence of either. The minimum desired value is the first; -organization discovery requires an explicit rationale. - -A future supported observer uses [policies.list/get](https://docs.cloud.google.com/identity/docs/how-to/list-get-policies) -with the separately approved `cloud-identity.policies.readonly` scope. Complete pagination and -[effective-policy reduction](https://docs.cloud.google.com/identity/docs/concepts/policy-api-concepts) -are required; a single raw policy is not necessarily the customer's effective policy. Unread -OU/group overrides remain a refusal. Honor the documented quota and do not derive identity -from the older SYSTEM-policy name/sort-order convention. - -The implementation's receipts bind customer/project/client/OU, typed setting, source, -evidence reference, observation time and expiry. A receipt producer remains responsible for -authenticating and durably retaining its evidence. The assessor is pure and cannot independently -verify a screenshot or API response referenced by a caller. Fixture receipts are not deployment -receipts. Matching scope/redirect lists are compared as sets, not presentation order. - -Validation: the focused Workspace claim suite uses a byte-identical import closure under the -existing 6 GiB limit. It covers missing observations, mismatch, tenant isolation, reader admission, -expiry/future timestamps, supported API setting identity, unresolved effective coverage and -scope-order equivalence versus extra privilege. See the branch's validation receipt for results. diff --git a/docs/plans/receipts/issue-cut1-command-boundary-2026-09-28.md b/docs/plans/receipts/issue-cut1-command-boundary-2026-09-28.md deleted file mode 100644 index 5fc6c7b9b3e..00000000000 --- a/docs/plans/receipts/issue-cut1-command-boundary-2026-09-28.md +++ /dev/null @@ -1,47 +0,0 @@ -# Issue command boundary — 2026-09-28 - -Base: `e286b29962c3b89904277c64d0e0c7cc25ddb2d0`, isolated branch -`codex/issue-cut1-toposort`. The live checkout and authentication handlers are unchanged. - -The Git carrier now owns an isolated snapshot for each operation and publishes against the -exact fetched remote ref. It checks the supplied issue-parent expectation in that snapshot, -then publishes event and operation receipt in one commit. A remote movement refuses; it never -silently rebases a stale command. This is a Git realization of revision CAS, not a migration -to the held fabric-storage implementation. - -Operation identity binds node, authenticated principal, payload and original precondition. -The committed receipt is checked before the current issue revision, so retry after another -comment returns the original event/ordinal. Clock time is committed result data. Another -operation with identical comment text is an intentional second comment. Native forms have -server-minted keys and explicit empty-revision fields. Enhanced assignment preserves those -keys; an inline lost-response retry retains the original command. - -The shared URL-encoded decoder handles native CRLF, Unicode, plus/percent encoding and -field-looking multiline text without ambiguous newline framing. Duplicate decoded names, -malformed percent escapes and invalid UTF-8 refuse. Missing expected revision is distinct -from explicitly empty expected revision. - -Detail reads refuse unreadable/forked/incomplete issue history and derive standing, assignment -and form revision from one event snapshot. The attempt observation is still not connected: -the page explicitly reports that gap instead of claiming no attempts. Recents are suggestions -and may degrade independently. This cut does not install a durable consumer for Fabric launch -obligations; the existing first-time inline dispatch path remains. Retrying a committed command -does not dispatch another worker. - -## Evidence and limits - -- Actual `.dag` command -> disposable Git remote -> readback: PASS, including same-key retry - after an intervening event, identical-body different command, stale revision and payload reuse. -- Forced concurrent publication: PASS, exactly one event and one operation receipt committed. - Fixture receipt: `/tmp/gunbc-issue-command-yzokh_mx/receipt.json`. -- Native Chromium POST fed into actual `.dag` decoder: PASS. -- Emitted assignment client in Chromium with a lost response: PASS for native-form and inline - retry identity, including preventing a different inline payload from reusing a pending key. -- Focused codec claims: 2 PASS. Focused carrier claims: 3 PASS. -- Workspace administration claims are independent: 5 PASS. -- Full serve witness closure: FAILED (exit 137 at the existing 6 GiB limit after serve typecheck). - This is not a passing integrated floor, merge approval or deployment receipt. - -All DAG executions use `MemoryMax=6G`, `MemorySwapMax=0`. Focused source closures are byte-identical -copies with source hashes, not edited test substitutes. The checked-in wet/browser controls -operate on disposable local fixtures, never production state. No memory ceiling was raised. diff --git a/docs/plans/repo-stability-2026-08.md b/docs/plans/repo-stability-2026-08.md deleted file mode 100644 index 048925cc365..00000000000 --- a/docs/plans/repo-stability-2026-08.md +++ /dev/null @@ -1,306 +0,0 @@ -# Repo stability plan — 2026-08-28 (living document) - -Working plan iterated with the operator. Subject: get the repository back to a -stable place and keep it there. Maintained on branch -`claude/repo-stability-priorities-frtsy4` until adopted. - -## Operator intent (recorded 2026-08-28) - -- CI required checks should take **~minutes at most**, not 40, not 100. -- The desired mechanism is **minimal invocation** — affected-set / build-system-style - caching of test results between runs ("only run what changed"). -- **Witness rule:** every witness is a short, one-pass transformation contract. - Target budget per witness: **<5ms, eventually <1ms guaranteed**. Long - corpus-walk / integration / performance witnesses are POLICY VIOLATIONS — - the current 44 BUDGET-REFUSED rows are considered erroring/failing now, - not "budget too small". -- The pattern to break: things reach "good enough" (e.g. 40-min CI) then spiral - (100 min) because nothing prices the derivative. v1 / reconcile / CI root work - keeps being avoided; it is admitted under the 2026-08-20 purpose ruling (in - support of v2 self-host; fixing issues is fine). - -## Measured state (2026-08-28, main @ e910a39) - -- `witnesses` required check: **red on every recent main run** with an - IDENTICAL set of 47 unexpected witness failures across runs (zero flake). - Latest measured: `planned=12949 executed=12949 passed=12615 known_red_held=33 - failed=47 stale_quarantine=1 interrupted_before_verdict=44 - completed_over_cost_requirement=2` (run 33145062452). -- Wall: ~85–100 min/run (was ~30 min on 2026-08-22 at routed=10439). - `compile.reconcile` in strict-preparation: 22 min over 4187 modules - (15 min over 3892 on 2026-08-24 → superlinear; ~92% attributed to - `typecheck_with_census_extra`). -- Roster growth: routed 10439 → 12949 in 6 days (+24%). -- Queue: 26 main-push runs queued at once (main pushes never coalesce); - operator: not a priority right now. -- srv1 belt/daily workspace: DOWN since 2026-08-20. `/opt/gunbc/gunbc` is - ~664 commits behind the admitted fleet revision; belt ticks exit 0 and - refuse to spawn (diagnosed in #9559). Convergence chain landed - (#9506 → #9555 → #9563 → #9576) but `fleet-converge.yml` is - workflow_dispatch-only and the apply has not been run. - -## Priorities - -### P1 — CI to ~minutes: minimal invocation + short witnesses - -The two whole-corpus terms (fold ~30 min, reconcile 22 min) reach minutes only -by not redoing unchanged work, never by optimization. Plan: - -1. **Hoist corpus walks out of witnesses into run-once indexes** - (`declaration_index` pattern: the required parse sweep derives the facts - once; witnesses read the index in ms). Dominant violator families among - the 44 BUDGET-REFUSED rows: `grammar_coverage_*` (65–69s), - `enforcement_live_*` (52–60s), `cost_coverage_*` (54–62s), - `vocabulary_containment` (~30s), plus assorted 5s rows. - This fixes ~40 of 44 policy violations AND removes minutes of cold cost. -2. **Closure-digest verdict store (the affected-set mechanism, done the - cache way).** Key: (witness identity, closure content digest, binary - identity). Hit ⇒ stored verdict replayed as this run's verdict; miss ⇒ - execute, fail-closed. No git-diff observation exists in the mechanism — - this is identity-derived, not diff-derived, which is what distinguishes - it from the deleted affected-set machinery and its absorbing failure - arms. Hosted in the v1 seed now (admitted: it gates all v2 work); the - durable artifacts (digest definition via `std.content_hash`, store - schema) survive the v2 cutover. Same treatment per-module for reconcile. -3. **Cold backstop cadence:** scheduled whole-corpus uncached run; - cached-vs-cold divergence is a typed line-stop (the doc's own purity - oracle). The cache is never trusted, only checked. -4. **Witness budget wall:** once violators are hoisted, enforce the short- - witness rule as a hard refusal (existing budget machinery already - refuses; today its refusals are ignored). Ratchet the ceiling down - toward the 5ms/1ms target as the population complies. -5. **Derivative gate:** required run holds its own cost against a declared - policy budget (a §5-legal oracle) so regrowth stops the line the day it - lands, not six weeks later. -6. stage0 cargo build: sccache / runner image — ordinary work, separate. - -First artifact (measurement, before building the store): compute closure -digests over the roster on the current tree, replay the last ~10 main -commits, report would-have-been hit rate + residual per-push cost. - -### P2 — Main back to green: the 47 standing reds - -Identical across runs; grouped by module (run 33145062452): -doc_reachability (x6, v1+v2 copies), sole_constructor audit probes (x6), -lens_module_gate (x3), quarantine_probe_disposition (x3), ci_budget_tree -(x3), cost_coverage (x3), compiler_closure ingest/emit (x4), guarantee -probes (x5), and singletons. Plus 1 STALE-QUARANTINE -(`duplicate_definition_binding_probe_test` passes; remove from -`v2.workflow.floor_expected_red`). Triage by shared root, not row-by-row; several sit in P1.1's corpus-scan -families and get cheap after the hoist. Dispositions must be honest: fix, or -expected-red with typed reason + trigger — never quarantine-to-green. - -### P3 — srv1 / roadmap daily workspace - -Run the fleet-converge plan → apply with the landed convergence chain; -verify by the belt's own evidence (next tick spawns; dispatch_preflight -refused_axis_count 5 → 0; fresh provider-events capture). Then consider a -scheduled convergence so a 664-commit drift can never silently accumulate again -(it was invisible for 8+ days because the belt exits 0). - -### P4 — Repo cleanup pass ("pristine") - -Top-level census (2026-08-28) and dispositions to decide per item: - -| Path | Size | Standing | Proposed disposition | -|---|---|---|---| -| `dag/` (2962 .dag) | 40M | live corpus | keep; audit `dag/examples/` (6 demo dirs — likely stale/scaffold) and `dag/config/` (1 file) | -| `src/v1` | 17M | frozen seed + 13M `stage0/` mirror | keep (seed); no growth | -| `src/v2` | 14M (1346 .dag) | active | keep; `experimental/` dir needs a disposition | -| `docs/plans/` | **20M, 250 files** | mostly stale/pre-cut | classify each: live / completed / superseded / bankrupt; delete-first for the dead (bulk of the 20M) | -| `dag/gunbc/plans/` | 66 files | dual home with docs/plans | decide the ONE home for plans; migrate or delete | -| `docs/briefs/` | 8 files, all 2026-08-24 | one-day lane briefs | likely bankrupt with the measurement corpus; delete or fold into carriers | -| `docs/probes/` | 1 file | recreated after the 2026-08-24 bankruptcy deleted the dir whole | violates the bankruptcy ruling's spirit; model or delete | -| `docs/runbooks/` | 5 files | hand-authored ops docs | §6 out-of-band-actuation tells; keep only with dissolution obligations | -| `docs/extdeps/lotes_azifa072/` | 428K | vendor pad-array data (CSV/SVG) | verify cited by a `.dag` authority; else relocate/delete | -| `tools/*.txt,*.tsv` (5 files, ~1.1M) | receipts/manifests at repo root of `tools/` | transcribed measurement output — the class the bankruptcy deleted; delete or model (`m1c_bulk0_residual.tsv` modified TODAY — find the writer first) | -| `fixtures/`, `test/fixture_roots` | 196K/20K | fixture carriers | keep; confirm all reachable from witnesses | -| `artifacts/bmc/` | 24K | committed artifacts? | verify generated-artifact registry covers or delete | -| `provider-runtime/codex/` | 24K | ? | classify: live realization or residue | -| `DESIGN.md` / `ROADMAP.md` | 160K/58K | generated projections | verified by `generated-artifact` phase; keep | - -Rule for the pass: every deletion goes through the delete-first census -(what refuses tells us what was load-bearing); anything kept must name its -consumer; transcribed measurement outputs are debt regardless of accuracy. - -### P5 — Plans content refresh - -After P4 shrinks the population: rewrite the surviving live plans (floor-cut, -namespace-cut, guarantee-recovery gap analysis, replacement-migration doctrine) -against the current tree, so plan prose stops seeding sessions with dead premises. - -## Open operator decisions - -1. Ruling wanted: identity-derived verdict caching (P1.2) is NOT the - deleted diff-derived affected-set machinery — confirm it may be built, - with the cold cadence as its honesty backstop. -2. Where do plans live: `docs/plans/` vs `dag/gunbc/plans/` (one home). -3. P3 apply touches a live host — dispatch when ready. -4. Main-push queue coalescing: deferred by operator (not a priority). - -## Log - -- 2026-08-28: created; measured state recorded; priorities agreed in session. -- 2026-08-28: P4 first cut — deleted 21 orphan docs (15 docs/plans, 5 docs/briefs, - 1 more) after a full reference census. Remaining candidate classes recorded in - session discussion: 27 docs referenced only by other docs (islands to cut whole), - ~200 docs/plans referenced from dag/src carriers, tools/ receipts (consumer: - srv1_residue_rehearsal path strings only), docs/probes single file (referenced by - lotes_azifa072.dag), runbooks (referenced by roadmap/actuate carriers). -- 2026-08-28: P4 second cut — deleted tools/ receipt dumps (5 files, operator: - derived data is never committed; only .dag derivation code), 33 more stale - off-topic plan docs (closure-safe: refs only from snapshot strings/each other), - and src/v2/experimental/ (empty quarantine, failed concept per operator). - DEFERRED: 112 old off-topic plans cited by live carriers (roadmap_authority - ~25, dag plan carriers, cli_run.rs, lens modules) — need carrier-row edits; - dag/examples/ (witness-consumed: declared_type_inhabitance, cost_estimate_float, - bootstrap_witness.rs); artifacts/bmc (bmc_fan_program carriers consume); - provider-runtime/codex (codex witnesses consume). -- 2026-08-28: P4 third cut — deleted docs/plans/receipts (13M, 73 files) and - docs/plans/measurements (456K) whole: persisted measurement dumps, operator-ruled - garbage (derived data is never committed; only the derivation code is). Every - in-tree mention verified to be prose (comments, note strings, srv1 snapshot rows, - a roadmap handback sentence) — no executing consumer reads any of these paths. - Those prose pointers now reference history, same status as any receipt citation. -- 2026-08-28: P4 fourth cut — deleted the 112 deferred stale plan docs; flipped - 48 plan carriers to PlanIsAuthorityOnly (registry row is what made each deleted - .md expected on disk — the a295e17415 precedent); stripped 45 comment-channel - pointers to deleted docs (13 files). DEFERRED to P5: string-field citations in - roadmap_authority.dag (~46 lines) and stale plan-carrier bodies — editing those - requires regenerating ROADMAP.md alongside. FOUND & FIXED IN PASSING: main @ - e910a39 (#9612) does not build — duplicate shared-artifact wall-fill definitions - re-added over #9609 (E0428 x3, cli_run.rs); deduped on this branch; main needs - the same fix (its CI runs were still queued when found). Parse sweep advisory - count unchanged vs main (164). NEXT CANDIDATE ROUND: delete the 48 stale - authority-only plan carriers themselves (imports/rosters/doc_graph edits), and - the docs/plans doc-to-doc islands. - -## Finding — per-invocation whole-tree pool tax (2026-08-28, CORRECTED same day) - -AN EARLIER REVISION OF THIS SECTION CLAIMED A CWD-GATED SWITCH (identical trees -<0.5s outside a checkout) AND IS REPLACED WHOLE: those fast controls were -PANICS — `repo_relative_path_normalized` aborts on roots outside the process -workspace root, and the discarded stderr was misread as fast success — the -fabricated-fast twin of execution-provenance loss. `git rev-parse` is workspace -discovery only, not a heavy-work toggle. - -ESTABLISHED by succeeding runs (entry compiled, NoSuchFunction reached) over -subset roots inside the workspace, entry `std/abi.dag` (imports nothing): -140 modules 3.5s · 1637 39s · 2962 (dag) 63s · 4308 (dag+src/v2) 81–99s. -LINEAR at ~20–25ms per POOL module, paid per process invocation, independent of -the entry (widest closure 110s vs smallest 81s on the same roots). - -MECHANISM (gdb stacks + code read; all hand-Rust in src/v1/stage0/src/cli_run.rs): -`resolve_entry_graph` routes every run through the process shared index -(`try_process_shared_index_for_pool`); building it (`new_multi_entry_index_shell`) -EAGERLY constructs `build_module_graph_facts_live` — a tolerant parse of EVERY -pool module (`reference_resolution_facts`, `module_declaration_facts`, import + -strict-reference adjacency at two tiers) — plus `build_module_path_index`, -another whole-pool parse. Hot leaves: `v1_std_core::build_newline_index` -(im::Vector push_back per element — plain-Vec shape) and `v1_std_core::intern` -String clones in `pre_intern_tokens`. - -WHY THE FACTS EXIST (per the in-code comment block): loader-tier adjacency and -selection-tier adjacency (affected-set selection). A single-entry run needs at -most one entry's loader closure but pays both tiers over the whole pool. The -facts ARE memoized (thread-local MODULE_GRAPH_FACTS_CACHE / PROCESS_RESOLVE_INDEX), -so one process pays once — but every PROCESS pays cold, and CI phases, -emit-compile entries, and belt ticks are one process per invocation. - -RELATION TO .dag: acknowledged hand-Rust area — 🟡 dissolve-on markers cite the -cli-run hollowing plan (`dag/gunbc/plans/cli_run_hollowing_plan.dag`, -`gunbc.cli_run_hand_rust_area_ledger`); `import_closure_live_paths` labels -itself "Host realization of v2.lens.module_graph.import_closure_live"; path -admission modeled by `gunbc.cli_run_repo_grant`. The MODEL is .dag; the -execution that costs the time is hand-Rust. - -NOT ESTABLISHED: the floor's 22-min `compile.reconcile` -(`typecheck_with_census_extra`, ~300ms/module) is a separate larger cost, not -attributed here. REPRO: in-workspace subset roots (untracked dirs), success -judged by the NoSuchFunction cause line, never by wall time with output -discarded; gdb -p -batch -ex "thread apply all bt" during the slow window. - -## cli_run.rs decomposition (started 2026-08-28, branch claude/repo-stability-priorities-frtsy4) - -50,536 → 46,443 lines; 13 clusters moved to src/v1/stage0/src/cli_run/ submodules -(pure code motion, build-verified, smoke-tested): test_migration, compile_clean, -non_fold_residue, class_b_census, languages_census, external_authority, -inert_carrier, witness_gates, emit_host, complexity_gates, doc_graph, -declared_refs, census_heads. Mechanics: submodule carries `use super::*` + the -parent's use-header; parent re-exports `pub(crate) use ::*` (paths stable); -bin-consumed fns get explicit `pub use`; private moved items widen to pub(crate); -eight report/plan types widened for the private-interface lint (CI: -D warnings). -lib.rs is generated and untouched — submodule declarations live in cli_run.rs, -matching the existing pattern (declaration_index, phase_profile, ...). - -DELIBERATELY NOT MOVED YET, to avoid conflicting with in-flight operator work: -the resolver/index core (resolve_entry_*, build_module_*, import_closure_*, -whole_tree_*, the shared-index thread_locals — the pool-tax subject) and the -floor runner (floor_*, run_required_*, budget/accounting). Move each in a quiet -window; the extraction recipe is in the split commits' messages. -- 2026-08-28 (later): the two deferred cores are moved with operator approval — - entry_resolve (75 items, ~2.5k lines: shared index, resolve store, module - graph facts, path index, typed-module cache) and required_floor_runner - (49 items, ~4.8k lines: run_required_floor, claim eval/measurement, - discovery, diff observation, resource sampling). cli_run.rs 50,536 → 38,636 - lines, 26 submodules. Remainder: ~450 fns of mixed verbs/helpers plus ~350 - embedded test fns; further carving is ordinary follow-up, no core left. - -## The dependency map (2026-08-29, standing — replaces re-deriving this in chat) - -The interpreter has four roles: R1 witness executor · R2 orchestration executor -· R3 v2's executor (v2 stages are .dag, so v2 compiles by being interpreted) · -R4 the host-effect seam (builtin handlers). Emitting .dag→Rust does NOT need -the interpreter (the v1 passes are native); interpretation is evaluation without -emission. Three separable decisions: - -1. REALIZATION STORE (backport of v2.std.materialize's model into a v1-host - persistent content-keyed store; digest authority already shared via - std.content_hash/fnv1a64). No dependency on the interpreter either way — - the interpreter becomes a CLIENT. Gives fast interpreted execution, CI in - minutes, fast v2 compile loop. v2 SELF-HOST DEPENDS ON THIS AND NOT ON - DECISIONS 2-3. -2. NATIVE PRODUCERS PER FAMILY (emit witnesses etc. into content-addressed - native bundles; store's producer flips per family; interpreter drains). - Optional per family; needs emitter perf + diagnostics work. -3. DELETE THE EVAL LOOP (only after 2 completes everywhere; R4 survives as the - runtime library with a new caller; may never be worth taking to zero — a - bounded evaluator for bootstrap/dev is near-free). - -digest authority ──► store ──► fast interpreted execution ◄── v2 self-host - ├──► native producers per family - └──────────┴──► interpreter drains ──► (optional) delete - eval loop; keep effect runtime - -## The one execution test (anti-planning: build this, then judge the plan) - -SUBJECT: persist the process-shared index's derived facts across processes, -keyed on content digest. Concretely: module path index + module graph facts -(and optionally the typed-module cache) in cli_run/entry_resolve.rs get a -disk-backed store under target/ keyed on (source file digests, binary identity). -ACCEPTANCE, binary and pre-registered: on an unchanged tree, the SECOND -`gunbc run --source-root dag --source-root src/v2 --entry dag/std/abi.dag` -completes in <10s (cold today: ~81–99s), AND the warm facts are byte-identical -to cold-derived facts (the §5 purity oracle, asserted by execution). A miss -(any file changed) re-derives fail-closed. If the warm run is not <10s or the -oracle disagrees, the store thesis as specified is refuted and the plan is -rewritten before anything else is built on it. -WHY THIS ONE: smallest end-to-end existence proof of the store (one producer, -one consumer, one oracle); kills the measured entry-independent pool tax; the -same mechanism CI-minutes and the v2 loop need; ~a day; pass/fail visible in -one command run twice. -- 2026-08-29: REGRESSION PINNED BY BISECT — main's converge actuator dies with - `trim expects a string argument, got Null`. First bad commit: 655f82a74 - (#9344, "Carry occurrence identity on every semantic v1 Node"). Mechanics at - the crash site (dag/gunbc/repo/repo_local_git_config.dag observe_binding_at_repo): - the effect result of git.Core.ConfigLocalGetInRepo reads `.success` fine - (the branch is taken) but `.value` comes back Null — a record-field read on - an effect result resolving to Null post-#9344, so the suspect is field - lookup / effect-result decode interacting with the new occurrence identity. - Repro: build any commit >= 655f82a74, run - `gunbc run --source-root dag --source-root src/v2 --entry - dag/gunbc/repo/repo_local_git_config.dag --function converge`. - Likely blast radius: any interpreted entry reading effect-result fields - (belt, deploys, gates). Not fixed here — #9344 is the operator's own thread. - Also fixed forward on this branch: #9656 omitted TypeEnv.unit_variant_index - in six bin/infer_semantics_witness.rs literals (main's full build is red). diff --git a/docs/plans/required-floor-in-flight-shared-fill-design.md b/docs/plans/required-floor-in-flight-shared-fill-design.md deleted file mode 100644 index b348bc8b631..00000000000 --- a/docs/plans/required-floor-in-flight-shared-fill-design.md +++ /dev/null @@ -1,35 +0,0 @@ -# Required-floor in-flight shared-fill refusal design - -Status: implementation design, following -`required-floor-in-flight-shared-fill-findings.md`. - -## Decision - -Keep the required floor's existing 500ms attempt-safety ceiling. When its CPU poll fires during -an admitted cross-claim fill, classify the interruption as `FillBudgetExceeded` only when the -claim's marginal CPU without that still-active fill remains within the ceiling. The refusal -carries the claim entry, prospective producer, fill CPU lower bound, marginal CPU lower bound, -and unchanged limit. - -This is not pre-publication fill credit. The clock still charges all uncommitted work and stops at -the same point. The change repairs the subject of the refusal: first-touch order no longer appears -as intrinsic claim cost when the active prospective fill is what crossed the bound. - -## Boundary and nesting - -`CrossClaimFillGuard` remains the one lifetime bracket. Its stack frame additionally retains the -producer and CPU start. At a poll, the outermost active frame identifies the prospective artifact; -its CPU is inclusive elapsed CPU less already-stored descendants. Those descendants remain netted -by the existing committed-fill accumulator, exactly as before. - -If marginal CPU alone already exceeds the ceiling, the ordinary evaluation-budget refusal wins. -If no admitted fill is active, behavior is unchanged. Store, abandon, fill receipts, completion -accounting, wall accounting, memo admission, and memo serving are unchanged. - -## Safety and evidence - -The refusal does not make an over-budget fill pass, raise a ceiling, retry a row, or create an -artifact receipt. Its stable cause token makes the population countable. A real evaluator-path -test must force the poll while an admitted fill is active and assert the producer is carried; a -matched unadmitted control must retain the ordinary evaluation-budget refusal. This instrument -can provide evidence for a separately modeled fill envelope later, but creates no such policy. diff --git a/docs/plans/restore-src-v1-required-floor-stall-finding.md b/docs/plans/restore-src-v1-required-floor-stall-finding.md deleted file mode 100644 index cec22b631da..00000000000 --- a/docs/plans/restore-src-v1-required-floor-stall-finding.md +++ /dev/null @@ -1,40 +0,0 @@ -# RESTORE-stall finding: widening the required floor's source roots to admit `src/v1` - -Standalone finding, split out of triage prose per parent-session request (dashboard node `adhoc-9b80ec49-d63`, session `zesty-newt-828`, 2026-08-19). It is the receipt for the RESTORE disposition ruled out repo-wide across `v1_dead_witness_tree_triage_receipt`, `v1_dead_witness_tree_triage_receipt_remainder`, and `v1_dead_witness_tree_triage_receipt_emitter_ambiguous_variant_owner` (`dag/gunbc/ci/ci_layer_roots.dag`) — those receipts cite this finding rather than re-deriving it. - -## The question - -Could the dead-witness-tree population under `src/v1/tests/claim` be restored to execution by the cheap route — adding `--source-root src/v1` to the required floor's invocation (`claim_executor --required-floor --source-root dag --source-root src/v2`) — instead of migrating or retiring each file individually? - -## Local measurement (this session, prior window) - -- **Invocation:** `claim_executor --required-floor --source-root dag --source-root src/v2 --source-root src/v1` (the one-token addition of `--source-root src/v1` to the standing invocation). -- **Head SHA at time of run:** not recorded in this session's surviving notes; the run was taken against this branch (`session/zesty-newt-828`) during the window that produced the 17-file triage, before either the emitter_ambiguous_variant_owner receipt or the remainder receipt existed. This is a named provenance gap. The qualitative result (stall, not slow-but-progressing) is not sensitive to the exact commit, since strict-preparation's cost is dominated by corpus size and import-graph shape, not by this triage's one-line prose edits — but no exact SHA is citable. -- **Local vs. remote:** run locally in this session's container (not via `ctrl-build --remote`), predating the guidance that heavy whole-tree operations prefer remote dispatch. It is a single-shot diagnostic, not a build, and its result (stall before any phase progress) makes host contention an unlikely confound: a resource-starved run would still log *some* forward progress at a slower rate, not zero. -- **Contention caveat:** the container's RAM cap is shared across `sessions.slice` with other sessions on the same host; a co-located heavy build could inflate wall-clock or add memory pressure. This cannot be fully ruled out for this run. It is addressed, not eliminated, by the corroboration below, measured on dedicated CI hardware with no co-tenant. -- **Result:** the process stalled 8+ minutes inside `strict-preparation` (the whole-corpus typecheck phase preceding any witness execution), RSS plateaued at approximately 7.2GB with no further growth, and no phase-progress log line advanced past the point already reached without `src/v1`. The process was killed: a plateaued RSS with no progress is the signature of a stall (thrashing, or an unreachable fixed-point), not of a slow-but-advancing computation. - -## Independent corroboration (relayed by parent session smart-ram-730, CI run 32196317824) - -A CI run on a dedicated `srv4` runner (no co-tenant contention) measured the **ordinary floor, without `src/v1` added at all** — the ceiling this triage's population currently sits below, not the widened case: - -- `rss_kb=7078696` (~7.0GB) -- `wall_s=540` (9 minutes) in `strict-preparation`, logged as `compile.reconcile done in 9 minutes` -- heavy paging during the run: `pswpin=231511094`, `pgmajfault=169789103` - -This is the cost of `strict-preparation` **today, over `dag` + `src/v2` alone** — the baseline the required floor already pays before any witness runs. Both observations land independently in the same ~7GB / high-single-digit-minutes regime on the same phase, so the local stall was not primarily an artifact of `src/v1`'s weight pushing an otherwise-cheap phase over a threshold: `strict-preparation` is already expensive at the two-root baseline. Adding `src/v1` (52 non-test `.dag` modules plus the 15-17 test files, per `witness_fold_src_v1_coverage_gap_note`'s count) pushes an already near-ceiling phase further. - -**Second data point (relayed by parent session smart-ram-730, CI run 32204338777, dedicated runner, same unwidened floor):** `rss_kb` peaked at 9,816,820 (~9.8GB) in the same `strict-preparation` phase this triage's own PR (#8486) exercised — against the ~7.0GB from run 32196317824. Both runs measure the identical unwidened invocation (`dag` + `src/v2`, no `src/v1`) on dedicated hardware, so the ~2.8GB spread is not host contention or a `src/v1` effect — it is `strict-preparation`'s own run-to-run variance at the two-root baseline. The honest ceiling claim is therefore a **range** (~7.0–9.8GB observed so far), not a single figure; the headroom between that range and this host class's actual OOM/thrash threshold is narrower than either measurement alone suggests, which strengthens the conclusion that widening to `src/v1` is not the cheap route. - -## Stall vs. slow classification - -Both signatures point to **stall**, not **slow**: - -1. The local run showed RSS plateau with zero phase-progress advancement over 8+ minutes — a computation still progressing would show continuing RSS growth or advancing log lines, not both flat. -2. The CI corroboration establishes that even the *unwidened* floor is already within single-digit minutes and ~7GB of whatever ceiling this host class enforces; paging activity (`pswpin`/`pgmajfault` in the hundreds of millions) is itself evidence of memory pressure at the baseline, before `src/v1`'s modules are added. - -Together, `--source-root src/v1` does not merely make `strict-preparation` *slower*; it very plausibly pushes an already-memory-pressured phase past a practical ceiling into OOM or genuine thrashing — the local kill preempted observing which. Re-running to distinguish "would eventually finish, slowly" from "would OOM or thrash indefinitely" was not attempted, since the practical consequence (RESTORE is not the cheap route the one-token diff suggested) is decided either way. - -## Conclusion - -RESTORE is ruled out for the whole `src/v1/tests/claim` population, repo-wide, on this evidence: a local stall (contention-caveated, but with a stall rather than slow-progress signature) independently corroborated by dedicated-hardware CI numbers showing the *unwidened* floor already runs close to whatever ceiling caused the local stall. This finding is the citation target for the RESTORE disposition in all three `v1_dead_witness_tree_triage_receipt*` rows; MIGRATE and RETIRE are the two remaining dispositions, applied per-file in those receipts. diff --git a/docs/plans/review-instrument-observations.md b/docs/plans/review-instrument-observations.md deleted file mode 100644 index b71839e3ced..00000000000 --- a/docs/plans/review-instrument-observations.md +++ /dev/null @@ -1,87 +0,0 @@ -# Review-instrument observations (not a census of this repository) - -Two findings about the **review and merge-readiness instrument**, recorded because they were each -found by accident while doing other work and are the kind of thing that is otherwise rediscovered -expensively. They are facts about the tooling, not about the seed or the `.dag` corpus, which is why -they are here rather than in [seed-string-decode-census.md](seed-string-decode-census.md). - -Both were observed on `gunb-ai/gunbc#10180` and independently reproduced by a second session. - -## 1. `stale_provider_count` does not fire, even where both operands are local - -A single `dashboard-ops reviews 10180` payload carried **three different shas at once**: - -| field | value | -|---|---| -| `reviews[0].sha` (the approving review) | `60aefd9d3acb684e` | -| `merge_criteria.head_sha` (the dashboard's own head) | `aff564ca8a7d2fc7` | -| `gh pr view --json headRefOid` (actual) | `2c9c08a439b5b0c2` | - -and, in that same object: `stale_provider_count: 0`, `stale_providers: []`, `approvals: 1`, -`meets_approval_rule: true`. - -**What is observed, stated before what explains it.** The approval sha differed from the dashboard's -own `head_sha`, both fields present in one object, and `stale_provider_count` still read 0. A later -payload on the same PR DID report `stale_provider_count: 1`, so the field is not simply inert. - -**Correction to an earlier reading.** This document first concluded "the comparison does not fire even -when it has everything it needs". Two observations now constrain it better, and the second refutes -that sentence as written: - -| observation | `head_sha` | provider's latest review sha | `stale_provider_count` | -|---|---|---|---| -| A | `aff564ca8a7` | claude @ `60aefd9d3ac` | **0** | -| B | `dee613521e4` | claude @ `dee613521e4`, codex @ `c29a33ce28e` | **1** (codex) | - -In B the field fires correctly. **Hypothesis that fits both, and it is a hypothesis, not a -measurement:** staleness is evaluated when a review is INGESTED, against the head known at that -moment, and stored — while `head_sha` is read live at query time. Under that reading A is a stored -verdict that was true when written and went false underneath, not a comparison that failed to run. -Distinguishing it would take a payload sampled at a known ingest boundary, which has not been done. - -**The operative rule is unchanged either way**, which is why the correction does not disturb it: a -stored-and-gone-stale verdict and a non-firing comparison are indistinguishable to a reader, and both -report 0 on an approval that is not on the current head. - -**Consequence for any merge decision.** Comparing the dashboard's `head_sha` to `gh`'s `headRefOid` is -NECESSARY BUT NOT SUFFICIENT — it catches the lag case and would have passed observation A the moment -the dashboard caught up, with an approval three heads old. The sufficient check is computed by the -reader: - -``` -gh pr view --repo --json headRefOid --jq .headRefOid -dashboard-ops reviews # compare each reviews[].sha to that value yourself -``` - -`stale_provider_count` is not evidence of anything on its own — it is right in B and wrong in A, and -nothing in the payload distinguishes the two cases for you. - -**The same applies to `request_changes_count` reaching 0.** On this PR a codex REQUEST_CHANGES on -`c29a33ce28e` disappeared from the counter after the next push. The findings WERE addressed in that -push — but the counter would read 0 either way, because a REQUEST_CHANGES is superseded by any push -regardless of whether anything was fixed. The commit and the reply are the evidence that the findings -were addressed; the zero is not. - -**A second, softer form.** An approval can also be superseded in PREMISE rather than in sha. #10180's -approving review reasoned explicitly from *"census-only … no code, no `.dag`, no seed changes"*; a -later commit added seed code. Even had the shas matched, the verdict's stated grounds no longer -described the diff. Read what a review says it approved, not only what it approved. - -## 2. A refused review burns its sha slot, invisibly to the counters - -In the same payload, review `59066` carried `status: "failed"` with: - -``` -worktree freshness check failed: HEAD is 95ef0941… but PR #10180 head is 44efc6eb… -— refusing to review a stale/wrong checkout -``` - -That refusal is correct behaviour: reviewing a checkout that is not the PR head would produce a verdict -about a different object. But the slot is consumed — `44efc6eb` is never reviewed, and no retry occurs. -A PR can therefore accumulate refused reviews that leave no trace in `approvals`, -`request_changes`, or `stale_provider_count`; only the `reviews[].status` / `error` fields carry it. - -**The pairing is the finding.** One half of this system refuses to review unless its checkout matches -the PR head EXACTLY; the other half reports staleness as 0 across a three-sha spread. One half is -strict about precisely the question the other half does not ask. Classify a review by its `status` and -`error` fields before reading any counter. diff --git a/docs/plans/review-sheet-and-census-lane-handoff.md b/docs/plans/review-sheet-and-census-lane-handoff.md deleted file mode 100644 index 8f346f3624e..00000000000 --- a/docs/plans/review-sheet-and-census-lane-handoff.md +++ /dev/null @@ -1,252 +0,0 @@ -# Review-sheet and census lane — state at wind-down - -Written 2026-09-20 by zesty-crane-846 on an operator instruction to wind down and record -remaining items, so the system can prioritise v1 performance and v2 migration. Nothing here is -in progress. - -**Read the census section before acting on any number in this lane's output.** The prospect rows -that exist are historical prototype output and are NOT authority for runner occupancy, spend, or -opportunity. That is the single most important thing this page carries. - -## What landed - -| PR | what | -|---|---| -| #11552 | the App-manifest acquisition MODEL and its route — **the flow is not executable end to end**, see below | -| #11581 | three format-converge defects a live spreadsheet found that no witness could | -| #11610 | the actuator consumes the plan's custody name instead of re-deriving it | -| #11564 | GitHub observation record and bounded scan producer — incomplete; completed by #11656 | -| #11669 | `gunbc.review_sheet_identity` — the review-sheet identity semantic kernel | -| #11656 | the three repairs #11564 merged without, plus a sealed scan outcome and an admitted page size | - -**No external prerequisite of this lane is still open.** #11552, #11564, #11656, #11669, #11671, -#11677 and #11679 are all merged. Earlier revisions of this page described #11656 as open and the -census token as queued behind #11671 → #11677 → #11679; that is stale and the blockers are gone. - -### #11552 acquisition is modeled, not achieved - -`gunbc.github_app_acquisition` instructed the operator to "paste the manifest below into the -form's manifest field, and submit". **That instruction was not executable.** GitHub's ordinary -App-creation page carries no manifest field; the manifest protocol requires a form POST carrying -a `manifest` parameter, and a GET renders the blank create page. This was observed live: the -operator followed the step and reported a form with no such field. - -**The instruction is now an artifact.** `census_app_registration_instruction` renders an HTML -document whose form POSTs to the registration endpoint, carries the declared manifest in a hidden -field under the parameter name upstream reads it from -(`extdeps.github.app app_manifest_form_parameter_name`), and carries the run's state nonce in its -action URL; `census_app_registration_instruction_receipt` writes that document to -`.gunbc/github-app--register.html` and refuses to print the instruction if the write failed. -The human act left is pressing its button under an owner session, which is the consent GitHub -requires and exposes no API for. - -**Four facts, kept apart.** REGISTRATION, INSTALLATION, CALLBACK CAPTURE and KEY CUSTODY are -separately established and joined by `census_app_acquisition_standing`, which refuses at the -earliest missing fact and names it. No later fact mints an earlier one: an installation reading -and a private key on disk do not establish a registration, and an installation naming another -App's id does not install this one. `census_app_acquisition_receipt` is the executing route — -three independent readings (the public app record, `GET /app/installations`, the handoff file), -exit zero only when all three hold. #11677's JWS/token route still consumes an App and an -installation that have already been admitted; it creates neither. - -**The two browser acts are owed, and they are NOT sufficient on their own.** No receipt in this -corpus establishes registration, installation or custody for the census App today, and the -acquisition receipt says so in those words rather than reading as done. But an earlier wording of -this paragraph said the browser acts were all that remained "in the model", and the diff it -described contradicts it: `live_installation_population` returns `PopulationUnreadable` on *every* -arm, including a successful decode, because this repository's REST transport models request headers -only and GitHub states whether another page exists solely in the RFC 8288 `Link` header. A short or -empty page is not closure. So after an operator submits the manifest, grants consent and places the -pem, `census_app_acquisition_receipt` still exits non-zero with `InstallationReadingUnavailable`. - -**What is also owed is that transport capability**, and it is declared rather than described: -`census_app_installation_population_frontier_rows` names it — a REST operation result carrying the -response's `Link` header value, sufficient for admitting an installation over a population the -authority itself closed, and explicitly NOT satisfied by a larger `per_page`, by stopping at a short -or empty page, or by routing the read through a CLI whose credential a PAT could satisfy. That -refusal is the honest state and should not be relaxed to make the runbook end green; the operator -should know before they start that the browser acts leave the receipt refusing for a reason that is -not theirs to fix. - -**Custody's second hop is pending a sibling lane.** The declared destination for the App private -key is `census_app_private_key_custody` in Secret Manager. This process cannot write there, so -`CustodyStanding` has exactly one established arm — `CustodyAtHandoffOnly` — and no constructor -for "the key is at its destination". The key belongs as a ROW in the closed custody roster the -`ntfy-publisher-token-onto-gcp-custody` node consolidates; minting a second delivery path here to -close the fact sooner would fork that authority. - -**Authorization pattern.** The registration effect is rostered in -`gunbc.auth.privileged_effect_census` and the selection lands on `HumanOnlyStep`: the surface is -human-only (no API creates an App), so every API pattern fails the surface gate, and the effect is -irreversible (the private key is returned once, and the App name is spent) so a witness is -required — which the human step is. - -## The one fact everything else rests on - -An empty `appProperties` search establishes **"this OAuth client received no matching rows"**. It -never establishes **"no legacy-marked file exists"**. `appProperties` are private to the -*requesting application* — the OAuth client id the token was obtained with — and nothing in this -corpus observes the application identity of its own token. A service-account principal is a -different subject, so comparing service-account emails does not establish visibility. - -That distinction is the whole content of the identity kernel, and it is why the kernel has no -"compatible empty search" arm at all. - -## Remaining: the review-sheet migration cuts - -The kernel landed in #11669. It does **not** repair the production converge; it provides the -authority a future repair is written against. The remaining cuts were designed but not built, and -they replace the approach in the closed #11596 rather than salvaging it — that PR decided the -meaning of each reading inside effectful routing, which is why it took ten review rounds. - -- **Cut 2 (A)** — LANDED. The shared marker-query constructor is - `gunbc.review_sheet_drive_converge` `review_sheet_marker_query`, one shape over a field - parameter; the Drive superseded-marker producer and the subject-bound operator-declaration - reader are `gunbc.review_sheet_legacy_extent_producer`; the decision over a standing is - `gunbc.review_sheet_identity_converge` `ensure_review_spreadsheet_on_identity`, which REPLACED - the observation-only `ensure_review_spreadsheet` rather than landing beside it. The production - caller is `gunbc.review_sheet_spreadsheet_actuator` `converge_review_spreadsheet`, reached from - the argv-free `converge_drive_half_cli`. Two of the kernel's three frontier rows retired by - execution; `LegacyFileSelection` remains. - **The behaviour change worth knowing before the next live run:** a current-marked file beside an - unsettled superseded extent now REFUSES instead of adopting, and the discharge is an operator - declaration at `review_sheet_legacy_declaration_path` carrying the subject in its content. -- **Cut 3 (C1)** — LANDED, as `gunbc.review_sheet_declared_id_admission`. `declared_id_candidacy` - reconciles a `LegacyFileSelection` against the intended subject, the standing and a contradicting - observation, in that order and before any request; `preflight_file_by_id` is a new - `drive.Files.GetFile` whose field mask names every required field of `DriveFile`, and - `admit_preflighted_file` binds the readback to the requested id rather than to the status. - `converge_review_spreadsheet` asks the admission first and reports - `SpreadsheetAdoptedByDeclaredId` for an admitted one. **The refusal that must not be widened:** a - selection beside an unresolved extent stays unresolved — the operator has said which file they - chose, not that no other lineage exists. The discharge is an `"absent"` declaration, which is a - statement about the world and has a kernel arm. - The operator declaration file now admits two `legacy_extent` values, `"absent"` and `"selected"`; - a `"selected"` one also carries `file_id`. Both carry all five subject members. -- **Cut 4 (C2)** — `UpdateFileProperties` PATCH and the readback that asserts the patched file id - rather than a count. The field mask must name every required field of its declared response - type; `fields=id` against a `DriveFile` decodes short and reports a landed write as a transport - failure. -- **Cut 5 (E)** — legacy-generation retirement. - -**Three standing constraints for whoever resumes this.** The closed #11596's combined migration -route must not be executed. The operator's live sheet was repaired by hand, and the ordinary -steady-state adoption path was exercised live — two GETs, zero writes, existing sheet adopted. -And the marker must not be relocated again until the producer and admission cuts are resumed, -because a second relocation with no producer is how the first duplicate was minted. - -## Remaining: the census API budget - -The census needs a GitHub App installation token to lift its rate limit. This is **not** a -build — it is a reuse, settled across three lanes. - -- `#11677` already signs an App JWT: `extdeps.auth.jws` for the RFC 7515 signing input, - `extdeps.tools.openssl` as an enrolled host CLI, and `github.AppInstallationAccessTokens.Create` - for the exchange. The key is read from a file, the signing input enters on stdin, the signature - leaves on stdout; no key material reaches the evaluator or argv. -- **PRIMITIVE-EGRESS-0 admits that route conditionally.** It bans *ambient* primitives — registry - names and interpreter arms realising pure computation the `.dag` could own. It does not ban a - modeled external tool whose subject is genuinely external. The condition: the subject must be - the **key custody boundary**, a signer interface over a key reference with openssl as one - handler. Modeled as "RSA is realized by openssl" it fails, as an ambient primitive wearing a CLI. -- **Do not add an `rs256_sign` host primitive.** CRYPTO-0 is actively deleting host crypto - primitives; a new one would be a second authority landing against that program. RSA is not in - CRYPTO-0 wave 1 and a future modeled RSA is a different subject anyway — key inside the - evaluator — so a keystore-shaped interface survives it. -- **The shape, from #11677's owner:** a second composed performer in `gunbc.github_effect_perform`, - not a second module. Factor the `clock → claims → jws → openssl → Create` prefix into one - function returning a `sole_constructor` installation-token carrier. Each performer consumes the - carrier inside its own call. Never return a bare `Secret`, and never introduce a second - environment-variable token a PAT could silently satisfy. -- **The live control** should run with the PAT route unavailable, so a successful code-search page - is attributable only to the token producer rather than proving a token could be rendered. - -Not blocked. #11671, #11677 and #11679 are all merged, so the route this describes is available -to build against today. - -## Remaining: ntfy publisher token onto GCP custody - -`gunbc.auth.approval_ntfy_deployment` `approval_ntfy_publisher_token_intervention` is a -`HumanIntervention` — an operator creates the ntfy user and records its token where the unit can -read it. The read side is already modeled. Delivery is not. - -The design is agreed across three lanes and is **a consolidation of #11679 after it lands**, not a -sibling module: move it to a custody authority and make the controller key its first caller row. - -- One fleet-converge mode, with the credential a dispatch choice over a **closed roster of custody - rows**. One mode per credential grows a hand-enumerated roster — a known stall. A free-text - credential input would be a stringly selector. -- Each row: `SecretRef`, path, owner principal, group principal, and a **mode arm**. `root:root - 0400` becomes the controller key's row rather than a constant. -- **The directory's owner/group/mode must be a row field too.** A `0640` token for a service group - needs a directory that group can traverse (`0750 root:`); today's check refuses any group - bit. -- Mode stays a **closed arm**, never a caller octal. Derive the exact-mode leg and the - forbidden-bits leg from the arm (`077` for 0400, `037` for 0640); the owner leg takes - `-user`/`-group` from the row. - -**A caveat that must not be lost.** A find-metadata readback establishes which principals *hold -permission bits*. It does **not** establish that a non-writer cannot open the store — that needs an -execution-as-another-user leg. `gunbc.rung_drop.approval_store_single_writer_by_agreement`'s -restoration trigger requires the stronger observation, and its row has since been amended to name a -permission-bits readback among the things that do *not* retire it. The custody work is -necessary-but-not-sufficient for that trigger. - -Not blocked. #11679 is merged, so the custody authority this consolidates into exists today. - -**Landed as a consolidation** into `gunbc.host_credential_custody_converge` (the #11679 module, -renamed because it no longer holds one key): fleet-converge mode `host_credential_custody_converge` -with a `credential` choice over `host_custody_credential_roster` — `ControllerAppKey` first, then -`ApprovalNtfyPublisherToken`. The delivery HumanIntervention is deleted; what stays human is the -**mint** (`approval_ntfy_publisher_token_mint_intervention`): creating the ntfy user and adding its -token to Secret Manager. Two operator acts remain before the first live receipt for the ntfy row: -the mint, and ensuring `gunbc.auth.fleet_secret_accessor_roster` -`approval_ntfy_publisher_token_accessor_row`. - -## Small, unblocked - -Seven unused imports across `gunbc.ci_workflow_scan_producer` (`CodeSearchPage`, -`code_search_hits_read`, `GithubObservationRequest`, `ObservationBudgetStanding`) and its witness -(`CodeSearchPageComplete`, `CodeSearchPageTruncated`, `ObservationCompleteness`). All predate -#11656 and were deliberately left out of it to avoid widening a head-pinned repair. One small PR. - -## The census rows are historical prototype output, not an authority - -`gunbc-private:sheets-census` at `94fa118` holds `strategy.ci_prospect_census` — 57 rows carrying a -17-column set matching the formatted sheet's schema, transcribed rather than derived. - -**Label them: historical prototype output; NOT current runner-occupancy, spend, or opportunity -authority.** The economic readings attached to those rows were shown not to have the meanings -assigned to them, and the specific errors are worth carrying because each is easy to repeat: - -- workflow **wall duration is not summed runner occupancy** — a run lasting an hour may occupy one - runner for a minute, or twenty runners concurrently; -- workflow-run **admission delay is not runner queue delay** — they are different waits with - different causes; -- a **provider declaration can outlive actual provider execution**, so a declared runner label does - not establish that provider ran the job; -- **provider adoption does not establish positive spend** — a self-hosted or free-tier runner is - adoption with no bill. - -So the derived totals that prototype displayed — a runner-minutes-per-day figure and an ARM-tier -economic projection — do not follow from what was observed, and the "five carry `CostOpportunity`" -classification must not be quoted as a finding. Treat the 57 rows as a shape demonstration. - -**The bounded scan producer is necessary and NOT sufficient.** It yields a declared candidate -population; it cannot reproduce these rows or establish any economic reading, because the facts -those readings need are job-level and the scan is workflow-level. The minimal rebuilding route is -job-level observation: - -``` -jobs?filter=all - → all pages and attempts - → actual runner / provider identity - → job occupancy - → pre-start observation - → cancelled occupancy - → provider execution standing - → private commercial projection - → operator-safe sheet rows -``` - -That arc is carried as its own roadmap item rather than left in this prose. diff --git a/docs/plans/route-gap-dormant-observation.md b/docs/plans/route-gap-dormant-observation.md deleted file mode 100644 index 9ce1fe57b84..00000000000 --- a/docs/plans/route-gap-dormant-observation.md +++ /dev/null @@ -1,199 +0,0 @@ -# Where the route-gap population drops out of the required run — finding, then proposed repair - -Lane: calm-koi-257 (claim-execution-route). Status: finding complete; repair BUILT and under review on -PR #13376 (coordinator ack covered all four conditions; reviews 38602/76419/76431 addressed). - -## The population and the run - -`v2.workflow.floor_route_gap.floor_route_gap_roster` (authority: `src/v2/workflow/floor_route_gap.dag`) -enrolls identities the floor executes and that reach a host effect the hermetic route has no arm for -(`NoMockResponse` 420, `FilesystemRemoval` 10, `UnpublishedMockCase` 8 among the 425 typed rows; 100 more -as bare identity strings in chunks 00–05). 525 distinct identities across 146 modules; every one currently -resolves against the tree (verified per identity: module present, `test fn` tail present — zero stale rows -on current main; an earlier count of 2 stale rows was an artifact of my worktree lagging main and was -withdrawn). - -On required run 37236808750 (merge_group, pr-13305, floor job 111537440982): - - [floor-cost-debt] floor_route_gap: 5 enrolled identity(ies) suppressed (cost-debt roster withholds them) - [floor-required-gate] floor_route_gap: 536 enrolled identity(ies) suppressed (module outside the required gate, never loaded) - [floor-route-gap] roster carries 5 enrolled identity(ies) - [floor-route-gap] 5 enrolled identity(ies) held as route-gapped; 0 unenrolled route gap(s) reported - -Arithmetic: 546 decoded = 541 route-gap-roster rows + 5 gate-inside rows enrolled in the floor's -grandfathered roster (`v2.workflow.floor_grandfathered_roster`). The route-gap source on the run's merge -commit carried 421 typed rows + 120 legacy strings (= 541; current main carries 425 + 120); the 5 carried -rows — `parse_test.parse_witness_floor_holds`, `parse_test.parse_witness_perf_holds`, -`namespace_import_closure_witness.namespace_import_closure_receipt_holds`, -`namespace_structural_root_exposure_generated_witness_test.namespace_structural_root_exposure_generated_witness_holds`, -`v1_dag_parse_witness.v1_dag_parse_witnesses` — are not in the route-gap chunks at all; the floor's -grandfathered roster declares them as gate-inside enrollments, and the runner decodes both sources into -one 546-row roster before the gate split. - -## The chain, and the three drop points - -For an enrolled identity the required run promises (route-gap .dag header): execute → gap → held; or execute -→ pass → stale-row red; or enrolled → did not execute → red. What actually happens for 541 of 546: - -1. **Discovery/import** — preparation ranges over the gate closure. The gate (`required_gate_prefixes`, 11 - prefixes + seed modules, cut 2026-08-29, rung drop "Required gate reduced to the compiler floor") admits - only 5 of the 146 modules carrying enrollments. 536 rows sit in modules never loaded: no plan, no - execution, no receipt. *Declared* — the bankruptcy names the population per run in - `required_floor_disposition.tsv` (`declined_outside_gate_closure=26844`, `declined_outside_required_gate=696` - on this run; summary line `declared=28274 offered=1430 routed=704`). -2. **Selection/admission** — the only mechanism asserting "enrolled ⇒ executed" is the route-gap join - (`required_floor_runner.rs`). `suppress_withheld` removes the 541 from the roster *before* the join, so the - join's universe is the 5 gate-inside rows. The suppressed list is produced **per identity with grounds** — - and then discarded: route-gap publishes only the two aggregate count lines above. Expected-red got the - per-identity treatment on 2026-09-01 (`v1_compiler_expected_red_roster_join.rs`: "THE DENOMINATOR IS THE - ENROLLED ROSTER, NOT THE SURVIVORS", suppressed rows recorded with their ground); route-gap never did. -3. **Decision** — the run greens with "5 held; 0 unenrolled gaps". 541 enrollments are in no ledger the run - publishes: the claim-cost TSV carries only executed identities (704 rows, 5 `host_effect_refused` — the - carried ones: `test.claim.parse_test.parse_witness_floor_holds`, `...parse_witness_perf_holds`, - `test.claim.namespace_import_closure_witness.namespace_import_closure_receipt_holds`, - `test.claim.namespace_structural_root_exposure_generated_witness_test.namespace_structural_root_exposure_generated_witness_holds`, - `test.claim.v1_dag_parse_witness.v1_dag_parse_witnesses`); the measurement receipt carries only - `standing` + `blockers`; the disposition/join TSVs are written by the floor job but **not uploaded**. - -## The (a)/(b)/(c) split (sampled, identity grain) - -- **(a) Deliberately outside the supported guarantee** — the wet/service families: machine intake - (`test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.*`, 44 rows), fabric (31), spark - serving (74), runner (13), codex supervised turn (21), workspace storage — claims whose execution needs - service fabric the hermetic required run does not attach by design. Their route-gap rows were enrolled from - the pre-bankruptcy era; the bankruptcy (declared 2026-08-29) is the standing disposition for the family, - and `DeclinedNoCiWetLane`/`DeclinedOutsideRequiredGate` type the planning-level fact per identity. For - these, a per-identity suppressed record on a required run is **measurement, not closure** — closure for - them is the gate/wet-lane decision already declared in the rung drop. -- **(b) Meant to support, unobserved** — the hermetic-adjacent families whose route gap is exactly the - missing mock arm the register exists to demand: filesystem/store mock arms (`artifact_store_fs_witness`, - `durable_exclusive_hold_file_store`, `materialization_store_local`, `effect_plan_bash`: operations Dir 127, - DirWithTemplate 130, DigestStdin 42, Run 46, Check 26) and the withdrawn `v2.test.execution.emit_on_demand_*` - family (probed 2026-10-01, amendment names the restoration trigger: "the restoration trigger above stands - whole"). The register's demand semantics — "a row leaves when a route is supplied" — require observation; - with no observing run, no arm gets built and the trigger cannot fire. The suppression comment's promise - ("observable again ... in the whole-corpus receipts run") is **a promise with no executor**: no workflow in - `.github/workflows/` (8 files, none scheduled for a corpus pass) runs it, and the rung-drop authority - itself rules the escape hatch out ("a receipt-only run outside the required path does NOT retire the row"). - So for (b) the absence of that run is not merely a gap — it is the defect: the demand register's only - claimed observation point does not exist. -- **(c) Defect (structural, live today at zero count)** — the class "enrollment the tree no longer declares". - The mechanism cannot see it: suppression counts it with the (a)/(b) rows, no arm refuses it, and the - roster header itself warns "an enrollment nothing observes is a row that can never ask to be removed". - Zero live rows today (audited); the class is one rename away, and nothing would notice. Also (c): the - route-gap contract's four-arm sentence still claims whole-roster observability and was never amended when - gate-bounded suppression landed (2026-08-29) — two authorities now disagree silently. - -## Proposed repair (one partition in the existing admission path) - -In `suppress_withheld` + the route-gap join in `required_floor_runner.rs`, partition the roster at identity -grain — the cost-debt partition precedent (gunbc#9684) and the expected-red join-report precedent -(2026-09-01), no new system, no new CI job: - -1. **Declared + gate-inside** → observed today, join as now (held / stale-red). -2. **Declared + outside-gate (or cost-debt withheld)** → typed, located, per-identity suppressed row carried - into the outcome (like the expected-red join report) and named per identity on the existing stderr - channel (pattern: the cost-debt "kept as record" line), each with its ground - (`OutsideRequiredGate` / `CostDebtWithheld`). Label: for (a) this is measurement; for (b) it is the - explicit disposition that replaces the green absence. -3. **Undeclared** (module absent from the discovery roots / identity tail absent) → **typed refusal**, red, - naming identity and roster. Fires on zero rows today; keeps it that way. -4. **Text amendments**: the route-gap header's four-arm sentence and the suppression comment's - "whole-corpus receipts run" promise are amended to state the real standing (dormant until the gate - widens; no receipts run exists and none retires rows). - -Acceptance path: author a fresh claim in an out-of-gate module, enroll it in `floor_route_gap`, and the -required run records it per identity with ground `OutsideRequiredGate` — or, if misnamed, refuses. Heavy -runs remote/CI only. - -## Repair as approved and built (coordinator ack, conditions + single-implementation round) - -The partition decision is now **modeled on the .dag authority** — -`v2.workflow.floor_route_gap.floor_route_gap_admission_partition` is the single implementation of the -route-gap admission decision, membership judgment included; the Rust call site marshals the run's real -values (suppressed identities with their declared ground arms, and the discovery walk's declared-identity -index WHOLE as a keyed map) into it through `run_in_context_with_args`, and the relation performs the -membership judgment itself with `map_contains_key` per row — the runner does no membership test, so -refuse-if-undeclared is decided on the authority. An earlier Rust classifier -(`route_gap_suppressed_undeclared`) was **deleted** rather than kept beside the call — one implementation, -a net reduction of seed decision surface; the seed receipt counts **+4 hand items**, all in the runner: the -shared entry name, the suppressed-row marshal, the declared-index marshal, and the shared production -decode+enforce (`route_gap_admission_decode_and_enforce`) — extracted from `run_required_floor` so the -pairing test drives the same function the run site executes, and the misnamed fixture produces the actual -`REQUIRED-FLOOR REFUSAL cause=RouteGapEnrollmentUndeclared`, not just the relation's refusal arm. The declared index arrives as a keyed map so the judgment -stays in .dag at one O(1) lookup per suppressed row: an earlier shape flattened the declared index for -the .dag to linearly re-scan per suppressed row (541 x 28,274 ≈ 15M interpreted string comparisons per -required run) — a cost-shape defect, fixed per DESIGN §6 rather than retired on "n is small here"; a -per-row Boolean read in Rust was the intermediate shape, retired because a Rust-side `contains_key` IS -the refuse-if-undeclared judgment. What would move even the marshal into the .dag is the self-emitted -claim executor owning the discovery walk and feeding the relation directly; deliberately not built. The -judgment could not have been expressed in .dag on its own earlier because the declared universe is -discovery-walk knowledge (the roster decodes in a hermetic frame whose subject is the gate closure); -modeling the RELATION and marshaling the index whole as a keyed map is the resolution of that. - -1. **The wall (closure)**: a refused row names an enrollment the tree does not declare (module or tail - absent from the discovery roots; the disposition index covers every declared witness identity, - gunbc#9684) and the run refuses with `cause=RouteGapEnrollmentUndeclared`, naming the identities and - the modeled entry that decided. -2. **Single authority**: the route-gap .dag header contract states the gate-bounded amendment (the two - further arms) beside the original four; the `suppress_withheld` stderr line no longer promises a - whole-corpus receipts run — it states the true standing; and the membership test has exactly one - implementation, on the authority. -3. **Measurement (labeled)**: every suppressed identity is named with the ground the modeled partition - returned for it, headed MEASUREMENT — for the (a) families this records declared dormancy and closes - nothing. The ground itself is a **declared coproduct** (round 3, per review 76626): `ground` carries - `v2.workflow.floor_route_gap`'s `FloorRouteGapSuppressionGround` (`OutsideRequiredGate | - WithheldCostDebt | DeclinedNoCiWetLane`, spelled arm-for-arm after the floor's suppression enum as - the seed realizes it — `v1.expected_red_roster_join`'s suppression ground, generated into the runner - as a Rust enum) instead of a free label; the marshal decodes the enum into the declared arm and the - run site refuses an arm the enum does not declare, so a fabricated ground can never pass as a label. - The arms cannot be shared by import because the eval universe's discovery roots are `dag` + `src/v2` - (no `src/v1`), so name-alignment is what keeps the two spellings one vocabulary. -4. **Not closed here — class (b)**: unchanged from the section above; the named follow-ups are gate - admission of those modules, or a dispatch instrument row running a probe roster with the join armed. - -**Evidence, pinned on both sides of the boundary:** -- **Floor-side route witness** — `test.claim.route_gap_partition_witness`, gate-admitted at exact module - grain in `required_gate_authored_modules` (the same mechanism and operator ruling as - `test.claim.discovery_census_witness` and `test.claim.seed_growth_admission_witness`): drives the - modeled relation over the shared fixture `src/v2/test/fixture/route_gap_admission_partition.dag` and - asserts both arms on the floor — the route, executed by a required run. Substrate inputs only - (constructed lists; no host effect, no service). -- **Seed-side pairing witness** — `route_gap_admission_partition_tests` in `required_floor_runner.rs` - drives the SAME entry by the SAME constant through `run_in_context_with_args` (the real call path the - run site uses) and asserts both arms at identity grain; a second test pins the marshal shape (identity - and declared ground arm, nothing else). Runs via the required `rust-unit-tests` lane (see the wall's - standing below — `DESIGN.md` "Building & checks"; the lane returned required on 2026-09-30). -- **One implementation**: with the Rust classifier deleted, the run site's route to the answer is the - modeled entry by construction; a run-path refusal names the entry that decided it. - -**The wall's real standing (corrected per review 76431 — an earlier revision of this paragraph claimed a -shipped fixture; the fixture is gone, per review 38602, and this is what actually holds):** - -- **Green by execution on the required path:** the partition IS the run's own admission step — on every - required run it executes over the real roster, names every suppressed enrollment per identity with its - ground (the `[floor-route-gap]` suppressed lines), and the reverse join decides over the observable - remainder. The consumer is the fold itself; nothing about the green arm is test-only. A freshly authored - claim shaped like the dropped population (declared, out-of-gate, enrolled) gets the same typed, located - disposition on that run as the 536 do. -- **Discriminating red: authored at unit grain AND enrolled on a required lane.** The misnamed-enrollment - control runs both ways this PR ships: - - **On the floor (REQUIRED, blocks merge):** the claim-home row in `v2.workflow.required_floor` - `required_gate_authored_modules` gate-admits `test.claim.route_gap_partition_witness`, whose third test - (`dag/test/claim/route_gap_partition_witness_test.dag`) drives the modeled partition over the shared - fixture and asserts the refused arm at identity grain — the wall's red executes on the `witnesses` - lane, which is a required merge gate, with substrate inputs only (constructed rows and the fixture's - keyed declared map; nothing shipped on the production roster, per review 38602's rejection). - - **At unit grain (REQUIRED):** the pairing witness in `route_gap_admission_partition_tests` drives - `run_in_context_with_args` — the real call path the run site takes — over the same fixture rows and - asserts both arms; it runs via `cargo test --release -p v1-compiler --lib`, which per `DESIGN.md` - "Building & checks" runs as the `rust_unit_tests` step of the required `rust-unit-tests` job, so a - unit-test red blocks a merge. (The doc previously cited a stale generated projection, - `docs/onboarding.md` line 175, saying the lane ran on no required step; the authority is `DESIGN.md`, - and `DESIGN.md` records the lane's 2026-09-30 return and the retirement of the - `rust_unit_tests_off_the_merge_path` drop.) - The standing caveat that remains is narrower: a *live roster* misname — a misnamed enrollment shipped - onto main's production roster — is not how the red is authored (rejected by review 38602: it would red - main forever and fabricate a gap the §5 oracle forbids); the red is authored from fixtures the claim - supplies. The (b) follow-up above — a dispatch instrument row that runs a probe roster with the join - armed — remains the path to authoring that red over the real production roster rather than a fixture. diff --git a/docs/plans/runner-service-capacity-convergence.md b/docs/plans/runner-service-capacity-convergence.md deleted file mode 100644 index 2bc7701cd9d..00000000000 --- a/docs/plans/runner-service-capacity-convergence.md +++ /dev/null @@ -1,213 +0,0 @@ -# Runner service capacity convergence - -**Status:** design, not implemented. Written 2026-08-25 from a live incident on srv1/srv3. - -## Why this exists - -Fleet convergence reported hosts converged while they served no traffic. srv1 held **50 slot -directories and 5 running services**; srv3 went from 8 directories to 21 while its running count -stayed at 5. Nothing in the pipeline could notice, because the convergence object is filesystem -membership and the subject that matters is **service capacity**. - -Filesystem convergence is a legitimate *subordinate* family; the defect is that it was read as -implying serving capacity. - -## The incident chain, in order, each blocker masking the next - -``` -1. ghrunner held no sudo grants -> apply could not reach installation -2. grants installed -> 13-16 runner trees could be materialized -3. install path does not activate -> directories existed, capacity unchanged -4. units enabled/started by hand -> pinned runner v2.334.0 reached GitHub -5. GitHub refused fresh 2.334.0 -> systemd restarted unusable processes forever -6. release replaced with v2.336.0 -> serving capacity finally rose -``` - -The privilege work was necessary and could never be sufficient: it proved the host could -execute the installer, never that the provider would accept what was installed. - -## Three defects, all still open - -### 1. Membership is not incarnation-sensitive - -`runner_slot_member_value_eq` compares only `slot_dir`. A 2.334.0 tree and a 2.336.0 tree at the same -path are the same member, so a release bump repairs nothing that already exists. - -**The receipt:** 29 slot directories were deleted by hand across srv1 and srv3 to force -re-provisioning — replacement could only be induced by turning an existing member into an absence. - -`LocalRunnerSlotIncarnation` carries slot, unit, unit_active and registration_name -- not the -installed artifact -- so it cannot express *same slot, same unit, same registration, different -release*, which is exactly the state that hurt. - -### 2. The install path cannot activate — INTENT LANDED, NOTHING ACTIVATES YET - -The emitted `apply.sh` carried `daemon-reload` and `disable`. Enable count 0, start count 0. - -The two are separate transitions that must not be re-fused: `enable` is durable boot intent, -`start` is immediate activity. Both consume the activation admission, because enabling authorizes a -future reboot to start the broker under readiness conditions that may no longer hold. - -**What landed.** `gunbc.runner_service_activation` models the two transitions, admits each one -separately against `admit_runner_activation`, and derives the emitted lines from -`gunbc.executor_privileged_operation`'s new `SystemdEnableUnit` / `SystemdStartUnit` — so the -sudoers grant and the executed command are one argv, and the fused `enable --now` the hand loop ran -has no grant on any executor host. `fleet_converge_plan` carries an activation family beside the -slot family, with its own line in `plan.txt` and its own refusal count. - -**What did NOT change — read this before quoting a green apply as converged capacity.** The -admission is unreachable in production; two blockers sit **in series**: no host observation -transaction exists, so every production caller supplies `ActivationReadinessUnobserved` and the -admission is never asked; behind that, `admit_runner_activation` requires a `HostCompilePoolReady` -whose producer answers `PoolNotDeclaredInTopology` fleet-wide under `CompilePoolInRunnerSlots`. -Flipping the placement changes nothing observable while the first stands — the pool receipt is a -field *inside* a readiness value that is never `Observed`. So both transitions refuse on every host -today, `apply.sh` prints a typed located refusal for each, and serving capacity does not rise from -apply alone. - -That state is a typed `GuaranteeStall` row — `runner_activation_reachable_green_stall` — not -prose, per §4b: a gate whose only reachable state is refusal must say so, or a reader cannot tell a -permanent structural refusal from a transient one. The row names the observation transaction as -the immediate blocker and the placement behind it, in that order, because naming only the second -yields a plan that does fleet-wide topology work and measures no change. - -**Unreachable in production is not inert.** §4b judges reachability against what a *fixture* may -author, and `test.claim.runner_service_activation` authors `ActivationReadinessObserved` and drives -the admitted enable/start path with it — so the emission has an authorable, exercised RED at the -fixture boundary. - -**What the item bought,** without inflation: an invisible hand loop became a named, -counted, located refusal. `enable count 0` was indistinguishable from a host with no activation -work; `activation-refusals=2` with its cause is not. - -**The host boundary is mitigated, not structural, and the safety row says so.** The grants install -the raw `systemctl enable ` / `systemctl start ` verbs for `ghrunner`, so the grantee can -run them directly, outside the admission. Three claims must be kept apart: *enable and start -are separately authorized* is structural (no derived sudoers line matches `enable --now`); *this -emitter cannot activate without readiness* is structural inside the modeled call graph; *the host -cannot activate without readiness* is **false**. `runner_activation_safety_guarantee` records -`Mitigatable` with the next-rung trigger — the job user stops holding the verbs, via a root-owned -helper that validates a receipt or a root-owned convergence service. A narrower sudoers match cannot -climb it: any grant that lets the sanctioned path run the command lets the grantee run it too. - -**Next.** The host observation transaction — the shared blocker named below — and only then the -placement flip, with a width decision re-derived per host from live `nproc`/`MemTotal` rather than -from `gunbc_jobserver_token_override_note`, which is stale for srv1. Until both land, capacity -increases stay manual, and the refusal in `apply.sh` says so. - -### 3. Provider admissibility is unmodeled - -Three different guarantees were collapsed into one pin: - -``` -ArtifactIntegrity these are the exact bytes and digest we chose HELD -ArtifactAvailability the vendor still publishes those bytes HELD -ProviderAdmissibility the vendor's control plane still permits them MISSING -``` - -An exact digest proved we faithfully installed the wrong operational version. - -`Listening for Jobs` is **not** acceptance -- the rejection arrives after that line. - -## Serving vs restartable capacity - -The distinction the incident exposed, worth carrying as a number rather than prose: - -``` -serving_count members providing capacity now -restartable_count members whose fresh incarnation the provider still admits -``` - -Before the replacement srv3 was `ServingButNotRestartable`: five runners on pre-deprecation -registrations, alive only until something restarted them. A fleet that meets load only while nothing -restarts is not converged, and today no carrier can say so. - -## The layered object - -Independently observed families composing into one verdict -- not a single `runner healthy` Bool: - -``` -RunnerMaterializationStanding = Absent | CorrectIncarnation | WrongIncarnation | Unobserved -RunnerEnablementStanding = Enabled | Disabled | Unobserved -RunnerActivityStanding = Active | Inactive | Activating | Failed | Unobserved -RunnerProviderStanding = ExistingSessionServing | FreshMessageChannelAccepted - | FreshJobAccepted | RejectedDeprecated | Unobserved -``` - -The host verdict is over the **exact desired GitHub identities**, never raw committed width -- srv3 is -precisely where those differ, and a fabric identity must never inhabit the GitHub service family. - -## The shared blocker: there is no host observation transaction - -Both remaining repairs wait on the same missing seam. `fleet_converge_plan` observes directory -membership and nothing else -- zero matches for `BuildCacheInstanceReady`, `HostCompilePoolReady` or -`admit_runner_activation`. It cannot answer: - -``` -which release is installed in this slot? -is the unit enabled? active? failed? -does this exact cache / compile-pool receipt exist? -does GitHub accept a fresh incarnation of this release? -``` - -The repair is a real observation transaction feeding several reconcilers -- **not** manufacturing the -receipts in the planner, and not an activation action that permanently refuses. - -**Every sub-observation needs its own unavailable arm.** One failed `systemctl` read must not erase a -successful installed-version observation, and one unavailable version read must not become -`WrongIncarnation`. - -## The compatibility probe, and why it need not run a job - -Two questions hide inside "will GitHub hand work to it": - -1. will the broker accept this fresh runner version on its message channel? -2. will the scheduler match and deliver an actual job? - -**Only the deprecation class is question 1**, and a no-job probe settles it. The runner opens a broker -session then polls a message endpoint, sending `runnerVersion` among other fields; the broker maps -`RunnerVersionTooOld` to exactly the access-denied failure observed on srv3. So a successful -message-poll from a *fresh incarnation* is the discriminator. A canary job is only needed for -question 2, which is not what deprecated us. - -Scope it per `release x architecture x scope` with a bounded validity age -- not per slot. - -## Do not make "latest" the authority - -That trades reproducibility for churn and is wrong during a progressive rollout or a bad release. -Policy: a reviewed pinned candidate plus a compatibility receipt plus a declared maximum -observation age. A new upstream release creates an *obligation* -- test, review the pin change, plan a -rolling replacement -- never an automatic fleet mutation. - -## Rolling replacement, not restart - -When the desired artifact changes, per slot: stage the correct artifact, drain the old broker, replace -the incarnation, start the new broker, prove fresh provider acceptance, then continue. - -**Do not restart the old runners first.** In this incident restart was exactly what converted latent -old capacity into visible failure. - -## Sequencing - -``` -materialization plan/apply -> independent materialization readback -fresh cache/pool observations -> activation admission -> enable -> start -> service readback -provider compatibility readback -> serving/restartable capacity receipt -``` - -An installation plan minted before readiness must never later authorize starting a runner: activation -needs a fresh subject and fresh observations. - -## Landed vs open - -**Landed** (PR #9152): one argv grounds both the executed command and its sudoers grant; run-as -narrowed `ALL` -> `root`; wildcards eliminated; the bootstrap principal recorded as a typed row with a -witness (`ubuntu`, not the deploy row's `ssh_target`); release bumped to 2.336.0. - -**Built, not landed** (`session/warm-tern-755-runner-activation`): the three-axis service standing with -its 13-row truth table and mutation receipt. Split out for having no production consumer -- which is -this document's shared blocker. - -**Open:** incarnation-sensitive membership; the host observation transaction; provider admissibility; -serving/restartable counts; the compile-pool placement flip that gives the activation admission a -reachable green. diff --git a/docs/plans/scm-corpus-manifest-design.md b/docs/plans/scm-corpus-manifest-design.md deleted file mode 100644 index 226f147d99a..00000000000 --- a/docs/plans/scm-corpus-manifest-design.md +++ /dev/null @@ -1,256 +0,0 @@ -# SCM `add`/`commit` — a commit freezes an authored corpus; a program is a projection of it - -Subject of gunbc#9891 finding 6, settled with the designated SCM reviewer before implementation. -The governing sentence: - -> **A repository commit freezes an authored corpus manifest. A semantic program is a separately -> bound ingestion projection of that corpus, not an alternative kind of commit subject.** - -## 1. The gap - -`gunbc.scm.cli` exposes `scm_log`, `scm_status`, `scm_init` — no `add`, no `commit`. `cli.dag` -documents the consequence about itself: it passes `empty_proposal()` because "the entry does not -carry what is staged", "a REAL limitation rather than a placeholder". So the kernel has no producer, -which is the §5 specification-without-execution gap the SCM design note also names. - -## 2. Two corrections to this note's own earlier drafts - -**Ingestion is modeled, not absent.** An earlier draft asserted otherwise. It searched -`dag/gunbc/scm`, found nothing, and let an empty subtree speak for the repository — *a partial -observer returning the negative value of a total observer*, the class whose ledger was -`gunbc.namespace_wave_admission`'s at this writing (that module was deleted with the -wave-admission wall, operator ruling 2026-09-19). `src/v2/compiler` models `01_tokenize`, `02_parse`, -`03_ingest`, `00_compile`. - -**Most of the proposed vocabulary exists.** `v2.compiler.source_authority` declares -`SourceRef { path, source_root, content_hash }` and `SourceRootIngest`, with -`source_root_ingest_build_from_source_refs`. Minting a manifest of `path -> content identity` would -be a second name for it. Finding 6's *substance* stands; its proposed vocabulary does not. - -## 3. `semantic_root` does not belong in the manifest - -Finding 6 phrases the manifest as `path -> semantic_root -> authored_source_identity`. That is -wrong for a deeper reason than present-day constructibility: **`semantic_root` is a derived -ingestion result**, depending on the source object, corpus context and imports, the ingestion rule, -language and rule versions, and possibly source-root and target configuration. Putting it in the -manifest fuses an input with a realization result, and makes the manifest unauthorable for -malformed or not-yet-ingested source — precisely the corpora a source-control system must still be -able to freeze. - -The authored manifest is therefore only: - - canonical corpus path -> exact authored-source target - -and the semantic relation lives in a separate carrier, whose load-bearing properties are that it -names the manifest it is about, names the ingestion rule/version, keeps each per-file semantic -result bound to the exact source identity, and derives the global program root rather than -accepting one supplied beside the corpus: - - type CorpusIngestionProjection { corpus, ingestion_rule, program_root, files } - type FileIngestionProjection { path, source, semantic_root } - -This preserves the case that proves the split: a comment-only edit changes authored bytes, so -manifest A ≠ manifest B, while the semantic root is unchanged. - -## 4. Object-store membership and edge admissibility are separate axes - -The manifest joins `ScmObject` **without** becoming a legal `StoredEdge` target: - - type ScmObject = SemanticNodeObject(ObjectRecord) - | AuthoredSourceObject(AuthoredSourceRecord) - | CorpusManifestObject(CorpusManifestRecord) - -`StoredEdge.target` remains exactly `SemanticNodeTarget`, so #9891's distinction is untouched. Each -kind gets its own target and its own three-way find outcome, so a walk cannot read one kind's -absence as another's: the manifest walk follows only authored-source targets, a semantic node only -semantic targets, an authored source nothing. - -`CommitClosure` is **not** silently widened — its subject is a semantic root. Either a separate -corpus closure, or a parameterized mechanism whose exported outcomes still distinguish -semantic-node requirements from authored-source requirements. - -## 5. `commit` must not refuse for want of ingestion - -This corrects an earlier proposal in this note that `commit` be withheld until ingestion is green. - -Freezing what was authored is **fully answerable today**: paths are known, bytes are known, source -identities and the manifest identity derive from them, and the exact authored state is -reconstructible. The missing fact appears only when a *semantic* question is asked — reconstruct the -node, integrate role requirements, validate, produce semantic closure. Those answer -`SemanticProjectionUnavailable { corpus }` or an ingestion refusal carrying real diagnostics. - -Refusing `commit` would fuse **recording state** with **establishing semantic validity**, which the -SCM design keeps apart. An invalid corpus, or one this compiler cannot ingest, must still be -committable if `commit` is to remain distinct from certification. - -So the canonical record becomes approximately: - - type RepositoryCommit sole_constructor { - reference: RepositoryCommitRef - corpus: CorpusManifestObjectRef - message: String - ancestry: CommitAncestry - } - -Two repositories may differ in *projection availability* while agreeing entirely about the commit. -Disagreement about which manifest a commit names stays forbidden; two projections under one -`(manifest, ingestion rule)` key yielding different roots must refuse as an authority collision. - -Compatibility with the existing semantic-root format belongs in a **versioned decoder**, not in the -canonical writer — and absent a measured durable population, root-migrate rather than keep a -permanent legacy arm. - -## 6. A naming fork to close before both sides gain consumers - -`gunbc.scm.checkout` declares a bare `Commit`, which role-requirement integration returns; the -repository record is `RepositoryCommit`. They currently meet on a semantic root. Once repository -history is corpus-rooted they stop meaning the same thing, so the bare one becomes -`SemanticCommit` / `ProgramRoot` before both gain production consumers — the same §3 fork this -lane's previous PR closed for `merge`. - -## 7. Scope, stated no larger than it is - -`add`/`commit` makes load-bearing: the authored-source store, the manifest, repository history, -persistence, `log`, `status`, and the ancestry spine. - -It does **not** make role-requirement integration load-bearing. That operation returns the separate -semantic commit whose only content is a node root; it does not produce a `RepositoryCommit`. -Joining those two worlds still needs ingestion, and eventually semantic-to-authored emission. - -## 8. Sequence - -1. **Model**: the manifest object, its ref and target, the three-way find outcome, the corpus - closure, `RepositoryCommit.corpus`, the `Commit` rename, the versioned decoder. -2. **Verbs**: `add` (stage), `status` (report staged), `commit` (freeze the manifest). - -Pending state is not option B: a *committed* manifest must be reachable from the commit, while a -*pending* one is intentionally not — `RepositoryEnvelope.checked_out` is already local workspace -state rather than authoritative history. - -## 9. Where a manifest is wrong, and where it is merely present - -Adding a third arm to `ScmObject` forces every exhaustive match over it to answer for a manifest, -including the commit-closure census and the v3 JSON encoder. Two of the three available answers are -wrong for reasons worth recording, because each is a failure class this lane has already committed -once. - -**Refusing when a manifest exists anywhere in the store** makes "serialize the semantic closure -rooted at P" fail on unrelated repository inventory. A manifest is wrong **at a -`SemanticNodeTarget`**; it is not wrong **in an `ObjectStore`**. That is the same -membership-versus-admissibility split that keeps `StoredEdge.target` exactly `SemanticNodeTarget`, -applied one layer up — and collapsing it is the absorbing fallback of §5, refusing a superset -because the precise subject was not derived. - -**Bumping the closure format tag** was rejected here on the grounds that the wire schema was -unchanged — and that reasoning was superseded by what the cut turned out to require. The v4 bump -that landed is the OPPOSITE claim: the writer can no longer emit `{source}` and the reader refuses -one, so the language got **narrower**, and holding a tag while the language shrinks is the same -false claim as holding it while it grows. `gunbc-scm-commit-closure-v4`, `gunbc-scm-repository-v3` -and `scm-object-identity-v2` are what shipped. - -> **Superseded in part.** The repository tag is now `gunbc-scm-repository-v4`: gunbc#10560 moved -> `RepositoryCommit.root` from a semantic node to a corpus manifest, so the member spelled `root` -> designates a different population while keeping its name — which is precisely why the tag had to -> move. `gunbc-scm-repository-v3` is refused BY NAME as retired rather than folded into unrecognized, -> and there is deliberately no v3 reader. The other two tags are unchanged, and that is the point of -> versioning them on separate axes: the closure format has its own subject, and the identity rule did -> not move because hashing did not. Read the tags from `gunbc.scm.repository_envelope`, not from this -> sentence — a transcribed tag rots without anyone touching either end. - -The cut is neither of the two arms above. `commit_closure_json_v2` already declared the obligation -this change discharges: every record the store holds is serialized, which it states is correct -*while nothing produces unreachable objects*. `CorpusManifestObject` is exactly the event that makes -that false, so the deferral's own dissolution trigger has fired. - -So the admitted carrier stops carrying the raw store and carries the **root-reachable semantic -closure**. The design draft above proposed a two-armed `ClosureObject` with a -`ClosureAuthoredSource` arm; that arm was found to be **uninhabited** during implementation, because -no typed path reaches an authored source from a semantic root, and the reviewer's ruling changed to -nodes only. What shipped is: - -- `WellKindedClosure { root: SemanticNodeTarget, nodes: List }` — one kind, not a - coproduct, because there is nothing else to be reached; -- the admission walks from the root; at each `SemanticNodeTarget` a node is included and recursed - through, an authored source and a manifest are each their own named wrong-kind population; -- the encoder's parameter therefore cannot name a manifest at all: unconstructible rather than - validated — §4b rung 4 rather than a commented-over rung 3. - -Two consequences bind the implementation, and one more was found by review. First, **one derivation -authority**: both `admit_well_kinded_closure` and the partial route must consume the same -`derive_semantic_closure(store, root)` result. Second, `well_kinded_store` **loses its name**: -returning a node list under a store-shaped name would say the carrier still holds a store. - -Third, and this is the one the draft missed: `sole_constructor` seals the record LITERAL, and `.dag` -has no module privacy, so a public mint beside it re-opens everything. The module shipped -`well_kinded_image(closure)`, which took an unchecked closure, discarded the derivation's census and -returned encoder input anyway. **The mint is deleted**; the literal appears once, inside the -else-branch of the census that authorizes it, and the admitted arm carries the unresolved population -so nothing walks the closure twice to re-derive it. - -Unrelated authored sources leaving the document is a consequence, not a regression. "It happened to -be in the repository store" was never semantic reachability. - -### The witness bar for this cut - -1. **Unrelated manifest control** — semantic root, valid node closure, an unrelated manifest in the - store: admission succeeds and the manifest is absent from the admitted closure. -2. **Manifest-at-node RED** — a `SemanticNodeTarget` whose locator holds a `CorpusManifestObject`: - a named wrong-kind refusal, never `absent`. -3. **Unrelated source control** — an authored source not reachable from the root is absent from the - emitted population. -4. **Partial path parity** — the ordinary and partial routes derive the same population; mutating - either back to the raw store takes a manifest fixture red. - -## 10. What the corpus closure does *not* get yet - -`CorpusClosure` is **not** modelled. An earlier revision of this section said it was — naming its -complete/partial/wrong-kind outcomes, its identity behaviour and its witnesses as landed — and no -such carrier exists. What exists is `ManifestSourceCensus`, which answers the manifest-source -reachability question over a finished store in three populations (absent, node-occupied, -manifest-occupied) and is consumed by both the checked repository writer and the repository reader. -That is the minimum the write-safety question needed; it is not a closure carrier and does not -pretend to be one. - -It gets no JSON codec here either. A named codec earns an actual boundary, and nothing yet persists -or transports a corpus closure; building one would add a representation with no consumer immediately -before a lane whose stated purpose is to stop doing that. - -The deferral does not extend to the repository codec. The moment `add`/`commit` persists a -repository holding manifest objects, commits naming them, or pending corpus state, the repository -format necessarily advances and must encode manifests — and that lands atomically with the first -path that saves such a repository, not before and not after. If the repository envelope turns out to -be the only representation `add`/`commit` needs, a standalone corpus document should never be built -at all. - -## 11. Corrections this document owes to its own earlier revisions - -Recorded here rather than silently rewritten, because a plan that quietly agrees with whatever -shipped is not evidence of anything. - -- **The `SourceRef` duplication warning (§2) was about a different construction.** The manifest that - shipped holds `(path, AuthoredSourceTarget)` and mints no second source identity, so the warning - stands as a rule and does not describe this head. -- **The manifest was specified as an ordered list and is now a function.** Ordered hashing made a - host directory traversal order part of the manifest identity, and admitted one path naming two - sources. Entries canonicalize by path and a duplicate path refuses. -- **`ClosureAuthoredSource` was an uninhabited arm**, as above. -- **The witness bar in §9 is met, with its second item widened.** A manifest at a semantic - requirement is a named wrong-kind refusal in the closure, the checked writer and the reader — the - draft only anticipated the closure. -- **That last sentence overclaimed the reader for one revision, and this is where it did.** When it - was first written the reader adjudicated one subject: a MANIFEST ENTRY's authored source. A - SEMANTIC CHILD naming a manifest or a file by digest still reached `RepositoryDecoded`, so the - sentence was true of the writer and only half true of the reader. Both subjects are adjudicated - now, against the finished decoded table, and the discriminators for each occupying kind execute in - `test.claim.scm_repository_envelope_witness` — but the claim preceded its second half, which is - the failure this document is a ledger of. -- **A wrong-kind requirement reached two different standings depending on how its reference was - spelled.** `uncontained` classified as `LoadRequirementAtWrongKind`; the same predicament reached - through a contained position classified as `LoadDocumentMalformed`, which is the one standing that - permits the supersession step. The predicament decides the standing now, and the paired claims run - the decoder, the classifier and the permission table rather than constructing standings by hand. -- **A "the occupant arrives after the referrer" witness never presented that order.** It changed - store insertion order, but `emit_in_dependency_order` follows each reference through the - kind-agnostic `find_object` and emits the occupant first regardless, so both fixtures produced the - same bytes. The controlled version rewrites the encoded `objects` array and asserts the order took - effect before asserting the refusal. diff --git a/docs/plans/scm-demo-cli-rebuild.md b/docs/plans/scm-demo-cli-rebuild.md deleted file mode 100644 index a82ebb689d7..00000000000 --- a/docs/plans/scm-demo-cli-rebuild.md +++ /dev/null @@ -1,322 +0,0 @@ -# SCM demo CLI — rebuild plan and API inventory - -A working `gunbc scm` CLI existed and was lost with an uncommitted `/tmp` worktree. This doc is -the durable record so the rebuild does not start from a survey again. - -It is a PLAN AND A HISTORICAL LEDGER, and the two are separated by section. Everything above -`## LANDED` is the plan and the baseline survey, written BEFORE this work and describing main as -it stood then; it claims nothing is built. `## LANDED` onward is the ledger, and it carries -execution receipts for what has actually shipped. Read a present-tense statement in the plan half -as a statement about the BASELINE, not about the tree today. - -## What already exists on main (verified by reading the modules) - -The read side is modelled and already reaches `CliWireResponse`: - -| module | symbol | shape | -| --- | --- | --- | -| `gunbc.scm.read_command` | `ScmReadResult` | `ScmReadAnswered { path, answer }` \| `ScmReadRepositoryUnavailable { cause }` | -| `gunbc.scm.render` | `scm_log_cli_response(result, target, capability)` | `-> CliWireResponse` | -| `gunbc.scm.render` | `scm_status_cli_response(result, target, capability)` | `-> CliWireResponse` | -| `gunbc.scm.render` | `scm_read_exit(result)` | `-> ProcessExit` | -| `gunbc.scm.log` | `repository_log(envelope)` | `-> CommitLog` | -| `gunbc.scm.status` | `repository_status(envelope, pending)` | `-> RepositoryStatus` | -| `gunbc.scm.repository_load` | `load_repository(path)` | `-> RepositoryLoad` | -| `gunbc.scm.repository_load` | `repository_load_refusal_lines(cause)` | `-> List` (takes `cause` ONLY, not `path`) | -| `gunbc.scm.repository_save` | `save_repository(path, repo)` | `-> RepositorySave` | -| `gunbc.scm.repository_envelope` | `empty_repository()` | `-> RepositoryEnvelope` | -| `gunbc.scm.repository_envelope` | `mint_repository_commit(...)` | `-> RepositoryCommitMint` | -| `gunbc.scm.repository_envelope` | `commit_at_reference(commits, reference)` | `-> RepositoryCommit?` | -| `gunbc.scm.checkout` | `checkout_repository_at(repo, reference)` | `-> RepositoryCheckout` | -| `gunbc.scm.authoring` | `empty_proposal()`, `author_requirement(proposal, requirement)` | `-> Proposal` | -| `gunbc.scm.object_store` | `empty_store()`, `store_contains`, `find_object`, `store_object_count` | — | - -## The gap, stated precisely - -**This was the baseline gap, and this change closes it.** As of the survey above, -`scm_log_cli_response` and `scm_status_cli_response` had NO consumer outside -`dag/test/claim/scm/scm_render_witness_test.dag` — the unwired-renderer state -`gunbc.cli_dispatch_surface` recorded: the answer computed and discarded because no host bound it. -`gunbc.scm.cli` is that consumer, and the `LANDED` section below carries its receipts. - -`dag/gunbc/cli_dispatch_surface.dag` declares an `scm` verb with an operand and `--path`, marked -`AbsentFromEmitMainRs`. - -## Two host shapes, and why the idiomatic one is not sufficient - -The corpus's established instrument shape is `fn check(subject) -> ProcessExit`, returning -`exit_failure(reason: join(lines, "\n"))` — see `tools.dag_compile_clean_gate` and its siblings under `dag/gunbc/instruments/`. `gunbc run` prints -that reason. **It can only emit text on FAILURE.** `log` and `status` must print on SUCCESS, so -that shape cannot express them, and returning `CliWireResponse` from `gunbc run` refuses (its -not-`ProcessExit` refusal prints the value, which is not a CLI). - -So the host binding is REQUIRED, and it is the piece to rebuild: - -1. DONE for read verbs. Still to do for WRITE verbs (`init`, `add`, `commit`, `checkout`): - they need `Document` builders, which only log and status have today, and `save_repository` - goes through `Filesystem.Write`, so they are host-effect entries rather than pure reads. - Original note: `dag/gunbc/scm/cli.dag` — one entry per verb returning `CliWireResponse`, composing - `load_repository` -> verb -> render, with a `RenderCapability { color: false, tier: Ascii, - cursor_addressable: false }` for a plain terminal. Write verbs (`init`, `add`, `commit`, - `checkout`) need their own `Document` builders; only log/status have them today. -2. Host side. **NOT the shape that landed, and deliberately so — this bullet is a FUTURE - ERGONOMIC SURFACE, not unfinished work required by the binding that shipped.** The original - plan was a `Commands::Scm` variant in `src/v1/stage0/src/main.rs` plus a `scm_verb` evaluating - the entry and writing `CliWirePrintable { bytes, exit }` to stdout. What landed instead binds - `CliWireResponse` ONCE in the generic `run_verb` outcome seam, so EVERY wire-returning entry - becomes reachable rather than only the scm family — one binding instead of one per verb. A - dedicated `gunbc scm` subcommand would be a nicer surface over the same answer; it is not - needed for the answer to reach an operator, and it is not part of this change. - -## LANDED — the read side reaches an operator - -`dag/gunbc/scm/cli.dag` (`scm_log`, `scm_status`) plus the `run_verb` outcome-seam binding. -Receipts, by execution against `dag/test/fixture/scm_repository_load/empty_repository.json`: - - $ gunbc run --entry dag/gunbc/scm/cli.dag --function scm_log --arg path= - no commits yet - exit 0 - - $ gunbc run --entry dag/gunbc/scm/cli.dag --function scm_status --arg path= - nothing checked out - 0 commits - nothing staged - - $ gunbc run --entry dag/gunbc/scm/cli.dag --function scm_log --arg path=/tmp/no-such-repo - cannot read repository at /tmp/no-such-repo - No such file or directory (os error 2) - repository unavailable - exit 1 - -The third is the load-bearing one: bytes printed AND a nonzero exit, the case an absorbing -implementation turns into silence or a spurious 0. - -### `init` — LANDED separately, via the create-only write (#10026) - -**init was CUT from the log/status change and landed on its own, as gunbc#10026.** The receipts -below were taken against the ORIGINAL prototype and are kept as its historical record; the shape -that actually landed differs, and the differences are the point. - -Why it could not ride along: the prototype observed an absence and then called `save_repository`, -which writes UNCONDITIONALLY. A racing actor between the observation and the write makes it -truncate the very bytes it claims to refuse — a TOCTOU the four-arm fold does not close, because -the fold decides on a fact that can stop being true before the write happens. Closing it needed an -atomic create-only write (`O_CREAT|O_EXCL`) as a new modeled file-transport verb, which is why it -became its own change with its own review. - -What #10026 landed, and what review then found still open in it: `Filesystem.WriteCreateNew` makes -the existence test and the creation one syscall, and `create_repository` routes through it. Review -`5089156132` then found six further defects in the init MODEL — among them that `ScmInitialized` -could contain a FAILED save, and that the `FilesystemEstablishedAbsence` authorizing the write was -matched as `_` and discarded while the actuation used an independently supplied path. Those are -being repaired in the follow-up on the same branch; do not read this subsection as describing a -finished init. - -`scm_init` takes a DIRECTORY and a NAME rather than a path, because presence is a fact about a -directory listing and recovering the directory by splitting a string would be a second, positional -naming scheme for something the caller already knows. It routes through -`extdeps.filesystem.filesystem_io` `filesystem_file_observation`, so absence is established from a -listing that succeeded and did not name the entry — never from a failed read, whose success channel -cannot separate absent from unreadable. Only the established-absence arm reaches `save_repository`. - -Two earlier revisions were fail-open and both were caught by external review before merge: the first -called `save_repository` unconditionally and NAMED the overwrite in a comment; the second proceeded -on `RepositoryFileUnreadable`, which is a guess about the host's permission model — a file can be -unreadable and perfectly truncatable — and again named the residual instead of refusing it -(review 58060 on gunbc#9864). - -Receipts, one per arm, `gunbc run --entry dag/gunbc/scm/cli.dag --function scm_init --arg -directory= --arg name=repo.json`: - - fresh directory [file] list … / [file] read … / [file] write …/repo.json (166 bytes) - initialized repository at /tmp/scminit/fresh/repo.json - - repository present refusing to initialize /tmp/scminit/existing/repo.json - a repository is already there, and init would replace its whole history - - foreign file present refusing to initialize /tmp/scminit/foreign/repo.json - a file is already there that is not a repository, and init would destroy it - and the file read back afterwards still holds its original bytes - - directory unlistable refusing to initialize /tmp/scminit/nodir/repo.json - the path could not be observed, so nothing about it is established -- - the directory /tmp/scminit/nodir could not be listed, so the absence of - repo.json is not established -- Permission denied (os error 13) - -The fourth is the load-bearing one: an observation that could not be made refuses rather than -widening to "nothing is there", which is the absorbing fallback DESIGN §5 names. - -Those init claims were hermetically enrolled in `test.claim.scm.scm_cli_witness` at the time — no -init refusal rendering as a success exit, the three refusals rendering differently, the unobserved -arm carrying the host's cause through. **They left with init and are NOT in this PR's witness**; -they travel with `scm-init-create-only`. Verified: `scm_cli_witness` on this branch contains no -init claim at all. - -### Evidence boundary, stated rather than implied - -Enrolled and executing in CI (`rust-unit-tests`, `cargo test -p v1-compiler --lib`): the five -`cli_wire_outcome_tests` covering the total map — bytes with the response's own exit, a rendered -answer that still fails, a renderer refusal not reading as empty success, the non-wire -fall-through, and the inherited `ExitFailure { code: 0 }` refusal. - -NOT enrolled: the end-to-end runs above are a MANUAL receipt. An integration test that executes -the binary would live in `src/v1/tests/`, which `clippy --all-targets` compiles and no CI step -runs (DESIGN: no CI step executes test targets outside `--lib`). A `.dag` witness cannot stand in -either — `scm_log` reads a file, and the SCM witnesses are `SubstrateInputsOnly`, refusing at the -hermetic boundary. So the class is `mitigatable` on the e2e path and the next-rung trigger is a -CI step that runs an integration target, not another test file. - -## Next increment: `add` and `commit`, and the one design step they need - -Still settled, because it is a fact about the HOST rather than about any object model: - -- **A host WRITE is permitted from `gunbc run`** — established by the init prototype above, which - created a file, and since confirmed by #10026 landing the create-only write. It is not a claim - that init's model is finished. The remaining write verbs are a modeling question, not a - permissions one. - -### SUPERSEDED BY #9891 — the recipe below is historical reasoning, not the current model - -**This section previously listed the following as "settled by execution". #9891 replaced the -identity model underneath it, so the recipe is now WRONG at exactly the boundary add/commit needs, -and it is kept only as the reasoning that led to the replacement.** It survived on main because a -plan that says "settled" is read as an instruction; that is the failure this heading exists to -stop. - -The superseded recipe said: content goes in via `store_node(store, n: Node) -> StoreOutcome`; a -file is built as a synthetic semantic `Node` via `node_synthetic`; a runtime name becomes a -`Symbol` with `symbol_intern_lexeme`; and -`mint_repository_commit(repository, root: ObjectId, ...)` requires `store_contains(root)`, so `add` -precedes `commit` and `commit` re-derives an added object's `ObjectId` by rebuilding the same node. - -**What #9891 changed, and why the recipe cannot survive it:** - -- Authored source is its OWN object arm, not a semantic node wearing a node costume. A file has - somewhere to live that is not a synthetic `Node`, which is the whole point of that change. -- A semantic-node reference and an authored-source reference are DISTINCT, so "rebuild the same - node and re-derive its `ObjectId`" no longer identifies one thing. -- `mint_repository_commit` takes a `SemanticNodeTarget`, not a bare `ObjectId`. Handing it an - authored-source identity is not a runtime refusal to be checked for — it does not typecheck. - -**SUPERSEDED — this paragraph became the dead recipe it warned about, and is corrected in place.** -It said `add` and `commit` are blocked on building a `CorpusManifestObject` shaped -`path → semantic_root → authored_source_identity`. Both halves are now false: - -- **The object exists.** `gunbc.scm.object_store` declares `CorpusManifestObject`, - `CorpusManifestRecord`, and `CorpusManifestEntry`, and `gunbc.scm.object_table_json` codes them. -- **Its shape is `{ path, source }` — path to AUTHORED SOURCE, and deliberately not to a semantic - root.** `object_store` states why: a semantic root is a derived ingestion result, and fusing it - into the entry would make a manifest unauthorable for malformed or not-yet-ingested source, which - is precisely the corpus a source-control system must still freeze. #10522 §5 ruled the same - separation — the authored snapshot is authoritative, structural interpretation is a derived view. - -**The actual boundary** is a *pending authored-source snapshot* authority shared by three consumers: -`add` updates it, `status` reads it, and `commit` consumes it. It is workspace-scoped and must -survive between invocations; it need not live in the repository document. - -Two constraints that paragraph got right and one it got wrong. Right: immediate object-store -insertion is not by itself the `add` model, and `ScmWriteOutcome` is still the right shape for the -verb's result. Wrong: `add` is not blocked ON THE MANIFEST OBJECT, which is the specific dependency this -paragraph asserted and which no longer exists. It remains blocked on real work — host acquisition of -file bytes, path admission, a durable selection to persist into, and conditional update of that -selection — and `status.dag`'s `StagedRole` is role-grain, so it is not that place. A removed -dependency is not a finished verb. - -**`status` moves with this cut rather than being an unchanged downstream reader.** Today -`scm_status` passes `empty_proposal()` and renders "nothing staged". Once source staging exists, -leaving that path in place would report "nothing staged" while authored-source changes are staged — -an observation correct about its input and wrong about the subject the user asked about. Paths must -not be translated into `StagedRole` to keep the existing renderer. - -**Staging carries the same conditional-write obligation as publication.** One `add` can race -another, and a `commit` can race a newer stage. Updates are conditional on the observed stage; a -commit consumes the exact staged snapshot rather than re-reading working files; and clearing after -commit consumes only the stage generation actually committed, so newer work is never erased. Under -the no-rebase workflow a moved target is a named stale-base condition, never permission to apply an -old stage to a different base. `save_repository` does not supply this — it checks encoding and then -performs an unconditional `Filesystem.Write`. - -Two further distinctions the verbs owe: an established unchanged candidate is not the same as an -unavailable or unreadable stage; and staged-versus-unstaged reporting needs a separate working-tree -observation, because comparing the stage to its base says nothing about whether the working files -changed again. - -### The design step — SUPERSEDED as a recipe; one principle survives - -**The composition below is no longer the instruction for the next increment, and the sketch under it -is unsound. Both are kept visible rather than deleted, because the sketch is the more instructive -half.** - -It said `add` composes `store_node` with `save_repository`, and `commit` composes -`mint_repository_commit` with the same save, over one outcome coproduct: - - type ScmWriteOutcome - = ScmWriteRepositoryUnavailable { cause: RepositoryLoadRefusal } - | ScmWriteRefusedByStore { collision: StoreOutcome } - | ScmWriteRefusedByMint { refusal: RepositoryCommitMint } - | ScmWritePersisted { save: RepositorySave } - -**Why the sketch is wrong: the outer classification and the inner payload can disagree.** -`RepositorySave = RepositorySaved | RepositorySaveRefusedByCodec | RepositoryFileUnwritable | -RepositoryWriteByteCountUnrepresentable`, so `ScmWritePersisted { save: RepositoryFileUnwritable { -path: "repo.json", error: "permission denied" } }` is constructible — a renderer selecting on the -outer `Persisted` arm announces persistence while its own payload says the file was unwritable. The -inverse is equally expressible: `ScmWriteRefusedByMint` accepts the successful `RepositoryCommitMinted` -arm. This is a fabricated-plausible-output shape, which makes it validation-where-construction-was- -available rather than a modelling nicety. - -**The governing requirement instead:** - -> A success outcome carries established success evidence for that exact operation. A refusal carries -> an actual refusal cause. Where publication cannot be established, that uncertainty remains an -> explicit outcome rather than being resolved into either. - -**What survives** is only the original motivation: compose operation outcomes without positional -ambiguity, so a renderer never encodes "which one failed" by argument position. **What does not -survive** is this coproduct, and also the composition itself — `store_node` does not preserve -authored source as authored source, and `save_repository`'s write is unconditional, so neither -supplies the staging contract stated above. Narrowing the payload to a successful save would not fix -that: an ordinary save and a *conditional* stage update are different guarantees. - -The shared result type is left undesigned here deliberately. A write-verb outcome invented at a call -site is the anemic modelling this repository keeps paying for, and so is one certified in a plan -before its operations have contracts. - -### `add` has no staging authority — SUPERSEDED, and its conclusion was the wrong branch - -This section's PREMISE stands and its CONCLUSION is withdrawn. The premise: `repository_status` takes -`pending: Proposal` as a parameter because what is staged is not a fact the repository document -carries, so a literal stage-now-commit-later has nowhere to persist to. That remains true, and it is -why `status` moves as part of this cut. - -The conclusion — that `add` therefore writes an object into the store immediately, with no staging, -and that this is "the honest reading of `add` for this substrate" — is **withdrawn**. It picked the -branch that was cheapest to build rather than the one that answers the question, and immediate -insertion is not staging: it cannot say which paths are selected for the next commit, and it cannot -be revised or cleared. The other branch was the right one — a staging authority has to exist first — -and it is workspace-scoped, not necessarily a repository-document member, so the premise never -implied its absence. - -## Constraints learned the hard way - -- A runtime name becomes a `Symbol` via `symbol_intern_lexeme(lexeme: name)` - (`v2.std.compilers.lexing`). `name as Symbol` REFUSES; an earlier conclusion that runtime names - were inexpressible was wrong. -- `RepositoryCommitRef` has no `ordinal` field. Read it with `minted_id_ordinal(id: ref.identity)` - and construct via `RepositoryCommitRef { identity: MintedId { ordinal } }`. -- Annotations inside a declaration body are parse errors; hoist them above the declaration. - An indented `//` is also a parse error — annotations are module-item grain only. -- There is no two-commit merge in the model. `gunbc.scm.role_requirement_integration` - (formerly `gunbc.scm.merge`, renamed because one spelling was carrying two materially different - contracts) integrates authored role requirements into ONE target commit; its vocabulary lives in - `gunbc.scm.proposal`. The operator's ask for practising merges/conflict resolution needs - commit-merge DESIGN first; it is not a wiring job. - -## Not in scope here - -Mirroring this repository's history through `gunbc scm` onto srv2 stays blocked on model work: -`ScmObject = SemanticNodeObject | AuthoredSourceObject | CorpusManifestObject` with one content -identity — **which now exists**; the manifest carries `{ path, source }` to authored source, NOT -path + semantic_root + authored_source_identity as this sentence originally said (see the corrected -boundary above) — and git correspondence confined to a mirror-layer -`GitMirrorCursor { source_commit, native_commit }`. diff --git a/docs/plans/scm-envelope-read-path-bounded-migration.md b/docs/plans/scm-envelope-read-path-bounded-migration.md deleted file mode 100644 index f4838e64bd9..00000000000 --- a/docs/plans/scm-envelope-read-path-bounded-migration.md +++ /dev/null @@ -1,53 +0,0 @@ -# The envelope read path: bounded, and the one-time rebuild to v7 - -Status: declared 2026-09-23. Authority: the production incident — -`belt_integrate_one_cli` over the `shell-typed-invocation` project envelope -(`attempt-state/projects/shell-typed-invocation.gunbc-repository.json`, a 104 MB -`gunbc-scm-repository-v6` monolith) exceeded ninety minutes at ~100% CPU and was -killed while reading; a rehearsal probe of the same read ran past two hours -before it too was killed. - -## What was unbounded - -The monolith decode admitted the object table entry by entry through the -per-object store verbs. `find_object` recomputes a content identity for every -already-held object on every question, and the insert copies the object spine -per append — so decoding an n-object table pays the square of n in interpreted -identity compares. At the 7,051-object project envelope that is ~25M identity -computations and the same again in spine copies; the read never completed. The -2026-09-21 intake measurement pinned the same shape (~50M compares holding one -belt tick inside a single seed commit for over an hour), and the JSON unescaper -additionally paid the square of each large source token (a 15 KB source cost -225 MB of accumulator copies) until the same day's span fix. - -## The fix - -One admission rule, two carriers. `gunbc.scm.object_store` now carries a -fold-shaped `StoreAdmission` accumulator (digest-keyed map, prepend-and-reverse- -once) beside `insert_admission`; the object-table decoder admits through the -accumulator, so the table decode is O(n log n) with the same idempotence and the -same collision arms. Object construction for sources, derived nodes, and -manifests is split into constructors behind the sole_constructor wall, and the -single-insert verbs are re-derived on them — one construction authority, one -admission rule. The sharded (v7) read keeps its own two linear passes (per-shard -decode, then `store_assemble_decoded`), now on the same accumulator. - -## The declared migration - -The production envelope is converted to the v7 sharded layout by the next -successful integrate over it — the integrate's save is already sharded, so the -conversion is a side effect of the first bounded run, not a separate tool, and -no monolith writer remains to recreate it. After that run: - -- every load is the sharded read: the index plus ~110 shard reads at the current production - scale (7,051 objects at a batch of 64), each decoded and admitted in linear passes; -- the v6 monolith reader stays in the tree for old documents and fixtures, but - it is no longer on any production path, so its remaining costs are bounded by - history, not by the read path this declaration governs. - -Rollback: the conversion writes the v7 index and shard files beside the -monolith's path; the pre-conversion monolith is not deleted by the save (the -sharded writer writes `.object..json` siblings and replaces the -index document's contents), so the v6 bytes remain recoverable from backup -state until the envelope's next writes settle. The load path dispatches on the -`format` tag, so a restored v6 document reads exactly as today. diff --git a/docs/plans/scm-merge-design.md b/docs/plans/scm-merge-design.md deleted file mode 100644 index c3f004388f1..00000000000 --- a/docs/plans/scm-merge-design.md +++ /dev/null @@ -1,571 +0,0 @@ -# SCM merge: the design ruling, recorded before implementation - -This document settles what merge IS in `gunbc.scm` before any merge code exists. It is written -first because a merge and publication consumer, once cut, fixes the vocabulary every nearby question is -then answered in — and a vocabulary cut against an unproven correspondence contract is the §3 -replacement-migration trap. - -**An earlier revision of this document opened by saying §5 decides the shape of -`CorpusManifestObject`. That was false and is corrected in §5:** that object already exists in -`gunbc.scm.object_store`, its shape is already ruled there, and it is not gated by this document. The -pending boundary is a consumer / commit-root cut, which is materially smaller. - -Nothing here is implemented. Where a question is open, it is marked open rather than resolved by -plausible reasoning. - -## 0. The workflow being served, stated exactly - -The operator's goal is **not** "resemble git". Git terms are familiar vocabulary; the interface is a -projection over the existing realizations (`gunbc.scm_compatibility.git` / `.mercurial` / `.pijul`). -The workflow is: - -- **squash-merge into main**, so an integration is ONE commit; -- **kill dev history after the merge** — the branch line is discarded, deliberately; -- **never rebase**; -- optimize for *simpler and faster merges*. - -Every ruling below is derived from that, and several of them differ from what a general two-parent -merge would need. Where a general merge would want a capability this workflow never exercises, the -capability is refused as authored redundancy (§2). - -## 1. What already exists, and must be consumed rather than duplicated - -**`gunbc.scm.ancestry`** supplies `ancestry_walk(start, history) -> AncestryWalk` with four arms: -`AncestryTraced`, `AncestryBrokenAt`, `AncestryStartNotInHistory`, `AncestryRevisitsACommit`. That -module deleted its own boolean ancestor predicate on the ground that `false` was answering for four -different facts, and it names this document's subject as the intended consumer: - -> When a real consumer arrives — merge-base, ahead/behind, a publication decision — it derives its -> answer from `ancestry_walk` and decides for itself what a broken chain means to IT. - -**So merge inherits an obligation, not just a function.** It must decide, per arm, what that arm -means to a merge, and it may not collapse them into a Bool or into one refusal. §4 does that. - -**`gunbc.scm.supersession`** supplies `HeadSlot { slot: String, generation: ObjectId }`, -`observe_generation`, and `supersede_head_slot` — a compare-and-swap whose evidence is minted from -the slot and cannot be supplied alongside it. Its subject is the **repository document generation**, -not a branch head. It is therefore a *precedent for the shape* of a named, CAS-guarded slot and -**not a reusable authority for commit refs**. Saying otherwise would be a nickname (§3). - -**`gunbc.scm.checkout`** supplies `checkout(store, commit) -> CheckoutOutcome`, already modeled with -its refusal population intact. - -## 2. RULING: no two-parent ancestry arm. A squash absorption is a receipt, not a parent. - -`CommitAncestry = RootCommit | DescendsFrom { parent }` is **already the correct shape** and must not -gain a `MergedFrom { left, right }` arm. - -The derivation is mechanical rather than a preference. A squash merge produces one commit whose -parent is the target's tip; the absorbed line is discarded by construction. So no commit this -workflow can produce has two parents, and an arm with no producer is what §4b names *worse than -absent* — it gets cited as a capability the model does not have. - -The stronger reason is that a second parent would be **actively wrong**, not merely unused. -`ancestry_walk` follows parents. A `MergedFrom` arm would make the walk traverse into the very line -the workflow exists to discard, so "kill dev history" would be false at exactly the layer that -answers questions about history. The feature would reintroduce what the workflow deletes. - -**But discarding the line must not silently discard the FACT.** If the squash commit records nothing -about what it absorbed, then "was this work merged?" is unanswerable, and an unanswerable question -that gets guessed at is the fabricated-plausible-output failure. The resolution is a distinction the -model can hold precisely: - -> A **parent** is a structural fact the ancestry walk follows. -> An **absorption** is a historical receipt the walk must NOT follow. - -So a merge commit carries a `MergeAbsorption` receipt naming the absorbed tip (and the derived base, -§4) as *data*, while its `CommitAncestry` stays `DescendsFrom { parent: }`. History -stays linear and traversal stays honest; the question "what did this absorb" becomes answerable -without reviving the line. - -### D1. The receipt is only real if a QUERY consumes it - -A receipt nobody reads is data, not evidence. This document therefore fixes the consumer before the -producer, because the producer's shape is determined by what the query must answer. - -**The query.** Given a source tip S and a target ref T: *does T's history contain a commit whose -absorption receipt names S?* It is scoped two ways and both are load-bearing: - -- **exact source.** The receipt names a specific absorbed tip. "Some commit from that line" is a - different, weaker question, and answering it while claiming the strong one is fabrication. -- **target lineage.** The answer is relative to T. A receipt sitting in a line T cannot reach does - not mean the work is in T. So the query walks T's ancestry (§4's arms apply, with the same per-arm - refusals) and inspects receipts along that chain — it does not scan the store. - -**Three facts stay distinct, and collapsing any two is the defect this section exists to prevent:** - -1. **Ancestry membership** — S is literally an ancestor of T. Under squash this is normally FALSE for - absorbed work, which is exactly why (2) exists. -2. **A committed absorption receipt** — some commit reachable from T recorded absorbing S. This is a - historical claim about an act, and it is the only one the receipt itself supports. -3. **Newly derived content comparison** — the content of S is present in T's current tree. This is a - fresh derivation over current state, not a receipt read. - -**(2) does not imply (3).** Work absorbed and later reverted still carries its receipt. So the query -answers *"was this integrated?"* and never *"is this effective now?"*; a consumer needing the latter -must derive it, and the outcome vocabulary must name which question it answered. Historical -integration and current effect are separately reported, never one boolean. - -**Survival of collection.** The query must remain answerable after the absorbed objects are collected -(the open question below). That is a constraint on the receipt's contents: it holds the absorbed tip's -identity and the derived base's identity as VALUES, so answering never requires dereferencing the -absorbed line. A query that needs the collected objects would make garbage collection silently -destroy history's answers. - -**Refusals.** Not-found is `NoAbsorptionRecorded`, distinct from `TargetHistoryUnwalkable { cause }` -carrying the §4 walk arm, distinct from `SourceNotInRepository`. "No" and "cannot tell" are different -answers. - -**Open:** whether the absorbed commits' objects are retained in the store or become unreachable. This -is a garbage-collection question with its own ruling, and it is deliberately not decided here. What -IS decided: the receipt names the absorbed tip whether or not that tip's objects survive, because a -receipt that silently becomes dangling is a worse artifact than one that names something collected. - -## 3. The minimum ref model, and why it is smaller than branches - -To merge, two lines must be designatable at once. Today `RepositoryEnvelope.checked_out` is a single -optional `RepositoryCommitRef`, so checking out the target loses the pointer to the work. - -The workflow bounds how much is needed, but two of the bounds this section first drew were deductions -the workflow does not license, and they are withdrawn rather than left to decide the model quietly: - -- **"Discarded at merge" does not mean "does not need to persist."** Retirement on success says nothing - about lifetime before success. A multi-command or resumable merge has a lifetime to preserve exactly - while it is unfinished, which is when the ref matters. -- **"Exactly one checked out" erases the empty and unborn repository.** `checked_out` is - `RepositoryCommitRef?` today for a reason, and a ref model that cannot express a repository with no - commits has dropped a state the current model holds. - -What the workflow does bound: no remote tracking, no upstream, no reflog, no branch-of-branch, no -rename. - -**Proposal:** the envelope holds a set of named commit refs, at most one of which is checked out — -"at most", because the withdrawal above governs and an unborn repository has none — -the minimum that lets two lines coexist. Naming is required (not merely a second anonymous slot) -because the merge outcome, the refusal diagnostics and the CLI surface all need to say *which* line, -and an anonymous "other slot" would force position to carry identity, which is the positional-naming -defect §3 rules against. - -**Open, and genuinely undecided:** whether merge should take its two refs as *arguments* — leaving -the ref set purely a CLI convenience — or whether merge should read them from the envelope. The -argument form is smaller and testable without a ref model at all; the envelope form is what makes -`merge ` expressible. My inclination is arguments for the operation and a ref set for the -surface, so the merge model does not depend on the ref model. Flagged for ruling. - -## 4. Base derivation: four walk arms, two sides, and the outcomes they force - -**The absorption query runs FIRST, and it does not walk the source.** An earlier revision of this -section put base derivation at the front, which left the §D1 counterexample alive at the consumer even -though the query beside it was correct. After M is published with parent T and a receipt naming S, S is -not on M's parent chain — so a re-request for S fell straight through to base derivation, and once S's -objects were collected the mandatory source walk refused before the surviving receipt was ever read. -**A correct query repairs nothing until the decision consumes it.** - -The ordering is therefore fixed: - -1. **Establish the target lineage and consult its committed exact-source absorption evidence, without - requiring the source objects.** This step needs the target's chain and the identity value carried in - each receipt; it dereferences nothing on the source side. -2. **Only if fresh reconciliation is actually needed** does the operation require the live source and - derive a base — and only then do the walk arms below apply to the source side. - -Two refusals get their place from that ordering. `SourceNotInRepository` refuses a NEW operation that -needs an unavailable source; it may never veto a historical receipt lookup merely because the source -was collected. And `NoAbsorptionRecorded` is supported only by a COMPLETE target walk that found no -matching receipt — an unwalkable target yields `TargetHistoryUnwalkable`, which is a different answer. - -**Identity continuity is a requirement, not a consequence of the bytes surviving.** Carrying the source -identity as a value fixes dereferencing; it does not by itself establish that the value still denotes -the same source. The receipt's subject is a source identity that is never reused for different content -within the scope the query answers over. Physical collection scheduling may stay open; identity -continuity may not be left to it. - -Step (2) is what the rest of this section specifies. `ancestry_walk` can answer four ways per side, and -this module owes a decision for each rather than one collapsed refusal. - -| what the walk says | what it means to a merge | -|---|---| -| `AncestryStartNotInHistory` | **the caller's fact.** A ref that is not in this repository. Refuse, naming which side. | -| `AncestryBrokenAt` | **damage.** The history is truncated; a base derived across a hole would be a guess. Refuse, naming the commit and its missing parent. | -| `AncestryRevisitsACommit` | **a cycle.** Refuse; the substrate is bounded and forward. | -| `AncestryTraced` (both sides) | a base can be derived from the two chains. | - -With both chains traced, the outcomes are: - -- **`SourceOnTargetParentChain`** — S is literally an ancestor of T. This is **ancestry membership, - and it is not receipt-backed absorption**; it is reported as its own fact. (`AlreadyAbsorbed` was - the earlier name and it was doing the work of two facts. Receipt-backed historical integration is - answered in step (1) above and never reaches this partition, so the four-outcome count here must - not be preserved by folding the receipt answer back in.) Both are also distinct from a - reconciliation that happens to compute an unchanged result, which is a **content** fact about a - fresh derivation and is evidence of no earlier integration whatever. - Neither answer silently suppresses an explicitly requested reapplication: a caller who asks to - reapply S after a revert is asking for step (2), and the historical receipt is not a refusal of it. -- **`TargetIsAncestorOfSource`** — the target's tip is in the source's chain. The target has not - moved since the branch started, so no content reconciliation is required. **This is now RULED - rather than open, because leaving it open left a loophole that defeats §2.** If publication is - allowed to advance T onto S's tip, then S's development commits become part of T's history — the - dev history the workflow exists to kill — and, worse, no absorption receipt is written, so §D1's - query returns `NoAbsorptionRecorded` for work that WAS integrated. Cheap reconciliation was - silently converted into a different publication. - The ruling separates the two: **reconciliation may be trivial; publication is uniform.** Merge - reports that no reconciliation was needed, and publication still produces a new commit M whose - `CommitAncestry` is `DescendsFrom { parent: A }` — the exact observed target tip — carrying an - absorption receipt naming S. M's root may be identical in content to S's root; that is a content - fact and does not license an ancestry shortcut. Fast-forward as a HISTORY operation is therefore - not available in this workflow, and the outcome name says "no reconciliation needed", not - "fast-forward". -- **`BaseDerived { base }`** — the lines diverged from a common ancestor. This is the real merge. -- **`NoCommonAncestor`** — two unrelated histories. Refuse; this is not a conflict, it is the absence - of a base, and reporting it as a conflict would misname it. - -A boolean or a single `MergeFailed` here would rebuild exactly the collapse `ancestry.dag` deleted. - -**One consequence for `gunbc.scm.ancestry`.** That module currently names its next-rung trigger as the -arrival of a consumer that merges two commits, on the expectation that such a consumer would force a -several-parents arm. §2 rules that expectation false: the consumer arrived and it does NOT want the -arm. That trigger is therefore superseded rather than satisfied, and it must be replaced rather than -deleted — a trigger removed with nothing in its place is an untracked stall (§4b(2)). The replacement -trigger this document proposes is the arrival of a workflow that must PRESERVE both integrated lines -as traversable history; until then `DescendsFrom` is the ceiling, not a stall. - -## 5. CORRECTED: authored source stays authoritative. Structural interpretation is a derived view. - -**The first cut of this section was wrong, and it was wrong in the way this document exists to -prevent.** It ruled that the merge subject is the semantic node graph and demoted -`CorpusManifestObject` to "a projection for emission, not the authority a merge reads". That -conclusion forked an authority this repository already holds, and it did so in a document whose -stated purpose is avoiding exactly that. The correction is recorded in place rather than quietly -rewritten, because a design note that hides its own reversal teaches the next reader nothing. - -**Two factual corrections come first, because the original section was reasoning from a false census.** - -1. `CorpusManifestObject` and `CorpusManifestEntry { path: NonEmptyStr, source: AuthoredSourceTarget }` - **already exist** in `gunbc.scm.object_store`, and `gunbc.scm.object_table_json` already encodes and - decodes them. This document's earlier framing of the manifest as unbuilt was wrong. -2. What has NOT changed is `RepositoryCommit.root`, which is still `SemanticNodeObjectRef`. **So the - pending boundary is a consumer / commit-root cut, not the creation of a missing object kind** — - materially smaller than "build the keystone", and it means `add`/`commit` are nearer than this lane - previously reported. - -**And the model had already ruled on the question §5 tried to settle.** `object_store.dag` states that -a manifest entry is path-to-authored-source *and deliberately nothing else*, because a semantic root is -a DERIVED INGESTION RESULT whose value depends on the source object, the corpus and its imports, the -ingestion rule, and language and rule versions. Carrying it in the entry would fuse an input with a -realization result and would make a manifest **unauthorable for malformed or not-yet-ingested source -— "precisely the corpora a source-control system must still be able to freeze."** That module names -the same comment-only-edit case used against §5 below. - -### The counterexample is information loss, not algorithmic difficulty - -Two authored snapshots differ only in a comment. §4c erases annotations from the semantic projection, -so **their semantic graphs are identical**. A merge handed only those graphs cannot determine which -authored change occurred. No choice of three-way graph algorithm recovers the distinction, because the -distinction was destroyed before the algorithm was reached. - -Authored placement adds a second obligation the graph cannot answer: where does a moved declaration -belong in the output, and what happens when independent additions select the same destination. Source -that cannot be ingested at all adds a third — it still needs an honest source-control disposition -rather than vanishing because the structural view is unavailable. - -### The separation this document now rules - -> **The authored snapshot is authoritative for what was authored. Structural interpretation and -> correspondence are DERIVED VIEWS used to reconcile changes. A merge produces a new authored -> snapshot, and must not silently discard distinctions its semantic view cannot represent.** - -That is not two authorities for one fact. *What was authored* and *an interpretation of what was -authored* are different questions, and §3's replacement-migration doctrine does not reach them: it -applies when X and Y answer the SAME semantic question and X is intended to disappear. Neither holds -here, and applying it anyway is what produced the error. - -Two consequences follow, and they are why the original binary was false: - -- **How a complete source snapshot is represented and frozen**, and **at what granularity changes are - compared and reconciled**, are INDEPENDENT choices. A path-to-source manifest can support structural - reconciliation; adopting a more conservative merge algorithm later does not inherently change the - manifest's shape. -- **So the manifest is not blocked behind merge.** The ordering constraint is narrower than this - document first claimed: *do not make the permanent merge and publication consumer depend on an - unproven correspondence or source-reconstruction contract.* Preserve the complete authored subject, - establish a bounded structural capability over it, and cut the consumer against the demonstrated - contract. - -If a lossless authored representation derived from a semantic graph is ever proposed, it is a -**materially different subject with a replacement proof to supply** — not something established by the -phrase "semantic node graph". - -### One correction to this document's own git comparison - -The original section claimed per-path merging means "the same path changed on both sides". That is not -git's general conflict predicate — git performs three-way merging of file *contents* and admits custom -merge drivers. So "two edits to different functions in one file" is not by itself a differentiating -demonstration, and it is withdrawn as one. The differentiator, if it exists, must be stated in terms of -correspondence guarantees (§5a), not in terms of a strawman. - -## 5a. The tractability boundary is CORRESPONDENCE and COMPOSITION - -An earlier revision of this section argued that `EdgeLabel = Named { name } | Positional` partitions -the merge problem, with named children alignable and positional children refused. That partition is -real and is retained below, **but it is not sufficient, and presenting it as the tractability answer -overstated it.** Alignment by label is one form of correspondence EVIDENCE; it is not correspondence. - -**`ObjectId` recognizes unchanged content. It does not identify one logical declaration across an -edit.** Three counterexamples bound the claim: - -- **Changed ancestors.** One side edits `f`, the other edits `g`. Both change their function's digest - and every enclosing digest up to the root. At root grain *both sides diverged*, so digest comparison - alone reports a conflict for two edits that never touched each other. Descent requires a justified - correspondence between components first. -- **Content identity is not occurrence identity.** Two logical occurrences may reference the same - stored subtree. Editing one must not edit the other, and a replacement keyed only on the old - subtree's content identity would hit both. -- **Renames and moves.** A name is evidence and a position is evidence, but neither is a stable - identity across precisely the change that alters it. - -And structural disjointness does not establish independence: one side may delete a declaration while -the other adds a caller, and two independently added declarations may collide on one name. The combined -graph still needs binding and admission judgments. **Behavioral compatibility is an additional claim, -never a corollary of touching different nodes.** - -### What the first capability must promise - -Deliberately narrower than arbitrary structural merge. **Once base-relative correspondence is -established**, the ordinary cases are straightforward: equal target and source agree; a side unchanged -from base yields to the changed side; otherwise descend only into components with an established -composition rule, or return an explicit contention or unsupported-correspondence outcome. **Absence -participates explicitly**, so deletion-versus-modification and competing additions cannot disappear -into a default. - -Three interface obligations, which this document fixes and the algorithm design later discharges: - -1. **What evidence establishes that these are the corresponding logical components?** -2. **What does the automatic arm guarantee about the combination it produces?** -3. **What does it return when either answer is unavailable?** - -The answer to (3) is a **named inability to decide** — never a guessed rename, never an automatic side -selection, and never a silent fallback to path-based merging. The `Positional` case above is one -instance of (3), not a separate mechanism: position is the only identity such a child has, occurrence -identity is deliberately outside content identity, and a sequence-alignment heuristic is what §4 rules -out in a closed system. - -### The performance claim is withdrawn until it has a denominator - -This document earlier implied structural merge serves the "simpler and faster merges" goal. That is -not established. The denominator is the whole operation — interpretation, correspondence and indexing, -the merge, source reconstruction, and admission — and `object_store` records its own list-backed -lookup and traversal as quadratic. **Content addressing does not by itself establish faster merging**, -and no speed claim should be made until measured against that full path. - -Also recorded: reducing the number of reported conflicts is not evidence of improvement, because a more -aggressive reconciliation can miss real conflicts. Any future claim here needs a discriminating -oracle, not a lower count. - -## 6. What merge produces, and what it must refuse - -A merge is a **candidate then a commit**, not one step: the candidate carries the derived base, the -absorbed tip, and — per §5's corrected output authority — the resulting authored snapshot, of which a -resolved graph is a derived view rather than the product. D5 below states the full binding. Committing -it is a separate act that advances the head. This -mirrors the shape `supersession` already establishes — observe, then compare-and-swap — and keeps a -merge from being authorized by anything other than the observation it rests on. - -Merge must refuse, never widen: an underivable base, a damaged history, an unrelated history, an -unresolved conflict. In particular there is **no "take ours" or "take theirs" fallback** available to -the merge itself. That is an escape hatch in §5's sense — a toggle whose only effect is to proceed as -if the refusal had not fired — and a resolution supplied by an author is a different, named input, -not a mode the merge can select on its own. - -### D5. The candidate is bound to the generation it observed - -"Observe, then compare-and-swap" is not enough on its own, because it does not say WHAT the swap -compares. A candidate is derived against a specific target tip; if the target moves between derivation -and publication, publishing the candidate silently discards whatever moved it — a lost update wearing -the shape of a successful merge. - -**An earlier revision routed publication through `observe_generation` and `supersede_head_slot`. That -was wrong at the level of the contract, not the field list, and it contradicted this document's own §1.** -`gunbc.scm.supersession` is a **malformed-document replacement policy**: its `standing_permission_refusal` -answers `none` for `LoadDocumentMalformed` **alone**, and returns `StandingDoesNotPermit` for every other -standing including `LoadComplete`. So an ordinary merge into a healthy repository would have reached -`StandingDoesNotPermit { standing: LoadComplete }` on that route — refused precisely in the normal case, -regardless of whether the generation matched. `SupersessionApplied` also yields a modeled slot and a -warrant; it does not install a commit or a receipt. - -**The policy must not be weakened to admit merge.** Its narrowness is the point. What `supersession` -supplies here is the SHAPE — observe, bind the observation, conditionally write — which §1 already -records as precedent, and merge publication owes its own admission contract over its own subject. Nearby -primitives exist and none is promoted by name alone: `std.durable_compare_and_set` declares its own -observation-to-slot binding boundary, and `save_repository` does checked encoding followed by an -**unconditional** write. Citing either does not establish this contract; this document fixes the -contract and does not implement the actuator. - -**The candidate binds the whole derivation, not a generation value.** A generation alone is not the -relation. The candidate carries: the repository and target slot; the exact observed target commit and -state; the source identity absorbed; the base evidence; the interpretation and rule inputs and any -author-supplied resolution; and — per the corrected §5 — the resulting **authored snapshot**, which is -the output authority, not a resolved graph alone. - -**Publication is conditional on that exact observed target**, and a mismatch refuses rather than -publishing; the candidate is then not publishable and must be re-derived. A candidate is not a value -that stays valid; it is a value indexed by the state it was computed from. - -**Successful publication binds the installed facts together.** The new commit M, its absorption receipt, -and the target advance become coherently available or none of them does — an uninstalled candidate or -receipt must never answer §D1's historical-integration query. Publication returns named success evidence, -and that evidence is what retirement consumes. - -These outcomes stay separately named, because each demands a different action. **The obligations -below govern, not their count** — `PublicationUnestablished` above is one of them, and publication- -success and retirement carry their own: - -- **`PublicationFailed { cause }`** — publication is ESTABLISHED not to have applied. Only an outcome - that establishes that may carry this meaning; a realization that installs the state and then loses - its acknowledgement must return `PublicationUnestablished` instead, and be reconciled rather than - replayed. Either the realization guarantees the ambiguity cannot arise, or the unestablished outcome - is preserved. A further attempt stays conditional on the exact observed target — "retry" never means - blind replay. -- **`TargetGenerationStale { observed, current }`** — the target moved. Re-derive; do NOT retry. -- **`WrongTargetSlot { expected, actual }`** — publication was attempted against a slot other than - the one observed. This is a caller error and must not be absorbed into staleness. -- **`SourceMovedSinceDerivation { observed_tip, current_tip }`** — the source line advanced after the - candidate was built. The candidate absorbs less than the caller now means by "the source". - -**Retirement checks the source AT RETIREMENT TIME.** Naming the absorbed source generation is necessary -and not sufficient, because this ordering is possible: derive a candidate for S, publish M successfully, -the source ref advances S → S₂, then retirement runs. Deletion is therefore conditional on that same -source slot still naming S. Otherwise the operation discards S₂ after honestly publishing only S. - -The outcome when it does not hold is **`IntegrationCompletedRetirementWithheld { published, source_moved_to }`** -— integration succeeded and the source moved — which is neither a deletion of newer work nor a claim -that integration failed. Retirement also consumes the successful-publication evidence above and never a -derived candidate: retiring on a candidate that then fails to publish destroys the line and integrates -nothing. And later collection respects other live references — absorbing one line does not authorize -deleting content something else still holds. - -## 7. The projection obligation - -`gunbc.scm_compatibility_shape` already carries what the operator asked for: opaque realization and -target handles, honest capture fidelity, projection plus independent read-back, and — its own words — -*"dispatch is a generic handler value, never `ScmKind` or a vendor switch. A fourth realization -supplies one handler and does not edit this module."* - -So merge does not get to invent a second presentation path. Whatever merge exposes must project -through that shape, or it becomes a parallel authority for "how this repository is presented to a -foreign realization" — the §3 fork, in the one place the operator explicitly asked for a projection. - -## 8. Author and timestamp: a bounded metadata contract, neither absent nor nearly finished - -This section has now been wrong in **both** directions, and both errors are recorded because the pair -is more instructive than either. - -**First error — claimed absence.** It said this repository has no modelled clock and no modelled -process identity, and pointed the lane at acquiring them. False: `extdeps.clock` declares `Clock.Now`, -`gunbc.clock_read` `clock_now_probed_at` consumes it, and `gunbc.worker_lifecycle` -`ProcessIdentityEvidence = ProcessIdentityObserved { pid, started_at, owner } | ProcessIdentityUnobserved -{ reason }` carries the other half. Directing work at re-inventing an available capability is the §2 -re-invention this project exists to prevent. - -**Second error — claimed the observation side was solved.** That was the same defect one level up. -Finding an evidence type is not a solved observation capability, and the specific trap is visible in -the helper: `probed_at_word` returns a `NonEmptyStr` on **both** arms, rendering absence as the word -`clock-unreadable`. So **accepting a rendered nonempty value would not establish that an instant was -observed.** That is not a defect in the helper — it is a display renderer doing its job — but it means -display text is not admission input, and a clock read is not automatically a timestamp bound to this -commit. - -### The bounded statement this section actually supports - -> Existing clock operations and adapters are reusable, and a process-identity evidence vocabulary -> already exists. Commit metadata still requires **an applicable observation producer**, **binding to -> the commit and its declared attribution role**, **a defined timestamp event**, and **an admission -> policy for unavailable evidence**. This document does not establish that the complete -> commit-attribution observation path exists, and does not direct the creation of another generic -> clock or identity schema. - -`gunbc.scm.ancestry` carries the first error in its own note; this document relayed it rather than -checking it, and both are corrected together. That annotation's retained conclusion — that modelling -these fields here would mean *inventing two facts to fill fields* — is also corrected: an unavailable -or unverified binding is a reason not to fabricate a value, and is not proof that modelling the fields -over existing observation carriers inherently fabricates anything. - -### What the four open pieces mean, and their order - -Each is a real question, and **they are ordered**: the admission policy comes last because it cannot -be decided before the field's subject and applicable evidence are. - -1. **Producer** — which operation observes the fact this commit's field claims, invoked where. -2. **Binding** — that the observation attaches to *this* commit and to a declared attribution role. - Reusing an observation means retaining the operation's failure contract, not its rendered output. -3. **Timestamp subject** — *which event* the field describes. Authoring, integration and publication - are different instants, and a commit that does not say which one it names is not carrying a fact. -4. **Admission** — whether a commit is publishable with the evidence unavailable. - -On (4), this document withdraws its earlier lean. "A merge receipt with no author is weak" does not -determine the policy: the existing source / base / target / result relationship already expresses a -meaningful integration fact, and attribution and time support *additional* claims. A contract requiring -an identified integrating actor should refuse when that actor cannot be established; a different -contract could admit an explicitly absent attribution. **Neither may render absence as observed -evidence** — that is the constraint, and it is the one `probed_at_word`'s two arms illustrate. - -One composition obligation with §D5: a missing-metadata check must run where it can refuse *before* a -write, because a missing observation discovered after a write does not turn an applied publication into -one established never to have applied. - -## 9. The evidence this design owes when it is built - -Stated now so the implementation cannot be declared done by typecheck (§5's -specification-without-execution): - -- one control per `AncestryWalk` REFUSAL arm, per side — `AncestryStartNotInHistory`, - `AncestryBrokenAt`, `AncestryRevisitsACommit` — each reaching a distinct, named merge refusal; - `AncestryTraced` is the success arm, and the controls it owes are the four traced/traced outcomes - below rather than a refusal; -- the traced/traced partition discriminated four ways: `SourceOnTargetParentChain`, - `TargetIsAncestorOfSource`, `BaseDerived`, `NoCommonAncestor` — each from a real merge and from each - other, and `SourceOnTargetParentChain` additionally discriminated from the step-(1) receipt answer, - since those are two facts and the earlier `AlreadyAbsorbed` name held both; -- a merge whose result is asserted by CONTENT, not by "it succeeded" — the mutation that returns an - empty or unchanged graph must go red; -- a refusal that stays a refusal: a conflict specimen with no author-supplied resolution must not - produce a commit; -- the absorption receipt asserted to be present AND the ancestry asserted to remain single-parent, - because the whole ruling of §2 is that those two facts coexist; -- **D1** — the receipt query answered on a target whose chain CONTAINS the absorbing commit and on one - whose chain does not, plus `NoAbsorptionRecorded` discriminated from `TargetHistoryUnwalkable` and - from `SourceNotInRepository`; and the absorbed-then-reverted specimen, which must report integrated - historically and not-present currently, since collapsing those two is the defect §D1 names; -- **D2** — the no-reconciliation-needed case asserted to produce a commit with `DescendsFrom { parent: - A }` and an absorption receipt naming S; the mutation that advances the head onto S's tip must go - red, because that mutation is precisely the loophole; -- **D4** — a comment-only-edit specimen whose semantic roots are equal and whose authored snapshots - differ, asserted to preserve the authored distinction; a changed-ancestors specimen (`f` and `g` - edited on opposite sides) asserted NOT to report a root-grain conflict; and a shared-subtree - specimen where editing one occurrence leaves the other unchanged. Each is a discriminating red for - a correspondence claim, not a demonstration that a merge algorithm exists; -- **D1 at the DECISION, not only the query** — a repeated exact-source request after M is published, - asserted to answer from the receipt rather than proceeding to another base-derived merge; the same - with the source objects COLLECTED, which must still answer (the control that would have gone red on - the earlier ordering); the same source requested against a DIFFERENT target lineage; a never-absorbed - source whose fresh reconciliation happens to compute an unchanged result, asserted NOT to report - historical integration; and a later target commit asserted not to disturb either answer; -- **D5** — a candidate published against a moved target asserted to refuse, discriminated from - `PublicationFailed`, `WrongTargetSlot` and `SourceMovedSinceDerivation`; **ordinary publication into a - HEALTHY repository asserted to succeed**, which is the control the superseded `supersede_head_slot` - route would have failed with `StandingDoesNotPermit { standing: LoadComplete }`; commit, receipt and - target advance asserted coherently available together, with an uninstalled receipt asserted not to - answer the D1 query; source movement AFTER publication but BEFORE retirement asserted to yield - `IntegrationCompletedRetirementWithheld` and to leave S₂ intact; and a failed publication asserted to - leave the source ref unretired. - -## 10. What this document does not claim - -It does not claim a structural merge algorithm is BUILT, and §5a's tractability finding is derived from the node model rather than demonstrated by a running merge. It does not settle the ref model (§3 open) or object retention for absorbed lines (§2 open), or author and -timestamp (§8). It does not claim a bounded structural correspondence capability is feasible — §5a records that as an -obligation to demonstrate, not a result. Its performance claim is withdrawn (§5a) rather than weakened. - -It does not authorize a merge implementation to be built. What it settles is the vocabulary those -consumers must preserve. **It does not gate `CorpusManifestObject`**: that object exists today in -`gunbc.scm.object_store`, this document's earlier claim to the contrary was false, and the ordering -constraint §5 actually establishes reaches only the permanent merge and publication consumer — not the -authored-source primitive, and not `add`/`commit`. diff --git a/docs/plans/scope-rank-view-design.md b/docs/plans/scope-rank-view-design.md deleted file mode 100644 index d4f8b79734a..00000000000 --- a/docs/plans/scope-rank-view-design.md +++ /dev/null @@ -1,248 +0,0 @@ -# The scope rank-view — resolve a scope instead of materializing one - -The seed builds a fresh set of name-keyed maps for every claim scope it enters. `[floor-scope-cost]` -already names the correction in the tree, beside the number it prices: *"a scope that is a view -rather than a rebuild costs nothing to enter."* This is the design for that view. - -It is a **replacement migration** (DESIGN §3): the per-scope materialized maps and the rank-view -answer the same semantic question — *which declaration does this name resolve to, in this scope* — -and one of them is intended to disappear. The model lands first, alone; the reader cutover follows -in its own PR, at the root, with no interval in which both representations are live authorities. - -## 1. What is materialized today, and what of it is scope-dependent - -`cli_run` `claim_scope_for` computes the scope's module `order` (own module, then the compiler's -precedence-ordered import closure, then the reference closure) and then builds two things over it: - -- the scope `item_registry` — a union of each scoped module's own registry, folded in `order`, - first write wins, with the authored-region rule deciding which collisions count as ambiguous; -- `v1.interpreter` `build_scope_indexes_with_module_order` — `fn_nodes` (a bare slot per name and a - qualified slot per declaration), `ambiguous_bare_function_names`, `file_module_paths`, - `file_import_bindings`, `service_ops`. - -gunbc#9809 removed the module-local half of that derivation: `ModuleScopeFragment` derives each -module's contribution once per prepared subject rather than once per scope containing it. What -survives is exactly the **fold** — the part that is not module-local, because it applies the scope's -precedence to fragments the subject already holds. - -The observation this lane rests on is that the surviving fold stores a *decision* that is cheaper to -compute than to store. Each entry of each per-scope map is the answer to "of the modules declaring -this key, which one does this scope rank first". That answer is a function of two things the process -already has: the corpus-wide set of declarers, and the scope's own ordering of modules. Materializing -it once per scope is DESIGN §2 redundancy in its plainest form — 660 copies of a table derived from -one corpus and 660 short lists — and DESIGN §3's second-authority problem, because from that point on -the map, not the order, is what a reader consults. - -## 2. The view - -Two carriers replace the maps. - -**Corpus-wide, one per prepared subject.** A `name -> declarers` index, each declarer carrying the -declaring module's identity alongside the declaration node, in a stable corpus order. This is the -fragment population of #9809 inverted from per-module lists into per-name lists; it introduces no -new derivation and no new authority, only a different join order over facts the subject already -carries. - -**Per scope.** A `module -> rank` map, where rank is the module's position in `order`, plus the -`authored_region` boundary already computed there. Its size is the scope's module count (mean 427, -max 878 at the measured run), not its item count. - -**Lookup.** Resolution intersects the two: take the key's declarers, keep those whose module has a -rank in this scope, and select by the slot's polarity. Nothing is materialized at scope entry beyond -the rank map, and every answer is derived at the ask. - -## 3. The polarity finding — the slots do not agree, and a uniform cutover is silently wrong - -The brief names the rule as "minimum rank". That is correct for the slot the prize is denominated in -and **not uniform across the maps being replaced**, which is the first thing this model exists to -record: - -| slot | seed rule | rank rule | -|---|---|---| -| `fn_nodes` bare | `or_insert` under `module_order` | lowest rank wins | -| scope `item_registry` | first-write-wins over `order` | lowest rank wins | -| `file_import_bindings` | `or_insert` over the fold | lowest rank wins (key is module-local, so no scope can discriminate) | -| `fn_nodes` qualified | unconditional `insert` | exactly one declarer; **admission-gated** (§4) | -| `file_module_paths` | unconditional `insert` | highest rank wins | -| `service_ops` | unconditional `insert` | **highest rank wins** | - -`service_ops` is the inversion that matters: a service operation key claimed by two scoped modules is -today the *last* module in precedence order, and a rank-view that resolved it by minimum rank would -silently redispatch it. The tree already carries the receipt for why this key is dangerous — the -`std.resources` `Filesystem` / `extdeps.filesystem` `Filesystem` pair, whose registry-merge collision -once dropped a service's operations into "unknown service operation" at runtime. - -`file_module_paths` is last-write-wins over a key that is module-local in practice (one module per -file), so no current input discriminates the polarity. The model files it at the seed's polarity -rather than at the one a reader would guess, and names what would discriminate it, because "no input -reaches it" is a ceiling to record, not a licence to pick either arm. - -## 4. The hard constraint, and the rung it puts at risk - -On record for this lane: *a scope's qualified `fn_nodes` must never resolve a name outside the -scope's admitting closure — a claim must not call a module its closure never admitted.* - -Today that holds **by construction and incidentally**: the qualified slot exists only because a -scoped module wrote it, so an out-of-scope declaration has no key in the map. The rank-view removes -that construction. The corpus-wide index holds every declarer in the subject, so an unguarded -qualified lookup would answer from a module the scope never admitted — a strictly new failure, and -one that fails in the direction that looks like success. - -So the migration's obligation is not to add a filter but to keep the guarantee structural: resolution -must have no arm that yields a declaration without having consulted the rank map, so an unadmitted -answer has no constructor. Concretely, the resolver returns only a declarer obtained *through* rank -membership; there is no accessor that reaches the corpus index and returns a node. DESIGN §3's rule -for a replacement migration is the same point stated generally: the minimum Y is not the smallest -thing that executes the happy path, it must preserve every required refusal. - -The model records this as a §4b subject with its previous rung, its target rung and the evidence that -would establish either — not as prose, so that a cutover which reaches the prize while lowering the -rung is a red row rather than an unnoticed regression. - -## 5. What the view does not make free — the enumerating readers - -Point lookups and membership tests become O(declarers of the key). Four readers instead **enumerate** -the scope registry, and for them the rank-view trades a cheap iteration over a scope-sized map for an -iteration over a corpus-sized one: - -- `v1.interpreter` `build_initial_env` — per claim, over data items; -- `v1.interpreter` `eval_data_initializer_values` — over data items; -- `v1.interpreter` `resolve_published_mock_keys` — over data items; -- `cli_run` `roster_entry_registry_cache` — over every key. - -The first three filter to `DataItem` immediately, so the view serves them from a kind-partitioned -corpus name list and iterates a fraction of the corpus rather than all of a scope. The fourth wants -every in-scope name and is the one row where the view is not obviously cheaper; it is named here so -the cutover measures it rather than discovering it, and its call frequency is the fact the migration -PR must carry. - -This is the design's honest boundary: the prize is scope *entry*, and a reader that enumerates a -scope pays at the ask what it no longer pays at entry. A cutover that quotes the entry saving without -this column would be reporting half a ledger. - -## 5b. The migration census — every site, its demand, and its cut order - -The brief sizes the reader migration at "~126 `item_registry` sites". That is the grep count over the -token across `src/`, and it is not the migration population: **most of those occurrences are a -different carrier's field of the same name.** Filing them separately is the first thing the census -does, because a cutover scoped to the grep would edit the typecheck layer and the emitter for no -reason, and a cutover that ignored the difference would edit the wrong authority. - -### What is not in the population (112 of 126) - -| kind | where | count | why it is not the subject | -|---|---|---|---| -| `TypedModule.item_registry` | emitters, `coproduct_reflection`, `owned_data`, `data_initializer_identity`, `v1_compiler_compile` | ~18 | the per-module registry — the authority the corpus index is *derived from*, and the one the fragment population of #9809 already reads. Untouched. | -| typecheck-layer `scope`/`fn_scope`/`lam_scope`/`local`/`state`/`dep_state`/`ctx` registries | `v1_compiler_infer` | ~30 | `v1_compiler_infer`'s own threaded registry at compile time. A different carrier with the same field name; it never sees a claim scope. | -| field declarations | `v1_compiler_infer`, `v1_compiler_infer_items`, `v1_interpreter` | 8 | type declarations, not reads. | -| empty-map struct initializers | test fixtures and empty contexts across 9 files | ~12 | `Rc::new(HashMap::new())`; nothing is read. | -| `item_registry_keys` | `v1_compiler_artifact`, `v1_compiler_compile` | 5 | a different field: the serialized key list of an artifact. | -| doc-comment mentions | `cli_run`, `v1_interpreter`, `coproduct_reflection` | ~9 | prose. | -| `build_qualified_item_registry` and its duplicate marker | `v1_compiler_emit_rust` | ~10 | the emitter's own qualified overlay, with its own fail-closed refusal. A separate authority on a separate key. | - -### The population (14 readers, 4 construction sites) - -Demands, and the arm each falls in. The fourth arm is one the reader census did not originally -carry and this sweep found: - -**Provenance test** — the reader looks the name up *in order to read `info.module_name`*, and decides -a builtin dispatch from it. These are the sites where a polarity error does not surface as a missing -name but as a **different function executing**, so they are the ones the qualified-admission RED and -the ambiguity line must both be exercised against: - -| site | decides | -|---|---| -| `v1_compiler.v1_interpreter` `try_witness_evaluation_dispatch` | whether a call is the witness-evaluation authority's own | -| `v1_compiler.v1_interpreter` `is_v4_bridge_family` | whether a name is one bridge family's, by declaring module | -| `v1_compiler.v1_interpreter` `is_v2_std_collection_map_grounded_fn` | whether a call takes the primitive map grounding | -| `v1_compiler.cli_run` `definer_module_for_name` | the declaring module of a name — on a `ResolvedGraph` argument, so whether it is in the population depends on which graph its callers pass, and that is the one row this census leaves open for the cutover to close | - -**Point lookup** — `v1_compiler.v1_interpreter` `eval_var` (the registry slow path behind the env -lookup) and `eval_data_item_value`. Both already re-resolve through `lookup_fn_from` immediately -after, so under the view they collapse into one resolution rather than two. - -**Membership test** — five sites, all `contains_key` guarding a call into a modeled function: -`call_test_claim_fn_bool`, the three `call_floor_kernel_would_skip` / `call_floor_row_would_skip` / -`call_floor_row_precompute_would_skip` guards, and `rerun_frontier_nodes_for_entry`. Each -becomes `resolve(name).is_some()`. - -**Enumeration** — the four readers of §5. - -**Construction** — `v1_compiler.cli_run` `claim_scope_for`'s registry fold (with its `winner_of` / -`ambiguous` bookkeeping) and `v1_compiler.v1_interpreter` `build_scope_indexes_with_module_order`. -These are the root: they are deleted, not migrated. - -Plus one site that looked like the hardest and is not. `v1_compiler.v1_interpreter` -`InterpContext::resolved_graph` hands out a whole `ResolvedGraph` built from the scope map, which -would force materialization to survive the cut for any consumer of it. **It has no consumer** — no -call site anywhere in `src/`. It is deleted with the maps rather than being the reason to keep them. - -### Cut order - -1. The resolver and the rank map land, and the **provenance tests and point lookups** move first — - they are the sites that read the winner rather than merely its presence, so they are where a - polarity or admission error is loudest, and moving them first means the discriminating REDs run - against the arm most likely to be wrong. -2. The **membership tests**, which are the same resolution asked for less. -3. The **kind-filtered enumerators**, which need the corpus kind partition to exist. -4. `roster_entry_registry_cache`, last, and only after its call frequency is measured — it is the one - reader the view does not obviously make cheaper. -5. The **construction sites and `resolved_graph`** are deleted in the same motion, which is what - makes this a root cut rather than a leaf-first refinement. Nothing in steps 1–4 may be merged with - the fold still standing: a surviving map is the attractor, and readers understanding both - representations is the parallel-authority state the migration exists to avoid. - -## 6. The scope population — 660 and 1,155 come from one instrument at two revisions - -The brief asks that the delta between 1,155 and 660 be reconciled rather than assumed. Both are -readings of the **same instrument**: the required floor's -`floor: N scope construction(s) for M distinct scope(s)` line, whose `M` is the distinct-scope count. - -- **660** is a current reading, from a named required-floor job on the #9809 branch, where - `[floor-scope-split] constructions=660` and `M=660` agree — constructions equal distinct scopes, - so the per-claim rebuild is already gone. -- **1,155** is a *transcription* of an earlier reading of that same line, sitting in the - `PreparedScopeIndexes` doc comment in `v1.interpreter` inside the sentence that records the - per-claim-rebuild finding ("9,573 reconstructions of maps that only 1,155 distinct scopes can - differ in"). Its companion figure, 9,573 constructions, is the one that has since been repaired to - equal the distinct count. - -So the delta is not two instruments disagreeing and not a scope-universe difference: it is one -instrument read at two revisions, with the older reading copied into prose. Whether `M` fell from -1,155 to 660 because the corpus moved, because the claim manifest moved, or because an intervening -lane narrowed the closure is **not decidable from either figure**, and this design does not guess — -the question is settled by re-deriving `M` on the current tree, which is precisely the "before" half -of §7's paired run. Naming that as the answer rather than picking one of the two numbers is DESIGN -§6's rule applied to the reconciliation itself. - -The transcription is the mechanism that made a stale reading look like a rival present-tense -measurement. **This PR deletes it**, replacing the literal pair in the `PreparedScopeIndexes` doc -comment with the name of the line that produces it. The number is not updated — updating it -re-commits the defect at a fresher value. The model keeps the row, because the reconciliation is a -finding about how the two figures were produced and survives the comment that carried one of them. - -## 7. Acceptance - -Paired required-floor runs, before and after, same instruments: - -- identical claim population, results, verdict and digests; -- identical scope identities (the `hash_combine` fold over `order` — the rank map is derived from the - same `order`, so a moved identity means the closure moved, not the view); -- `[floor-bare-name-ambiguity]` **byte-identical between the pair**. The line moves if and only if - bare-name precedence forks, which is the exact failure a rank-view can introduce. -- `[floor-scope-split]` as the prize measurement: the `indexes` and `registry` terms go to - approximately the rank-map build, and `order` is untouched. - -The oracle is the paired run, never a literal (DESIGN §5): the relayed figures for the ambiguity line -were measured on a pre-#9809 corpus and are re-baselined by the "before" half of the pair, not -asserted. - -## 8. Order of work - -1. **This PR — the model only.** `gunbc.scope_rank_view` and its executing claim. No reader moves. -2. The rank-view carriers land beside the fold, with the equivalence law executing over a fixture - corpus whose two entries rank one colliding bare name oppositely — the discriminating RED that - `scope_fragment_memo_equivalence` established the shape of for #9809. -3. The root cut: the materialized maps are deleted and every reader is fixed forward onto the view in - one motion. Not leaf-first — a surviving map is DESIGN §3's attractor, and readers that understand - both representations are the parallel-authority state the brief forbids. diff --git a/docs/plans/seed-string-decode-census.md b/docs/plans/seed-string-decode-census.md deleted file mode 100644 index acf26ecd6cf..00000000000 --- a/docs/plans/seed-string-decode-census.md +++ /dev/null @@ -1,608 +0,0 @@ -# Seed string-decode census — the authority/realization coupling is spelling-keyed - -**Subject.** The Rust seed reads values produced by `.dag` authorities through the interpreter and -addresses them BY SPELLING: type name, variant name, field name and entry-function name are string -literals in Rust. The seed and the authority share no Rust type, so a rename on the `.dag` side has -NO COMPILE-TIME LINK to the decoder that depends on it. gunbc#9975 discovered this by accident when a -migration renamed what `local_repo_wet_schedule` reads. This document reports the population, the -mechanism, and the §4b ceiling. It repairs nothing and enrols nothing. - -> **SCOPE, BEFORE ANY NUMBER BELOW.** Every count in this document was discovered by MATCHING A -> SPELLING, and the population it ranges over is defined by an ABSENCE — decode sites with no -> compile-time link. A search cannot enumerate an absence: a site written in a spelling the search did -> not anticipate is indistinguishable from a site that does not exist. **Every number here is an -> ESTIMATE WITH NO KNOWN SIGN.** Re-derivation with a parser moved P4 by +68 (42 → 110) and moved P7 -> in BOTH directions at once (`--function` 35 → 30, `--entry` 6 → 8): a line-keyed instrument misses -> what formatting separates and over-claims what proximity suggests. Per-primitive status — measured, -> or unmeasured — is in the table's own column. - -**What this document turned out to be.** It began as a census of a seed defect. The P6 incident below -— the class reproducing in a different language, against a different file, within the hour, on the -author repairing it — shows that it is not seam-specific: the class needs only a substitution whose -failure arm widens, and not the seed/authority seam at all. So what is measured here is ONE SEAM of a -primitive that is not seam-specific, and the population below is a FLOOR rather than a ceiling. - -Per DESIGN §7 the finding is not "the seed is wrong" — a realization lagging its authority is the -expected state. The finding is that the COUPLING is spelling-keyed, which is a safety property -independent of how much seed remains. - -## Deriving the census - -Subject population: the 90 hand-written `.rs` files under `src/v1/stage0/src` — every file whose -first line is not `// Generated by v1 compiler`. Generated mirrors are excluded on purpose: they are -re-emitted from `.dag`, so a rename propagates into them and this class cannot arise there. - -Right-hand side of the join: declarations scanned out of the 4,617 `.dag` files under `dag/`, -`src/v1`, `src/v2`, `tools/`, `test/`, `fixtures/`. **This is a declaration scanner, not the -compiler's own namespace authority**, and it is keyed on the BARE name — so the authority attribution -below is an upper bound wherever a name has several declarers (8 of 79 do: `Refused` ×7, `Failed` ×6, -`Present` ×4, `Done` ×3, `ExitSuccess`/`None`/`Suppressed`/`Empty` ×2). 70 of 79 names have exactly -one declaring module; those attributions are exact. - -## The decode primitives — seven, and each pass that widened the search found more - -The set of PRIMITIVES is much narrower than the set of call sites, and it is what makes the census -closeable. **P1–P5 bottom out in four `InterpContext` methods** (`sym`, `sym_eq`, `field`, `resolve`), -declared together in `v1_interpreter`. **P6 and P7 do not touch `InterpContext` at all** — P6 is a -`str::replace` over file text and P7 is a subprocess argument vector — and that is precisely why they -were missed: every earlier sweep was keyed on the interpreter surface, so a name crossing into `.dag` -by any other route was outside the search by construction. The primitives are grouped by the ROUTE a -name takes, not by a shared implementation. - -| # | primitive | shape | sites (hand `.rs`) | how counted | -|---|---|---|---|---| -| P1 | `ctx.sym_eq(sym, "Name")` | type/variant name test | 87 (79 distinct names) | **argument parser — gap measured, 0** | -| P2 | `ctx.field(fields, "name")` | field lookup by literal | 110 (62 distinct names) | **argument parser — gap measured, +7 over the line regex** | -| P3 | `ctx.resolve(sym).as_str()` matched against string-literal arms | rendered-name dispatch | 61 | line regex — LOWER BOUND, gap unmeasured (match arms rarely wrap, so the gap is likely small; that is a guess, not a measurement) | -| P4 | `run_in_context(ctx, "entry_fn", …)` | call-by-name into the authority | **110 literal, of 135 call sites**; the other 27 are enumerated one-by-one below | **argument parser — gap measured, +68** | -| P5 | file-local wrappers (`field_str`, `field_value`, `field_list`, `variant_field`) over P1–P3 | indirection layer | 29 over those four names | argument parser for those four names; the WRAPPER SET itself is a lower bound — wrappers were found by naming convention, so a wrapper named otherwise is invisible | -| P6 | `str::replace` rewriting a live-HEAD `.dag` file's own text | spelling-keyed EDIT, not lookup | 5 (3 + 2, in 2 producer fns reached from 6 call sites) | read exhaustively in 1 file — complete FOR THAT FILE; `str::replace` over `.dag` text elsewhere is unswept | -| P7 | subprocess ARGV: `--entry ` / `--function ` | call-by-name through a second door | **30 spelling-keyed `--function` (12 distinct), 8 spelling-keyed `--entry` (7 distinct)** — see the re-derivation below | **token scanner — gap measured, and it ran in BOTH directions**; the assembled-argv blind spot is closed by mechanism, not by a zero | - -**The set is SEVEN KNOWN, NOT PROVABLY CLOSED**, and the method matters more than the number. - -**P7 is the find that matters most.** Entry and function names cross into `.dag` through SUBPROCESS -ARGV — 35 `--function` literals across `bin/interp_recorded_fixture_witness.rs`, `bin/claim_batch.rs` -and `pre_push.rs`, 6 literal `--entry` `.dag` paths, 11 distinct names. It is the same coupling through -a second door, it is larger than the primitive this census started from, and it is invisible to every -`run_in_context`-keyed search. A reader would least expect this door, which is exactly why it went -unmeasured. - -**Method, stated because it is the transferable part.** Each pass of this census was keyed on a -SYNTAX, and each re-keying found more: a `sym_eq`-keyed sweep missed P3 and P5; a `run_in_context`-keyed -sweep missed P6 and P7. Both misses were committed by this document's author AFTER being warned that a -grep written from the known specimen returns only the specimen's shape. The pass that found the -subprocess door was keyed differently — on **any route by which a name crosses from Rust into the -`.dag` world**, not on a spelling of a call. That is the formulation any future sweep should use, and -it is why the count above is reported as known-not-closed. The same error recurred inside the -instrument at smaller scale: locating P7's names needed `test fn`, not `fn` — a declaration scanner -keyed on one keyword misses a whole declaration form. - -**P6 was the only primitive in the set that could fail OPEN, and this PR repairs it.** -`str::replace` returns its input unchanged when the pattern is absent, so an ordinary edit to the -`.dag` literal made the scratch copy silently unrewritten. The two arms differed and only one was -loud: a missed module-path rewrite produced a same-name duplicate that the module-path collision wall -refuses, while a missed `/tmp` rewrite made the witness write to the SHARED path instead of its -per-run scratch directory — unreported, with the damage landing as cross-run interference in some -other session's witness. Being caught by a neighbouring wall is a property of that wall, not of this -rewrite, so **all five substitutions now refuse** — three in `unique_fs_witness_entry` and two in -`closure_scale_witness_entry`, the two producer functions reached from six call sites — not just the -silent one. The -refusal names the PATTERN and the SOURCE FILE, because whoever trips it will be editing the `.dag` -with no reason to know a Rust harness depends on its literal text. Evidence: a discriminating RED -(pattern edited out → refuses, asserting the refusal names both), a positive control (pattern present -→ substitutes, so the refusal is not a function that refuses everything), and a **counterfactual** -that runs bare `str::replace` on the identical input and asserts it answers with the source unchanged -and no error — because `substituted` did not exist before the repair, the RED alone would show only -that a function which refuses, refuses, and not that the mechanism it replaced failed open. All three -in `substituted_refuses_on_absent_pattern`. Honest placement: these execute under -`cargo test --workspace`, not under the required lane, which runs `--lib` only and merely compiles bin -targets. The incident that produced the counterfactual is recorded in its own section below. - -**Mint side** — the same coupling in the opposite direction, where the seed CONSTRUCTS a value the -`.dag` side then destructures: 262 `type_name:`/`variant_name: ctx.sym("Lit")` sites and 198 -`ctx.sym("field")` field-name sites, over 170 distinct minted type/variant names. - -### What my search covered, and what a decoder using some other primitive would look like - -The instrument is keyed on P1–P5, which are name-keyed. A decoder that avoided names entirely would -have to be **positional** (`fields[0]`, `fields.get(0)`) or **shape-only** (matching `Value::Record` -without inspecting `type_name`), or **textual** (`format_value`/`type_label_public` rendered and then -substring-matched). I looked for all three: - -- positional: **2 sites**, both in one `#[cfg(test)]` block in `v1_interpreter.rs`, and both still - assert the resolved NAME beside the index. Not a live decode mechanism. -- rendered-text: **0 sites** in hand `.rs` — `format_value` appears 38 times and `type_label_public` - 46 times, all inside refusal messages, never as the discriminant. -- shape-only: present, but it is a distinct class (order/arity coupling, not spelling) and is not - what this census is about. - -So the name-keyed primitives are the whole live population, and P3/P5 are exactly what a grep written -from the #9975 specimen (`sym_eq`) would have missed: 97 further sites in 9 files. - -## The finding: a safety claim asserted BY SUBTRACTION over a population nobody enumerated - -The count correction below is the receipt. **This is the finding.** - -The first version of this census's P4 row read: - -> 42 literal (of 142 total; **the other 100 take the name from a `.dag`-supplied roster and are NOT -> exposed**) - -That bolded clause is the sentence that says most of the surface is fine, and it was reached by -arithmetic: 142 minus 42. **The 100 was never a set.** Nobody enumerated it, nobody inspected a -member, and no property of any member was ever checked — yet it was handed an exculpatory property -and it carried the weight of the whole primitive's safety. - -Both of its operands were also wrong. 142 was a raw textual grep that counted the `pub fn -run_in_context` definitions and doc-comment mentions; the call-site count is 135. And 42 came from a -regex requiring the literal on the same LINE as the callee, which missed 68 wrapped calls. So the -subtraction produced a phantom population of 100 where the real complement is 27. - -**Why it passed.** A subtraction over an unenumerated complement *reads as arithmetic rather than as a -claim.* It has no verb like "I checked" to interrogate, it inherits the authority of the two counts -beside it, and both of those counts looked like measurements. It passed the author, it passed review, -and it passed the manager who commissioned the census. Nothing in the sentence's shape invites the -question "which 100?". - -**The rule.** A complement is not evidence. If a population is small enough to matter to a safety -claim, it is small enough to enumerate — and if it cannot be enumerated, the honest disposition is -*undetermined*, never *not exposed*. - -## The 27 non-literal P4 sites, enumerated - -Enumerated rather than subtracted, one row each, because replacing a subtraction over 100 with a -subtraction over 27 would rebuild the same defect at a fifth of the scale. - -| site | second argument | disposition | -|---|---|---| -| `cli_run.rs:31408` | `CI_FLOOR_COMMIT_WITNESS_SCHEDULE_FN` | **RustLiteral** — a Rust `const` | -| `cli_run.rs:21376` | `basis_constructor` | **RustLiteral one hop** — `let basis_constructor = match kind { … }` selecting between Rust literals | -| `cli_run/witness_gates.rs:494` | `basis_constructor` | **RustLiteral one hop** — same value, passed in | -| `cli_run.rs:21405` | `constructor` | **RustLiteral one hop** — `match` arms including `"witness_cost_seed_timed_out_event"` | -| `cli_run.rs:4249` | `fn_name` | HelperParameter | -| `cli_run.rs:17354` | `function` | HelperParameter (`run_witness_verdict_diagnostic`) | -| `cli_run.rs:17460` | `fn_name` | HelperParameter (`eval_census_string_fn`) | -| `cli_run.rs:18514` | `function` | HelperParameter (`run_value`) | -| `cli_run.rs:23786` | `fn_name` | HelperParameter (`call_test_claim_fn_bool`) | -| `cli_run/required_floor_runner.rs:345` | `function` | HelperParameter (`run_claim_failure_receipt`) | -| `cli_run/required_floor_runner.rs:376` | `function` | HelperParameter | -| `cli_run/required_floor_runner.rs:3331` | `qualified_name` | HelperParameter | -| `pre_push.rs:110` | `function` | **HelperParameter, traced** — `eval_fn(plan, "pre_push_zero_sha_authority")`, `eval_fn(plan, "pre_push_source_roots")`: Rust literals | -| `pre_push.rs:282` | `function` | HelperParameter (`eval_fmt_recipe`) | -| `required_regen_host.rs:4027` | `function` | HelperParameter — a local closure `let call = \|function: &str\|` | -| `required_regen_host.rs:7217` | `function` | HelperParameter — same shape | -| `v1_interpreter.rs:2549`, `:2577`, `:2610` | `name` | HelperParameter, inside `#[cfg(test)]` | -| `cli_run/required_floor_runner.rs:3013`, `:3043` | `&qualified` | **Computed** — `format!("v2.workflow.required_floor.{func}")`; the PREFIX is a Rust literal and is itself exposed | -| `cli_run.rs:19378` | `&qualified_declaration` | **Computed** — `format!("{declaration_module}.{declaration_name}")` from `.dag`-supplied parts | -| `cli_run.rs:19082` | `&function` | Undetermined | -| `cli_run.rs:18659` | `projection` | Undetermined | -| `cli_run/shared_fill.rs:542` | `&format!("fixture.tmshare.{entry}")` | **Computed over a synthetic fixture** — no live authority | -| `v1_interpreter.rs:21175` | `&format!("fixture.{module}.total")` | **Computed over a synthetic fixture** — no live authority | -| `v1_interpreter.rs:4864` | `entry_fn` | Not a call site — the internal delegation inside `run_in_context_with_args` | - -**Reading this table honestly.** *HelperParameter is not a safe disposition — it moves the question one -stack frame up, it does not answer it.* Two were traced to Rust literals at their callers, and nothing -suggests the others differ; but they were not traced, and a reader must not convert them to "safe". -**Not one row in this table says "reads its name from a `.dag` roster and is therefore not exposed"** — -the property the deleted sentence claimed for a hundred sites is claimed here for none, because it was -never established for any. - -## What the spelling-to-parsing gap cost, measured - -Re-derived with a brace-aware parser that reads each call's actual argument list instead of matching a -line: - -| primitive | line-keyed regex | argument parser | miss | -|---|---|---|---| -| P1 `sym_eq` | 87 | 87 | 0 | -| P2 `ctx.field` | 103 | 110 | 7 | -| **P4 `run_in_context`** | **42 (reported "of 142")** | **110, of 135 call sites** | **68** | - -**The blind spot is a function of FORMATTING, not of meaning**, which is why the miss rate is not -uniform: a line-keyed pattern sees a call only when the literal lands on the same line as the callee, -so the gap rises with argument count. `sym_eq(sym, "Name")` never wraps; `run_in_context_with_args(ctx, -"name", &args, false)` is long enough that `rustfmt` breaks it at 68 sites. Those 68 are ordinary -literal entries — `"generated_artifact_body_for_path"`, `"committed_generated_artifact_paths"`, -`"live_read_selection_manifest_live"`, `"resolve_channel_policy"`, `"ci_batch_summary_text"` — 21 in -`required_regen_host.rs`, 18 in `cli_run.rs`, 9 in `cli_run/required_floor_runner.rs`, the rest across -six more files. - -P3, P5, P6 and P7 have NOT been re-derived this way; their status is in the primitive table's own -column, and where the gap is unmeasured it says so rather than implying zero. - -## The class reproduced on the author, inside the repair, within the hour - -This is recorded as evidence rather than as an anecdote: it is the only observation in this document -that shows the class arising rather than being inventoried, and it happened to the person writing the -inventory. - -While adding the counterfactual test to the P6 repair — the repair whose entire subject is -`str::replace` failing open on a missed pattern — the editing script used a Python `str.replace()` -whose pattern did not match, because the escaping of `\n` and `\"` differed between the script and the -file. Every signal available agreed with success: - -| signal | said | actual | -|---|---|---| -| `str.replace()` return | a string | the file, unchanged | -| `cargo fmt` | "modified 1 file" | an unrelated reformat | -| command exit code | `0` | the edit never applied | -| remote `cargo test` | **green** | ran the two tests that already existed | -| `test result: ok. 2 passed` | ok | the third test was never written | - -**The count was the only tell, and the colour was not.** `2 passed` where `3` was expected is the -entire discriminating signal; it was caught by reading the run's test NAMES out of the log rather than -its exit code, which was luck of habit and not of design. - -Three things follow, and none of them are about Python. - -1. **The primitive is not `.dag`- or seed-specific.** A silent no-op on a missed pattern reproduced in - a different language, against a different file, one hour after the paragraph explaining why it is - dangerous was written. That is the strongest available evidence that the population estimated above - is a floor rather than a ceiling: the class does not require the seed/authority seam to occur, only - a substitution whose failure arm widens. -2. **Assert the pattern is PRESENT before replacing.** The edits in this PR that worked did exactly - that; the one that vanished did not. That is the whole difference between them. -3. **A green run is not evidence that a test EXISTS.** A test run reports the tests that are there, - never the ones you meant to add — so a vanished edit and a passing suite are indistinguishable by - colour, exit code, or any single-number summary. Read names and counts. The same shape appears as a - vacuous `0 passed; 0 failed; N filtered out`, and both are caught by ARITHMETIC rather than by - suspicion. - -## Which authorities the seed decodes - -25 unambiguously-attributed `.dag` modules, by decoding site: - -| authority | names decoded | decoder | -|---|---|---| -| `v2.std.live_read` | 10 | `cli_run/live_read_decode.rs` | -| `v2.workflow.floor_diff_observe` | 9 | `cli_run/required_floor_runner.rs` | -| `gunbc.output_policy` | 6 | `cli_run.rs` | -| `std.observation` | 5 | `cli_run.rs`, `cli_run/witness_gates.rs`, `cli_run/test_module_hygiene_bridge.rs` | -| `gunbc.observation_seed_render` | 4 | `cli_run.rs` | -| `tools.dag_compile_clean_scope` | 4 | `cli_run/compile_clean.rs` | -| `gunbc.generated_artifact_emit` | 3 | `generated_artifact_boundary_host.rs`, `behavioral_receipt_host.rs` | -| `gunbc.cli_wire`, `gunbc.test_module_hygiene`, `extdeps.render.terminal`, `std.process`, `v2.std.live_tree`, `v2.workflow.floor_discovery_producer` | 3 each | `cli_run.rs` (+ bridge) | -| `gunbc.githooks_pre_push_plan` | 2 | `pre_push.rs` | -| `v2.workflow.floor_terminal_ledger_wire` | 2 | `cli_run/terminal_ledger_publish.rs` | -| `extdeps.transports.rest`, `std.access` | 2 each | `v1_interpreter.rs` | -| `v2.workflow.wet_evidence`, `v2.workflow.floor2_prepared_subject`, `v2.workflow.floor_route_gap` | 1 each | `cli_run/required_floor_runner.rs` (the #9975 site) | -| 9 further modules | 1 each | `cli_run.rs`, `v1_interpreter.rs` | - -**Load-bearing:** `required_floor_runner.rs`, `compile_clean.rs`, `witness_gates.rs`, -`live_read_decode.rs`, `terminal_ledger_publish.rs`, `generated_artifact_boundary_host.rs` and -`cli_run.rs` are all in the library the required lanes reach through `claim_executor --required-ci`. -`pre_push.rs` is the local hook. I did NOT measure per-site execution coverage; reachability of the -FILE is not execution of the SITE, and that gap is the census's main residue. - -## Instances: one confirmed, one retracted - -**1 — #9975 (`v2.workflow.wet_evidence`), known and standing.** Four spellings, all refused on the -required path, no Rust compile failure. This remains the only confirmed instance. - -**2 — `roadmap_acceptance_event_history`: RETRACTED. Not a defect, and the retraction is the more -useful finding.** `cli_run.rs` calls `run_in_context(&ctx, "roadmap_acceptance_event_history", true)` -and no declaration of that name exists **at HEAD** — `gunbc.roadmap_authority` carries -`roadmap_acceptance_event_history_load` instead. I reported that as a live break. It is not, and the -reason is a defect in my own census METHOD rather than in the seed. - -That decoder's subject is **revision-addressed**, not HEAD. It is the one-time carrier-introduction -bootstrap in `gunbc.roadmap_acceptance_history_observation`: it fires only when -`git_show_path_absent_at_ref` reports the JSONL carrier ABSENT at the merge-base, and the text it -decodes is `git.Core.Show(ref: baseline_sha, path: dag/gunbc/roadmap/roadmap_authority.dag)` — the -merge-base revision's authority, not the worktree's. - -The two facts are complementary, and they were bound in a single commit. At `bfaaf3e4ee7^` (#7791) -`fn roadmap_acceptance_event_history()` exists and the carrier is absent; at `bfaaf3e4ee7` the -carrier exists and the function is gone. That commit both introduced -`dag/gunbc/roadmap_acceptance_event_history.jsonl` and removed the function. So the arm's own guard — -carrier absent at merge-base — implies the old spelling is present in the text it is about to read. -The seed's spelling is correct for exactly the revision set in which the decoder can fire, and -unreachable everywhere else. - -**The probe question, answered.** The `#[ignore]`d test pins -`git show 9ce6526c528:dag/gunbc/roadmap_authority.dag`. `9ce6526c528` is an ancestor of -`bfaaf3e4ee7^`, carries the function, and has no carrier — it is a faithful representative of the -revision class this decoder serves. **The pin is legitimate and must stay.** A "live probe that goes -red on a rename", which is the reflex repair, would be WRONG here: live resolution is not this -decoder's subject, so such a probe would assert a property the code never claimed and would have to -be deleted or exempted the moment it went red. The `#[ignore]` is separately declared (live-corpus, -minutes per test) and is not what hid anything. - -**The class this actually names, and it is a census-method class:** -*a spelling-keyed decode whose subject is a REVISION-ADDRESSED text must be joined against the -revision it reads, not against HEAD.* Joining against HEAD reports every historically-correct -spelling as a break. My scanner had no notion of the decoder's subject revision, so it could not -distinguish "the seed lags the authority" from "the seed reads an older authority on purpose". Any -future instrument over this class inherits that requirement: the join key is (name, subject -revision), not name. - -**Consequence for the population above.** P4 is the only primitive whose sites can be -revision-addressed in this way, and the attribution below shows exactly one such site — this one. The -P1/P2/P3/P5 sites all decode values produced by the CURRENT resolved graph, so the HEAD join is the -right join for them. The retraction therefore narrows to P4 and does not disturb the rest — but the -census reports it because a reader who copies the method would repeat the error. - -**Mint-side residue:** 6 of 170 host-minted type/variant names have no `.dag` declaration at all — -`EmitHostTransportResult`, `FilesystemReadResult`, `TargetModelish`, `PortLocus`, `OtherDiagnostics`, -`UnrelatedDecision`. These are not rename victims; they are shapes the host owns with no corpus -authority, which is the §3 half of the same coupling: nothing on either side can be renamed -*together* because there is no other side. - -## P4 subject-revision attribution (second pass) - -The retraction above established that the join key is (name, subject revision). - -> **THE 68 ARE NOW ATTRIBUTED — see "Closing the 68" below.** The table in this section covers only -> the 42 sites the line-keyed regex found, and its conclusion stays struck, because a conclusion is -> not rescaled onto a population it was not derived over. The 68 were answered separately and by a -> different question. - -The 42 sites the first pass found, attributed by reading each caller's context construction: - -| subject | sites | how established | files | -|---|---|---|---| -| **HEAD / live worktree** (ctx built from `default_source_roots()` / `workspace_root()`) | 17 | context construction reads the live source roots | `required_regen_host.rs` 4, `cli_run/required_floor_runner.rs` 4, `cli_run/materialization_provider_consumer.rs` 4, `cli_run.rs` 2, `cli_run/terminal_ledger_publish.rs` 1, `cli_run/test_module_hygiene_bridge.rs` 1, `derived_realization_schedule.rs` 1 | -| **In-file synthetic source** (subject is the `r#"…"#` literal beside the call; no tree at all) | 24 | the module text and the entry name are authored together in the same expression | `bin/auth_declared_but_unwired_witness.rs` 10, `bin/interp_recorded_fixture_witness.rs` 4, `cli_run/shared_fill.rs` 4, `v1_interpreter.rs` 4, `compiler_tests.rs` 2 | -| **Revision-addressed** (`git.Core.Show` of a merge-base ref) | 1 | `cli_run.rs:680`, the carrier-introduction bootstrap — the retracted candidate | `cli_run.rs` | - -**~~The residue is closed.~~ STRUCK.** Of the 42 attributed sites, exactly one is revision-addressed -and it is the one already adjudicated; the 24 synthetic sites cannot drift against any tree, since -their authority and their consumer are the same expression; and the 17 HEAD-subject names all resolve -today. **All three statements are about 42 of 110 sites.** The remaining 68 have not been attributed, -so the residue is OPEN: it is not known whether any of them is revision-addressed, and no claim is -made that they are not. - -P7's 35 `--function` names are all HEAD-subject and all 11 distinct names resolve today -(`filesystem_write_keystone_holds`, `witness_write_then_read_roundtrip`, -`clock_freshness_keystone_holds`, `env_freshness_keystone_holds`, `diagnostic_redfish_keystone_holds`, -`http_pilot_rest_keystone_holds`, `w_site_label_is_the_module_path_as_package`, and four others), each -in a `dag/test/claim/*.dag` entry named as a literal `--entry` path beside it. - -So of the P4/P7 call-by-name axis, this section attributes 52 sites (HEAD-subject, 0 stale); the -remaining 68 are answered in the next section by a different question, and none is revision-addressed. - -## Closing the 68: the question was re-shaped twice, and the first re-shaping was still a spelling - -The 68 unattributed P4 sites are closed. The method took two attempts, and the failed first attempt is -kept because it is the same defect this document is about, committed for the third time, in the -paragraph that claimed to be immune to it. - -**First re-shaping — right idea, wrong population.** A call site has no subject revision; the -`InterpContext` it runs in does, and many sites share one context. So the question became *which -context constructions are built from revision-addressed source text?* — and the population was given -as "**86** `InterpContext::new` / `make_eval_context` constructions". **That population was itself -discovered by grepping two constructor NAMES.** It is a spelling wearing a mechanism's clothes, and -review 59276 refuted it with a positive hit: contexts are also constructed through -`InterpContext::with_runtime_options` and `InterpContext::over_scope_indexes`. The 86 was a lower -bound presented as a complete population — the exact move retracted twice above. - -**The remedy the review proposed is the weaker of the two available**, and this is the one place this -correction does not simply take the reviewer's instruction. "Expand the census to all constructors" -re-keys on a longer list of names, so a constructor added tomorrow escapes it again. The stable fix is -to anchor on the CONSTRUCTION ITSELF. - -**Second re-shaping — anchor on the struct literals, of which there are three.** - -| carrier | struct-literal construction sites | consequence | -|---|---|---| -| `InterpContext` | **1** — inside `over_scope_indexes`, *derived by reading the constructor chain, not by grepping the brace: `InterpContext *{` returns 20 hits of which 19 are `fn … -> InterpContext {`, since a return type followed by its opening brace is textually identical to a construction* | `new`, `with_fixture_store` and `with_runtime_options` all funnel into it; so does every future constructor, because there is nowhere else to build one | -| `PreparedScopeIndexes` (its only content-bearing input) | **1** — inside `build_scope_indexes_with_module_order`, which takes `graph: &ResolvedGraph` | every context's content therefore comes from a `ResolvedGraph` | -| `ResolvedGraph` | 21, of which 13 are empty test graphs, 4 are the compile path, and **4 are the resolved-graph cache** | the cache is the only one whose bytes are read from disk rather than freshly compiled | - -**The cache is not a third route, and it is refused into that shape rather than assumed into it.** Its -key is `subject_digest_for_closure(sources: &[Rc])` — derived from the sources themselves — -and a header whose stored subject differs from `expected_subject` is REJECTED -(`CacheRejectReason::BackendKeyMalformed`), not served. So a cache hit is content-identical to the -sources the caller already holds; it is a memo keyed on the subject, not an independent origin. - -**The closure, now anchored on constructions rather than on names.** Every context's decodable content -reaches it through one struct literal, from one `PreparedScopeIndexes` literal, from a `ResolvedGraph` -that is either freshly compiled from `SourceFile`s or restored from a cache keyed on those same -`SourceFile`s. And `SourceFile`s arrive by exactly two routes: - -| route | how source text reaches a context | revision-addressed instances | -|---|---|---| -| a disk path under a `--source-root` | including `fs::write` of externally-obtained text into such a directory | **1** — the roadmap carrier-introduction overlay, already adjudicated | -| in-memory `SourceFile { content: … }` / `single_source(…)` | | **0** | - -That one context's call is `cli_run.rs:680`, a single-line site counted in the original 42. **It is not -among the 68, so none of the 68 is revision-addressed.** - -**ORIGIN IS IRRELEVANT, and that is the strength of the argument.** Git, a network fetch, an archive, -synthesis — to be decoded, text must become a context's source, and every origin enters by one of the -two routes above. A fetch composed with a write is not a third route; it is the first route with a -different upstream. - -*An earlier revision bounded this with an observation instead: "no network fetch or archive extraction -exists in this tree". That was FALSE — `v1_interpreter` carries a live `ureq` REST transport, -dispatched by method — and it is the kind of false a reader trusts precisely because it was labelled an -observation, and observations read as checked. It was deleted rather than repaired: it was a sentence -that can rot, guarding a claim that cannot. Recorded rather than silently removed, because the weaker -sentence was written to bound the stronger one and ended up being the only part that could be wrong.* - -**What remains genuinely unknown**, stated as unknown: whether any `.dag` program composes a fetch with -a write into a directory later passed as a `--source-root`. Not found, not exhaustively searched. It -does not affect the closure — that composition is the first route — but it matters to anyone reasoning -about where a context's bytes originate. - -**Why the HEAD-vs-synthetic split within the 68 was NOT enumerated.** It changes no answer: both are -HEAD-time, both are typed by the trigger identically, neither can drift against a revision. It would be -the same completeness-priced-as-completeness declined for the 14 `HelperParameter` rows. - -## P7 re-derived: the line-keyed instrument's error was not one-signed - -P4's re-derivation found 68 sites a line-keyed regex had missed, and the obvious generalisation is -"line regexes undercount." **That generalisation is wrong, and P7 is the counterexample.** Re-derived -with a token scanner that reads the next token after each flag literal — skipping whitespace, commas -and line comments, so wrapping is irrelevant: - -| flag | occurrences | literal next token | literal inside the adjacent expression | computed | spelling-keyed total | census had said | -|---|---|---|---|---|---|---| -| `--function` | 35 | 29 | 1 | 5 | **30** (12 distinct) | 35 (11 distinct) — **over** by 5 | -| `--entry` | 41 | 3 | 5 | 33 | **8** (7 distinct) | 6 — **under** by 2 | - -**Over-counted on one flag, under-counted on the other, from the same instrument in the same pass.** -`--function` was reported as 35 because 35 is the count of the FLAG, and every flag occurrence was -assumed to carry a name; 5 of them take their name from a variable. `--entry` was reported as 6 -because the old query asked whether a `.dag` literal appeared on the following line, which conflates -*adjacency* with *containment*: `ws.join("dag/test/claim/x.dag")` is not a literal next token but it -does carry a spelling, and 5 such sites were missed while 3 truly-adjacent ones were counted. - -So the correct lesson from P4 is narrower than it looked: **a line-keyed instrument's error has no -sign.** It misses what formatting separates and over-claims what proximity suggests, and which one -dominates depends on the syntax at hand, not on the instrument. - -**The assembled-argv blind spot is closed by MECHANISM, not by a zero — and the closure is stated as -ENTRY, not as origin.** The previous entry said argv built as a `Vec` elsewhere would be -invisible "in principle". That is true of the scanner, so the question was re-shaped the way the 68 -were. - -*A first attempt at that re-shaping was incomplete and is recorded rather than quietly replaced.* It -argued that the flag string must be either a LITERAL (all 35 occurrences scanned above) or -SYNTHESIZED (and there is no synthesis — the only `format!("--…` in the hand `.rs` set builds an error -message). **That disjunction has a third arm: a string can be READ rather than written or built** — -from an environment variable, a config file, a JSON or TOML payload, a recorded fixture, or argv -forwarded into the process. Reading is neither a literal nor a synthesis and it leaves no `format!` to -find, so the two-armed version was a closure with a hole in it. - -**The correct form is the one this document already uses one section earlier: ENTRY, NOT ORIGIN.** -Every argv that reaches a subprocess is CONSTRUCTED at one of exactly two sites, and both are wholly -inside the scanned population: - -| spawn site | argv construction | forwarded input | -|---|---|---| -| `pre_push.rs` `run_claim_batch` | `.arg()` chain: `--entry --function `, then `for arg in suffix_args` | ONE caller, passing the literal array `&["--claim-run"]`; `entry` and `function` come from `ActiveGate::DocWitness` read out of the `.dag` plan | -| `bin/interp_recorded_fixture_witness.rs` `run_claim_batch(args: &[&str])` | the caller's array, verbatim | 30 call sites, every one a literal `&[…]` array | - -So a value's ORIGIN — literal, synthesized, or read — does not matter. To become an argument it must be -interpolated at one of those two constructions, where it appears as a non-literal and is classified as -*computed* above. A zero from a grep would have established nothing; this establishes it, and it -establishes it without needing to enumerate the ways a string can come into existence. - -**Two facts of different kinds hold this up, and they must not be read as one.** The ENTRY argument is -STRUCTURAL: a value becomes an argument only by being interpolated at a construction, so it is visible -there as a non-literal whatever anyone does tomorrow. The POPULATION reading is CONTINGENT: that -`suffix_args` has one caller passing `&["--claim-run"]`, and that all 30 call sites of the other -`run_claim_batch` pass literal arrays, is true of this tree TODAY. **A second caller forwarding a read -string breaks the second half silently** — no test fails, no gate fires, and the sentence above stays -green while it is false. Nothing enforces it. Stating the two in one breath would make a rung-1 fact -wear a rung-4 argument's clothes, which is this document's own recurring defect in another costume; -the typed-decoder trigger is what eventually makes the contingent half unnecessary. - -**The computed sites are not hidden spellings, and their origins are named rather than assumed.** The -33 computed `--entry` values and 3 of the 5 computed `--function` values are the P6 scratch-copy paths -built by `unique_fs_witness_entry` / `closure_scale_witness_entry`, whose `.dag` spellings are already -counted under P6. `pre_push.rs`'s `entry` and `function` come from the `.dag` plan itself -(`ActiveGate::DocWitness { entry, function }`, read out of `pre_push_active_kinds`) — **authority-supplied, -so not a Rust spelling and not exposure.** That is the disposition the retracted P4 sentence claimed -for a hundred sites without checking; here it is claimed for four, by reading them. - -## The identifier-preserving shape axis — a real, unmeasured sibling - -Ruled by the design authority and recorded here rather than discovered later. The class is not "every -`.dag` rename silently breaks the seed"; it is **any change to a `.dag` schema element consumed -reflectively by host code is a host↔dag ABI change with no compiler-enforced edge**. A rename is one -trigger. The class also includes changing a qualified function name, changing a record tag, renaming -or NESTING or ADDING or REMOVING or RETYPING a field read by name, adding an arm to a host-side closed -match, changing a serialized wire spelling shared by `.dag` and Rust, and — decisively for this -document — **changing optionality, cardinality or shape while preserving every identifier**. - -**Every primitive P1–P7 is NAME-keyed, so the identifier-preserving shape axis passes all seven and -still breaks the decode. The population measured here is the NAME-KEYED SUBSET and nothing more.** -This is not hypothetical: the one case examined closely in this census (§ instance 2) was half a shape -change — the `.dag` side changed the result from `List` to a `Load` coproduct -alongside the rename, and the seed's `Ok(Value::List(events))` arm would have been wrong even had the -name matched. - -The ceiling below is unaffected, and that is the point: a generated typed decoder makes a shape change -a type error exactly as it makes a rename one. **The ceiling is right; only the census is narrow.** - -## Rung and ceiling - -**Rung found at: 1 (mitigatable), and the honest reason is better than expected.** Every P1/P2 decode -site I read fails CLOSED — the catch-all arm is a typed `Err` naming the expected type, not a -default (`require_permitted_transport`, `discover_floor_corpus_rows_from_host_facts`, -`expand_explicit_pairs_or_refuse`, the `live_read_decode` chain). So the class is NOT -silent-wrong-answer. It is **silent at compile time and loud only where executed**, and #9975 is the -evidence: four spellings refused on the required path with nothing failing to compile. - -That refinement changes what the class COSTS and therefore how it ranks. A silent-wrong-answer class -is priced by the harm a fabricated output causes downstream; this one is priced by the delay between -a rename landing and the first execution of the affected path — the cost is deferred detection, not -corrupted output. It ranks above a merely cosmetic class and below a fail-open one. - -**Compiler-silent, not necessarily execution-silent.** It is loud and correct when the decoder -actually runs. It becomes operationally silent by four routes: the path does not run; a fixture -bypasses it; a default absorbs the mismatch; or a stale artifact answers instead. (Adopted from the -design authority's ruling, which reached this independently of the code reading above.) - -A caution that falls out of the retraction above: because the class is loud-when-executed, the -temptation is to hunt it by static join, and a static join over the WRONG subject manufactures -findings. That happened once in this census, to me, in the space of one pass. - -**Attainable ceiling: 3, structurally guaranteed, and possibly 4.** The class is decidable and the -authority already exists: the seed EMITS Rust mirrors from `.dag` today. If the decode consumed a -generated mirror type instead of a spelling, a rename would be `rustc` E0433/E0599 — construction, -not a check, and it climbs the mint side at the same time (a generated constructor cannot name a -field the type does not have). - -**Next-rung trigger — the CAPABILITY, not an artifact:** *emission of a typed decoder -(`Value` → mirror type) and a typed constructor for every `.dag` type the seed decodes or mints, -sufficient to replace every P1–P7 and mint site above, and to make an identifier-preserving shape -change a type error as well as a rename, such that no hand-written spelling of a `.dag` -type, variant or field name survives in `src/v1/stage0/src`.* Partial emission does not retire the -class: any site left on a spelling keeps its own exposure, so the trigger is stated over the -population, not over one module. - -**The trigger needs an explicit version-boundary arm, and without it it is UNSATISFIABLE.** A -generated decoder binds the type as it exists at HEAD. A site that decodes REVISION-ADDRESSED source -text must read the shape that revision had, so typing it against HEAD is not safer — it is wrong, and -it is wrong in the direction that produces a confident answer about the wrong schema. One such site -exists (the roadmap carrier-introduction bootstrap) and the closure above establishes it is the only -one, but one is enough: as originally worded, "no hand-written spelling survives" cannot be satisfied -there, because there is no HEAD type to bind to. - -So the trigger carries a second arm: *for every revision-addressed decode, either a decoder bound to -the revision's own schema, or a TYPED REFUSAL at the version boundary — never a HEAD-typed decoder -applied to older text.* This matters more than its one-site population suggests, because §4b(3) makes -the trigger the whole check: **an unsatisfiable trigger is worse than a weak one.** It can never be -retired, so the class parks below its ceiling permanently behind a row that still reads live — which -is rung inflation arriving by a different door than the usual one. - -**What is explicitly NOT the answer.** A lens or test that greps the seed for spellings and joins -them to `.dag` declarations — i.e. an executable version of this document — is validation standing -where construction is available, and it would additionally inherit the bare-name ambiguity noted -above (`Refused` ×7). It belongs only as the interim mitigation if the emission capability is -deferred, and then as a declared §4b(3) row, not as the climb. - -## Residue this census did not close - -- Per-site execution coverage. File reachability is not site execution; the P4 entry names refuse - loudly *when run*, and I did not establish which run. This is the residue that matters most: it is - what separates a spelling that is stale from a spelling that is merely unexercised. -- **The identifier-preserving shape axis, in full.** Named above, deliberately not measured in this - pass. It is the largest single gap between this document and a closed answer. -- **The transitive origin of the 16 `HelperParameter` rows** in the P4 enumeration. Two were traced; - the rest move the question one frame up and are not answered. DELIBERATELY not closed: the typed - decoder types literal-origin and `.dag`-origin names identically, so the distinction dissolves when - the trigger lands, and completing the inventory of a population scheduled to stop existing is - completeness priced as completeness. -- **The HEAD-vs-synthetic split within the 68**, for the same reason — it changes no answer. -- Whether any `.dag` program composes a fetch (the live `ureq` REST transport in `v1_interpreter`) - with a write into a directory later passed as a `--source-root`. Not found, not exhaustively - searched, and stated as unknown. It does not affect the closure — that composition is route A with a - different upstream — but it matters to anyone reasoning about where a context's bytes originate. -- **P3 and P5 re-derivation.** P1, P2, P4 and P7 are done; P3 and P5 carry line-keyed counts and are - floors with an unmeasured gap — and after P7, "floor" is the wrong word: the gap has no known sign. -- P6's fail-open arm is described but not sized beyond its 5 substitutions in 2 producers; - `str::replace` against `.dag` text elsewhere in the tree was not swept. Given the reframing above, the wider sweep is the - substitution-whose-failure-arm-widens class, not the `.dag`-text subset. -- Two findings about the REVIEW instrument surfaced while this census was in flight and are recorded - separately, in [review-instrument-observations.md](review-instrument-observations.md), because they - are facts about the tooling rather than about the seed. -- The `.dag` FILE-PATH axis: 420 distinct `"*.dag"` string literals in hand `.rs`, most synthetic - fixture paths. A path rename is the same spelling-keyed coupling in a different alphabet; I did not - separate the live paths from the synthetic ones. -- The mirror-emitted files were excluded by construction. That exclusion is sound for THIS class and - is not evidence about any other. diff --git a/docs/plans/service-interface-and-uses-carriers.md b/docs/plans/service-interface-and-uses-carriers.md deleted file mode 100644 index 4c24cfe1588..00000000000 --- a/docs/plans/service-interface-and-uses-carriers.md +++ /dev/null @@ -1,150 +0,0 @@ -# XL-2: service interface, realization binding, and `uses` — carrier design - -Status: design only, no behaviour change. Owner: XL-2 (quiet-seal-543). Decisions recorded below were ruled by the XL-2 manager on 2026-10-01. -Governs: `gunbc.recurring_failure_mode` `service_interface_member_has_no_carrier`, `gunbc.recurring_failure_mode` `uses_clause_has_no_carrier`, and the two `v2.workflow.compile_door_cause_ownership` rows for `body_lowering_reason_service_realization_unreachable` and `body_lowering_reason_uses_clause_unmodeled`. -History: #12277 (declaration-grade service lowering), #12816 (`uses` parses and refuses at the clause). - -## The question - -Two refusals in `v2.compiler.body_lowering_fold` keep whole modules out of the XL-2 reference census, which reads the **full** normalize route, not census grade: - -- `body_lower_service_decl` refuses every service carrying a set-aside member as `body_lowering_reason_service_realization_unreachable`. -- `body_lower_fn_uses_refusal_optional` refuses every fn carrying a `uses` clause as `body_lowering_reason_uses_clause_unmodeled`. - -For each member, this document asks what fact it is, who consumes it, and which existing authority already owns it. It then gives the smallest lowering change after which every member either lowers into its carrier or refuses at a located diagnostic. No member is dropped. - -## Population (measurement, to be re-derived) - -Measured by a source grep at the base of this change. This is a sizing, **not an oracle** (DESIGN §5). The authoritative population is the set of modules the CI census run refuses under each reason, re-measured after each PR below lands. - -| Subject | Modules | Note | -| --- | --- | --- | -| declares a `service` | 114 | all refuse whole on the full route today | -| — with a realization member (`transport` / `config` / `exit` / `response` / `mock_response`) | 111 | | -| — with only interface members (modifiers, io tails) | 3 | | -| fn or pattern header carrying `uses alias: Type` | 30 (97 rows) | no overlap with the service modules; no row reads its alias in the body | - -**Consequence:** an operation-modifier carrier alone, the trigger the failure-mode row named, unblocks 3 modules. The service frontier is the realization members. The `compile_door_cause_ownership` flip trigger already said so, and this design adopts that sentence as the scope. - -## Inventory: what each member means - -### Service members - -| Member | Fact | Layer (DESIGN §3) | v1 semantics | Downstream consumer | -| --- | --- | --- | --- | --- | -| `operation Op { input {…} output {…} }` | an arrow of the interface | interface | operation signature | resolve (call targets), infer (call typing). **Lowered today.** | -| io field `= default` | the value an omitted input takes | interface | `v1.compiler` `make_field_node` `default_value` | infer (call-site arity: a defaulted field may be omitted), eval | -| `readonly` | a declared claim that the operation's effect shape is `ReadEffect` | interface (a claim about the arrow's effect) | `OperationModifier.Readonly` (`v1` `00_core` `field_init_operation_modifier`) | `std.effects` `check_modifier_vs_derivation`; effect_demand (a read never needs a write grant) | -| `idempotent` | a declared claim that the effect shape is idempotent | interface | `OperationModifier.Idempotent` | `std.effects` `check_modifier_vs_derivation` / `is_idempotent_effect`; retry admission (DESIGN §4b names a non-idempotent effect under retry) | -| `hermetic` | a declared claim that the operation is admissible on the hermetic execution mode | interface (execution-mode axis, not an effect shape) | `OperationModifier.Hermetic` | `std.execution_mode` `Hermetic`; effect_demand keys mode as part of the demand | -| io field `from "key"` | which wire field fills a declared output | **realization** — a decode projection of one transport's observation | `field_node_from_key` | the transport's output projection (target_model / eval). It is not interface: another transport of the same shape need not have a wire key | -| `transport shell {…}` / `rest {…}` / `file` / local | which handler realizes the operation | realization | `classify_transport` | `std.effect_grant` `HandlerBinding` (`RealTransport`) via `extdeps.transports.shell` `ShellTransportConfig` / `extdeps.transports.rest` `RestTransportConfig` | -| `config {…}` | handler configuration at service grain | realization | service-level transport defaults | the same transport config, applied to each operation | -| `exit {…}` / `response {…}` | how an observation (exit status, streams, body) projects onto declared outputs | realization | output projection of the observation | the interpreter's declared-output projection (`v2.std.operation_realization` feeds it a `TransportObservation`) | -| `mock_response {…}` | one published response arm of one operation | realization (a hermetic-replay handler) | published mock | `std.hermetic_replay` `PublishedMockCase`, selected through `HandlerBinding.Replay` | - -### `uses alias: Type` - -| Fact | v1 semantics | Owning authority today | -| --- | --- | --- | -| the resources a fn's body demands | `v1` `04_items` `ResourceRequirement` (`binding_name`, `resource`), consumed by the Rust emitter's call-site binding | **D13** (`gunbc.plans.demand_engine_program`): for a transparent body, demand is DERIVED (`DependencyDemand`, produced once by resolution). Authored restatement rows are deleted (USES-0, `docs/plans/uses-occurrence-census.md`). Binder, opaque-contract and policy rows move to their own carriers. Derivation does not yet name Network (`gunbc.rung_drop` `network_requirement_unrepresented_after_uses_cut`) | - -## One carrier per fact - -### Interface: carried on the operation's Arrow and on its input binders (ruled 2026-10-01) - -The first draft of this section put modifiers as edges on the operation node and defaults on the payload field. Neither is admitted by the substrate: `v2.std.node` `arrow_signature_edges_conform` counts every Arrow named edge other than the body and the declared order as the one type-binder edge, and a payload field is a bare name-to-type edge with no slot for a default. The side-chat ruling (option A) places them: - -1. **Effect and execution-mode claims are Arrow contract edges, kept apart.** `readonly` and `idempotent` are effect claims, checked against the derived effect shape by `std.effects` `check_modifier_vs_derivation`. They lower onto `^arrow_effect_claims_edge`. `hermetic` is a `std.execution_mode` claim and lowers onto `^arrow_execution_mode_claim_edge`. The surface `OperationModifier` stays one syntax vocabulary, and lowering projects each arm to its semantic home. Arrow conformance enumerates the legal labels and target shapes. Duplicates and unknown labels refuse. Contract edges are not binders, and canonical identity is order-independent. -2. **A default belongs to the binder, not to its type.** One binder representation (a required type, an optional single default, closed labels, one reader and builder) is reused by fn parameters, service io fields and record fields. Because that changes the existing name-to-type binder shape, it is a replacement migration (DESIGN §3): every producer and reader moves, and the two shapes never coexist. - -Once these carriers exist, `InterfaceMemberUnmodeled` has no producer for modifiers or defaults. Wire keys move to the realization carrier (below), so that set-aside kind is deleted, not left empty. - -### Realization: a sibling node, never inside the interface - -Per DESIGN §3, transport is one of N handlers bound to the interface shape, and "the dispatch that selects a realization is itself realization". So the realization members lower into a **sibling** of the service, never into its interface. - -**Correction (PR3a, 2026-10-02).** The first draft said `transport` / `config` lower "onto the transport's existing config record" (`ShellTransportConfig`, `RestTransportConfig`, `FileTransportConfig`). They cannot. Those records are the host's runtime configuration (working directory and environment; base URL, credentials and TLS posture; base path). No operation authors them, and none carries what an operation does author: shell `argv` / `stdin`, rest `method` / `path` / `query` / `headers` / `body`, file `path` / `verb`. Lowering onto them would be a meaning fork. `PublishedMockCase` is a key with no body. The rulings (XL-2 manager, 2026-10-02): - -1. PR3 splits into 3a (model), 3b (lowering) and 3c (cut). -2. Declared bindings live per kind in `extdeps.transports.`, and the runtime configs are not forked. -3. Infer typing of exit, response and mock arms against the declared outputs comes later. In PR3, infer refuses at the sibling under its own owned cause. -4. The sibling attaches by **MIRROR**. - -**MIRROR.** Each service emits, beside its interface: - -``` - -> { shell -> { Find -> LEAF } } -LEAF = Conj { Run -> ENTRY, ..., -> CONFIG? } -ENTRY = Conj { -> TRANSPORT, -> ARMS?, -> ARMS?, -> ROWS? } -``` - -- **The mirror keys each entry by the operation's path, structurally.** That is the same path `v2.std.operation_argv` `OperationRef` names at run time, so no second identity is minted. -- **Prefix segments reuse the spine's marked namespace bodies.** Services sharing a prefix therefore merge through the spine's one merge rule, with no new graft rule. -- **The root is unspellable, so no source can name an entry.** `v2.compiler.symbol_index_fill` indexes nothing beneath it, so an entry is not a declaration and can neither collide with nor shadow its operation (ruling condition 1). -- **Each operation has at most one entry, and an entry names a declared operation** (ruling condition 2, enforced by the wall). That an operation authoring realization facts has an entry is the lowering's obligation (3b). - -**3a grounding: what each vocabulary reuses rather than mints.** - -| Fact | Home | Reused or added | -| --- | --- | --- | -| transport kind | `std.fidelity` `TransportClass` (`ShellLocal`, `RestNetwork`, `FileBoundary`, `LocalDirect`) | reused; `local` has no declared binding and refuses as unmodeled | -| response status code | `std.types` `HttpStatus` (100–599) | reused | -| response status class (`5xx`) | `extdeps.ietf.http_semantics` `HttpStatusClass`, RFC 9110 §15 | added beside `HttpMethod`; the IETF fact, not the transport's | -| exit status | an integer (`extdeps.process.posix_exit` rows); `extdeps.transports.shell_declared` `ShellExitPattern` = `ShellExitCode { code }` \| `ShellExitNonzero` | an exit status is never passed through an HTTP type | -| shell `from` key | `extdeps.transports.shell_declared` `ShellOutputChannel` | completed: it had no consumer and lacked `exit_code` (135 rows); transcribed from the seed's `ShellResultChannel` / `shell_result_channel_of_key` | -| file `from` key | `extdeps.transports.file_declared` `FileOutputChannel` | added; transcribed from the seed's `FileResultChannel` / `file_result_channel_of_key` | -| rest `from` key | open (RFC 8259 member names) | no closed vocabulary | -| declared binding fields | `ShellBindingField`, `RestBindingField`, `FileBindingField` in each kind's module | added; closed by the corpus, and an unknown field refuses | -| HTTP method | `extdeps.ietf.http_semantics` `HttpMethod` | reused | - -**What the full route does with the sibling.** -- **Resolution** resolves its references, which is what puts them in the census. -- **Each stage that cannot yet consume the sibling** refuses at it under its own cause. The old blanket `service_realization_unreachable` therefore becomes per-stage located causes, and once no member is set aside, `ServiceSetAside` and both set-aside reasons are deleted (3c). - -**3b depends on an unrecorded language gap**, now `gunbc.recurring_failure_mode` `string_interpolation_read_as_literal_text_by_v2`. Most transport strings interpolate an input (`"{repository_path}"`), and v2 reads that as literal text. Until v2 lowers interpolation, 3b refuses each interpolating transport string, located. - -### `uses`: no carrier (ruled: follow D13), with a prerequisite - -A v2 carrier for the authored row would be a second authority for `DependencyDemand`, the fact D13 rules DERIVED for every transparent body. **But D13's derivation is not yet complete for Network.** `gunbc.rung_drop` `network_requirement_unrepresented_after_uses_cut` records that derived demand (`ItemInfo.service_names`) names the services a Network effect reaches and never Network itself, and that earlier deletions of `uses net: Network` rows were a silent rung loss. The D13 follow-up audit found that nearly every live row binds Network. So a Network row is **not yet a restatement**, and deleting it now would repeat that loss. - -Resolution of this half: - -- **The refusal stays.** `body_lower_fn_uses_refusal_optional` keeps refusing at the clause. Until the prerequisite lands, the authored clause is the only carrier of a Network requirement, so a fn carrying one stays out of the census rather than lowering without its requirement. -- **Prerequisite (the rung drop's restoration trigger):** D13's `DependencyDemand` carrier derives each resource requirement keyed on the resource declaration's identity (`std.resources.Network`), so that the derived demand of every function authoring `uses net: Network` names Network. -- **Then** the restatement rows are retired under D13's criterion, measured by resolution, not grep. -- What could survive is a named dependency binder (a distinct subject), an opaque contract (no body) or a policy envelope. Each moves to its own carrier when one first appears. The live corpus has none, so none is designed here. - -## The smallest lowering change, member by member - -| Member | Lowers into | Refuses (located) when | -| --- | --- | --- | -| `readonly` / `idempotent` | `^arrow_effect_claims_edge` on the operation's Arrow | repeated on one operation | -| `hermetic` | `^arrow_execution_mode_claim_edge` on the operation's Arrow | repeated on one operation | -| io `= default` | the input field's binder default (2b) | the value reader refuses the expression | -| io `from "key"` | projection row in `^service_realization_binding` | the key is not a string literal | -| `transport` / `config` | transport config record in the sibling | the transport kind has no config record | -| `exit` / `response` | output projection in the sibling | the value reader refuses the expression | -| `mock_response` | `PublishedMockCase` row in the sibling | the case has no operation it can key on | -| any other member | — | `body_lowering_reason_service_member_unread` (unchanged) | -| `uses` entry | — (no carrier) | always: `body_lowering_reason_uses_clause_unmodeled` (unchanged) | - -## Rollout - -| PR | Content | Unblocks | Behaviour change | -| --- | --- | --- | --- | -| 1 (this) | this document; `uses_clause_has_no_carrier` trigger amended to D13; DESIGN CLI-section sentence amended (separate hunk) | — | none | -| 2a | Arrow contract edges (`^arrow_effect_claims_edge`, `^arrow_execution_mode_claim_edge`) and their conformance in `v2.std.node`. **DESIGN §3c declared frontier** (see below) | — | substrate only | -| 2b | the one binder representation with an optional default: a replacement migration of every binder producer and reader, measured first | — | yes | -| 2c | lowering of modifiers and io defaults onto 2a and 2b; discriminating red per refusal row above | the 3 interface-only services | yes | -| 3a | the realization sibling's model: per-kind declared-binding vocabularies, `HttpStatusClass`, the MIRROR shape and its conformance wall (`v2.compiler.service_realization`), and the symbol-index arm. **DESIGN §3c declared frontier** (trigger: 3b) | — | substrate only | -| 3b | lowering of transport / config / exit / response / mock_response / `from` into the sibling; the wall runs on every lowered service | the realization-bearing services whose strings do not interpolate, up to the next stage's refusal | yes | -| 3c | delete `ServiceSetAside` and both set-aside reasons; per-stage located causes for any stage that cannot yet consume the sibling | — | yes | -| 4 | flip `service_interface_member_has_no_carrier` and the cause-ownership rows; re-measure the census on the CI population | — | ledger only | -| `uses` (separate lane) | first the resource-keyed `DependencyDemand` carrier (the rung drop's restoration trigger); only then retire restatement rows by D13's criterion, measured by resolution | up to 30 modules, and none before the carrier lands | carrier, then source rows deleted | - -No model lands in PR 1. - -**2a is a DESIGN §3c declared frontier, not dangling.** In 2a, its consumers by execution are the conformance wall itself (`v2.std.node` `arrow_signature_edges_conform`, reached by every `well_formed` check of an Arrow) and the discriminating test that exercises it. Its *production* consumer is 2c: the operation-modifier lowering in `v2.compiler.body_lowering_fold` `body_lower_operation` emits the two edges, and the resolve, infer and translate readers gain their metadata arms in the same change. **Transition trigger:** 2c lands. If 2c is abandoned, the two edges and their conformance are deleted, not left standing. 2a lands ahead of 2c because the ruling asked for the substrate change to be reviewable on its own. That separation is the stated reason for the frontier. A model with no consumer in its own change is dangling by DESIGN §3c. - -**Caveat, stated so it is not read as a promise.** No service has ever reached resolve or infer on the full route. PR 3 will surface new downstream refusals, located at the sibling or the operation under the consuming stage's cause. The count of services that reach the census is therefore re-measured on the CI population after PR 3, not inferred from the grep above. diff --git a/docs/plans/serving-front-door.md b/docs/plans/serving-front-door.md deleted file mode 100644 index fe93742f634..00000000000 --- a/docs/plans/serving-front-door.md +++ /dev/null @@ -1,93 +0,0 @@ -# Serving front door: closing the second door onto the serving groups - -Item 3 of [dogfood-route-manual-interventions](dogfood-route-manual-interventions.md). The decision home is `gunbc.serving.serving_front_door`. The redemption chain's home is `product.capacity.redemption`, realized in `gunbc.fabric_event_log`. - -## What landed in this change - -- **The permit** (`ServingSeatPermitClaims`). It holds the claims a successful `gunbc.harness.harness_seat` `harness_bind_seat` already has: - - the group, the exact seat partition and the co-tenancy class; - - the structured seat reference (`HarnessSeatReference`: attempt = work identity, offer key = launch, stamp, round, head); - - the `product.capacity.lease` grant identity and fence generation; - - the caller principal and the lease term. -- **The signed representation is injective.** `serving_seat_permit_signing_input` length-prefixes every field, including the protocol, which is taken from the claim itself. As a result: - - No content in one field can move a boundary into its neighbour. - - A protocol rewritten after signing fails the MAC join. - - Issuance refuses an unexpected protocol. - - The permit header's decoder (owed under Proposal A) must consume exactly this encoding. -- **The ledger join is on the exact seat pool.** The door is configured with `HarnessSeatPoolIdentity` values minted by the seat authority (`harness_seat_pool_identity`: group, class, partition, ceiling, root). It then checks, against the ledger: - 1. The permit's partition equals the pool partition for its class. A prefix match is not accepted, so `group-b-not-a-seat` refuses. The selected pool must belong to the door's own group, and two configured pools for one class refuse rather than one being chosen. - 2. Exactly one acquisition of the reference exists, for exactly one seat. - 3. That acquisition was made by the grant event the permit names. - 4. The fence generation equals the acquisition's position in the chain. - 5. The partition folds under the pool's own root and ceiling. - 6. The seat has not been released. - 7. The recorded actor equals the authenticated caller. - 8. The recorded acquisition time plus the recorded term is still in the future. -- **Admission is a transition, not a read** (`product.capacity.redemption`): - - **Redeem.** A seat moves `Unredeemed → InFlight { request, holder } → UseEnded`. The front door appends the redemption to the seat's redemption chain, beside its pool partition on the same fabric event log, by compare-and-set (`gunbc.fabric_event_log` `fabric_grant_redeem`). Only an admitted redemption forwards. - - **Concurrent and replayed admissions.** A second admission of the same permit, concurrent or replayed, re-reads `InFlight` and refuses (`permit-already-redeemed`). - - **Release while in flight.** Every seat release passes the redemption gate in `release_retry`, so a sender's release while a request is in flight refuses. - - **Stream end.** The door ends the use (`fabric_grant_use_end`, exactly once per request) and then releases. - - **Recovery.** A door that dies after redeeming leaves `InFlight` standing. The seat stays held and is charged to the redeemed request and proxy. It is ended only on *observed* quiescence (`front_door_recover`), never by a timer. Recovery releases only `UseEnded`; a seat that was never redeemed (`Unredeemed`) is left with its holder. -- **Minting.** `gunbc.harness.harness_cli` `harness_place_for` mints the permit after the seat is bound and records the principal it acts for as the acquisition actor; it no longer takes a free-text actor. If no permit can be minted, the seat is released and placement fails. -- **Counting.** `front_door_tally` keeps one count per refusal cause, plus the number of forwarded requests. -- **Router contract.** `gunbc.serving.router_realization` gains the forbidden arm `ForwardWithoutPermit`. -- **Witnesses** are in `test.claim.serving.serving_front_door_witness_test` (35 claims). They include: - - the RED-first case; - - each pool-join red (non-seat partition, another group's partition, class/partition mismatch, unserved class, zero amount, duplicate acquisition, wrong fence generation); - - a protocol rewrite; - - the ruling's outer-field collision; - - all five redemption controls: two concurrent admissions give exactly one forward; a replay refuses; a sender release while the stream is in flight does not free the seat; stream end permits exactly one release; controller death leaves recoverable, charged state. - - Mutation runs disabled each new wall in turn, and its witnesses went red: the separator-joined signing input, the unsigned protocol, partition equality, the amount check, redemption ignoring state, and in-flight release. - -## Proposal A: the forwarding realization (NOT applied; needs bold-bee-114's go) - -**Constraint.** The interpreter is never on the token stream. Admission, which includes the redemption, runs **once per request**, and the body then streams without interpretation. - -**Shape.** Use an auth-subrequest. A stock reverse proxy listens on the public `:30000`. For each request it makes one subrequest to a decision endpoint, which evaluates `front_door_admit_on_store` with the request's identity and the proxy's identity. Only a 2xx response streams to vLLM on loopback. The stream's end, whether complete, client-aborted or upstream-failed, must reach `front_door_stream_end` exactly once for that request. If the proxy cannot guarantee that hook, the seat stays `InFlight` and is recovered only on observed quiescence. - -**Owed before Proposal A can be applied:** -1. **A measured admission latency budget.** Measure p50 and p99 of the decision endpoint, which now includes one compare-and-set, against group-b's time-to-first-token. -2. **The permit header wire form and its decoder.** It must decode exactly the length-prefixed signing encoding. -3. **The stream-end hook and the quiescence observation** that `front_door_recover` consumes. One candidate is vLLM's per-request state, observed through `gunbc.serving.engine_progress`. -4. **Tally persistence.** - -**Consumption.** Until Proposal A lands, `front_door_admit_on_store`, `front_door_stream_end`, `front_door_recover` and `front_door_access_decision` have no production caller. This is a declared frontier, and its trigger is Proposal A's go. The redemption gate in `release_retry` *is* consumed today, by every harness release. - -## Proposal B: the host change (NOT applied; needs bold-bee-114's go, because it takes group-b's traffic) - -On each serving group's head (group-b: 192.168.1.236): -- vLLM binds to `127.0.0.1`. -- The front door takes `0.0.0.0:30000`. -- The permit key is materialized at `serving_seat_permit_key_path`, readable only by the door and the placement authority. - -This is converged through the existing `gunbc.spark` serving deployment authority. The door and the loopback rebind move in one step. - -**Owed population before Proposal B (rollout correction from the ruling).** Only `harness_place_cli` mints a permit today. The probe, worker, reviewer, auditor and supervisor paths in `gunbc.harness.harness_cli` acquire a seat and then call `harness_run_turn` with the URL, ignoring the seat record. Their acquisition actors are free-text role identities (`worker:`), not the authenticated principal the door requires. Moving vLLM to loopback before they change would refuse the factory's own harness traffic. Each of these paths must do one of two things: -- **(a)** Mint and present a permit under a principal the door authenticates, record that principal as the actor, and end its use at the door. -- **(b)** Use an explicitly modeled internal route that a bypasser cannot reach. - -Choosing between them is part of Proposal B's go. - -## Credential: the permit key - -- **Key id:** `serving-seat-permit-2026-10` (HMAC-SHA-256). With HMAC, the verifying key is also the issuing key, so custody is the boundary. -- **The ledger and the redemption chain are the authority; the MAC only protects the claims.** A holder of the real key cannot forward by minting: - - A permit for a seat that does not exist, sits in another pool, belongs to another caller or another launch, claims another fence generation or stretches its own expiry is refused on what placement recorded. - - A permit for a seat it does hold forwards at most one request at a time. -- **What the caller check can tell apart today.** There is one service principal (`principal:gunbc/workflows/fabric`). So the check separates humans from the service, but not one service sender from another. Per-sender principals are credential issuance, which belongs to the managed-identity row (#13068), and this change does not fork it. -- **Issuance, rotation and revocation drill** are owned by `credential-lifecycle-revocation` (gunb-ai/gunbc#13068). This key is registered there as a member that row must cover. - -## Migration note: what the existing senders must change (stays manual) - -- **ctrl dashboard router.** - - Delete its load score and its host selection. - - Per request, call `harness_place_cli` and present the permit, in the header form fixed under Proposal A item 2. - - Do **not** release a seat that the door has redeemed. The door releases at stream end, and a sender release refuses while the request is in flight. A seat that was placed but never forwarded is still released with `harness_release_cli`. - - Authenticate as Fabric service evidence. -- **ac-dialogue batch.** - - Stop POSTing raw requests. - - Take one seat per in-flight request and present that seat's permit. - - A batch wanting N concurrent requests holds N seats. A permit is redeemable once, so one permit cannot carry two requests. -- **Which identities may request grants.** This stays an operator decision at placement. diff --git a/docs/plans/shared-pool-byte-correction.md b/docs/plans/shared-pool-byte-correction.md deleted file mode 100644 index 50acad00c97..00000000000 --- a/docs/plans/shared-pool-byte-correction.md +++ /dev/null @@ -1,151 +0,0 @@ -# The shared-pool byte correction (owner directive 2026-09-26) - -Amends docs/plans/moa-memory-modeling.md. The 738/1035 block result is -coherent; the 125,010,432-byte result beside it was dimensionally wrong by -~106× and is withdrawn (it becomes a RED control). - -## The defect - -The allocation-plan fold computed: - -```text -per-seat bytes = Σ group.blocks_at_requested_length × group.page_size_bytes -``` - -But `spec.page_size_bytes` is the page represented by that cache-group -specification — NOT the physical byte cost of taking one block ID from the -shared pool. At the pinned revision vLLM derives a separate physical pool -quantity (`_get_kv_cache_bytes_per_block`): for GLM-5.3-Flash the shared pool -block size comes from the packed MLA and index pages; `num_blocks = -available_memory // bytes_per_block`; the worker allocates one backing tensor -of `bytes_per_block × num_blocks`, and cache groups OVERLAY that allocation — -a block ID is owned by one group at a time. The check: 738/1035 blocks is -71.30% of the pool; against TP3's 17.29 GiB arena that is ~12.33 GiB of -block-equivalent capacity, not 0.116 GiB. The fold implied ~169 KB/block vs -the ~17.1 MiB/block the arena and inventory imply. - -## The corrected model - -```text -KvAllocatorCarve — physical resident pool allocated at startup -KvAdmissionDemand — logical blocks required by the promised population -``` - -(Calling both "KV bytes" is what enabled the mistake.) - -The allocation plan must carry: - -```text -allocator_blocks -physical_pool_bytes_per_block -allocated_pool_bytes -``` - -with the old per-group field renamed `group_spec_page_size_bytes` (and -`group_blocks_per_max_request`) so it cannot be read as the shared pool block -size. Conservation controls: - -```text -allocated_pool_bytes == allocator_blocks × physical_pool_bytes_per_block -allocated_pool_bytes <= every rank's admitted KV arena -arena − allocated_pool_bytes < one physical pool block - (modulo block override, null-block reservation, profile rounding) - -request_blocks = Σ exact group blocks at requested length -request_block_equivalent_bytes = request_blocks × physical_pool_bytes_per_block -``` - -The 125,010,432 result is a RED control: 738/1035 blocks cannot occupy <1% -of the same pool. - -## The verdict structure - -The block wall is authoritative; the byte wall is a conservation CHECK on the -pool's construction, not an independent capacity answer: - -```text -PoolConstructionProved { physical_pool_block_bytes, allocator_blocks, - allocated_pool_bytes, per-rank supply } -RequestBlockCostProved { exact group roster, blocks_per_request } -CacheAdmissionProved { request_blocks <= usable_allocator_blocks } -``` - -`CacheFitProved` carries: limiting_rank, seats, length, blocks_per_seat, -blocks_used, blocks_reserved, hard_ceiling. If bytes appear, they are named -`block_equivalent_bytes` — never ordinary memory demand or physical headroom. - -## The cache frontier (block wall) - -| Seats | Blocks used | Blocks left | Reserve | -|---:|---:|---:|---:| -| 6 | 738 | 297 | 28.70% | -| 7 | 861 | 174 | 16.81% | -| 8 | 984 | 51 | 4.93% | -| 9 | 1,107 | −72 | REFUSED | - -These are shares of the already-allocated block inventory, NOT additional -process-memory allocations (vLLM allocates the whole pool at startup). - -## Reserve policy — hard ceiling ≠ guaranteed service - -```text -CacheReservePolicy - = NoOperationalReserve - | ReserveBlocks { blocks } - | ReserveFullLengthSeats { seats } - | ReserveBasisPoints { basis_points } -``` - -Standings: hard cache ceiling 8; normal guaranteed seats selected by policy; -burst/cache-only ceiling 8. First production policy candidate -`ReserveFullLengthSeats { seats: 1 }` → **7 normal 262K seats**. Six remains -the conservative restoration point (28.7% reserve). Eight is not advertised -as normal guaranteed service until transient and SLO proofs support it. - -## Mixed-length service (beyond 8 sessions) - -The uniform "every seat may reach 262K" promise caps at 8. More ACTIVE -sessions are possible when not every session reserves 262K: - -```text -SeatClass { max_context_tokens, guaranteed_count } -ServingCapacityIntent { guaranteed_classes, max_active_sequences, - max_batched_tokens, cache_reserve_policy } -``` - -Admission prices the actual reservation population through the same -allocation plan (fixed KDA per-sequence, length-growing MLA, pooled KPool, -fixed tail, grouping/padding) — NO second seat calculator. Until a real -request-admission boundary enforces the mixed budget, `max_num_seqs = N` -exposes N slots that may ALL submit maximum-length requests, and the safe -proof prices that maximum behavior. - -## Cache fit is still not the whole arm - -The bounded run established initialization + per-rank profiles + the plan; it -did NOT execute six simultaneous 262K requests. Before promoting seven or -eight: (1) pinned-source prefill derivation at the deployed revision; -(2) profile-shape binding (a 6-seat activation/graph receipt does not prove -an 8-seat profile); (3) a bounded falsification run at the derived worst -legal prefill/decode shape; (4) service capacity/SLO evidence (throughput, -latency, preemption, failure behavior). Final result shape: - -```text -ServingCapacityPoint { workload_intent, cache_standing, prefill_standing, - decode_standing, static_residency_standing, - reserve_policy, slo_standing } -``` - -— a Pareto frontier, not one magic tuple. - -## Operational sequence (owner-set) - -1. Interrupt the byte-wall completion (done). -2. Land the shared-pool byte correction + RED control. -3. Recompute the exact cache frontier (6/7/8 by one fold). -4. Close the 262K prefill obligation for six seats. -5. Restore persistent serving at 262K × 6 with the planted-residue - convergence proof and exact readback. -6. Evaluate 262K × 7 under one-full-seat reserve. -7. Retain 262K × 8 as hard/burst ceiling until phase + SLO evidence. -8. Add mixed-length token-weighted admission for stranded block capacity. diff --git a/docs/plans/shell-dag-census-0a-brief.md b/docs/plans/shell-dag-census-0a-brief.md deleted file mode 100644 index 52185585c32..00000000000 --- a/docs/plans/shell-dag-census-0a-brief.md +++ /dev/null @@ -1,694 +0,0 @@ -# SHELL-DAG-CENSUS-0A — dispatch brief (derived shell execution census) - -Status: dispatch brief for the next shell→dag cut. Authored 2026-08-20 against `4cec10f6`, corrected twice before dispatch (see "Corrections that produced this brief"). Not a design authority — the carriers it describes become the authority when they land. - -# Revised call - -The 0A contract changes in four material ways: - -1. **`36/1 at 4cec10f…` is a historical calibration receipt, not an oracle and not a permanent expected count.** -2. **Every fact states its evidence grain.** Declared type, static reachability, interpreter semantics, and runtime observation are separate claims. -3. **The argv representation-ambiguity repair is removed from scope.** #8549 already closed it at the seam. -4. **Instrument validity includes formatting, subject identity, exact-token discrimination, and message integrity—not merely detector-logic mutations.** - -The dispatch precondition remains unmet. `main` is still `4cec10f66a3d84cbde631e20ca9feaa31457d88c`; #8652 remains open, #8653 is closed unmerged. Since your message, #8652’s body claims a clean two-generation fixed-point protocol was run, but its CI checkbox remains open and the repair is not on `main`. The prerequisite is unchanged: the merged `main` SHA itself must pass required regen, fixed point, and the full witnesses workflow. fileciteturn121file0L1-L7 fileciteturn114file0L4-L13 fileciteturn115file0L4-L13 - -# The corrected historical calibration - -At the exact historical subject: - -```text -subject SHA: -4cec10f66a3d84cbde631e20ca9feaa31457d88c - -EmitArtifactThenThinRun occurrences: - 1 variant declaration - 36 production match arms across 14 modules - 4 dag/test/claim occurrences ----- - 41 total occurrences - -Production-arm body partition: - 1 static shell-realizing route - 35 no static shell route - 0 unresolved/unknown -``` - -The sole static realizer is: - -```text -gunbc.host_effect_realize.realize_converge_on_host - -> EmitArtifactThenThinRun - -> ExistingHostQuiescentReload - -> realize_converge_in_process - -> retained script execution -``` - -The number `36` must **not** be compiled into the detector as its expected current population. 0A must run the completed detector against a worktree pinned to that exact SHA and produce a dated calibration receipt, and separately run against its own branch head and publish the current population. - -A disagreement has this disposition: - -```text -HistoricalCalibrationDisagrees { - historical_receipt - detector_result -} -``` - -It is neither “the detector is wrong” nor “update the expected count”; it stops acceptance until the discrepancy is explained. - -The omitted Codex arm is now an explicit control. Its `EmitArtifactThenThinRun` branch returns `CodexSupervisedSessionApplyRefused`, but only a body read establishes that it performs no execution; the return variant’s name is not the evidence. fileciteturn120file0L2-L7 - -# Evidence grain is part of every fact - -The prior brief used “reach” too loosely. Given the current refinement and coproduct construction gaps, 0A must not imply that a statically visible route is a successfully executable runtime route. Rename the central result from: - -```text -MayReachShell -``` - -to: - -```text -StaticShellRoute -``` - -and make the basis explicit. - -## Required grain vocabulary - -```dag -type EvidenceGrain - = AuthoredSyntax - | DeclaredType - | StaticControlFlow - | StaticValueFlow - | InterpreterSemantics - | RuntimeObservation - | ConstructionGuarantee -``` - -A fact may cite more than one grain, but it may claim only what those grains establish. - -| Fact class | Evidence grain | What it may claim | What it must not claim | -|---|---|---|---| -| File/declaration/body inventory | `AuthoredSyntax` | This body or arm exists at this SHA | It executes in production | -| Declared argument or return type | `DeclaredType` | The author declared `List`, `String`, etc. | The runtime `Value` has that representation | -| Call edge | `StaticControlFlow` | The body contains or transitively selects this call | The call is dynamically reached | -| Value origin/path | `StaticValueFlow` | This authored expression can feed this parameter or channel | The runtime value satisfies its refinement or coproduct shape | -| Shell-sink meaning | `InterpreterSemantics` | This operation/channel interprets bytes as a shell program when evaluated successfully | A valid carrier necessarily reaches the interpreter | -| Shell route | `StaticControlFlow + StaticValueFlow + InterpreterSemantics` | An authored path can attempt shell interpretation | A shell process was spawned | -| Runtime carrier shape | `RuntimeObservation` or `ConstructionGuarantee` | The observed/guaranteed runtime representation is this shape | Anything inferred solely from the declared type | -| Runtime process execution | `RuntimeObservation` | This process actually executed with these channels | Whole-corpus coverage | -| Final migration disposition | Reviewed classification in 0B | This static route belongs to runtime, foreign, bootstrap, or pending work | Completeness of the derived population | - -## Consequence for the detector - -The detector must not prune a route because: - -```text -the declaration says List -the declaration says NonEmptyStr -the declaration says a particular coproduct -a refinement should hold -an arm appears unconstructible from the declared type -``` - -At 0A grain, every syntactically authored branch remains part of the control-flow population unless a stronger construction wall is separately cited. - -The detector may report both: - -```dag -type DeclaredCarrierShapeFact { - occurrence: OccurrenceId - declared_type: String - basis: DeclaredType -} - -type RuntimeCarrierShapeReading - = RuntimeShapeUnobserved - | RuntimeShapeObserved { - shape: RuntimeValueShape - receipt: DeclarationRef - } - | RuntimeShapeGuaranteed { - shape: RuntimeValueShape - wall: DeclarationRef - } -``` - -Most 0A rows should carry: - -```text -runtime_shape = RuntimeShapeUnobserved -``` - -That is not a defect in 0A; it is an honest statement of its static grain. - -The sharp-ant `258 operations / five declared argv shapes` measurement may be retained as a **declared-type side receipt**, but it is not an 0A acceptance condition and cannot establish that a runtime `Value` is a list, string, or `ProcessArgvExpansion`. - -# Class 1 is struck completely - -The reissued brief must contain no work item to repair `push_shell_argv_tokens`’ former list-versus-concatenated-string ambiguity. - -#8549 already made that position refuse with a typed, located diagnostic when the same free monoid admits both readings, preserving native list expansion, codepoint-monoid decoding, and the explicit `ProcessArgvExpansion` route. A completed prerequisite, not remaining SHELL-DAG work. fileciteturn119file0L4-L14 - -For 0A, direct argv execution is simply a negative discrimination control: - -```text -shell.Exec.RunArgv - -> direct process carrier - -> NOT a POSIX/Bash program sink -``` - -A malformed or unexpected runtime representation may cause a refusal; it does not turn direct process execution into shell interpretation. - -# Instrument robustness contract - -The five failures you identified become five independent controls — not one generic “bad grep” class, because each has a different remedy. - -| Failure observed | Required 0A control | -|---|---| -| Too narrow to contain the answer | Recursive full-body traversal fixture with the real sink more than ten nested nodes below the arm root | -| Pointed at the wrong subject | Subject identity carrier: exact commit, roots, inventory digest, and an anchor file that must occur in the selected corpus | -| Format-induced false zero | Surface-format invariance pair plus a nonzero control partition in the same run | -| Over-broad token false positive | Exact parsed identity control: `words` must not match `keywords` | -| Messenger altered the result | Structured receipt file with content digest and read-back; no count transmitted through shell interpolation or hand-transcribed prose | - -## 1. No source-text grep as the authority - -The production detector must consume parsed bodies or a typed body-fact projection. - -It must not classify routes using: - -```text -grep -regular expressions over source text -line windows -substring search -file-name patterns -variant-name patterns -comment or prose matching -``` - -A text search may locate candidates during development; it may not produce the committed census. - -If current body projections cannot preserve callee identity, branch identity, and argument edges, the worker must stop and open a separate substrate projection increment, never fill the gap with a source-text approximation. - -## 2. Format-equivalence fixture - -Two fixture files should express the same semantic route with deliberately different formatting: - -```text -fixture A: - ordinary one-line call and string layout - -fixture B: - calls split over lines - nested indentation - comments between syntax nodes - long strings using continuation/layout forms - arguments placed many lines below the callee -``` - -After normalizing fixture-specific path and occurrence identity, their derived facts must be equal: - -```text -normalize(census(fixture_a)) -== -normalize(census(fixture_b)) -``` - -This is the direct control against the wrapped diagnostic-string false zero. - -## 3. Same-run dirty control - -Every production census invocation must also scan an isolated control partition containing exactly: - -```text -1 known shell route -1 direct-argv no-shell control -1 substring near miss -1 deeply nested route -``` - -The output remains partitioned: - -```text -production_result -control_result -``` - -A production result of zero is admitted only when the control partition reports its exact expected population in the **same process, with the same detector implementation and settings**. - -This encodes: - -> When a scan returns zero, the first hypothesis is the scan. - -## 4. Orthogonal observation - -At least one positive observation must not flow through the census fold. - -A suitable control is: - -```text -directly parse one planted fixture --> locate its exact callee node --> locate the exact program-channel argument edge --> assert both exist -``` - -This control may reuse the parser, but it must not call: - -```text -shell_route_facts() -shell_sink_facts() -shell_producer_facts() -``` - -or their shared selection predicate. - -The historical `36/1` audit is a second, human-produced orthogonal calibration. Its earlier `24` error shows why it cannot be the authority; the corrected measurement remains useful as an independent comparison. - -## 5. Exact identity, never token substrings - -All operation, function, variant, and field matching must use parsed declaration or symbol identity. - -The fixture matrix must include: - -```text -words -> intended exact symbol hit -keywords -> no hit -swordsmith -> no hit -``` - -Likewise, `Run` must not match `RunArgv`, and `ShellCommand` in prose must not match a `ShellCommand` constructor. - -## 6. Receipt integrity - -The detector should emit a structured artifact, for example: - -```text -target/shell-dag-census//facts.tsv -target/shell-dag-census//summary.json -``` - -The summary must carry: - -```text -subject commit -source roots -source inventory digest -detector declaration or binary identity -schema version -files attempted -files parsed -parse refusals -declarations -bodies -match arms -sink occurrences -static routes -unknown routes -facts artifact digest -``` - -A witness must read the artifact back and prove: - -```text -reported row count == parsed artifact row count -reported digest == content digest of artifact -reported subject == requested subject -``` - -The PR body may quote that artifact but must not be the result's only home. - -No command may place report text containing backticks, dollar signs, braces, or shell metacharacters inside an interpolated shell string. The artifact path—not its content—is the handoff. - -# Revised result types - -```dag -type StaticShellReachability - = NoStaticShellRoute - | StaticShellRoute { - route: ShellRouteFact - } - | StaticShellRouteUnknown { - cause: ShellCensusRefusal - } - -type ShellRouteFact { - root_consumer: DeclarationRef - owner: DeclarationRef - arm: BodyArmIdentity? - sink: ShellSinkFact - call_path: List - branch_path: List - source: ShellProgramSourceReading - declared_carrier: DeclaredCarrierShapeFact? - runtime_carrier: RuntimeCarrierShapeReading - basis: List -} - -type ShellSinkFact { - owner: DeclarationRef - occurrence: OccurrenceId - language: ShellLanguage - channel: ShellProgramChannel - interpreter_path: List - basis: List -} - -type ShellProgramSourceReading - = RawLiteralSource - | StaticStringCompositionSource { - producers: List - } - | GrammarEmittedSource { - emitter: DeclarationRef - } - | MaterializedFileSource { - writers: List - path_identity: FilePathIdentity - } - | ExternalSource - | MixedSource { - parts: List - } - | SourceUnknown { - cause: ShellCensusRefusal - } -``` - -The names make the ceiling visible: - -```text -StaticShellRoute -``` - -does not claim: - -```text -RuntimeShellExecuted -``` - -A later wet receipt may add: - -```dag -type RuntimeShellExecutionReading - = RuntimeShellExecutionUnobserved - | RuntimeShellExecutionObserved { - route_key: ShellRouteKey - receipt: DeclarationRef - } -``` - -That is not required to complete 0A. - -# Reissued dispatch brief - -> ## SHELL-DAG-CENSUS-0A — derive static shell-program routes from parsed bodies -> -> **Precondition** -> -> Do not branch until #8652 is merged and the resulting exact `main` SHA passes: -> -> ```text -> claim_executor --required-regen -> claim_executor --required-regen-fixed-point -> full witnesses workflow -> ``` -> -> A branch-local claim or first-generation result is not sufficient. Record the exact first green `main` SHA as the census subject. -> -> **Goal** -> -> Derive, from parsed `.dag` bodies, every authored path that can attempt POSIX/Bash program interpretation. The population must be derived from the corpus and interpreter semantics, never from remembered bridge names, filenames, imports, variant names, refusal types, line windows, or source-text grep. -> -> This is a **static may-route census**, not a runtime-execution guarantee. -> -> **Required evidence grain** -> -> Every fact carries one or more of: -> -> ```text -> AuthoredSyntax -> DeclaredType -> StaticControlFlow -> StaticValueFlow -> InterpreterSemantics -> RuntimeObservation -> ConstructionGuarantee -> ``` -> -> A declared type may be recorded, but no runtime representation may be inferred from it. Do not prune routes using refinements, declared coproduct shape, or expected interpreter representation. -> -> **Required corpus partitions** -> -> Scan all authored `.dag` files under the declared production roots. Scan tests and fixtures too, but report them in separate partitions: -> -> ```text -> Production -> Test -> Fixture -> ``` -> -> No file may disappear through exclusion. Each attempted file becomes either: -> -> ```text -> Parsed -> ParseRefused { path, cause } -> ``` -> -> **Required body population** -> -> Enumerate: -> -> ```text -> fn bodies -> func bodies -> service operation bodies -> data initializers that feed executable plans or function values -> every nested if/match/loop/closure body -> every match arm at occurrence identity -> every direct call and service invocation -> ``` -> -> A later realizing sub-arm makes the containing arm `StaticShellRoute` even when an earlier sibling or first branch refuses. -> -> **Required shell channels** -> -> Recognize shell interpretation through: -> -> ```text -> TransportScript/direct script arguments -> sh or bash -c command arguments -> sh or bash -s standard-input programs -> wrapped forms such as sudo -S sh -s -> remote command strings with modeled shell interpretation -> materialized script files later executed -> GitHub Actions run bodies -> cron command bodies -> git-hook programs -> grammar-emitted Bash artifacts -> ``` -> -> The semantic seed may identify what interprets bytes as shell. It may not enumerate current call sites. -> -> **Required value flow** -> -> Trace program values backward through: -> -> ```text -> bindings -> parameters and returns -> record fields and variant payloads -> list/fold/map constructions -> concat/join/interpolation -> renderer calls -> file write/read joins where identity is structural -> transitive helper calls -> recursive call-graph fixed points -> ``` -> -> An unresolved relevant call or unjoinable file flow becomes `StaticShellRouteUnknown`, never `NoStaticShellRoute`. -> -> **Required body law** -> -> Return shape, variant name, reason-field name, comments, and prose are irrelevant. -> -> The result is decided only from transitive body behavior: -> -> ```text -> NoStaticShellRoute -> StaticShellRoute -> StaticShellRouteUnknown -> ``` -> -> Explicit controls must cover: -> -> - a refusal represented as a success-valued record; -> - a refusal represented as `HostnameSetCasApplyFailed { stderr: ... }`; -> - `CodexSupervisedSessionApplyRefused` under `EmitArtifactThenThinRun`; -> - a realizer behind an earlier refusing branch; -> - a classifier returning `Bool` or a variant label with no execution. -> -> **Historical calibration—not authority** -> -> Run the finished detector against: -> -> ```text -> 4cec10f66a3d84cbde631e20ca9feaa31457d88c -> ``` -> -> and publish a dated receipt reconciling: -> -> ```text -> 41 total EmitArtifactThenThinRun occurrences -> 1 declaration -> 36 production match arms across 14 modules -> 4 test occurrences -> -> 36 production arms: -> 1 StaticShellRoute -> 35 NoStaticShellRoute -> 0 StaticShellRouteUnknown -> ``` -> -> This measurement must not become a permanent hard-coded count. A disagreement blocks acceptance for investigation; it does not automatically indict the detector. -> -> **Required live discoveries** -> -> Without using their names as seeds, the detector must find: -> -> - the one historical converge realizer above; -> - all four `spark_managed_access_apply` script producers flowing through `stdin_payload` to `sudo -S sh -s`; -> - every remaining `retained_*` route; -> - direct `RunArgv` routes as no-shell controls; -> - the Codex and hostname refusal disguises as no-shell arms. -> -> **Instrument controls** -> -> The same production implementation must pass: -> -> 1. **Deep-body control:** the sink sits more than ten nested nodes below the matched arm. -> 2. **Subject control:** output carries exact SHA, root set, inventory digest, and reaches a planted anchor under every intended root. -> 3. **Format-invariance control:** semantically identical, differently formatted fixtures produce equivalent facts. -> 4. **Exact-identity control:** `words` matches; `keywords` and other substring near misses do not. -> 5. **Messenger-integrity control:** structured output round-trips with row count and digest unchanged. -> 6. **Orthogonal anchor:** a direct parsed-node assertion sees one planted sink without invoking the census fold. -> 7. **Same-run dirty control:** the control partition reports its exact nonzero population in every production census invocation. -> -> A zero production count is inadmissible unless all seven hold in the same subject run. -> -> **Class 1 status** -> -> Do not modify `push_shell_argv_tokens` for the former free-monoid list/string ambiguity. #8549 already made that ambiguity a typed located refusal. Preserve it and use direct argv as a negative shell control. -> -> **Deliverables** -> -> ```text -> typed sink, route, producer, arm, evidence-grain, and refusal carriers -> corpus-derived producer -> deterministic structured facts artifact -> deterministic summary artifact -> historical 4cec10f calibration receipt -> current-head receipt -> fixture and mutation-control suite -> reconciliation predicates -> ``` -> -> **Non-goals** -> -> ```text -> no shell migration -> no bridge deletion -> no route disposition table -> no runtime-value guarantee from declared types -> no CLI-AUTHORITY tool/argv census -> no srv* survival decision -> no build-cache wiring decision -> no stage0 mirror edit -> no grep-generated production population -> ``` -> -> **Stop condition** -> -> If the available parsed-body facts cannot preserve exact callee identity, argument edges, branch identity, or interprocedural value flow, stop. File the missing typed projection as a separate substrate increment. Do not substitute text scanning. -> -> **Merge bar** -> -> ```text -> exact-base main green before branch -> zero production parse refusals -> zero unexplained StaticShellRouteUnknown rows -> all corpus/count reconciliation laws hold -> all instrument controls discriminate under mutation -> historical 36/1 calibration reconciled or discrepancy explained -> current-head receipt published -> facts artifact deterministic on repeat -> receipt row count and digest round-trip -> full CI green -> ``` - -# Trust predicate - -The 0A result should expose a single predicate with no runtime overclaim: - -```text -static_shell_census_trusted = - subject_identity_holds - && corpus_denominator_nonvacuous - && production_parse_refusals == 0 - && unexplained_static_route_unknowns == 0 - && control_partition_matches - && deep_body_control_holds - && format_invariance_holds - && exact_identity_control_holds - && orthogonal_anchor_holds - && messenger_integrity_holds - && route_sink_producer_arm_reconciliation_holds - && repeated_run_digest_equal -``` - -It should **not** be named: - -```text -shell_execution_guaranteed -shell_runtime_population_complete -shell_migration_complete -``` - -The strongest honest conclusion from 0A is: - -> At this exact source subject, the parsed authored corpus contains this complete, non-vacuously derived population of static paths capable of attempting shell interpretation; every row states whether its carrier shape is merely declared, statically flowed, construction-guaranteed, or runtime-observed. - ---- - -## Corrections that produced this brief - -Recorded because each falsified an input the brief had already been written against, and a reader who finds only the final text would re-derive them. - -1. **The acceptance oracle was wrong.** An earlier revision required the detector to reconcile against a manual 24-arm `EmitArtifactThenThinRun` audit. Measuring that audit in order to pin it falsified it: at `4cec10f66a3d84cbde631e20ca9feaa31457d88c` the tree carries 41 occurrences — 1 variant declaration, **36 production match arms across 14 modules**, 4 under `dag/test/claim/`. Neither manual audit reached 36, and neither named `gunbc.host_effect_codex_supervised_turn`. The conclusion was unaffected — exactly one arm realizes — but with 24 pinned as the oracle, the first *correct* detector would have reported 36 and the reconciliation step would have read as a detector bug. Hence `HistoricalCalibrationDisagrees` stops acceptance for investigation rather than indicting either side. - -2. **Declared types constrain less than the brief assumed.** The argv reachability probe found 258 shell-transport operations whose argv elements resolve to five shapes, none typed `Map`, `Set`, `Record` or nullable. That is a *declared-type* result, and declared types are not construction-enforced here: 3939 `WhereRefinementUnenforced` across 612 files, and separately any coproduct in this tree can be constructed by naming the type with no variant tag, refused only at runtime by the interpreter. Hence the `EvidenceGrain` vocabulary and the rule that the detector may not prune a route because a declaration says `List`. - -3. **A scoped repair was already closed.** The brief's original Class 1 item — the argv representation ambiguity — was closed on main before dispatch, by the lane about to be assigned it. It survives only as a negative control. - -4. **Prior art exists with the exact blind spot this census is built to avoid.** `v2.lens.effect_reach` classifies host-effect sinks and carries a `ShellExecRunSink` variant, but `sink_kind_for_callee` selects by name equality against a remembered callee-text list, everything else falling to `UnknownHostEffectSink`. It therefore cannot see `sh -c`, `sh -s` on stdin, or a `sudo -S sh -s` wrapper — the hole the hand census carried before `gunbc.spark.managed_access_apply` was found. Disposition: **supersede, not extend** — extending inherits its selection principle. - -## Where the instrument controls came from - -The seven controls are not theorized: each is a failure that occurred while producing this brief, and they are **not one class**: - -| control | the failure it encodes | -| --- | --- | -| deep-body | an instrument too narrow to contain the answer — a realizing sub-arm sat ten lines below anything the command could print | -| subject | an instrument pointed at the wrong subject — a stale head, a mirror rather than its authority | -| format-invariance | a **confident false zero**: a grep for a diagnostic string returned 0 on both refs while the author was looking at that sentence, because it wraps across a backslash continuation | -| exact-identity | a **confident false positive**: a pattern containing `words` matched inside `keywords` and returned seven irrelevant maps | -| messenger-integrity | the report of the previous item reached its recipient with the words deleted, because backticks inside a shell command were substituted away | -| orthogonal anchor | every one of the above was caught only because an observation existed that did not come through the instrument | -| same-run dirty partition | a zero is admissible only when something non-zero is proven visible in the same run | - -The generalization, and why a zero production count is inadmissible unless all seven hold: **when a scan returns zero, the first hypothesis is the scan.** diff --git a/docs/plans/shell-dag-census-0a-projection-blocker.md b/docs/plans/shell-dag-census-0a-projection-blocker.md deleted file mode 100644 index af5e5718136..00000000000 --- a/docs/plans/shell-dag-census-0a-projection-blocker.md +++ /dev/null @@ -1,369 +0,0 @@ -# SHELL-DAG-CENSUS-0A — projection blocker and derived shell-interpretation seed - -Status: finding. Produced under the stop condition in -[shell-dag-census-0a-brief.md](shell-dag-census-0a-brief.md), which reads: - -> If the available parsed-body facts cannot preserve exact callee identity, argument edges, -> branch identity, or interprocedural value flow, stop. File the missing typed projection as a -> separate substrate increment. Do not substitute text scanning. - -Subject: `a6ca6882d18114b52532c0804dc89f97b441f493` (first `main` SHA satisfying the brief's -precondition — #8652 merged, `witnesses` workflow SUCCESS on that exact SHA, run 32370279129). - -No detector was built; no text-scanning census was substituted. Below: (1) why the available -fact surface cannot carry 0A, axis by axis, each with its remedy, and (2) the shell-interpretation -seed derived from what actually interprets bytes as shell, needed under whichever remedy lands. - ---- - -## 1. The whole fact surface `.dag` can read - -`src/v1/stage0/src/coproduct_reflection.rs` exposes exactly three live-fact accessors: - -| accessor | item filter | `.dag` surface | payload | -| --- | --- | --- | --- | -| `eval_concept_decl_facts_live` | `ItemKind::TypeItem` | `v2.std.concept_index` `ConceptDecl` | `qualified_name`, `name`, `node` | -| `eval_fn_arrow_decl_facts_live` | `ItemKind::FnItem \| ItemKind::FuncItem` | `v2.std.fn_index` `FnArrowDecl` | `qualified_name`, `name`, `output`, `params` | -| `eval_data_init_decl_facts_live` | `ItemKind::DataItem` | `v2.std.data_index` `DataInitDecl` | `qualified_name`, `module`, `name`, `literal_fp` | - -That is the entire typed body-fact projection. `ItemKind::ServiceItem` exists in the seed's item -vocabulary and has **no accessor**. - -The one prior art, `v2.lens.effect_reach`, reads that surface and carries a `ShellExecRunSink` -variant — but `sink_kind_for_callee` decides by `string_eq` against a remembered callee-text list -(`"Run"`, `"shell.Exec.Run"`, `"Exec.Run"`), everything else falling to `UnknownHostEffectSink`. -The brief's disposition on it is *supersede, not extend*; the reason is in the table above, not -in that function: the surface it reads cannot express the alternative. - -## 2. Six axes, six remedies - -### Axis 1 — service operations and transports are absent entirely - -The fact that decides what interprets bytes as shell lives in transport declarations: - -```dag -service shell.Exec { - operation Run { - input { script: TransportScript } - transport shell { argv: ["bash", "-s"] stdin: script.body } - } -} -``` - -`ItemKind::ServiceItem` has no accessor, so no `.dag` consumer can read the operation, the argv, -or the stdin channel. The brief permits exactly one seed ("what INTERPRETS bytes as shell") and -forbids the alternatives (a file list, a call-site roster, a variant-name list). In this tree the -permitted seed is obtainable *only* from these declarations, so a census over the available -surface cannot have a permitted seed at all. - -**Remedy:** a typed service/operation/transport declaration projection. - -### Axis 2 — `FnArrowDecl.output` is a wiring-liveness skeleton, not a body - -`marshal_stmt_sequence` folds statements right-to-left carrying a live-reference set back from -the return, and grafts a `let b = rhs` **only** when `b` is referenced downstream by code that -itself reaches the return. Its own comment states the intent: a parameter used only inside a dead -`let` must be absent so it flags as a dead wire. Correct for wiring liveness; fatal here. A shell -call whose result is bound and not consumed is **absent from the input**, so `NoStaticShellRoute` -is returned for a body that contains a route. - -**Proven by execution, not by reading the marshal.** A fixture with two functions of identical -shape — one binding the call and not using it, one returning it — folded through -`fn_arrow_decl_facts_live`, atom identities collected from each `output`: - -```text -fn fixture_dead_let_shell() -> String { - let unused_result = fixture_sink(program: "echo DEADLETMARKER") - "returned-without-using-unused_result" -} -fn fixture_live_named_args() -> String { - fixture_sink2(program: "echo LIVEMARKER", args: "echo ARGSMARKER") -} - -DECL probe.fixture.fixture_dead_let_shell ATOMS: (empty) -DECL probe.fixture.fixture_live_named_args ATOMS: fixture_sink2 | echo LIVEMARKER | echo ARGSMARKER -``` - -The dead-let arm yields **nothing** — callee identity and program literal both absent; the live -arm yields both. Same construct, opposite verdict: the loss is the projection's, not the probe's. - -This is a structural false zero upstream of the detector. None of the brief's seven instrument -controls can catch it: each is a control over the detector, and the loss has already happened in -the detector's input. - -**Remedy:** a body projection whose fold is total over statements — effect positions retained -independently of return reachability. - -### Axis 3 — no named-argument edges - -`marshal_generic` emits positional child edges plus string-literal atoms, and -`hoist_call_arg_string_literal_edges` lifts literals from arbitrary depth up to the call node, -discarding which argument they came from. Argument labels never appear. The brief requires "the -exact program-channel argument edge", and §4 below shows why: in -`["sh", "-c", "command -v \"$1\"", "sh", "{command}"]` the program is the *third* element and -`{command}` is data, while in `["sh", "-c", "{command}"]` the same-named value *is* the program. -Positionally-erased arguments cannot distinguish those. - -The same executed run shows it directly: `fixture_sink2(program: "echo LIVEMARKER", args: "echo -ARGSMARKER")` projects to `fixture_sink2 | echo LIVEMARKER | echo ARGSMARKER` — three atoms in -authored order with **no labels**. Nothing says which literal was `program:` and which `args:`; -the ordering is an accident of authoring, not a carried fact. - -**Remedy:** argument edges labelled with the authored argument name, and positional index -preserved, at each call. - -### Axis 4 — no arm or occurrence identity - -Match arms are undifferentiated positional children. Nothing in the surface can construct the -brief's `BodyArmIdentity` or `OccurrenceId`. Beyond the missing field: the historical calibration -is stated at arm grain (36 production arms across 14 modules, 1 realizing), so it is not -reproducible against this surface at all — the detector could neither agree nor disagree with it. - -**Remedy:** stable occurrence identity on body nodes, and arm identity on match arms. - -### Axis 5 — callee is an authored lexeme, not a resolved identity - -Callee atoms, string literals, record-construction spellings and variant names all arrive as the -same `atom_identity_node` kind. The brief's exact-identity control has two halves. The easy half -passes: `words` and `keywords` are different atoms. The hard half fails: `"ShellCommand"` as a -string literal and `ShellCommand` as a constructor are the same atom, so a prose or literal -mention is indistinguishable from a use — the false-positive direction of the same control. - -**Remedy:** resolved declaration identity on callees, and a node-kind discriminator separating -literal, callee, constructor and variant occurrences. - -### Axis 6 — the registry is the entry's import closure, not the corpus - -Measured, not read: a probe folding `fn_arrow_decl_facts_live()` under -`--source-root dag --source-root src/v2` reported **1,698** fn/func declarations. The corpus -declares **41,965**. The surface is the *entry's loaded-module closure* — about 4% of the tree, -and its content is whatever that entry happened to import. - -This is a declared frontier, not a discovery. `v2.lens.affected_set.corpus_dependency_view` -already guards it: `corpus_dependency_view_per_pr_substrate_ready` calls -`fn_arrow_decl_substrate_is_whole_tree`, and when it is false -`ensure_corpus_dependency_view_per_pr_substrate` routes to a host refusal reading -`corpus_dependency_view per-PR execution refused ... (blocked-on-#6239)`. Fail-closed, correctly. -A census over this surface inherits that refusal. - -It is fatal for 0A specifically because the brief's corpus requirement is a denominator claim — -"Scan all authored `.dag` files under the declared production roots ... No file may disappear -through exclusion" — and here files disappear through *non-import*, silently, with no per-file -`ParseRefused` row to count them. That is DESIGN's empty-observation narrow: the population never -loaded is indistinguishable from the population carrying no route. - -**Remedy:** a corpus-grain producer whose denominator is an enumerated file set, not an import -closure. - -## 3. The worked proof — `gunbc.spark.managed_access_apply` - -The brief names four script producers reaching a host through `stdin_payload` behind -`sudo -S sh -s`, and requires that a correct detector find them **without being told their -names**. That chain proves the axes above are a capability gap, not an API preference: each hop -is one of them. - -In `privileged_leg`: - -1. `portable_remote_words(raws: ["sudo", "-S", "sh", "-s"])` — the interpreter words are a list - literal in a **named argument** (axis 3). -2. `match … { Present { value: words } => … }` — the words are bound by a **match-arm binder** - (axis 4; and the binder is not a fn parameter, so it is not even emitted as a - parameter-reference atom). -3. `shape_fleet_ssh_exec(…, words: words)` then `ssh_session_exec_portable_words_with_stdin(client_args: …, stdin_payload: …)` - — interprocedural flow through **named arguments** into a **service operation** (axes 1, 3, 5). -4. `ssh.Session.ExecPortableWordsWithStdin` declares `transport shell { argv: ["ssh", client_args] stdin: stdin_payload }` - — the shell program travels on the **transport stdin channel** (axis 1). - -No hop in that chain is visible to the available surface. A detector over it finds these four -producers only by being told their names — the defect this cut exists to end. - -## 4. The derived shell-interpretation seed - -Whichever remedy lands, the census needs a vocabulary for what interprets bytes as shell, derived -rather than remembered. The finding: the discriminator is **positional, not a program-name -list**, and the shell-interpretation locus is **not only the `extdeps` declaration layer**. - -Corpus-wide (`dag/`, `src/v2/`) there are **262** `transport shell` declarations. The -overwhelming majority are **direct execs** whose transport is merely *named* shell — `find`, -`test`, `mv`, `chmod`, `uname`, `mktemp`, `systemctl`, `tmux`, `xorriso`, `jq`, `wc`, `whoami`, -`true`. No byte of their argv is parsed as a shell program. - -**17** sites carry an argv literal whose word list names a shell interpreter, partitioned by -*locus* — the load-bearing part: - -| locus | count | files | -| --- | --- | --- | -| `extdeps` `transport shell` declarations | 6 | `extdeps/shell/exec.dag` (2), `extdeps/shell.dag`, `extdeps/ssh/session.dag`, `extdeps/entropy/entropy.dag` (2) | -| product **fn-body `ArgvCommand` record constructions** | 10 | `gunbc/fleet_probe_identity_observe.dag` (6), `gunbc/fleet_host_key_enrollment.dag` (4) | -| test | 1 | `test/manual/command_runner_local_argv_receipt_test.dag` | - -The 10 product-body sites matter more than the 6 declarations: they are `ArgvCommand { argv: -["sh", "-c", …] }` records built **inside fn bodies**, one with a *computed* program -(`argv: ["sh", "-c", pin_cmd]` in `fleet_host_key_enrollment`). A projection covering only -service declarations — axis 1 alone — would find the 6 and miss the 10, including the only -computed-program site in the tree. Both the declaration seed and real bodies are required. - -Four structural rules fall out, none a name list: - -1. **The interpreter is at the program position, and some wrappers re-root that position — but - `--` does not tell you which.** 46 argv literals contain a `"--"` element. 41 are - `ssh`-prefixed, where `--` genuinely re-roots: the words after it are a fresh argv executed on - the far host, which is why `["ssh", "{host}", "bash", "-s"]` is a shell route and - `["sshpass", …, "--", "test", "-w", "{path}"]` is not. The other 5 are not one class: - `["systemd-run", "--unit={unit}", "--collect", "--", command_argv]` re-roots into a new program - (not a shell); `["cargo", "run", "-p", package, "--bin", bin, "--", args]` passes *arguments* to - an already-named program; `["git", "-C", "{repo}", "ls-files", "-u", "--", "{path}"]` is plain - POSIX end-of-options and re-roots nothing. - - So `--` is overloaded three ways and no syntactic rule separates them. Which reading holds is a - fact about the *host program's* CLI contract — `ssh(1)`, `systemd-run(1)`, `cargo`, `git(1)` — - an `extdeps` citation duty (DESIGN §3), not something the census may infer from the token. The - census must consult a per-program re-root contract, and where none is modeled the honest answer - is `StaticShellRouteUnknown`. Treating `--` uniformly as "re-root" would read - `git ls-files -- {path}` as executing `{path}`; uniformly as "end of options" would miss every - remote route. This is the single place in the seed where a *new modeled authority* is owed - rather than a derivation. - -2. **The flag selects the channel.** `-c`/`-lc` puts the program in the *next argv element*; `-s` - puts it on **stdin**. These are different edges, and a census reading only argv misses every - `-s` route — including `shell.Exec.Run`, the tree's main script route. -3. **A program-position value is not necessarily a program.** `shell.PosixCommand.Check` declares - `argv: ["sh", "-c", "command -v \"$1\"", "sh", "{command}"]`: the script is a *fixed literal* - owned by the declaration and the caller's value is bound to the shell's positional `$1`, so it - never enters the text parsed as shell. Reading `{command}` as a program source is a false - positive. Two of the `entropy.dag` routes are the same shape — fixed literal scripts with an - interpolated `head -c {count}` length. The discriminator is which argv slot a value occupies, - which is why axis 3 (argument edges) is not a nicety. -4. **Some operations are undecidable at their declaration.** `ssh.Session.ExecPortableWords` and - `ExecPortableWordsWithStdin` declare `argv: ["ssh", client_args]` — the program position is - *inside* an opaque expansion. Whether they interpret shell depends entirely on what a caller - flows into `client_args`; the spark caller flows `["sudo", "-S", "sh", "-s"]` and makes it a - shell route. At the declaration these are `StaticShellRouteUnknown`, never - `NoStaticShellRoute`. - -Rule 4 is load-bearing for the increment: the seed alone is insufficient, and the census is -necessarily *declaration seed joined with interprocedural value flow*. Neither half is optional, -and the value-flow half is exactly what axes 2–5 remove. - -A fourth locus exists, named so it is not mistaken for absence: three test fixtures carry -`service … transport shell { argv: [\"sh\", \"-lc\", \"{script}\"] }` inside **string -literals** of `.dag` source (`pipeline_transport_emit_rest_shell_witness_test.dag`, -`transport_script_wall_compile_red_test.dag`). Those are program text *about* shell transports, -not declarations; they belong in the brief's Test partition, and are the exact false-positive -shape axis 5 cannot discriminate. - -## 5. The full-fidelity route: measured, and it is a third outcome - -The full-fidelity route does exist in `.dag`: `v2.compiler.source_authority` -`parse_dag_source_ast` takes a `Medium` to `Outcome` where -`ParseTree = Node`, with `normalize`/`resolve` above it giving `ResolvedTree = Node`; and -`Filesystem.List`/`Filesystem.Read` are callable from `.dag` -(`v2.workflow.floor_discovery_producer` already walks scan dirs with both). - -But `parse_dag_source_ast` has **no consumer**. Its only two call sites are inside -`canonical_dag_source_parse_print_law`, whose name occurs exactly once in the tree — its own -definition. A parse/print law nothing executes: DESIGN §5's specification-without-execution -class, in the compiler's own source authority. So "the route exists in `.dag`" was not evidence -that it works, and it had to be measured. That specimen is worth a row wherever the -enforcement-intent registry ends up, independent of this census. - -### Correctness: one named grammar class refuses a large fraction of the corpus - -Three arms in one run, the first a positive control so a refusal cannot be blamed on the harness: - -```text -CONTROL_3LINE (the tree's own "module m / fn add" fixture) => ACCEPTED -REAL_SMALL (dag/gunbc/bash_materialized_transport.dag) => ACCEPTED -REAL (dag/extdeps/shell/exec.dag) => REJECTED - reason = parse_grammar_choice_overlap_residue -``` - -The route is real — it parses authored corpus source, not just fixtures — and its failure is a -**located, typed refusal naming a specific grammar deficiency**, the fail-closed -`ParseRefused { path, cause }` shape the brief anticipates. - -`extdeps/shell/exec.dag` is not a corner case. On a random 10-file sample of the real corpus: - -```text -A src/v2/std/constraint_satisfaction_predicate.dag -R dag/test/claim/filesystem_read_hermetic_witness.dag parse_grammar_choice_overlap_residue -R dag/test/claim/wet_hermetic_equivalence_witness_test.dag parse_grammar_choice_overlap_residue -R src/v2/test/claim/.../cargo_fmt_dead_param_test.dag parse_grammar_choice_overlap_residue -A dag/test/fixture/sole_constructor_sealed/admitted_caller.dag -A dag/extdeps/transports/file.dag -A src/v2/workflow/host_discovered_owned_data_manifest.dag -A src/v2/lens/structural_similarity.dag -R src/v2/test/claim/round_trip/source_authority_contract_test.dag parse_grammar_choice_overlap_residue -A dag/extdeps/cache/catalog_placement.dag - -6 accepted, 4 refused -``` - -The grouping is the finding, not the rate: **all four refusals, and the `shell/exec.dag` refusal, -carry the same reason** — `parse_grammar_choice_overlap_residue`. One grammar deficiency with many -victims, not scattered file-specific problems. That cuts both ways: the route is a single repair -away from a much larger accepted population, and no census can run on it until that repair lands, -because the brief's merge bar is *zero* production parse refusals and the refusal set includes the -census's own seed file. - -### Affordability: measured, and it is the third outcome - -Correctness alone would send out a detector that cannot run, so accepts-or-refuses was the wrong -frame. Three outcomes: refuses (substrate cut); accepts and is affordable at corpus grain -(projection lands first, census rebases); and **accepts but is unaffordable at corpus grain**, -which is neither. Measured on a random 40-file sample, ascending by size, one process per run: - -| files | source bytes | wall | exit | -| --- | --- | --- | --- | -| 1 | 372 | 63.1 s | returned `A` | -| 10 | 5,332 | 67.5 s | returned 6 A / 4 R | -| 40 | 388,527 | 187.3 s | **`EXIT=137` — OOM-killed after 34 files** | - -Two numbers fall out, pointing at different walls: - -- **Time is not the wall.** 63.1 s → 67.5 s for nine more files is ≈ **0.49 s marginal per file** - against ≈ **62.6 s fixed overhead** — paid to stand the whole corpus up before any question is - asked, which is itself the shape under discussion. -- **Memory is the wall, and it is not about big files.** The kill came at file ~34 of 40 with only - **94 KB of source consumed**, on files of ~7.8 KB each — the 212 KB outlier sorts last and was - never reached. So ~94 KB of parsed source exhausted the runner's ≈1.6 GB budget. Whether that is - parse-tree retention, interpreter heap growth, or both is **not** established by this probe and - not claimed; what is established is that the process cannot hold the result of parsing 34 small - files, against a census subject of 3,733 files and 31.3 MiB. - -The census fold accumulated only a short result string, so the retention is not the probe's -accumulator. A corpus-grain fold over this route does not fit in a process today. - -### The verdict: outcome 3, and DESIGN has already rejected this shape once - -Recorded because it converts a performance observation into a repository ruling. DESIGN §6's -lens bullet records the deletion of the corpus-wide censuses in #8140 (2026-08-11) and states the -defect in general terms: *"the unit of computation was the world, the unit of fact was one -module's authorship, and the price was paid by every consumer that wanted a witness roster and -nothing else."* Its declared next-rung trigger is the seed-side shape: an authorship fact -*"belongs on the module's own declaration, checked at ingestion where the module is parsed -anyway — one module's facts from one module's source — rather than reconstructed corpus-wide by a -consumer that wanted something else."* - -That is axis 1's remedy with a precedent and a cost argument attached. A `ServiceItem` accessor -plus a non-lossy body projection pays per module at the point the module is *already* being -parsed; a corpus-wide parse fold re-parses the world on every run to answer a question about -shell routes — 62.6 s of fixed world-acquisition before the first question, and an OOM before the -34th file. - -The measurement lands on outcome 3: the corpus-parse route is the exact cost-shape defect this -repository deleted machinery over, and it additionally fails the correctness axis today. **The -recommended increment is therefore the ingestion-side projection, not a census-side fold**, and -0A rebases onto it: - -1. a `ServiceItem` accessor carrying operations, transports, argv and stdin channels (axis 1); -2. a non-lossy body projection — total over statements (axis 2), labelled argument edges (axis 3), - arm and occurrence identity (axis 4), resolved callee identity with a node-kind discriminator - (axis 5); -3. a corpus-grain denominator that is an enumerated file set rather than an import closure - (axis 6). - -`parse_grammar_choice_overlap_residue` is worth filing separately whichever route wins: a single -named grammar deficiency refusing a large fraction of authored source in the compiler's own -parser, currently invisible because the only code path that would surface it — -`canonical_dag_source_parse_print_law` — has no callers. diff --git a/docs/plans/spark-fleet-hand-edit-gap-analysis.md b/docs/plans/spark-fleet-hand-edit-gap-analysis.md deleted file mode 100644 index 3a9dea2a470..00000000000 --- a/docs/plans/spark-fleet-hand-edit-gap-analysis.md +++ /dev/null @@ -1,180 +0,0 @@ -# Spark fleet: hand-edited state with no modeled authority - -**Status 2026-09-02, corrected.** Every row below was established by reading the live -hosts and comparing against the module named as its authority. The instrument for the -desired side is `gunbc.spark.serving_unit_render spark_serving_desired_user_unit_text`; -for the live side, the unit file and `/api/ps` on each host. - -## Correction, and it is the most important thing in this document - -**The first version of this analysis was measured against the wrong tree, and its two -headline findings were false.** It was derived by running the renderer from a session -branch based on `373b8d11`, and `main` had since advanced. Re-derived against -`origin/main`: - -- **Context is NOT diverged.** `spark_serving_desired_context_length` is `1048576` on - main, matching the live hosts. The claimed `131072` was this branch's stale value; main's - own annotation records that exact number as the thing it replaced. -- **`OLLAMA_NUM_PARALLEL` is NOT missing.** `extdeps.ollama.server_env` carries it as a - typed axis, `serving_desired` carries a `PositiveSlotCount` of 4, the renderer emits - `ollama_num_parallel_env_assignment`, and - `test.claim.spark.spark_serving_unit_render_witness_test` holds the renderer to it. - -So **the freeze recommendation that followed from those two rows is withdrawn.** On those -axes a convergence run would not degrade the fleet; it would agree with it. - -The failure was mine and it is worth naming as its own class, because it is the exact -defect this repository spends its review budget on: I ran a real instrument, got a real -number, and read it as a fact about the fleet when it was a fact about **my branch**. A -measurement is only as current as the tree it was taken from, and a session branch is not -the authority. Re-derive against `origin/main` before reporting a divergence. - -## Why this document exists - -The fleet is being assembled by hand while the model that is supposed to own it is -elsewhere. That is a legitimate way to move, but it has a specific failure mode worth -stating up front, because it is already loaded and armed: - -**A convergence run today would degrade the fleet, silently and without failing.** The -modeled desired unit differs from the live unit on both serving hosts in ways that all -point the same direction — narrower context, no concurrency, a different model. Nothing -in that path refuses; the unit rewrites, the service restarts, and it serves. The numbers -every serving decision is currently reasoned from would be invalidated without a single -error. - -So this is not a tidiness ledger. It is the list of things that must land before -`gunbc.spark` convergence may be actuated against `srv5`/`srv6` again. - -## The divergences - -### 1. Context window — WITHDRAWN, see the correction above - -Main desires `1048576`, which is what the hosts serve. No divergence. - -### 2. Concurrency — WITHDRAWN, see the correction above - -Main models the slot count as a typed `PositiveSlotCount` of 4, renders it, and witnesses -the rendering. No divergence. - -### 3. Served model — CLOSED - -`gunbc.spark.serving_desired` selected a gpt-oss build while both serving hosts ran -DeepSeek-V4, and the two are not variants of one choice — different publisher, family, -artifact and runtime footprint — so no evidence gathered against one answered for the -other. - -`spark_serving_desired_manifest` now names the DeepSeek manifest, resolved through -`gunbc.ollama_model_resolution` with the provenance arm that records it as locally -materialized rather than upstream-published, and `gunbc.spark.serving_model_admission` -requires an exact runtime version AND a manifest-identity match before a local artifact is -admitted. The divergence INSIDE the model closed with it: the rendered `Description=` was a -second, stale spelling of the model name and is now derived from -`spark_serving_desired_model_ref` rather than authored beside it. - -What this did NOT close, and the reason the ordering below still starts here: the desired -model is now correct, and reproducing that artifact from desired state remains impossible, -which is carried as a declared §4b(3) rung drop rather than as silence. - -### 4. Residency: modeled and live both silent on `OLLAMA_KEEP_ALIVE` - -Neither the desired unit nor the live unit sets it, so a 94 GB model is evicted after the -default idle period and the next request pays a full reload. Measured today: both `srv5` -and `srv6` reported `none resident` minutes after serving a request. Any latency -measurement that does not declare residency state is measuring two different things -depending on when it ran. - -### 5. The second pair is entirely outside the model — AND IS NOT TWO SERVING HOSTS - -This section originally described the pair as two independent ollama serving hosts. That -was the state when it was written and it is not the state now, and the difference is -load-bearing rather than a refresh: `192.168.1.232` runs a **llama-server front door** on -`:30000` whose weights live on `192.168.1.233` behind an `ggml-rpc-server` reached over the -RoCE fabric. They are ONE serving unit split across two machines, not two of anything, and -a roster that admits them as two serving cells would plan an 86.7 GB load onto the weights -peer — which has roughly 16 GiB free and swap disabled, so the load is an OOM kill that -takes the front door down with it. - -`gunbc.spark.serving_unit_observed` now carries that subject with the split as a -constructor condition. The membership gap below is unchanged and is what remains open; -what changed is what membership would have to mean. - -`spark-c2b1` (192.168.1.232) and `spark-ac79` (192.168.1.233) are provisioned, serving, -and unknown to every authority that should own them: - -- no `SparkCellRoleAssignment` — `gunbc.spark.cell_role` knows only `srv5` and `srv6` -- no fleet slot, so `gunbc.network_identity_subsumption` cannot bind them -- their static DHCP reservations are refused by design by that same module, so they have - no modeled network identity even in principle -- different service user (`briansrls`, not `gunbc-automation`) -- different install root (`/usr/local/ollama`, not the install-paths authority's tree) -- different unit name, and on `.232` not an ollama unit at all -- `OLLAMA_KEEP_ALIVE=-1` on the ollama that `.233` still runs alongside its weights role, - which the modeled unit has no field for — and that ollama is itself the hazard in the - paragraph above, because it WILL accept a load the weights peer cannot survive - -The runtime itself is the one thing that IS aligned: the pinned `v0.32.9` arm64 asset was -installed after verifying its SHA-256 against `extdeps.ollama.binary_release`, so the -runtime materialization identity matches the first pair rather than drifting to `latest`. - -### 6. Credentials are hand-carried - -`gunbc.spark.credential_workflow` models a `spark-administrator-password` `SecretRef` -resolving through `gunbc.auth.secret_ref_credential`, and its own note says wet entry -points name attempt identity and host on argv and never plaintext passwords. The password -in use today was passed in chat and now exists in at least two session transcripts. SSH -keys were installed by hand on top of it. - -The modeled path exists and was bypassed. That is the §6 out-of-band actuation tell in its -plainest form, and the credential should be rotated once the fleet is stable. - -### 7. ~~Runner configuration lives in the environment, and the identity model reads argv~~ - -**CLOSED.** `gunbc.model.ollama_choice` no longer keys a realization on argv. The carrier -is `ObservedOllamaLaunchConfiguration`, a closed projection of the two environment-backed -causal axes — `server_default_context` and `serving_slots` — and -`ExactRuntimeConfigurationIdentity` hashes the canonical wire those values render to -through `ollama_context_length_env_assignment` and `ollama_num_parallel_env_assignment`. -So the two runners this section said would collide now carry distinct identities. - -Three details of the closure are worth carrying forward, because they are not what this -section asked for: - -- It was **replaced, not widened**. Argv was never the right subject; the authority for - what configures an Ollama server is `extdeps.ollama.server_env`, which already holds the - variables as typed axes. -- The partition is **owned by the module, not supplied by the caller**. The predecessor - took a `varied_flags` list as an argument, which let any caller erase a causal axis from - the fixed mode and thereby authorize evidence transport across it. -- Identity runs **typed values first, wire second** — the module never parses an observed - string, because a string-to-integer admission here would be a second numeric authority. - -The ruling this section asked for was given and is recorded at item 4 below. - -## What has to happen, in order - -1. ~~**Decide the served model in the model.**~~ CLOSED — see §3. The desired row names the - DeepSeek manifest and admission requires an identity match, not a name match. -2. **Model residency.** `OLLAMA_KEEP_ALIVE` appears nowhere on main, so a 94 GB model - evicts on the default idle timer and every measurement silently depends on when it ran. -3. **Admit the second pair** — cell roles, host identities, and whatever network identity - can be bound given that static reservations are refused by design. Admission must admit - ONE serving unit, not two serving cells; see §5. -4. ~~**Widen the realization carrier to environment.**~~ CLOSED — see §7. The ruling was: - consume a runtime-owned closed projection of causal configuration, not the whole - process environment. `OLLAMA_CONTEXT_LENGTH` and `OLLAMA_NUM_PARALLEL` are - configuration; `OLLAMA_HOST` is deployment identity; `OLLAMA_MODELS` is - artifact-resolution provenance already subsumed once the artifact digest is joined. - It landed in #9897, and `ObservedOllamaLaunchConfiguration` carries exactly that - partition. -5. **Route credentials through the modeled `SecretRef`** and rotate the current one. - -## The standing risk this document is really about - -After the correction, and after the served-model divergence closed, the remaining -divergences are narrower still but they are the same class: residency, the second pair, -and credentials are facts about the fleet that no authority owns. The danger is not that the model is wrong on -those axes — it is that it is SILENT on them, so nothing refuses when they drift. - -The generalization worth keeping is the one the correction taught: a divergence report is -a measurement, and a measurement is only as current as the tree it was taken from. This -document was itself an instance of the failure it exists to catalogue. diff --git a/docs/plans/srv1-sudoers-drift-observation.md b/docs/plans/srv1-sudoers-drift-observation.md deleted file mode 100644 index 335c7ee0821..00000000000 --- a/docs/plans/srv1-sudoers-drift-observation.md +++ /dev/null @@ -1,86 +0,0 @@ -# srv1 sudoers drift — observation receipt (2026-09-07) - -Authority for the class: `gunbc.recurring_failure_mode.capability_arrives_outside_the_converged_path`, -Specimen C. This file is the detail behind that row's specimen entry. - -**Why this file exists.** The observation was made on a live host while wiring the KVM applier that -row calls for. If the host is converged, the observed state is gone — so the record has to outlive -it. A chat message is not a record. - -**Handling constraint (operator ruling).** Grants are described by SHAPE — binary, run-as, whether an -argument restriction is present — and never transcribed as copyable authorization lines. The point of -this document is the *shape of the drift*, and a verbatim line would add nothing to that while making -the file itself a liability. - -## What was observed - -On `srv1`, at the time of observation: - -- The **modeled** grant artifact for this host, `provisioning/srv1/gunbc-ghrunner.sudoers`, is - **absent from the host's sudoers directory**. It is not installed under any name. -- An **untracked, hand-maintained file** stands in its place. It is not in this repository: no - authority row, no generator, no ignore rule, and no path that any modeled emission writes. -- Its mtime was **the same day it was found**, so it is maintained, not vestigial. A backup file - beside it, dated some weeks earlier, lacks three of the grants present in the live file — so those - three were **added** in the interval. - -### The shape of the divergence - -| | modeled artifact | installed file | -|---|---|---| -| run-as | `root` | `ALL` | -| argument restriction | **exact argv**, one line per permitted invocation | **none** — bare binary path | -| population | per-path, per-unit, enumerated | seven binaries, unrestricted | - -Several of the seven binaries are ordinary root escalations when granted without argument -restriction — this follows from each tool's own documented behaviour (editing the privileged -configuration, altering account group membership, creating accounts, writing arbitrary -root-owned files, running arbitrary units). **No escalation was attempted or tested.** The property -is inherent to unrestricted-argument grants on those binaries and does not need demonstrating. - -The grantee is the account that **executes CI jobs** on this host. That is what makes this a live -privilege boundary rather than only a provenance gap. - -## Why this blocked the work in flight - -The applier being wired runs one modeled argv — a supplementary-group grant for the KVM device. -While an unrestricted grant for that binary stands, **that argv is admitted by the hole, not by the -modeled line.** A success would therefore prove the over-broad grant works, not that convergence -does: the instrument and the subject share a path. That is a corrupted instrument, not a deferred -cleanup, which is why the work stopped here rather than proceeding and filing the finding. - -## The discriminator, fixed in advance - -**Stated in the authority row, not here.** The discriminating negative control, its positive -control, and why neither alone is sufficient are carried by -`gunbc.recurring_failure_mode.capability_arrives_outside_the_converged_path` — the row whose specimen -this document details. It is written there because that is the carrier the class lives on, and -restating it here would give one fact two independently editable homes. - -What this document adds is only the *occasion*: the discriminator was fixed **before** the evidence -existed, because a post-convergence green is uninformative unless what would falsify it was named -first. - -## A second, independent fact recorded at the same time - -Applying the group grant does **not** make the device usable by anything already running. The live -runner listener process holds a fixed supplementary-group set that does not include the device group. -`usermod -aG` edits the account database; it cannot alter the credentials of a running process. The -executor acquires the group only in a **new session** — i.e. after the service restarts. - -This is why the reconciler models `applied, pending new session` as an outcome distinct from both -`applied and now writable` and from failure. Collapsing them would report either a false success or a -false failure; which one occurs is determined by re-observation, never assumed. - -## How to re-derive - -Read-only, from a shell on the host: - -- list the sudoers drop-in directory with a long listing (names, sizes, mtimes) -- grep that directory for `NOPASSWD` to see each grant's run-as and whether an argument follows the - binary path -- search the same directory for the modeled artifact's filename to confirm presence or absence -- read the runner listener's `/proc//status` `Groups:` line, and compare against the device - group's gid from `getent group` - -Nothing above modifies the host. diff --git a/docs/plans/t5b-closure-bearing-serde-debug-decision-2026-08-21.md b/docs/plans/t5b-closure-bearing-serde-debug-decision-2026-08-21.md deleted file mode 100644 index 22574f76d97..00000000000 --- a/docs/plans/t5b-closure-bearing-serde-debug-decision-2026-08-21.md +++ /dev/null @@ -1,163 +0,0 @@ -# T5b decided: closure-bearing declarations split into two dispositions, not one repair - -**Session `tidy-dove-648`. Work item:** `node://adhoc-c3017faf-ad6` — "E0277 root T5b (35 sites, -largest): serde/Debug is demanded over closure-bearing values and NO derive can be added — decide -the modeling question, do not repair." This document is that decision. It does not touch -`v1.trait_derive_emit`, `v1.04_emit_info`, `v1.05_emit_rust`, or `v2.std.compilers.target_model` — -the repairs it hands off are named at the end, scoped but not implemented here. - -**Inputs.** Three prior receipts, re-verified against the live `.dag` declarations rather than -trusted as transcribed numbers (§3's citation rule — symbols below are grep-checked by name, not -carried as positions): - -- `docs/probes/e0277_partition_2026-08-21.md` (PR #8712, on `main`) — first framed root 1 ("serde/Debug - derive on a function-valued carrier") as impossible-to-derive and structural. -- `docs/plans/self-host-cargo-refusal-root-partition.md` §11.19 and §11.23 (on `main`) — named the - same root T5b, attributed 44 sites to ~12 enclosing declarations, and posed the open question - verbatim: *"should a declaration whose value contains a function be serializable at all?"* -- `docs/probes/e0277_root_partition_2026-08-21.md` (session `bright-moth-92`, PR #8731, **not yet - merged**) — re-measured T5b at E0277-only, site grain: **35 sites**, concentrated in - `v2_std_runtime.rs` (13), `v2_std_compilers_target_model.rs` (11), and three compiler-stage files - (11). Cited here as evidence, not as merged fact — its branch is `origin/session/bright-moth-92`. - -## The decision - -**T5b is not one modeling question with one answer. It is two populations that look identical at -the trait-bound grain and require opposite treatment.** "Drop serde/Debug wherever a closure -appears" is wrong for the second; "keep serde/Debug and find a wrapper" is wrong for the first. -Distinguishing them is the modeling work this root needed. - -### Population 1 — process-local realization values: drop serde/Debug/PartialEq, keep Clone only - -`v2.std.runtime` `ValueInterpreter`, `TransformInterpreter`, `BranchInterpreter`, `LoopInterpreter`, -`BindInterpreter`, `MatchInterpreter`; the `v2.std.runtime` `RuntimeBehaviorInterpreter` coproduct -wrapping them; `v2.std.runtime` `InterpretationAlgebra`; `v2.compiler.01_tokenize` -`CompiledLexRule`/`LexWalkAcc`; `std.algebra` `PartialFunction`; and `v2.compiler.05_eval` -`EffectIoEvalBundle` / `EffectIoEvalContext` / `EffectIoYieldOutcome` are, by construction, -**evaluator plumbing that exists only inside one compile process.** None is written to disk, sent -across a process boundary, or logged as a debug artifact — each is built from a live -`EvaluationAlgebra`/handler table at run start and discarded at the end. `Serialize`/`Deserialize`/ -`Debug` on them was never a consumer need; it was the derive roster applied uniformly regardless of -what the declaration is. - -**This is not a new call.** `v1.trait_derive_emit` `v1_emit_struct_derives` already special-cases -`has_fn_fields` to emit `std.trait_derive_shape` `fn_field_derive_traits()` — Clone, nothing else — -and `docs/plans/self-host-cargo-refusal-root-partition.md` §11.19 states it: *"`CompiledLexRule` -emits `#[derive(Clone)]` and nothing else, because `fn_field_derive_traits()` is Clone-only — -correctly, since `Rc` is not serializable and not `Debug`."* Population 1's decision: -**apply that existing, correct rule consistently.** Per DESIGN §5, whether a declared type -transitively reaches a function value is decidable and structural, so this is a construction wall, -not per-declaration validation — one authority (reachability), derived once. - -**Why 32 of these 35 sites exist despite the correct rule: it is wired for structs but not -coproducts, and the transitive walk cannot see through either.** Confirmed by reading -`v1.04_emit_info`, not inferred from the site count: - -- `v1.05_emit_rust` `enum_derives` calls `v1.trait_derive_emit` `v1_emit_enum_derives` with **no - `has_fn_fields` parameter at all** — `v1_emit_struct_derives`'s `has_fn_fields` branch has no - enum counterpart, so a coproduct is never routed to `fn_field_derive_traits()` whatever its - variants carry. Hence `RuntimeBehaviorInterpreter`, a coproduct whose every variant wraps a - closure-bearing struct, still derives the full `payload_coproduct_derive_traits()` roster - (Debug/Clone/PartialEq/Serialize/Deserialize) — 18 of the 44 occurrences / a majority of the 35 - E0277 sites by itself. -- `v1.04_emit_info` `build_type_summary`'s enum branch hardcodes `field_type_map: empty_map()` for - every enum, where its struct branch populates `field_type_map` from the real fields. - `v1.04_emit_info` `type_summary_reaches_fn` walks exactly that map for transitive - closure-reachability, so enum variant payload types are **structurally invisible** to the - existing fixpoint (`v1.04_emit_info` `close_fn_fields`). `v2.std.runtime` - `InterpretationStructureWitness` (6 sites, holding only `Symbol` fields) is not a second modeling - case but this blind spot's collateral, most likely misattributed by the census's 200-line - proximity heuristic to a neighboring declaration that legitimately fails. It is expected to - dissolve once the two gaps above close — not a third disposition. - -**Handoff (repair, not modeling — do not do this in this PR):** thread `has_fn_fields` through -`v1_emit_enum_derives` as `v1_emit_struct_derives` already consumes it, and populate -`field_type_map` for enum variant payload fields so `type_summary_reaches_fn` sees through a -coproduct as it does a struct. Both are decidable, mechanical, and covered by the existing fixpoint -infrastructure — completing an existing wall, not authoring a new one. - -### Population 2 — dispatch fused into an interface record: split it out, don't strip the record - -`v2.std.compilers.target_model` `ProducedDeclSupport` is different in kind, not degree: - -``` -type ProducedDeclSupport - = ProducedDeclUnwired - | ProducedDeclWired { - render: fn(Node) -> Outcome - scaffold_relation_rule_name: Symbol - scaffold_base_row: Node - } -``` - -`ProducedDeclSupport` is a field of `v2.std.compilers.target_model` `TargetModel`, which is not -process-local plumbing but the **per-target-language configuration record** built once per -language (`rust_target_model()`, `python_target_model()`, and eight more call sites across -`src/v2/extdeps/languages/*.dag`, `src/v2/extdeps/{github,bmc}/*`, `src/v2/extdeps/formats/*`) -and consumed pervasively through the emit pipeline. Stripping `Debug`/`Serialize`/`Deserialize` -from `TargetModel` would be a real loss — comparing, inspecting, and (per the `06_translate` -consumers) potentially persisting target-model facts is what this record exists for, unlike a -`ValueInterpreter` closure table nobody prints. Population 1's answer does not transfer; this is -where §11.23's "operator question" lives. - -**The field is redundant with a fact already on the same variant.** `ProducedDeclWired` already -carries `scaffold_relation_rule_name: Symbol` — a named, resolvable identity for *which* rule -renders the scaffold. Its `render` field is that rule's own dispatch, embedded a second time as a -live closure beside the name that identifies it — the shape DESIGN §3 rules out: *"the dispatch -that selects a realization is itself realization... A pure spec is dispatch-free; a `std` -projection that matched over its realizations would have to name them, fusing dispatch back in."* -`ProducedDeclWired` should be the *interface* fact (which rule, over which base row) that -`TargetModel` carries as configuration; embedding the closure makes it carry the *realization* -too, which is why it alone among its siblings cannot be part of a serializable record. - -**Decision: remove `render` from `ProducedDeclWired`.** The variant keeps -`scaffold_relation_rule_name: Symbol` and `scaffold_base_row: Node` — plain data, derivable under -the standard record roster. The `fn(Node) -> Outcome` behavior -moves to a peripheral, non-serialized dispatch table (a `Map Outcome<...>>` or -equivalent registry) keyed by `scaffold_relation_rule_name`, resolved only where a scaffold is -rendered — never carried inside `TargetModel`. This is the named-resolvable-reference answer from -§11.23's three options, chosen over "drop serde/Debug" (wrong here — throws away a real -requirement) and over "split into description + realization as two fields on the same record" -(redundant — the description, the rule name, already exists; a second field beside it would -nickname one identity twice, which DESIGN §3 forbids). With `render` gone, `ProducedDeclSupport`, -`TargetModel`, and everything containing them keep full Debug/Clone/PartialEq/Serialize/Deserialize -— the fix removes a misplaced fact, not the record's strength. - -**SUPERSEDED, AND IN THE SAME DIRECTION.** The `render` field is gone, but not into a peripheral -registry keyed by `scaffold_relation_rule_name`: rendering is now DECLARED ROWS on the variant -(`v2.std.compilers.target_model` `ProducedDeclRenderRows`, read by the single shared fold -`produced_decl_render_from_rows`), and each target declares its own signature order as data -(`v2.extdeps.languages.rust` `rust_produced_decl_render_rows`, `v2.extdeps.languages.c` -`c_produced_decl_render_rows`). A registry would have kept the fn alive one indirection away — still -origin-bound, which is exactly what blocked the required floor from sharing one evaluation of a -target model across claim frames (`v2.workflow.floor_pure_producer_share`). Rows delete the fn -outright, so `TargetModel` is a pure content value; the interface/realization split is preserved, -the target still owns its order, and the compiler holds no per-target arm. - -**The other four `v2_std_compilers_target_model.rs` sites are very likely the same collateral -pattern as `InterpretationStructureWitness`, not independent decisions.** -`v2.std.compilers.target_model` `TargetDeriveSupplementalGenericBoundContractAuthority`, -`TargetDeriveSupplementalGenericBoundContract`, `TargetCollectionRealization`, and -`TargetRepresentationParameterSlot` — confirmed by source read to be **an empty struct**, -`TargetRepresentationParameterSlot {}` — hold no function field directly or (by inspection of their -declared fields) transitively. Their likeliest cause is proximity to `ProducedDeclSupport` within -the same module/derive-refusal cluster — the 200-line-window attribution effect the prior receipt -flagged for `InterpretationStructureWitness`. **Do not author a fix for these four independently.** -Re-measure this file after the `render` removal lands; whatever remains is a real, distinct defect -and gets its own row. - -## What this decision resolves and what it hands off - -| population | declarations | disposition | this document | handoff | -|---|---|---|---|---| -| 1 — realization values | `ValueInterpreter`+5 siblings, `RuntimeBehaviorInterpreter`, `InterpretationAlgebra`, `CompiledLexRule`, `LexWalkAcc`, `PartialFunction`, `EffectIoEvalBundle`/`EvalContext`/`YieldOutcome` | Clone-only; serde/Debug never legitimate | decides + grounds the rule already implicit in `fn_field_derive_traits()` | wire `has_fn_fields` through `v1_emit_enum_derives`; populate enum `field_type_map` in `v1.04_emit_info` | -| 1 (collateral) | `InterpretationStructureWitness` | expected to dissolve with population 1's repair | names it, does not fix it | re-measure after the repair, do not pre-emptively touch | -| 2 — dispatch-in-interface | `ProducedDeclSupport`/`ProducedDeclWired` | drop the embedded `render`; dispatch by the existing `scaffold_relation_rule_name` in a peripheral registry | decides the split and which existing fact survives as the identity | remove the field, add the registry, rewire the ~10 `TargetModel` construction sites that build a `ProducedDeclWired` | -| 2 (collateral, unconfirmed) | `TargetDeriveSupplementalGenericBoundContractAuthority`, `TargetDeriveSupplementalGenericBoundContract`, `TargetCollectionRealization`, `TargetRepresentationParameterSlot` | presumed collateral of population 2; not independently decided | names the four, declines to assume | re-measure after population 2's repair; triage what remains then | - -**What is not claimed.** This document does not re-run the probe, so it confirms neither that the -35-site count drops to zero nor that population 2's four collateral declarations resolve on their -own — both are expectations grounded in the reachability mechanism being fixed, not measured -outcomes. Per DESIGN §16 of the shared partition doc, a site count measures where the compiler -pointed; closing this decision is a repair PR's job, verified by re-running -`docs/probes/curated_cargo_probe_one.sh` against the same entry set after both handoffs land. diff --git a/docs/plans/the-emit-near-ceiling-family-is-not-a-defect.md b/docs/plans/the-emit-near-ceiling-family-is-not-a-defect.md deleted file mode 100644 index 4bc28ea5b6d..00000000000 --- a/docs/plans/the-emit-near-ceiling-family-is-not-a-defect.md +++ /dev/null @@ -1,116 +0,0 @@ -# The emit near-ceiling family is not a defect - -*A negative result, filed so the next lane skips four plays that do not work. Produced by -jolly-ferret-412 while owning the charge subject behind the required floor's 500ms per-claim CPU -ceiling. Every figure below names the producer that re-derives it; where a number and a producer -disagree, the producer is right.* - -## What this closes - -Three `v2.test.emit` identities refused the required floor on run `33695697323`. The question -routed to this lane was whether the emit family carries a repairable cost defect. **It does not.** -Four rows, two clusters, 292–350ms off the floor, none individually remarkable; what put two of -them over 500ms is the run they landed in. - -That answer is only useful with the plays that were tried attached, because each looks obviously -correct from the outside: - -| play | outcome | where it is recorded | -| --- | --- | --- | -| serve the shared producer across claim frames | **measured and refused**, three candidate rows, every one made its consumers worse | `v2.workflow.floor_pure_producer_share` `floor_cross_claim_refused_candidates` | -| find the one dominant shared producer, as gunbc#10133 did | **refuted twice**, independently | `eval_steps` fingerprint in `docs/plans/floor-cost-distribution-near-the-ceiling.md`; and the present-vs-absent runs behind the refused rows above | -| repair the worst row per-producer | **8ms**, against a question 87 rows deep | the top-down table in the same memo | -| read the interrupted rows' printed cost as a margin | **not a cost at all** — an interrupted row reports the ceiling that interrupted it | `v2.workflow.required_floor`, and the in-band sentence the floor prints beside it | - -## The four rows, measured off the floor - -Two of the three refusing identities were `budget_interrupted`, so their cost was **unbounded -above** — `required_floor_claim_cost.tsv` records them at 505/503 and 508/504, which is the -interrupt point, not a measurement. A row with no upper bound cannot be ranked, cannot be compared, -and cannot be shown to have improved by any repair, so the first step was to bound them. - -Instrument: `claim_batch --source-root dag --source-root src/v2 --entry --functions -`, one entry per module, no floor and no budget. - -| identity | floor | off-floor | -| --- | --- | --- | -| `produced_decl_two_targets_render_own_order` | ≥503 interrupted | 346ms | -| `rust_produced_decl_emits_named_add` | ≥504 interrupted | 294ms | -| `rust_produced_decl_emits_named_add2` | 489 passed | 292ms | -| `rust_produced_decl_name_discriminates` | 527 over-budget | 350ms | - -**No absolute figure crosses between those columns.** The off-floor arm is a different -architecture and a different execution context; every conclusion here uses within-column ordering -only, four rows measured under identical conditions in each column. - -## Two rows, two mechanisms - -`named_add` off the floor is 294ms against its sibling `named_add2` at 292ms — the same row within -1% — while on the floor the sibling completed at 489 and it was interrupted. **Contention**, and -three instruments that share no mechanism agree: - -1. µs/step from the floor artifact: 3.238 against the sibling's 2.990, 8.3% slower per unit of - work — derived from `cpu_ms` and `eval_steps`, which are sampled in consecutive statements after - `run_claim` returns and netted by the same shared-fill rule, so the ratio survives reordering. -2. off-floor sibling parity, above. -3. required multiple: both siblings needed ~1.70x to cross, above the measured p90, and exactly one - of them did. - -`two_targets` is the opposite. Its 188,416 steps at interrupt exceed every sibling's *completed* -total, so it does more work — but off the floor it lands at 346ms beside `name_discriminates` at -350ms. **Biggest in work, ordinary in time**, and time is what the ceiling adjudicates. A work -bound is not a cost property; inferring one from the other is the error this row exists to -document. - -## What the ceiling is actually adjudicating - -Eleven floor runs joined on claim identity: `33661252708`, `33664119594`, `33667640710`, -`33671317370`, `33688140761`, `33695697323`, `33699325123`, `33699540412`, `33700216949`, -`33700681714`, `33701938720`. - -**No identity crossed twice.** Four runs had crossings; not one identity recurs. Producer: -`gunbc.floor_cost_distribution` `crossing_recurrence` and `recurring_crossers`, whose empty answer -over single-run crossings is the finding rather than the absence of one — and whose positive -control in `test.claim.floor_cost_distribution_witness` is what keeps the empty answer from being -vacuous. - -**Crossings cluster by family within a run.** Run `33688140761`: ten crossers, all ten in -`test.claim.self_host_compile_phase_live_gate_witness`. Run `33695697323`: three, all in the emit -produced-decl family. A run tips a family; which identity inside it tips is arbitrary. - -**The run factor is large.** Over 296 identities present in all eleven runs with median cpu ≥100ms, -taking each row's cross-run median as its own baseline, the per-run median factor runs -0.858 … 1.154 — 1.35x best to worst on the *median* row, not a tail. The near-ceiling population -tracks it: rows ≥350ms per run come out 3 on the 0.959 run and 36 on the 1.154 run. Producer: -`identity_inflation_permille` composed with `permille_percentile`. - -So the run factor decides **whether** anything crosses; a row's baseline cost decides **which -family** is exposed when it does. The line adjudicates a product of two things and attributes it to -one. - -## The instrument that needs no control population - -`eval_steps` is host-independent and byte-identical for the large majority of identities across a -run pair. For those rows the only thing that could have changed did not, so any cpu movement is -inflation **by construction** — no normalisation, no control population to choose and defend. -Producer: `identity_inflation_permille_at_equal_work`, which drops any row whose step count moved -rather than smoothing it. - -Its reach must be reported beside it — `count` it against `identity_inflation_permille` — because a -clean instrument over three rows is not a measurement of a corpus. - -## What is NOT claimed - -- **The vintage confound is load-bearing.** The eleven runs span different tree vintages, so "no - identity crossed twice" is confounded by repair: the ten `live_gate` crossers stopped recurring - partly because that family was fixed. This is not a pure contention result. -- **Equal steps is equal EVALUATOR work, not equal host work.** A row whose time sits inside one - opaque host call reports few steps in both runs; `v2.workflow.required_floor` carries a declared - rung drop for that region. -- **No safe-cost figure is derived here, and one was withdrawn.** A `500 / p90` derivation was - circulated by this lane and retracted: a p90 answers a per-row question while the ceiling refuses - the whole *run*, and the stratum quoted stopped one bucket short of the population that can - actually reach 500 — where inflation rises again rather than continuing to fall. Both errors ran - in the unsafe direction. The stratified table with n per bucket belongs with the decision-maker, - and the quantile choice is theirs. -- **Nothing here is a recommendation about where the line should sit.** diff --git a/docs/plans/throughput-qualified-convergence-design.md b/docs/plans/throughput-qualified-convergence-design.md deleted file mode 100644 index f78f2e4fc44..00000000000 --- a/docs/plans/throughput-qualified-convergence-design.md +++ /dev/null @@ -1,255 +0,0 @@ -# Throughput-qualified convergence — one interface, one bound subject, one frontier - -Standing: a design under revision, not a landed capability and not yet enrolled in DESIGN. It has been -through three review rounds (side-chat, 2026-09-17); the corrections are recorded in place rather than -absorbed, because several of them are the kind of error this document exists to make harder. - -## The defect this exists to close - -**A convergence today verifies its CONFIGURATION and stops. Neither subject verifies the RATE the -configuration was chosen to deliver.** The configuration is a proxy for the goal, and reading the goal -off the proxy is the gap. - -Two live instances, each found by looking: - -- **Serving, Group B, 2026-09-17.** A converge reported success and was independently verified on four - hosts: image derived, pin correct, checkpoint loading, fp8 admission holding, KV law single-sourced. - Every one of those is true and none is about collective transport. The arm was running every NCCL - channel over `NET/Socket/0` with no IB device present, which the converge could report full - agreement through because transport was not in its comparison. -- **CI.** `docs/plans/ci-humming.md` derives a runner-slot count from a memory budget across T0-T6 and - never measures jobs per hour afterwards. T1 carries the right instinct one level down — a cap is not - effective until the live cgroup is read back — and stops at configuration. - -## Convergence and qualification are two facts, not one (corrected) - -An earlier draft said "converged" should mean configuration agreed AND the rate was met. That is -wrong, and the error is worth keeping: a realization can match its desired image, checkpoint, argv, -transport plan, capabilities, devices and rank population exactly while missing a throughput floor. -That is not configuration drift, and redefining convergence to cover it destroys a distinction the -operator needs — *state agreement* and *service qualification* have different remedies. - -`std.goal_assessment` already owns the remedy-free relation between a caller-supplied goal and an -independent observation, and deliberately keeps observation, assessment and remedy apart. So the -shape is a composition, not a redefinition: - - ConfigurationStanding = ConfigurationConverged | ConfigurationDiverged | ConfigurationIndeterminate - ThroughputAssessment = GoalSatisfied | GoalDiverged | GoalIndeterminate | GoalAssessmentRefused - ConfigurationConvergedReceipt -- mintable ONLY by matching ConfigurationConverged - ThroughputGoalSatisfiedReceipt -- mintable ONLY by matching GoalSatisfied - - type OperationalQualification sole_constructor - = RealizationQualified { - configuration: ConfigurationConvergedReceipt - throughput: ThroughputGoalSatisfiedReceipt - } - | RealizationNotQualified { - configuration: ConfigurationStanding - throughput: ThroughputAssessment - } - -**Two revisions of this carrier were wrong, in opposite directions, and both are worth keeping.** The -first offered `ConfigurationNotConverged | ThroughputNotQualified` as alternative negative arms, which -forces a reader to pick one when a realization can be configuration-diverged AND throughput-diverged -at once -- so the negative arm now carries both complete standings, and no priority fold can lose -evidence when both are non-positive. - -The second declared `RealizationQualified { configuration_receipt, throughput_assessment }` with the -assessment typed as the whole coproduct. That field proves only that SOME assessment is present, so -`RealizationQualified { throughput_assessment: GoalDiverged { .. } }` is constructible and the carrier -certifies nothing. `sole_constructor` confines construction to one module; it does not refine which arm -of a supplied coproduct is present. The refinement has to be in the TYPE: a receipt mintable only by -matching the satisfied arm. That error is precisely the one §4b names -- *a brand, wrapper or -`Validated` is cosmetic until construction and acceptance enforce the distinction* -- committed in -the paragraph that cites it. - -The structural rung is earned only when this implication holds by construction, and not before: - - RealizationQualified => ConfigurationConverged AND GoalSatisfied - -This also repairs the rung argument. A configuration convergence with no throughput result is a -legitimate value, not a defect. The invalid state to remove is a `RealizationQualified` that does not -imply both positives — and THAT is structurally impossible once the two receipts are refinement types, -because no unsatisfied assessment can be carried into the positive arm. Enrolling every production -route remains mechanically preventable. Two different rungs for two different claims, which is what -§4b(1) asks for. - -## Why this is one interface (DESIGN §2, horizontal) - -"16 decode streams against a serving arm" and "N concurrent builds against a runner pool" share an -interface: within a declared window, apply a declared workload to a subject, derive a rate from a -counter the system itself keeps, and assess it against a floor that is not a copy of the last reading. - -The partition matters more than the sharing, and DESIGN §3b's fabric row already states it for -capacity: **selection is shared; occupancy is not.** - - shared: observation and assessment interface, rate derivation, goal assessment - subject-specific: isolation acquisition, workload realization, counter producer, - subject identity, occupancy and release mechanism - -So serving's termed `product.capacity.lease::LeaseGrant` is NOT the CI window's carrier. Compute -occupancy is a timeless `LeaseIdentity` settled by `file_compare_and_set`. An earlier draft of this -document cited `std.temporal_effect::HeldLease` for both, which carries an epoch and an observed state -and **no deadline at all**; a later draft then cited `LeaseGrant` for both, which crosses the very -boundary the roster draws. Each subject brings its own occupancy carrier. - -## The parts, each with the failure it prevents - -### 1. A quiet read is not a reserved window -A CAS stops two probes believing they hold one claim. It does not stop a production request arriving -after `num_requests_running == 0`, GitHub dispatching ordinary work after a queue check, or an -already-admitted operation becoming active mid-measurement. Isolation is therefore a standing the -subject-specific handler PRODUCES, not something the generic fold assumes: - - ProbeWindowStanding - = WindowIsolated { grant, deadline, drain_receipt, exclusion_receipt } - | WindowSharedWithDeclaredTraffic { traffic_receipt } - | WindowIsolationUnread { cause } - -Serving realizes it by route withdrawal or a dedicated endpoint, a drain, and a fence against -non-probe requests; CI by a dedicated runner population withdrawn from ordinary labels, drained, with -probe-only dispatch. Without an exclusion receipt the reading is still useful production telemetry — -it is simply not a controlled qualification, and must not be recorded as one. - -### 2. The offered load is a policy, and the receipt constrains it rather than manufacturing it -Declaring the load is necessary; grounding the FIGURE is the rest. But an earlier correction here -introduced circularity — "concurrency derived from the admitted policy, and the policy owes a capacity -receipt" — which has the receipt manufacture the workload that then verifies it. The clean relation: - - QualificationProtocol declares the offered load (e.g. 16 concurrent) - ObservedCapacityReceipt may admit or refuse that policy as safe or plausible - ThroughputObservation establishes the delivered rate AT that load - -A contract may legitimately require 16 concurrent before anyone has found maximum capacity. And -`max_num_seqs` is a **scheduler ceiling, not a workload authority**: a launch configured for 16 may be -qualified at 8, 16 or another declared load depending on the contract being tested. - -### 3. Backlog is what is observed; saturation is an inference, and capacity a further one -Two drafts got this wrong in opposite directions — first "queue depth > 0 means saturated", then "a -sweep establishes it". vLLM's own metric definitions treat running as current execution and waiting as -backpressure; neither defines a physical-saturation verdict. `16 running / 0 waiting` can be fully -occupied, and `16 running / 5 waiting` can be waiting on `max_num_seqs` or KV admission with no -hardware bottleneck identified. So: - - queue > 0 does NOT establish saturation - queue == 0 does NOT establish unsaturation - -The reading names only what was observed — `BacklogObserved` | `NoBacklogObserved` | `BacklogUnread` — -and a capacity inference is a separate operation with its own declared load progression and plateau -criterion. **For an operating floor, capacity need not be inferred at all:** *under protocol P at -offered load L, did the subject deliver at least floor F?* is answerable whether or not L saturated -anything. - -*Correcting this document's own figures:* the 2026-09-17 socket readings retained no queue trace, so -they are `BacklogUnread` and `CapacityUnestablished`. The 8-to-16 scaling argument (aggregate 27.31 to -51.19 while per-stream moved 3.41 to 3.20) is useful causal reasoning and is **not** a queue -observation; assigning `UnsaturatedAtOfferedLoad` from it, as an earlier draft did, was an inference -reported as a reading. - -### 4. Subject equality alone does not make two rates comparable -`serving_performance_subject` is a configuration fence, and its own source says launch identity is a -separate boundary — several launches may share one subject. The observation therefore carries more -than the subject: - - ThroughputObservation { - subject the effective configuration - protocol concurrency, work identity, sizes, stopping rule, warmup and cache rules - realization the exact serving incarnation or runner population - window_standing isolated, shared under declared traffic, or unread - counter_producer who kept the count - counter_span before, after, and continuity across the interval - operation_ledger every offered operation reached an admissible terminal outcome - raw_carrier the bytes the reading was taken from - observed_at - } - -**Counter continuity** is its own field because a counter that reset, rebound or switched incarnation -mid-window yields a delta that is arithmetic rather than a measurement. - -**Operations and rate units are different grains**, which an earlier draft conflated by counting -"attempted, completed and refused tokens". For serving the operation is a request and the unit is a -generated token — and a request can emit tokens and *then* fail, so the ledger cannot be denominated -in tokens. For CI the operation is an exact Work/job and the unit a completed work occurrence; "jobs -per hour" means something only over homogeneous work or a declared normalization, because a one-minute -no-op and a fifteen-minute clean build are not interchangeable units. - -### 5. The rate is derived, never stored -`gunbc.harness.harness_throughput` already states this law for its own stream — counts and interval -are carried, the rate is a function of them, and storing it alongside would be a second representation -free to disagree with its inputs. Cited, not re-coined. - -### 6. The floor is not a copy of the last reading -DESIGN §5: a merge-blocking literal needs a controlled fixture, an external or versioned authority, an -explicit policy budget, or a monotone debt contract. A floor pasted from this morning's run collapses -to `measure() == measure()`. So the floor is a declared operating budget someone owns, or a monotone -contract raised deliberately, with provenance either way. The verdict is `std.goal_assessment`. - -## Conformance (DESIGN §3b) - -Rows touched, with module names as the modules declare them (an earlier draft prefixed two of these -with `gunbc.`, which is the file path and not the module): - -- **fabric / compute** — `product.capacity.lease::LeaseGrant`, `product.fabric.selection::select_supply`. - The row already states the partial sharing this design inherits: shared selection, separate - occupancy producers and linearizations. -- **leasing / locking / grants** — the serving window as a termed grant; the CI window through - compute's own timeless identity. `std.durable_compare_and_set::CasExpectation` is the linearizer and - is not by itself an exclusion receipt. -- **process observability / reporting** — `std.observation::ObservationEvent`, - `std.observation::RecordedObservation`. -- **decision / selection** — deliberately NOT touched while the probe only assesses. The moment it - ranks configurations, `std.decision` is the home and §3d binds. - -## Evidence standing for readings already taken - -An earlier draft argued the socket readings qualify as a receipt BECAUSE the arm is gone and the -question cannot be put again. That is wrong: irreproducibility establishes urgency to preserve -evidence, never provenance for it. The honest standing follows what was retained: - -| retained | standing | -|---|---| -| exact subject, protocol, incarnation, raw counter and request traces, interval | subject-bound historical observation | -| summarized numbers with no raw producer carrier | historical reported reading | -| no queue trace | `BacklogUnread` | -| successor moved transport AND other axes | no transport-only attribution | -| no exact workload identity | protocol-unbound historical claim | - -The 2026-09-17 socket figures are a **historical reported reading**: summarized aggregate and -per-stream rates, no retained raw carrier, no queue trace. They are worth recording as that, and they -are not a `RecordedObservation`. - -The arm they were taken against was converged by a lane outside the modelled transaction, so a floor -set from them grounds a claim about the SUBJECT and never about a transaction that did not run. -`AdoptedOutOfBandRealization` is this document's PROPOSED name for that standing and has no home in -the tree — it is used here in the indicative and is not yet vocabulary (review 67372). Its nearest -neighbour is `docs/plans/scaffold-admission-doctrine.md`, which owns out-of-band ACTUATION, and the -two are adjacent rather than the same question: that doctrine governs whether a hand-authored actuator -may merge, while this standing labels how an OBSERVATION OF ITS RESULT may be cited. Either it is -given a home when the design settles, or it is dropped in favour of whatever already answers that — -and until one of those happens, naming it in the indicative here is a §3 nickname waiting to happen. - -## Staging - -1. **Record the socket figures at their honest standing** — historical reported reading, `BacklogUnread`, - `CapacityUnestablished`, `AdoptedOutOfBandRealization`. -2. **The serving probe** — extdeps rows for the completions route and the metrics counters; window, - protocol, ledger, counter-span and rate folds; per-layer witnesses on supplied inputs with one - inhabitance claim that runs the real endpoint. -3. **The CI subject is a FRONTIER, not a bound subject** — which is why this document's title says one - bound subject and one frontier. `ci-humming.md` is the June slot/cgroup program and carries the - older control/apply architecture; current main has a Firecracker/JIT runner-attempt planner working - through fabric identities and execution grants. Stage 3 therefore begins with a producer-and-consumer - census of the CURRENT tree, not the historical plan, covering at least: exact Work and source-tree - identity, runner image/kernel/rootfs, microVM shape, host class, slot count and memory envelope, - jobserver and resource controls, runner/JIT revision, toolchain revision, cache topology and cache - state, and dispatch policy. Which of those invalidate a prior rate is decided by that census. -4. **The CI probe**, binding the same interface to dispatched builds. -5. **Qualification** — `OperationalQualification` composed from the two standings, with - `RealizationQualified` unmintable without its assessment. - -## Not yet done, stated plainly - -This plan is **not enrolled in DESIGN**. DESIGN.md is generated from `gunbc.design_document` and is -never hand-edited; a plan is linked from the section that governs it. A file existing under -`docs/plans/` is not that linkage. Enrolment lands when the design settles, and until then this -document governs nothing. diff --git a/docs/plans/typed-module-cross-run-materialization.md b/docs/plans/typed-module-cross-run-materialization.md deleted file mode 100644 index b00ca3a7baf..00000000000 --- a/docs/plans/typed-module-cross-run-materialization.md +++ /dev/null @@ -1,109 +0,0 @@ -# Typed-module materialization across runs - -> **Status:** DELETED (2026-10-03), after measurement. The measurement this file was written to -> run was taken on BuildBuddy (a 65 GB runner, one `claim_batch --claim-run` process over 5 entries, -> re-derived by arming `GUNBC_TYPED_STORE_PERSIST` against the same invocation unarmed): armed, the -> cold run exceeded a 720 s cap against ~150 s unarmed without evaluating an entry, and wrote -> ~7.3 GB in TWO entries; a second armed run also exceeded the cap and wrote nothing new. So each -> entry was a multi-gigabyte snapshot rather than one module, the store was a large net loss, and -> nothing bounded its root or cleaned it. The realization (`PersistentTypedStore`), its arming -> switch, its `cli_run` call sites and the `gunbc.floor_materialization` provider row were deleted -> in one change; the cross-process demands that row discharged are still declared there and now -> refuse with no provider. Sections 1-4 below describe the deleted design and are kept only as the -> record the open compiler-identity roadmap node still cites; nothing below is current behaviour. -> The replacement is a `TypecheckModuleRequest` store realized through -> `extdeps.realization.materialization_store_local`, encoded per module, bounded by a byte ceiling -> with oldest-generation eviction, under a scoped root that cleans itself. -> **Authority it answers to:** `DESIGN.md`; the ladder's admission rules in -> `std.materialization_ladder`; the retention vocabulary in `std.cache_interface`. -> **Framing:** relief during the v1 → v2 migration. Not a parallelism project. -> **Supersedes the design landed in #11779**, which this file replaced in place, as that design's -> own status line prescribed for acceptance. Three of its claims did not survive implementation and -> are withdrawn rather than quietly dropped: the `SharedStateFrame` and `CasTier` choices (§5), the -> statement that no content key existed, and the cross-push relief its key could not deliver (§2). -> Its projected figures — 8.3 min → ~1 min, 32 min → ~20 min per push — were never measurements and -> are not carried forward; §3 is what would establish them. - ---- - -## 1. What this is - -The typed-module snapshots the in-process tiers already produce now have a host-local, -content-addressed backing, so the value survives the process that computed it. It is one ladder -rung — a second provider over the identity `union-typecheck-store(dag,src/v2)` that the in-process -provider already covers — and not a new cache: the byte transport, the content key and the seam -are the ones that were already there, and only the backing changed. - -## 2. What it delivers, and what it does not - -**Delivered: same-build, cross-process reuse.** Two processes running ONE built binary over one -corpus — the witness fold and the generated-artifact drift gate — each pay typecheck preparation -in full today. The second one now reads the first one's results. - -**Not delivered: cross-push reuse.** The typed-module content key's compiler-identity term is -`resolved_graph_cache::transform_content_digest()`, the content hash of the running binary, and -`src/v1/stage0/build.rs` embeds `GUNBC_BUILD_IDENTITY` — the checkout's commit — into that binary -through `cargo:rustc-env`. A rebuild at any new commit, INCLUDING a corpus-only `.dag` commit, -therefore re-keys every entry. Any claim that an unchanged module survives a push is unsupported -until compiler identity is derived from something invariant under a corpus-only commit, which is a -change to that derivation and not to this store. **The design this file replaces asserted that -relief; that assertion does not follow from the key it proposed, and is withdrawn.** - -The bound is also narrower than "preparation" within one run: a hit skips `collect_parent_envs` -and the typecheck compute, which is where the preparation wall is spent, but the module is still -parsed and resolved, because that is what derives the key. Parse is not relieved. - -## 3. The measurement that decides whether this was worth it - -Not yet run. It is three demonstrations, and the first two are what "working" means here: - -1. **Cold → warm, separate processes.** Run the fold and then the drift gate over one corpus under - one built binary with the store armed, against the same pair with it unarmed. Compare end-to-end - wall, peak RSS, and the fold's subject digest — which must be EQUAL, because a store that - changes a verdict is a defect and not a saving. Read the `[typed-store-persist]` line on the - floor receipt for hits, misses, rejections, refusals, throughput and LIVE OCCUPANCY. -2. **An invalidation control.** Edit one module in the corpus and confirm that module and its - dependents miss while the rest hit, and that no obsolete result is served. -3. **A failure control.** Point the store at an unwritable directory, truncate an entry, and fill - it past its ceiling. Each must produce a counted cold or refused path. A verification mismatch - must be reported as `persist_rejected`, never folded into an ordinary miss. - -The Rust-level evidence for (3) and for the store's own properties is -`persistent_typed_store_tests` in `v1_compiler.shared_typecheck_store`, including the ceiling -refusal, the truncation rejection, the key-mismatch rejection and the residency-versus-throughput -distinction. Those are unit-level; (1) and (2) are the production-path receipt and are outstanding. - -## 4. How to arm it - -Off by default. Three environment variables, read once per process: - -- `GUNBC_TYPED_STORE_PERSIST=

` — the host-local directory. Arming is naming it. -- `GUNBC_TYPED_STORE_PERSIST_MAX_BYTES=` — overrides the modeled ceiling - (`floor_typecheck_store_persist_cap_bytes`). - -The SOURCE-ROOT SET is not an environment variable: it is taken from the index that asks, because -resolution is root-relative and the roots are a fact about the computation rather than an -operator's assertion about it. A second root set inside one process refuses. - -## 5. The four operational choices, and why - -| Choice | Settled as | Why | -|---|---|---| -| Location and ownership | One host-local directory named by the operator, outside the checkout. No daemon, no network service. | The tier is `ArtifactTier`/`LocalFilesystem` by `tier_axes`; anything more is a different provider with a different correctness surface. | -| Capacity and replacement | `ByteCapacity` with an exact limit, `RefuseNewStore`. | The ladder refuses `ReleasedNever` without a ceiling AND without observed occupancy, so both are obligations. `RefuseNewStore` over LRU because replacement is a second mechanism this bridge does not need: a full store declines, counts, and the run is merely cold. An entry-count bound over a variable-size payload establishes no byte bound. | -| Warm-seeding | Ordinary runs populate it. No seeding workflow. | A seeding job is a standing cost defended by a benefit nobody has measured yet. | -| Verification | The cold/warm procedure in §3, operator-invoked. | A scheduled job costs a full preparation by definition and is not a prerequisite for the first relief. | - -## 6. Deletion trigger - -This is a bridge over the v1 seed, and it ends with its consumer, not with a date: **when the -authoritative native route replaces the v1 preparation this store sits behind, its persistence -realization, its arming switch and its provider row are removed together.** v1 deletion does not -wait for this optimization to become a general platform. The rows are all named -`floor_typecheck_store_persist_*` in one module and the realization is one section of one file, so -the removal is a deletion rather than an untangling. - -## 7. Non-goals - -Threading the resolver, re-sharding the fold, and moving the fold to a hosted runner are all out -of scope. This change removes repeated work rather than distributing it. diff --git a/docs/plans/unconsumed-module-census.md b/docs/plans/unconsumed-module-census.md deleted file mode 100644 index 374f99356bb..00000000000 --- a/docs/plans/unconsumed-module-census.md +++ /dev/null @@ -1,1098 +0,0 @@ -# Corpus-wide unconsumed-module census - -**Execution record:** the disposition of this population -- what was deleted, and the typed -reason every held row survived -- is -[`unconsumed-module-residue-disposition.md`](unconsumed-module-residue-disposition.md). -This document stays the authority on how the population is derived. - -**Status: census only. Nothing is deleted by this document.** It exists to be reviewed -before any uprooting, per the dispatch: *"337 is too many to delete on one session's -judgment."* - -Operator directive it serves (2026-08-21, verbatim): *"yes please make sure to clean up -anything without consumers that we don't need, or get them actually consumed."* Both arms -are live — a module that **should** be consumed and is not is a missing-consumer defect, -not residue. - -## 1. The defensible number - -| quantity | value | unit | -| --- | --- | --- | -| `.dag` modules under `dag/` + `src/v2` | 3816 | DistinctModuleCount | -| consumed by discovery, not by import (`/test/`, `*_test.dag`, `/lens/`, `/manual/`, `/fixture*`) | 1928 | DistinctModuleCount | -| additional roots: named by an entry row (argv `--entry` or an `*entry*:` path field) | 48 | DistinctModuleCount | -| additional roots: carrying a v1 seed mirror in `src/v1/stage0/src/.rs` | 79 | DistinctModuleCount | -| reachable from those roots through imports **and qualified calls** | 3518 | DistinctModuleCount | -| **unreachable on imports, qualified calls, entry rows and seed mirrors** | **298** | DistinctModuleCount | -| — of those, **consumed by bare-symbol whole-pool reference** (§2 defects 6 and 7, re-scored 2026-08-22) | **≥ 92** | DistinctModuleCount, `LowerBoundOnly` | -| **unreachable after the re-score — the population as it now stands** | **≤ 206** | DistinctModuleCount, `LowerBoundOnly` | -| — of those, **residue on every decoded surface** — what a deletion lane consumes (§4h) | **≤ 112** | DistinctModuleCount, `LowerBoundOnly` | - -**Every number in this table is a BOUND, not a measurement, and the inequality signs are -load-bearing.** They sit in the cells because *a bounded number quoted without its boundary -becomes an exact one in the next reader's hands* — which is how **298** left this document and -became a deletion list. 206 and 112 are `LowerBoundOnly` for the same reason 298 was, and the -named blind surfaces (§2: dynamically composed argv, host-side invocation naming neither path -nor module, an entry row declared inside an unreachable module, and the service-namespace -credit of §2 defect 7(e)) can only move modules **out** of both. A reader who decodes an -eighth surface should expect both to fall again; that is the standing working, not failing. - -**The 298 is an over-count by at least 92 modules — roughly 31% — concentrated in the batch -this document proposed to delete first.** It is in the headline table because the headline -number is the one anyone acts on. **The two numbers answer different questions and must not be -substituted:** ≤ 206 is the *unresolved population*; ≤ 112 is what a deletion lane may -actually consume — the 94-module difference is `AMBIGUOUS-SHARED-ONLY` plus `MISSING-FILE`, -rows unresolved at identity grain rather than proven residue. Method and buckets: §2 defect 6; -residue list: §4h. - -Every number here carries its unit, and two are never interchangeable: a **DistinctFileCount** -(how many files name a thing) and a **SourceOccurrenceCount** (how many times it is named). -Reporting one as the other produced the false escalation in §4b; the discipline is cheap: name -the unit. - -**298, not 337.** The inherited figure was an upper bound over a different question (*zero -importers*), correctly labelled as such. Two independent corrections move it in opposite -directions and do not cancel: - -- *Zero-importer is too narrow.* It cannot see an **island** — a cluster whose members import - each other and nothing outside imports any of them. `extdeps/colo/` is one: 18 of its 19 - modules have zero importers, and the nineteenth (`extdeps.colo.types`) has eighteen, all - from inside the island. Reachability from roots catches all 19. -- *Zero-importer is also too wide.* See the instrument defect in §2. - -Re-derived from scratch; the inherited number was not reused. - -## 2. Instrument: the universe it decodes, and the defects found in building it - -**One cause produced most of what follows.** Every defect below except 1 and 4 has the same -shape: *a conclusion drawn without reading the surface that would settle it.* Defect 3 scored -the accelerator family dead without reading qualified calls; defect 5 froze two `tools/` rows -against a real re-add anchor without reading the `entry:` field surface, and ten more of that -class turn out to be invoked (see the re-score below); defect 6 scored `module_refs` dead -without reading bare-symbol resolution; defect 7 scored `pr_digests` dead without reading -variant constructors, in an instrument built to catch exactly that. The sequencing inversion -in §5 is the same failure applied to a decision. **In every instance the reasoning was locally -valid and the missing surface was the whole error**, so the review question is not "was this -argument sound" but "which surface settles this, and did you read it". - -**The rule that catches all of it, stronger than any defect below:** *a control derived from -the measurement it controls does not discriminate that measurement's blind spot.* Defect 7 was -invisible to every counter this census produces — the reachable count and the -CONSUMED-DECISIVE count both come from the defective instrument, so they could not register -the case it could not see, and read as reassurance *precisely because they stayed consistent*. -What caught it was the **required floor**, an instrument sharing no method: it refused a -deletion with `unresolved type MergeReadinessVerdict`. A census's own numbers are never its -control; only an independent mechanism is. - -**This rule is self-demonstrating twice in its own filing.** Defect 7 was checked against this -census *only* because an independent instrument refused on a different branch — nothing here -prompted it, and the counters had been stable across every revision. And the three further -defects inside its fix (generic headers, multi-line variant records, `operation` declarations) -were found by **hand-reading individual rows before publishing a number**; no counter surfaced -any of them, and the counters stayed consistent while all three were live. For anyone -extending this document: **before publishing a count, read enough rows behind it to be wrong -out loud** — and treat a number that has never disagreed with you as untested, not confirmed. - -A corollary governing how this document is read: **where a confidence label and an explicit -standing disagree, the standing governs.** `RESIDUE-UNMENTIONED` reads as strong evidence of -deadness; §6's `LowerBoundOnly` says no class in this population is blind-spot-free. The label -sat on the thing being sequenced and the standing three sections away, so the label won and 13 -live-called modules went into the lead deletion slot. A sequencing decision citing a class -name rather than the standing is unevidenced however reasonable the name sounds. - -The same disease one level down, in the mechanism layer, is what made finding **f** plausible: -**a name that implies enrolment while the mechanism keys on something else.** Floor discovery -keys on the **file suffix**; 200 modules are *named* `v2.test.*` without it, and 198 declare -no test at all (§4h). The name asserts a test-hood nothing grants. Both corollaries are one -rule — a word doing work only a mechanism can do — and the second is self-demonstrating: this -census read those names as enrolment until the suffix was checked. - -**That rule has three independent sightings, which makes it a pattern**, found by three lanes -on one night: - -1. **200 modules named `v2.test.*`** whose file is not `_test.dag`, of which 2 declare a - test (§4h). Named as tests, discovered as nothing. -2. **`commit_closure_round_trip_probe`** (§4g): a carrier claiming its persistence is - *"verified by direct execution"* while the probe it names **is enrolled nowhere and nothing - executes it** — a §4b rung-honesty defect, volunteered by the module's own author, not - found by any census. -3. **Vacuous witnesses** in the `spark` lane, reported separately. - -The variation says where to look next: the unread surface is **discovery** in (1), -**enrolment** in (2), and **assertion content** in (3). Same shape, three mechanisms, and -nothing could refuse any of them — which is why each survived. **A claim that no mechanism can -refuse is not evidence, whatever it asserts about itself.** - -Attribution, because a lesson read as one session's mistakes gets discounted: **defects 1 and -2 were the dispatching lane's, self-reported** in the brief that commissioned this census. -**Defect 3 was in both instruments** — theirs and this one's first pass. **Defects 4 and 5 -were this census's own**, neither caught by its author: 4 by review of the first revision, -after it had produced a false escalation (§4b); 5 by an architecture ruling asking which call -surfaces the number covered. Two of five found by the measurer; three by someone reading the -measurement. - -1. **Module name is not derivable from the path.** Names come from the declared `module` - line. All 3816 files carry one (checked; zero missing). -2. **Test modules are consumed by discovery, not by imports.** Floor discovery is by - **file suffix**, not module prefix — `cli_run.rs` `floor_discovery` selects - `rel.ends_with("_test.dag")`. So the exclusion is by path/filename, and a module named - `v2.workflow.ci_materialization_emit_test` living in `ci_materialization_emit_test.dag` - *is* discovered even though its name is not under a `test.` namespace. -3. **NEW — qualified calls are consumption without an import.** `.dag` admits a - fully-qualified call with no import statement: - `dag/test/claim/accelerator_demo_gpu_witness_test.dag` calls - `gunbc.accelerator_demo_gpu.witness_m5_gpu_execution_lane_count_grounded()` and never - imports the module. An import-line census scores that module dead. Adding - qualified-reference edges (over string- and annotation-stripped source, so prose mentions - create no false edges) moved the population 333 → 303 and returned the whole - `gunbc.accelerator_demo_*` family to *reachable*. **Any earlier orphan number in this repo - built from import lines alone over-reports by roughly 10%.** - -**No instrument is committed with this document, deliberately.** A hand-authored census script -beside a substrate that already reads the module graph is the §6 manual-application tell — the -durable form is a lens over the same `Node` tree, a separate, larger piece of work. The method -is stated at the grain needed to re-derive the population independently, and §2's three -defects are what a re-derivation must reproduce to agree. - -4. **NEW — a bare substring is not a reference, and one module name is a suffix of another.** - `v2.std.verification` *contains* `std.verification`, so a substring scan reported - `std.verification` as cited by 128 files; boundary-anchored (not preceded by - `[A-Za-z0-9_.]`, not followed by an identifier character) the true count is **6 - occurrences, one of which is the module's own `module` line**. Same class as defect 3 from - the other side — an instrument reading text where it should read structure — and caught by - review, not by me. Every mention count in §3 and §6 is boundary-anchored; the census - document is excluded from its own scan, which is not pedantry: including it silently - reclassified 40 rows out of RESIDUE-UNMENTIONED by naming them. **Rule for whoever measures - next: never a bare substring for a module name.** Re-measuring the whole population this - way moved only 4 rows — a narrow class, but fatal on exactly the row the census had - escalated. - -5. **NEW — an entry row is not always spelled `--entry`.** The first revision decoded argv - `--entry` and a narrow `entry_file` form, not the `entry: "dag/…"` / `_entry_file: - String = "…"` field rows that `gunbc.ci_spec` and the emitted workflows actually use — 764 - SourceOccurrenceCount of that shape against 116 of the argv shape, so the undecoded surface - was the *larger* one. Decoding it moved the population 303 → 298 and returned three modules - to consumed, two of which this document had classified FROZEN-PENDING-RE-ADD on the - strength of a re-add anchor: `tools.floor_effect_gate_witness` and `tools.ci_heal_dispatch` - are not frozen, they are **invoked**. A carve-out argued from a named anchor was still - wrong, because the instrument had not read the settling surface. - -6. **NEW — whole-pool unqualified resolution is consumption with neither an import nor a - qualified name.** v2 resolves a bare symbol against the **whole module pool**, not a - containment scope, so a consumer may use a declaration while naming neither the declaring - module nor its file. `src/v2/lens/extdeps_shape_transport_policy/` `module_refs.dag` is the - worked case: **26 consumer files, all floor-discovered `*_test.dag`, and all 26 name - neither `v2.lens.extdeps_shape_transport_policy.module_refs` nor its path** — they write - `extdeps_cargo_build_module` bare and the resolver finds it. All 9 of the module's - declarations are consumed this way. **Scope of that 26, so a narrower probe's smaller - number is not reconciled by splitting the difference:** it counts consumers of *any* of the - module's 9 declarations. A probe of the single symbol `extdeps_cargo_build_module` returns - **13 consumer files, none naming the module or the path**. Both figures are correct at - their own scope; the class is identical. *One caveat, because it recurred:* run that probe - after this document names the module and it returns 14 with 1 naming it — the fourteenth is - **this file**. Defect 4's rule (the census is excluded from its own scan) is repeated at - the receipt because contamination arrives the moment the finding is written down. This - class is invisible to a module-name surface and a file-path surface **by construction**: - neither string occurs in any consumer. Defect 3 is its near neighbour and does not cover it - — a qualified call at least spells the module name, which is what makes defect 3 detectable - by a name scan and this one not. - - *Direction, and what is NOT claimed.* Like every §2 surface this one can only move modules - **out** of the population. It was measured on one module, reached by re-deriving this - census independently; **the whole 298 has not been re-scored against it**, so how many rows - it moves is unmeasured and no estimate is offered — §6's `LowerBoundOnly` standing already - covers this and needs no revision here. **Rule for whoever measures next, beside the - defect-4 rule:** a bare declaration name is a reference too, and in `src/v2/lens/` it is - the *normal* one. Restrict such a scan to declaration names **unique corpus-wide**, or it - reports collisions on common words (`Stage`, `Review`, `Permission`) as consumption — - measured, and the reason this receipt counts unique-owner declarations only. - -### The defect-6 re-score of the whole population (2026-08-22) - -Defect 6 was found on one module, so the population was re-scored against it. **Method**, so -it can be re-derived and disagreed with: for each of the 298, take its *declared symbols* — -not its module name or path — and ask whether any other `.dag` file names one of them -**bare**. Three restrictions, each removing a false-positive class that was **observed, not -anticipated**: - -- **`.dag` consumers only.** Whole-pool resolution is a resolver behaviour; a mention in - `.md` or `.rs` is prose, not consumption. -- **Comments and string literals stripped with a character scanner, not a regex.** The first - pass used `"(?:[^"\\]|\\.)*"` and *failed on real `.dag` strings* — - `dag/gunbc/plans/axiom_syllogism_lens.dag` embeds prose containing `\{` interpolation - escapes, the regex terminated the literal early, and the exposed prose scored - `std.syllogism` as consumed. Caught by hand-reading a sample, not by the instrument. -- **Identifiers preceded by `.` or followed by `:` are not references.** The first excludes - a qualified call's tail (`extdeps.ebay.ebay.create_offer` must not score `create_offer` - as bare) and field projection; the second excludes record fields and parameter labels. - -**Each filter is reported with its cost, because the intermediate values let a reader judge -them:** the raw pass scored **98**; excluding qualified-tail and field/label identifiers took -it to **93**; the character-scanner string strip to **91**; defect 7's variant extraction and -its three corrections to **93**; the (e)/(f) corrections below to **92**. A bare final number -would hide that no single filter dominates — itself evidence that none is a fudge factor tuned -to a target. - -**Attribution is by unique ownership, which makes a hit decisive.** A bare symbol declared by -exactly one module attributes to that module alone. A symbol declared by several does not, and -is never counted as consumption here. - -| bucket | count | meaning | -| --- | --- | --- | -| **CONSUMED-DECISIVE** | **92** | a uniquely-owned symbol named bare by a **reachable** `.dag` file. Consumed. | -| DEAD-CONSUMER-ONLY | 34 | named bare only by other modules *inside* the 298 — the island shape of §4a, still residue. **Deletes as a group or not at all**, see below | -| AMBIGUOUS-SHARED-ONLY | 93 | named bare only via symbols several modules declare; no attribution possible | -| STILL-UNCONSUMED | 78 | no bare reference on any surface | -| MISSING-FILE | 1 | appendix row whose path no longer exists | - -Verified by hand-reading the reference site, not by trusting the count: -`extdeps.filesystem.posix` ← `posix_entry_kind(t: S_IFSOCK)`; -`gunbc.host_runner_memory_cap_plan_emit` ← `expected_runner_memory_cap_apply_sheet(host:)`; -`tools.infer_semantics_witness_transport` ← `run_infer_semantics_witness()`; -`gunbc.install_media`, `gunbc.hostname_allocation`, `v2.extdeps.languages.wasm`. **Zero of -the six import the module or qualify the name.** - -**Where the correction lands, which matters more than the total:** - -| disposition (§3) | population | consumed via defect 6 | -| --- | --- | --- | -| RESIDUE-EMPTY | 8 | **0** | -| RESIDUE-UNMENTIONED | 67 | **12** | -| RESIDUE-DOC-ONLY | 28 | 12 | -| FROZEN-PENDING-RE-ADD | 13 | 10 | -| PROSE-NAMED | 79 | 36 | -| CITED-AUTHORITY | 103 | 22 | - -**RESIDUE-UNMENTIONED is the row to read.** §3 calls it *"Delete. Highest-confidence residue"* -and §5 sequences it as batch B2 because it is *"least exposed to the blind spots, because a -module named nowhere in any surface is not waiting on a surface to be decoded."* Sound -reasoning, false premise: the surface it was waiting on had not been decoded, and **12 of -those 67 are consumed by live callers**. The batch selected *because* it was safest carries -the most concentrated risk of deleting working code. RESIDUE-EMPTY at 0 of 8 is the control -showing the instrument is not finding consumption everywhere — a module declaring no symbols -cannot be bare-referenced, and it scores zero. - -**FROZEN-PENDING-RE-ADD, 10 of 13, is the second correction and repeats §2 defect 5 exactly.** -Those rows were frozen against a named re-add anchor; ten are *invoked now*, by live witnesses -— `tools.infer_semantics_witness_transport` is called bare by -`src/v2/test/claim/infer_semantics_witness_test.dag`. As with -`tools.floor_effect_gate_witness` before them, the anchor was real and the conclusion still -wrong, because the instrument had not read the settling surface. - -**The ambiguity is a finding, not only instrument noise.** 93 modules are unresolvable because -their symbols are not uniquely owned, and the distribution is not uniform: -`extdeps_external_authority_anchor` is declared by **102 modules** (the boilerplate citation -anchor of §3's extdeps duty), `extdeps_model_scope` by 27, and `main` by 11. Under whole-pool -resolution a bare `main` has 11 candidate declarers. **The repository already measures this -class every run** — the floor prints `[floor-bare-name-ambiguity] scopes_affected=961 of 1339 -names_total=87040 worst_scope=125`, so 72% of scopes carry at least one ambiguous bare name. -That line is the corroborating instrument for this bucket, independent of this census. For -*this* document the meaning is narrow: those 93 rows are **unresolved, not consumed** — they -stay in the population, and no deletion should read their ambiguity as evidence either way. - -**What this re-score does NOT claim.** Not that 206 is the true count: `DEAD-CONSUMER-ONLY` -and `AMBIGUOUS-SHARED-ONLY` are both unresolved, the appendix rows were not individually -re-read, and §6's `LowerBoundOnly` standing is unchanged and now applies to 206. It claims one -thing, enough to block a deletion: **at least 92 of the 298 have a live caller.** -206. It claims one thing, and it is enough to block a deletion: **at least 92 of the 298 -have a live caller.** - -7. **NEW — coproduct VARIANT CONSTRUCTORS are declared symbols, and declaration extraction - read only `fn`/`func`/`data`/`type`/`const`.** Defect 6 established that a bare symbol is a - reference; this is the same surface missed on its constructor half. `gunbc.pr_digests` owns - the type name `MergeReadinessVerdict` but its variants are `Ready` and `NotReady`, and - `gunbc.code_change_workflow` names `Ready` **bare, with no import**. Extraction that stops - at the `type` line does not own the variants, so `pr_digests` scored residue while - consumed. **Found by an independent instrument — the required floor, refusing a deletion - with `unresolved type MergeReadinessVerdict` and `8x undefined variable Ready` — not by - reasoning about the census.** Re-scoring with variants moves the population: - CONSUMED-DECISIVE 91 → **92**, STILL-UNCONSUMED 96 → **78**, and the §4h residue list 131 → - **112**. - - *Three further defects were found inside the fix, all by hand-verifying rows before - publishing the number, each producing a **false** consumption claim:* (a) a **generic** - type header (`type UpsertDecision

`) defeated variant extraction, so - `std.upsert_decision` did not own `Apply` and `gunbc.apply` looked uniquely to own it; (b) - a **multi-line variant record** truncated the scan region, so `llvm_ir` did not own - `Select` and `extdeps.transports.sql` looked to own it; (c) `operation Parse {` and - `operation Delete {` are **declarations in the flat service namespace**, and were read as - references. Each created *false uniqueness* — the mechanism that turns a collision into a - confident wrong attribution. Filter costs across the whole re-score: **98 → 93 → 91** - (defect 6 filters), then **→ 93** with variants and the three corrections. - - **A fourth extraction bug exists and this document's numbers postdate its fix; a fifth is a - real gap with measured zero exposure.** Both were found by the deletion lane running the - four discriminating cases published with the extractor — the argument for shipping - discriminators rather than bug descriptions, since the check found a bug its author did not - have. (d) **Same-line coproducts**: `type PrerequisiteKind = Capability | Credential | …` - puts every variant on the type line, so a scan anchored to lines *starting* with `=` or `|` - never sees them. A region-based extractor handles it, and all four cases pass on the - extractor that produced the counts above, so **every count in this document postdates (d)** - — re-verified, not assumed. (e) **`operation` / `service` / `resource` rows are - declarations** in the flat service namespace (338 / 8 / 5 in the corpus). This instrument - subtracts them from the *consuming* side but does not credit them to the *declaring* side, - so a module whose declarations were **only** operation rows would own nothing and could - score residue while consumed — the delete-a-live-module direction. **Measured exposure on - this population: zero.** No row of the 298 declares nothing-but-operations, and **not one - of the 20 operation-bearing rows sits in STILL-UNCONSUMED** (13 AMBIGUOUS, 5 CONSUMED, 2 - DEAD-CONSUMER-ONLY) — the bucket where a false residue would be dangerous contains none. - Mechanism real, no victim here; a lane extending this instrument to another population must - credit them. - - **(f) A generic type PARAMETER is a binder, not a reference — one false consumption - claim, found while implementing (e).** `type GraphInvariant { … }` binds - `Projection`; it does not reference `v2.std.projection`, which was that module's *only* - link and had scored it CONSUMED-DECISIVE. Excluding the parameter list of a generic - declaration moves it to AMBIGUOUS and takes CONSUMED-DECISIVE 93 → **92**. - - ***(f) has two halves, and the obvious fix reaches only one.*** (f1) is the parameter list - in the header; **(f2) is every use of the bound name inside that declaration's body** — - `type GraphInvariant { projection: Projection … }`, where the field type on the - next line is a *second, independent* false reference that survives stripping the header. - Reported by the deletion lane, whose extractor still failed the discriminator after - implementing (f1) exactly as first described here. - - **The tempting fix for (f2) is the delete-a-live-module direction — this document shipped - it.** The first implementation here shadowed the bound name **file-wide**, which is wrong: - a name bound as a parameter in one declaration may be **genuinely referenced in another**, - and removing those references moves a live module *into* residue. The shadow must be scoped - to the binding declaration's region — the region walk the variant extractor already - performs, run over the reference side. Correcting it **restored 5 real references and moved - 5 modules out of STILL-UNCONSUMED**, taking the §4h residue list 117 → **112**. Each of - those five was a deletion candidate solely because of an over-broad fix to a false-positive - defect. - - **The symmetry is three-way, not two.** One construct — a generic header — *defeats* - declaration extraction (bug (a)), *inflates* reference extraction in the header (f1), and - *inflates it again* in the body (f2); the body half survives the obvious fix for the header - half. A binder read as a reference is the representation-vs-subject error this document is - about, at the smallest grain the language has. - - **(e) was implemented, and implementing it confirmed the zero.** Crediting - `operation`/`resource` names moved **zero** rows — the count-based prediction held. A first - cut also credited the service's own short name (`service gcp.Metadata` → `Metadata`), was - blamed for a **false** consumption claim on `extdeps.cloud.gcp.sts`, and was withdrawn. - **Both halves of that were wrong, and the correction is recorded rather than quietly - reverted.** The false claim was caused by **(f)** — the consumer named `Metadata` as a - generic type parameter — and the service credit merely stood next to it; with (f2) scoped, - restoring the credit moves **zero rows** and `gcp.sts` stays non-consumed. And the stated - reason ("no evidence any consumer names a service bare") was refuted by the deletion lane's - seventh discriminator: **`dag/extdeps/realization/artifact_store_fs.dag` writes - `Filesystem.Write`, `Filesystem.Read` and `Filesystem.Delete` with no import naming - `Filesystem`** — a consumer bare-resolving an undotted service through the whole-pool - namespace. Worse, withdrawing the credit is the *own-nothing* direction: `Filesystem` is - declared **twice** — `service Filesystem` in `extdeps/filesystem/filesystem_io.dag` and - `resource Filesystem` in `std/resources.dag` — so dropping the service credit hands - `std.resources` **false sole ownership** of a symbol another module visibly declares. The - credit is restored, justified by correctness of ownership since the measured effect is nil. - *Both wrong answers in this paragraph came from the fix, not the defect* — now the pattern - rather than the exception, and the reason every row that moves is read before a number is - published. - - **Two non-equivalent fixes for (c), distinguished rather than reconciled.** Subtracting - declaration-keyword names from the *consumer's* reference set (this document) is more - precise; adding them to the *declaring* module's owned set (the deletion lane) is more - conservative, because it pushes a row to AMBIGUOUS where subtraction correctly leaves it in - STILL-UNCONSUMED. They disagree in a known direction, and **for a deletion lane the - conservative error is the right one** — so the implementations are deliberately not - unified: a residue *count* comes from the precise one, a deletion *set* from the - conservative one. - - **Direction, and one row that went the other way.** At population level this defect can - only move modules *out* of residue. At *row* level it is not one-directional: - `extdeps.access.zanzibar` moved CONSUMED-DECISIVE → AMBIGUOUS, because counting variants - made a symbol it appeared to own uniquely into a shared one. Losing an attribution is a - correction too. - -**The universe the 298 is over.** Call surfaces decoded: `import` lines; fully-qualified -`module.symbol` references (string- and annotation-stripped); argv `--entry` path literals; -`*entry*:` path-field rows in `.dag`, `.yml`, `.rs`, `.md`, and shell; v1 seed mirrors by -filename. Surfaces **not** decoded, each of which can only make the population smaller: - -- **Dynamically composed argv.** `gunbc.roadmap_serve` is invoked with - `--function ", svc.serve_function` — the function is a field read, not a literal. Any - entry whose *path* is likewise composed is invisible to this instrument. -- **Host-side invocation in Rust that names neither the path nor the module.** -- **An entry row declared inside a module that is itself unreachable** — admitted here as a - root, which over-admits consumption. `v2.workflow.product_receipt_stage` is rooted this - way by a dead declarer. - -**Controls, run on every pass** (a zero is readable only beside a nonzero): -`v2.compiler.compile`, `gunbc.spark.serving_desired`, `gunbc.clock_read`, `v2.std.node` -all score *reachable*; `gunbc.accelerator_demo_gpu` scores *reachable* only after defect 3 -was fixed, and it is retained as the standing discriminating control for that arm. - -**Known limits of this instrument, stated rather than left to be found.** It cannot see a -module invoked by a path assembled at runtime, and it treats an `--entry` argv literal -anywhere in the tree — including in a doc — as a root, deliberately generous: the census -over-admits consumption, so the population is a floor, and every row still needs the §3 -mention check before deletion. - -## 3. Dispositions - -Assigned mechanically from evidence, then read. The rule for each class is stated so the -row can be re-derived and disagreed with. - -| disposition | count | rule | what it means | -| --- | --- | --- | --- | -| RESIDUE-EMPTY | 8 | ≤5 lines — a `module` line and nothing else | Delete. No content to strand. | -| CITED-AUTHORITY | 103 | declares an `ExternalAuthority` anchor | **Do not sweep.** The value may be the citation (DESIGN §3 extdeps duty), not a call. Needs a per-island decision, §4. | -| PROSE-NAMED | 79 | named by live (reachable) `.dag`, `.rs`, or `.yml` source, boundary-anchored | Deleting strands a citation. Each needs the mention read before it moves: superseded, missing-consumer, or delete-with-citation-repair. | -| FROZEN-PENDING-RE-ADD | 13 | the `.dag` side of a capability whose invoker a cut removed, where the re-add is named | **Not residue.** DESIGN §3 frozen-X: deleting these deletes what the re-add queue exists to re-attach. Each row names its anchor, §4d. | -| RESIDUE-DOC-ONLY | 28 | named only in `.md`, receipts/TSVs, or other dead modules | Delete; repair the doc citation in the same diff. | -| RESIDUE-UNMENTIONED | 67 | not named anywhere in the tree outside itself | Delete. Highest-confidence residue. | - -Per-module rows: appendix, §6. - -**ENTRY-INVOKED is zero here by construction.** The brief's first job was to build the entry -index and subtract; it is built (48 entry-row roots + 79 seed mirrors) and subtracted *before* -the population is formed, so every entry-invoked module is already outside the 298. The first -revision's index was smaller (34 roots) because it decoded only the argv spelling; §2 defect 5 -is what that cost, and why this section reports the index by *surface* rather than as one -number. - -## 4. Named findings - -These are the results worth a decision, as opposed to a row. - -**a. `extdeps/colo/` — a 19-module island, zero consumers.** Real colocation vendors (Equinix, -CoreSite, Iron Mountain, QTS, …) with cited authority anchors, importing `extdeps.colo.types` -and each other, consumed by nothing. One decision, not nineteen: does a siting consumer exist -or is it planned? If not, the island is the largest single deletion in the census. Same shape, -smaller: `extdeps/formats/elf/` (7), `extdeps/container/oci/` (5), `extdeps/boot/` (5), -`extdeps/ebay/` (6), `extdeps/tcgplayer/` (5), `extdeps/llm/` (7). - -**b. `std.verification` — delete it; the escalation this census originally raised was an -instrument artifact.** The first revision reported 120 citing files and asked for an operator -decision. That count was a substring match against `v2.std.verification` (§2, defect 4). -Boundary-anchored and verified independently of the review that caught it, the real citation -surface is small and the deletion ordinary: - -- **5 genuine name references, in 2 files**, both plan carriers: - `gunbc.plans.resolver_type_name_collision_wall` and - `gunbc.plans.realization_measurement_loop`. -- **1 path reference the name-based scan could not see** — `src/v2/test/fixture/` - `frontier_probe_elision_boundary_overlay.dag` pins `"dag/std/verification.dag"` by path - **with a content hash**. A fixture pinned by content hash is a consumer: the deletion must - move it, not just the prose. This site was not in the review's count either; a name-only - census would have found it during the deletion instead of before it. - -The second-authority half is measured, not inferred: `dag/std/verification.dag` is 604 bytes -with 0 importers (`AssertKind`, `AssertionClaim`, `TestCase`); `src/v2/std/verification.dag` -is 13,813 bytes with 118 importers (`TestgenTier`, `TestClassification`, …). Same name, same -subject space, and the small one has no consumers — the §3 second-authority shape. Delete the -604-byte module and repair all three sites in one PR: a plan claim still true points at -`v2.std.verification`; one no longer true retires with the module. A pointer to a deleted -authority is not an acceptable landing state. - -**c. `src/v2/extdeps/formatters/` (9) and `typecheckers/` (2) — cited config models, no -consumer, no mention anywhere.** rustfmt, prettier, gofmt, black, ktfmt, clang-format, -swift-format, google-java-format, lean4-format, mypy, pyright. Modeled upstream config -surfaces with zero readers. Standing irony: DESIGN's fixed-point rule for the emitted mirror -is *about* rustfmt, and `v2.extdeps.formatters.rustfmt` is not what implements it. -Missing-consumer or residue — not a mechanical call. - -**d. `dag/gunbc/instruments/` — 13 FROZEN, 5 residue, 2 that turned out to be invoked, and the -split is per-module.** These are the `.dag` sides of capabilities whose invokers the floor cut -(2026-08-15) and the regen root cut (2026-08-18) removed. DESIGN's CI paragraph names a -**re-add queue** with a restoration trigger, and a module the queue exists to re-attach is §3 -frozen-X, not residue — deleting it means re-authoring it worse from memory later. So the -carve-out is granted **per module against a named anchor**, never to the group: - -- **Eight witness transports** — `parse`, `bootstrap`, `dag_collect_fingerprint`, - `infer_semantics`, `interp_recorded_fixture`, `effects_rest_transport`, - `auth_declared_but_unwired`, `v1_dag_parse`. Anchor: each wraps a Rust binary that - **`gunbc.ci_release_bins` (live, reachable) still declares as a CI release artifact**. - The binary is retained and the `.dag` invoker is the unattached half — the definition of - frozen. -- **`tools.merge_admission_capture_transport`** and **`tools.merge_admission_current_context`** - (merge-admission stamping). Anchor: the gate is named on DESIGN's own unguarded list. -- **NOT frozen after all: `tools.floor_effect_gate_witness` and `tools.ci_heal_dispatch`.** - The first revision froze them on the unguarded-list anchor. Decoding the `entry:` field - surface (§2, defect 5) shows both are named by live entry rows — **invoked**, never in the - population. Recorded rather than quietly dropped: an anchor can be real and the conclusion - still wrong when the instrument has not read the settling surface. -- **`tools.dag_compile_clean_seam`, `_seam_transport`, `_shard_transport`.** Anchor: DESIGN's - compile-clean entry-point trigger — **the weakest of the three anchors, flagged as such**: - prose, no binary, no queue line. If that trigger is judged not a queue entry, these three - separate out as residue. - -**Not on any queue, therefore residue:** `tools.build`, `tools.readme`, -`tools.roadmap_dispatch`, `tools.codegen` (empty), and `tools.gunbc_ci` — the last is -**superseded**, not merely unreferenced: it shells out to the old `gunbc ci` generate-and-run -script, and `gunbc.witness_floor_workflow` → `.github/workflows/witnesses.yml` is what runs CI -now. That is the §3 second-authority find in this cluster. - -Related, and why this cluster is worth naming: **12 of the 298 declare `main()`** — an entry -shape with no argv anywhere naming it. Four are in `tools/`; the rest are `examples/` and -three `extdeps` witnesses. An entry that lost its invoker is exactly the residue a -delete-first cut should surface loudly, and it did not, because nothing downstream could -refuse. - -**e. Eight empty modules.** `std.list`, `std.containers`, `std.import`, `std.rational`, -`tools.codegen`, `v2.bin.main`, `v2.std.inhabitant_bridge`, -`v2.std.type_expr_projection_row_schema` — a `module` line and whitespace. `v2.bin.main` -is the one to look at twice: an empty `main` is a name reserving a seat. - -**f. `src/v2/extdeps/language_model/` — 15 unmentioned rung modules** (`*_r2a`, `*_r2b`, -`*_r3_external` across Go/Python/Rust/TypeScript) beside `*_r1_test.dag` siblings that -*are* discovered. The `_test`-suffixed rungs run; their non-suffixed peers do not, and -nothing names them. Likely a ladder that stopped being climbed — but "the rung above the -one we execute" is a claim about intent, so it is flagged, not classified. - -**g. Three modules that landed AFTER this census and are unconsumed — none residue.** Surfaced -by a second census run independently on 2026-08-22 (population: every `.dag` module scoring -zero on module name *and* file path; the 14 it returned were 11 instrument artifacts of -defects 3 and 6 plus these three). All three postdate #8803, which is why §6's appendix does -not carry them. Dispositions, one per module: - -*Why a naive re-run disagrees with the 298, recorded so the next reader does not re-derive -it.* That second run scored 14 modules unreachable; **none are in this document's 298**, and -there is no contradiction. Eleven were reachable all along through surfaces this document -already names: ten by fully-qualified reference (§2, defect 3 — the run required an *exact* -match against the module name, so `extdeps.bmc.access.redfish_rbac_policy` matched nothing -because the reference string is *longer* than the module name), and one, `module_refs`, by -whole-pool resolution (§2, defect 6 — the surface that run contributed). The -`gunbc.accelerator_demo_*` four-module cluster it returned is the family §2 defect 3 records -as moved back to *reachable*, and `gunbc.accelerator_demo_gpu` is retained in §2's controls as -the standing discriminator for that arm. The remaining three are the rows below. **The general -shape, the part worth keeping:** an import-line or exact-name census over this corpus does not -merely under-count, it reports a **structural zero indistinguishable from a true zero** — -deleting `module_refs` on such a reading would have taken 26 live floor-discovered witnesses -with it while the census showed nothing. Before building a census instrument, search -`docs/plans` for the census. - -- **`gunbc.empty_decl_file_checkpoint_bypass`** (`183e50a3469`) and - **`gunbc.generic_binder_field_projection_deficit`** (`aecb1fed927`) — **KEEP; being - unconsumed is their correct state, not a defect.** Both are DESIGN §4b error-class filings: - declared rung found-at, ceiling with reason, next-rung trigger, dissolution condition. - §4b(2) *requires* a class below its ceiling to carry that row, and nothing in §4b makes a - code consumer part of the requirement — the row is the filing. Deleting them deletes the - safety ledger, and a future census scoring them residue is re-deriving a question this row - answers. **The ending event is each carrier's own stated dissolution condition, both bounded - events rather than an unbounded "later":** for the first, `lookup_checkpoint` refusing on an - empty `decl_file` with every production call site threading identity; for the second, v2 - inference resolving a generic coproduct's type argument into the arm binder. For a reader - checking whether these should carry witnesses: `generic_binder_field_projection_deficit` - states in its own header why it does not — §5 construction-over-validation: its - discriminating RED was *refused at resolve* because the invalid state has no constructor, so - the predicate and its witness were deleted rather than kept as a green that cannot go red. - -- **`gunbc.scm.commit_closure_store`** (#8807) — **DELETE. Cause: staged orphan at the wrong - grain.** Answered by the `#8820` author (SCM lane, `gentle-eagle-360`), who disposed of it - *and* refuted both arms this row originally offered — recorded because a false cause - attached to a true deletion is exactly the class this document exists to catch, and this row - had asserted both arms as the live possibilities. - - **Arm A ("the envelope grew its own save/load and this deletes with the grain it served") - was false.** `gunbc.scm.repository_envelope` contains **zero `Filesystem` operations and not - one `func`** (verified independently here) — a pure codec, `RepositoryEnvelope ↔ JsonValue`. - Nothing superseded this module, so "replaced by the envelope" would have been a false cause. - - **Arm B ("the envelope should consume it") was false, and is the load-bearing half.** The - grains differ: this module persists **one root and its closure**, while a repository has an - empty initialized state with *no root*, several commits over one shared node population, and - a checked-out selection. Wiring the envelope to a carrier that demands a root would force - `init` to invent a phantom commit — a grain mismatch dressed as a fix. - - So it was never wired, and the layer that will do this job is repository-grain and gets - written that way regardless. Deleted rather than frozen because **a surviving X is an - attractor** (DESIGN §3): while it stands, nearby persistence questions keep being answered - in a vocabulary already scheduled to die. *Citation for this row is `gentle-eagle-360`'s - disposition message, deliberately **not** a pointer into the module — the module is going - away, and a citation into it dies with it.* - - **One defect surfaced with the disposition, volunteered by its author, not found by this - census.** The module's header claims its persistence is *"verified by direct execution in - Wet mode"* and names `commit_closure_round_trip_probe` as the executable subject. True when - it was run **by hand**; not true now. **The probe is enrolled nowhere and nothing executes - it** — verified here: no reference to it exists anywhere outside its own module, and the - file is not `_test.dag`, so floor discovery never sees it. A carrier asserting executed - evidence for a probe that does not run is a §4b **rung-honesty** defect — the reported rung - exceeds what executed evidence establishes — and that is why the claim survived - unchallenged: nothing could refuse it. The class is in §2's common cause, one sighting of - three. - -**h. The list the cleanup directive actually points at is 112, not 298 — and two rows in it -are not what their names say.** Combining the re-score's buckets: **78 STILL-UNCONSUMED (no -bare reference on any surface) + 34 DEAD-CONSUMER-ONLY (named bare only from *inside* the -population — §4a's island shape, deleting as a group or not at all) = 112 modules that are -residue on all three decoded surfaces.** That is the defensible starting point for the -operator's *"clean up anything without consumers"* arm. The other 186 are not: 92 have live -callers, and 93 are unresolvable at identity grain and need a per-row read first. **The list -was 131 before defect 7, and 117 before (f2) below** — of the 19 that left it, only 2 became -consumed; the rest moved to AMBIGUOUS, meaning no longer provably unconsumed rather than -proven consumed. That distinction is why the ambiguous bucket exists. - -Two observations from reading that population, neither captured by the disposition classes: - -- **`v2.test.*` is a name, not an enrolment, and 22 of the 112 are the gap.** Floor discovery - is by **file suffix** (§2, defect 2), so a module *named* `v2.test.…` is discovered only if - its file ends `_test.dag`. Corpus-wide, **200 modules are named `v2.test.*` whose file does - not end `_test.dag`, and exactly 2 of those declare a `test fn`.** Most are ordinary support - modules consumed by real witnesses; the 22 here are consumed by nothing, and their names and - paths disagree — `v2.test.language_model.go_r1` lives at - `src/v2/extdeps/language_model/go_r1.dag`, `v2.test.algebra_laws.zip_eq_list_equality` at - `src/v2/std/algebra_laws/`. This subsumes and sharpens finding **f**: those language-model - rungs are not merely unclimbed; they are named as tests, declare no test, sit outside any - test path, and execute nowhere. **A name that implies enrolment while the mechanism keys on - something else is the more useful form of that finding** — the ladder metaphor invites - "climb it"; the measurement says "nothing here was ever wired to run." -**The 35 island rows carry a constraint that is not a preference, and it belongs on the row -rather than in a report.** A per-module verdict over a mutually-referencing island is -incoherent: **each member looks consumed until its neighbours go**, so scoring them one at a -time returns "consumed" for every one and the island never becomes eligible. They delete as a -group or not at all, and the group is the connected component, not the directory. Otherwise -the deletion lane meets this as a surprising refusal partway through a batch — the census -working, but expensively. - -- **`gunbc.spark.provisioning`'s appendix row no longer resolves** — the path it names does - not exist. It was one of the two rows §5 excluded from every batch as `fierce-lynx-647`'s - area. The deletion was deliberate — ruled by the lane that commissioned this census, on the - grounds that the model predated knowledge of the real procedure — so the row, not the - deletion, is the defect. Recorded because an appendix row pointing at nothing is the - staleness class this document polices elsewhere, and a receipt that the population has a - clock on it. - -**A hazard for whoever measures next, cheap to hit and silent.** A fresh worktree can be -**shallow-grafted** — the 2026-08-22 run found its clone rooted at a single 4608-file import -commit dated six days earlier, so `git log ` reported that graft commit as the first -commit of every older module. Any census arm asking *was this consumed until a recent cut* -(the question separating a severed consumer from §6 experimental residue) silently answers -from a truncated history. `git rev-parse --is-shallow-repository` before trusting a history -claim; `git fetch --unshallow` fixes it. - -## 5. Proposed sequencing (for approval, not execution) - -One red in a 298-file deletion blocks everything, so: small coherent batches, each its own -PR, each verified by `claim_executor --required-ci --source-root dag --source-root src/v2` -with `failed=` read and PASS counted against the roster. - -1. **B1 — RESIDUE-EMPTY (8).** Nothing to strand; a pure control batch that proves the - deletion pipeline and the floor both behave. -2. **B2 — RESIDUE-UNMENTIONED, non-extdeps (53 of 67). SUPERSEDED BY THE DEFECT-6 RE-SCORE — - do not run this batch as written.** The rationale was that an unmentioned module has no - citation to repair and is least exposed to a blind spot; the re-score (§2) found **12 of - the 67 consumed by live bare-symbol callers**, so this batch would delete working code, and - the ordering argument that put it first is what made that risk invisible. It becomes - eligible again only over the rows surviving the re-score, and B1 replaces it as the batch - to establish the mechanics on (RESIDUE-EMPTY scored 0 of 8 consumed — a module declaring no - symbols cannot be bare-referenced). -3. **B3 — RESIDUE-DOC-ONLY (28).** Deletion plus the doc/receipt citation repair in the - same diff. -4. **B4 — the five `tools/` rows NOT on the re-add queue (finding d)**, including the - superseded `tools.gunbc_ci`. The 13 FROZEN-PENDING-RE-ADD rows are in no batch: they stay - until their queued gate is re-derived, and their disposition is recorded here so a future - census does not re-derive the question and answer "residue". -5. **B5 — `std.verification` (finding b)** alone: delete the module, repoint the two plan - carriers, and move the content-hash fixture pin, all in one PR. -6. **B6+ — the extdeps islands (finding a, c)**, one island per PR, each gated on whether - the citation is the deliverable. - -PROSE-NAMED (79) gets no batch until each row's mention has been read; several will resolve to -*missing consumer* and be wired up rather than deleted — the directive's second arm. - -**Excluded from every batch:** `dag/gunbc/spark/` — `fierce-lynx-647` owns that area and is -mid-census there. Two of the 298 (`gunbc.spark.provisioning`, -`gunbc.spark.managed_access_apply`) fall in it and are reported here for their benefit -only. `gunbc.spark.provisioning` is a live instance of the dangling-annotation hazard: -`extdeps/systems/nvidia_dgx_spark_setup.dag` names it as a fact's home. - - -## 6. Completeness standing - -**`LowerBoundOnly`.** 298 resolved-unconsumed modules over the universe declared in §2. Not -`CompleteForDeclaredUniverse`, and the difference is not modesty: three call surfaces are -named undecoded in §2, and each can only *remove* modules from the population, never add. Two -prior revisions each lost modules to a newly decoded surface (333 → 303 → 298) — the empirical -case for the standing. - -What the instrument can support: *298 resolved consumers-of-none over universe U; standing -LowerBoundOnly; blind spots as named.* What it cannot, and this document nowhere claims: -*there are exactly 298 unconsumed modules.* The second sentence dies the moment someone -decodes a fourth surface; the first survives it and says what would change it. - -This standing is why §5's batches are ordered as they are — and the 2026-08-22 re-score is the -case study for why the standing exists. The original ordering argued RESIDUE-UNMENTIONED first -because *"a module named nowhere in any surface is not waiting on a surface to be decoded."* -Valid argument, false premise: a surface remained undecoded (§2, defect 6) and 12 of that -batch had live callers. **A `LowerBoundOnly` standing is not compatible with treating any -batch as blind-spot-free**, and the deletion order must follow from the standing rather than a -class name: B1 (RESIDUE-EMPTY) leads because a module declaring no symbols is unreachable by -*construction* on the bare-symbol surface, not because nobody has mentioned it. - -### Two broken entry strings, confirmed - -Decoding the entry-row surface also surfaced consumption that is *declared and broken* — -neither consumed nor residue, invisible to the import graph. Confirmed by reading both sites, -not by the count: - -- **`dag/gunbc/spark/grant_install.dag`** — `gunbc.ci_spec` `gunbc_ci_spark_grant_install_invoke` - names it as an entry with function `spark_grant_install_ci_wet`. **The file does not - exist.** -- **`dag/gunbc/spark/serving_durability.dag`** with function - `"spark_serving_durability_ci_wet"` — named by `gunbc.ci_spec` *and* by - `.github/workflows/fleet-converge.yml`. The file exists; **that function is not in it** - (its `fn`s are `spark_serving_boot_id_probe_argv`, `..._from_probe`, - `spark_serving_reboot_transition`, `spark_serving_durability_verdict`, `..._is_proven`, - `..._wire`). - -Both are in `dag/gunbc/spark/`, `fierce-lynx-647`'s area — reported, not touched. A sibling -lane found the same two independently: a second instrument agreeing, not a second report of -one measurement. - -**Not defects, named so a future sweep does not "fix" them:** the raw scan also flags -`dag/a.dag`, `dag/mini.dag`, `dag/gunbc/live_deploy/WRONG_ENTRY.dag`, -`dag/test/claim/__no_such_entry_zzz.dag` and similar. Those are *deliberate negative controls* -inside witness tests — an entry that must fail to resolve. An instrument reporting them is -producing false positives, which is what §7's last case exists to catch. - -## 7. Calibration benchmark for the next audit instrument - -Today's failures, written as cases rather than prose, so a future instrument calibrates -against a suite instead of rediscovering all five. Each case names a real subject in this -tree, the wrong answer, and the honest answer. - -| # | case | subject | wrong answer | required answer | -| --- | --- | --- | --- | --- | -| 1 | **cardinality** | `std.verification` | one number for "how cited" | **both** units: SourceOccurrenceCount 6 **and** DistinctFileCount 3 — they are different questions | -| 2 | **exact identity** | `std.verification` vs `v2.std.verification` | substring match folds them | boundary-anchored: the two stay separate, and the suffix relation never merges them | -| 3 | **representation vs subject** | any modeled capability | "spelling absent ⟹ capability absent" | a typed model present with zero occurrences of its shell spelling means capability PRESENT, spelling ABSENT — **no absence conclusion** | -| 4 | **qualified use** | `gunbc.accelerator_demo_gpu` ← `dag/test/claim/accelerator_demo_gpu_witness_test.dag` | orphan (no import line) | caller edge PRESENT; an import-line census must declare itself incomplete | -| 5 | **string-bound entry** | `dag/gunbc/spark/serving_durability.dag` + `spark_serving_durability_ci_wet` | consumed (path resolves) | **resolution REFUSED** — path present, function absent; neither consumed nor residue | -| 6 | **positive no-finding** | `dag/gunbc/live_deploy/WRONG_ENTRY.dag`, `dag/a.dag`, `__no_such_entry_zzz.dag`; and any healthy imported module | flagged as broken/orphaned | **no finding** — deliberate negative controls and ordinary live modules must come back clean | - -Case 6 is the one most likely to be skipped and the one that catches a broken audit: a suite -built only from defects is passed by an instrument reporting *everything* as suspicious. Cases -1, 2, 4, 5 each have a live specimen in this tree, so the suite is executable against the real -corpus rather than a fixture someone must maintain. - -## 8. Appendix — the 298 rows - -### RESIDUE-EMPTY — 8 modules - -| module | path | lines | live-source mentions | -| --- | --- | --- | --- | -| `std.containers` | `dag/std/containers.dag` | 2 | — | -| `std.import` | `dag/std/import.dag` | 2 | {'dag': 1} `dag/gunbc/commit_workflow.dag` | -| `std.list` | `dag/std/list.dag` | 3 | — | -| `std.rational` | `dag/std/rational.dag` | 5 | {'dag': 2} `dag/gunbc/doc_graph_roots.dag` `dag/gunbc/econ/acquisition.dag` | -| `tools.codegen` | `dag/gunbc/instruments/codegen.dag` | 5 | — | -| `v2.bin.main` | `src/v2/bin/main.dag` | 4 | — | -| `v2.std.inhabitant_bridge` | `src/v2/std/inhabitant_bridge.dag` | 4 | — | -| `v2.std.type_expr_projection_row_schema` | `src/v2/std/type_expr_projection_row_schema.dag` | 4 | — | - -### RESIDUE-UNMENTIONED — 67 modules - -| module | path | lines | live-source mentions | -| --- | --- | --- | --- | -| `config.codegen_paths` | `dag/config/codegen_paths.dag` | 20 | — | -| `examples.html_markup_smoke` | `dag/examples/html_markup_smoke/html_markup_smoke.dag` | 66 | — | -| `examples.js_site` | `dag/examples/js_site/js_site.dag` | 185 | — | -| `examples.js_site_emit` | `dag/examples/js_site/js_site_emit.dag` | 76 | — | -| `examples.nominal_distinctness_twin` | `dag/examples/nominal_distinctness_witness/twin.dag` | 15 | — | -| `examples.nominal_distinctness_witness` | `dag/examples/nominal_distinctness_witness/witness.dag` | 19 | — | -| `gunbc.assimilate.bmc_wif_canary_bootstrap` | `dag/gunbc/assimilate/bmc_wif_canary_bootstrap.dag` | 126 | — | -| `gunbc.ci_oom_reclassify` | `dag/gunbc/ci/ci_oom_reclassify.dag` | 87 | — | -| `gunbc.cursor_sdk_secure_api_key` | `dag/gunbc/cursor_sdk_secure_api_key.dag` | 73 | — | -| `gunbc.devboot.vertical_receipt` | `dag/gunbc/devboot/vertical_receipt.dag` | 68 | — | -| `gunbc.host_converge_delta` | `dag/gunbc/host/host_converge_delta.dag` | 119 | — | -| `gunbc.install_media` | `dag/gunbc/install_media.dag` | 43 | — | -| `gunbc.parse_allowlist` | `dag/gunbc/parse_allowlist.dag` | 20 | — | -| `gunbc.plans.wave2_prep_design` | `dag/gunbc/plans/wave2_prep_design.dag` | 205 | — | -| `gunbc.provider_standing_live_probes` | `dag/gunbc/provider_standing_live_probes.dag` | 194 | — | -| `gunbc.srv4_seeded_install_media_artifact` | `dag/gunbc/srv4_seeded_install_media_artifact.dag` | 47 | — | -| `gunbc.tools.bmc_onboard_validate` | `dag/gunbc/tools/bmc_onboard_validate.dag` | 22 | — | -| `gunbc.tools.ebay_listing` | `dag/gunbc/tools/ebay_listing.dag` | 143 | — | -| `gunbc.tools.roadmap_spawn_request` | `dag/gunbc/tools/roadmap_spawn_request.dag` | 36 | — | -| `std.binding` | `dag/std/binding.dag` | 7 | — | -| `std.syllogism` | `dag/std/syllogism.dag` | 82 | — | -| `tools.build` | `dag/gunbc/instruments/build.dag` | 34 | — | -| `tools.readme` | `dag/gunbc/instruments/readme.dag` | 70 | — | -| `tools.roadmap_dispatch` | `dag/gunbc/instruments/roadmap_dispatch.dag` | 17 | — | -| `v2.extdeps.formats.csv` | `src/v2/extdeps/formats/csv.dag` | 136 | — | -| `v2.extdeps.formats.openapi` | `src/v2/extdeps/formats/openapi.dag` | 459 | — | -| `v2.extdeps.formats.toml` | `src/v2/extdeps/formats/toml.dag` | 132 | — | -| `v2.extdeps.formatters.black` | `src/v2/extdeps/formatters/black.dag` | 87 | — | -| `v2.extdeps.formatters.clang_format` | `src/v2/extdeps/formatters/clang_format.dag` | 893 | — | -| `v2.extdeps.formatters.gofmt` | `src/v2/extdeps/formatters/gofmt.dag` | 14 | — | -| `v2.extdeps.formatters.google_java_format` | `src/v2/extdeps/formatters/google_java_format.dag` | 22 | — | -| `v2.extdeps.formatters.ktfmt` | `src/v2/extdeps/formatters/ktfmt.dag` | 81 | — | -| `v2.extdeps.formatters.lean4_format` | `src/v2/extdeps/formatters/lean4_format.dag` | 189 | — | -| `v2.extdeps.formatters.prettier` | `src/v2/extdeps/formatters/prettier.dag` | 191 | — | -| `v2.extdeps.formatters.rustfmt` | `src/v2/extdeps/formatters/rustfmt.dag` | 286 | — | -| `v2.extdeps.formatters.swift_format` | `src/v2/extdeps/formatters/swift_format.dag` | 110 | — | -| `v2.extdeps.typecheckers.mypy` | `src/v2/extdeps/typecheckers/mypy.dag` | 29 | — | -| `v2.extdeps.typecheckers.pyright` | `src/v2/extdeps/typecheckers/pyright.dag` | 47 | — | -| `v2.std.generic_instantiation` | `src/v2/std/generic_instantiation.dag` | 36 | — | -| `v2.std.projection` | `src/v2/std/projection.dag` | 19 | — | -| `v2.test.algebra_laws.is_prefix_of_prefix_check` | `src/v2/std/algebra_laws/is_prefix_of_prefix_check.dag` | 90 | — | -| `v2.test.algebra_laws.zip_eq_list_equality` | `src/v2/std/algebra_laws/zip_eq_list_equality.dag` | 93 | — | -| `v2.test.language_model.go_r1` | `src/v2/extdeps/language_model/go_r1.dag` | 59 | — | -| `v2.test.language_model.go_r2a` | `src/v2/extdeps/language_model/go_r2a.dag` | 59 | — | -| `v2.test.language_model.go_r2b` | `src/v2/extdeps/language_model/go_r2b.dag` | 59 | — | -| `v2.test.language_model.go_r3_external` | `src/v2/extdeps/language_model/go_r3_external.dag` | 59 | — | -| `v2.test.language_model.python_cross_runtime_drift` | `src/v2/extdeps/language_model/python_cross_runtime_drift.dag` | 49 | — | -| `v2.test.language_model.python_l2_cross_target_parity` | `src/v2/extdeps/language_model/python_l2_cross_target_parity.dag` | 81 | — | -| `v2.test.language_model.python_r2a` | `src/v2/extdeps/language_model/python_r2a.dag` | 58 | — | -| `v2.test.language_model.python_r2b` | `src/v2/extdeps/language_model/python_r2b.dag` | 53 | — | -| `v2.test.language_model.python_r3_external` | `src/v2/extdeps/language_model/python_r3_external.dag` | 58 | — | -| `v2.test.language_model.rust` | `src/v2/extdeps/language_model/rust.dag` | 256 | — | -| `v2.test.language_model.rust_r2a` | `src/v2/extdeps/language_model/rust_r2a.dag` | 57 | — | -| `v2.test.language_model.rust_r2b` | `src/v2/extdeps/language_model/rust_r2b.dag` | 75 | — | -| `v2.test.language_model.rust_r3_external` | `src/v2/extdeps/language_model/rust_r3_external.dag` | 57 | — | -| `v2.test.language_model.typescript_r2a` | `src/v2/extdeps/language_model/typescript_r2a.dag` | 58 | — | -| `v2.test.language_model.typescript_r2b` | `src/v2/extdeps/language_model/typescript_r2b.dag` | 54 | — | -| `v2.test.language_model.typescript_r3_external` | `src/v2/extdeps/language_model/typescript_r3_external.dag` | 58 | — | -| `v2.test.nat_semiring.rung_0_to_2_three_targets` | `src/v2/std/nat_semiring/rung_0_to_2_three_targets.dag` | 95 | — | -| `v2.test.nat_semiring.rung_l1_go_compiler_slice` | `src/v2/std/nat_semiring/rung_l1_go_compiler_slice.dag` | 58 | — | -| `v2.test.nat_semiring.rung_l1_python_runtime` | `src/v2/std/nat_semiring/rung_l1_python_runtime.dag` | 96 | — | -| `v2.test.qualified_name.from_node` | `src/v2/std/qualified_name/from_node.dag` | 222 | — | -| `v2.workflow.bmc_lifecycle_roundtrip` | `src/v2/workflow/bmc_lifecycle_roundtrip.dag` | 38 | — | -| `v2.workflow.ci_v1_compiler_test_targets_compile_gate_emit` | `src/v2/workflow/ci_v1_compiler_test_targets_compile_gate_emit.dag` | 110 | — | -| `v2.workflow.floor2_prepared_subject` | `src/v2/workflow/floor2_prepared_subject.dag` | 182 | — | -| `v2.workflow.gha_expression_fidelity` | `src/v2/workflow/gha_expression_fidelity.dag` | 22 | — | -| `v2.workflow.probe_selector_host_health` | `src/v2/workflow/probe_selector_host_health.dag` | 43 | — | - -### RESIDUE-DOC-ONLY — 28 modules - -| module | path | lines | live-source mentions | -| --- | --- | --- | --- | -| `examples.cost_estimate` | `dag/examples/cost_estimate/cost_estimate.dag` | 29 | — | -| `examples.gunbhub_serve_program` | `dag/examples/gunbhub_serve_program/gunbhub_serve_program.dag` | 60 | — | -| `examples.interp_test` | `dag/examples/interp_test/interp_example.dag` | 40 | — | -| `gunbc.code_change_workflow` | `dag/gunbc/code_change_workflow.dag` | 371 | — | -| `gunbc.floor_resolve_realization` | `dag/gunbc/floor_resolve_realization.dag` | 28 | — | -| `gunbc.hand_lens_host_bridge_scaffold_watchdog` | `dag/gunbc/hand_lens_host_bridge_scaffold_watchdog.dag` | 46 | — | -| `gunbc.host_runner_memory_cap_plan_emit` | `dag/gunbc/host/host_runner_memory_cap_plan_emit.dag` | 110 | — | -| `gunbc.hostname_allocation` | `dag/gunbc/hostname_allocation.dag` | 148 | — | -| `gunbc.language_subject_scope_scaffold` | `dag/gunbc/language_subject_scope_scaffold.dag` | 10 | — | -| `gunbc.p3a1_self_fork_homonym_disposition` | `dag/gunbc/p3a1_self_fork_homonym_disposition.dag` | 10 | — | -| `gunbc.pr_digests` | `dag/gunbc/pr_digests.dag` | 72 | — | -| `gunbc.site.register_principles` | `dag/gunbc/site/register_principles.dag` | 12 | — | -| `gunbc.spark.managed_access_apply` | `dag/gunbc/spark/managed_access_apply.dag` | 453 | — | -| `gunbc.tools.card_intake` | `dag/gunbc/tools/card_intake.dag` | 216 | — | -| `gunbc.tools.cron_tag` | `dag/gunbc/tools/cron_tag.dag` | 67 | — | -| `gunbc.tools.grounding_confirm` | `dag/gunbc/tools/grounding_confirm.dag` | 115 | — | -| `gunbc.witness_family_fanout` | `dag/gunbc/witness_family_fanout.dag` | 65 | — | -| `shared.dag_util` | `dag/shared/dag_util.dag` | 44 | — | -| `std.exec_format` | `dag/std/exec_format.dag` | 37 | — | -| `std.patterns` | `dag/std/patterns.dag` | 24 | — | -| `v2.extdeps.bmc.lifecycle_fidelity` | `src/v2/extdeps/bmc/lifecycle_fidelity.dag` | 146 | — | -| `v2.extdeps.formats.json` | `src/v2/extdeps/formats/json.dag` | 52 | — | -| `v2.extdeps.formats.json_schema` | `src/v2/extdeps/formats/json_schema.dag` | 103 | — | -| `v2.extdeps.formats.yaml` | `src/v2/extdeps/formats/yaml.dag` | 85 | — | -| `v2.extdeps.github.expression_fidelity` | `src/v2/extdeps/github/expression_fidelity.dag` | 54 | — | -| `v2.std.rust_leaf_model_claim` | `src/v2/std/rust_leaf_model_claim.dag` | 61 | — | -| `v2.test.workflow.host_discovered_owned_data_manifest` | `src/v2/workflow/host_discovered_owned_data_manifest.dag` | 19 | — | -| `v2.workflow.ci_stage0_partition_compile_gate_emit` | `src/v2/workflow/ci_stage0_partition_compile_gate_emit.dag` | 103 | — | - -### FROZEN-PENDING-RE-ADD — 13 modules - -| module | path | lines | re-add anchor | -| --- | --- | --- | --- | -| `tools.auth_declared_but_unwired_witness_transport` | `dag/gunbc/instruments/auth_declared_but_unwired_witness_transport.dag` | 12 | `auth_declared_but_unwired_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.bootstrap_witness_transport` | `dag/gunbc/instruments/bootstrap_witness_transport.dag` | 12 | `bootstrap_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.dag_collect_fingerprint_witness_transport` | `dag/gunbc/instruments/dag_collect_fingerprint_witness_transport.dag` | 12 | `dag_collect_fingerprint_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.dag_compile_clean_seam` | `dag/gunbc/instruments/dag_compile_clean_seam.dag` | 110 | compile-clean entry point — WEAKER ANCHOR: a prose restoration trigger in DESIGN, no bin and no queue line | -| `tools.dag_compile_clean_seam_transport` | `dag/gunbc/instruments/dag_compile_clean_seam_transport.dag` | 124 | compile-clean entry point — WEAKER ANCHOR, as above | -| `tools.dag_compile_clean_shard_transport` | `dag/gunbc/instruments/dag_compile_clean_shard_transport.dag` | 43 | compile-clean entry point — WEAKER ANCHOR, as above | -| `tools.effects_rest_transport_witness_transport` | `dag/gunbc/instruments/effects_rest_transport_witness_transport.dag` | 12 | `effects_rest_transport_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.infer_semantics_witness_transport` | `dag/gunbc/instruments/infer_semantics_witness_transport.dag` | 12 | `infer_semantics_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.interp_recorded_fixture_witness_transport` | `dag/gunbc/instruments/interp_recorded_fixture_witness_transport.dag` | 13 | `interp_recorded_fixture_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.merge_admission_capture_transport` | `dag/gunbc/instruments/merge_admission_capture_transport.dag` | 32 | merge-admission stamping — named on DESIGN's unguarded list | -| `tools.merge_admission_current_context` | `dag/gunbc/instruments/merge_admission_current_context.dag` | 134 | merge-admission stamping — named on DESIGN's unguarded list | -| `tools.parse_witness_transport` | `dag/gunbc/instruments/parse_witness_transport.dag` | 20 | `parse_witness` bin, still declared in `gunbc.ci_release_bins` | -| `tools.v1_dag_parse_transport` | `dag/gunbc/instruments/v1_dag_parse_transport.dag` | 12 | `v1_src_dag_parse` bin, still declared in `gunbc.ci_release_bins` | - -### PROSE-NAMED — 79 modules - -| module | path | lines | live-source mentions | -| --- | --- | --- | --- | -| `direct_rust_door_ingest_fixture` | `src/v2/compiler/self_host/direct_rust_door_ingest_fixture.dag` | 6 | {'dag': 1} `src/v2/compiler/self_host/direct_rust_door_fixture.dag` | -| `examples.weather` | `dag/examples/weather/weather.dag` | 48 | {'rs': 1} `src/v1/stage0/src/bin/bootstrap_witness.rs` | -| `extdeps.bmc.mock_corpus` | `dag/extdeps/bmc/mock_corpus.dag` | 71 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.cloud.gcp.mock_corpus` | `dag/extdeps/cloud/gcp/mock_corpus.dag` | 110 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.cron.mock_corpus` | `dag/extdeps/cron/mock_corpus.dag` | 22 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.diagnostic.mock_corpus` | `dag/extdeps/diagnostic_mock_corpus.dag` | 39 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.ebay.mock_corpus` | `dag/extdeps/ebay/mock_corpus.dag` | 79 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.filesystem.mock_corpus` | `dag/extdeps/filesystem/mock_corpus.dag` | 22 | {'dag': 2} `dag/gunbc/extdeps_scope_frontier.dag` `dag/gunbc/plans/m4_universal_hermetic_corpus.dag` | -| `extdeps.git.mock_corpus` | `dag/extdeps/git/mock_corpus.dag` | 167 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.github.mock_corpus` | `dag/extdeps/github/mock_corpus.dag` | 71 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.linux.mock_corpus` | `dag/extdeps/linux/mock_corpus.dag` | 15 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.llm.mock_corpus` | `dag/extdeps/llm/mock_corpus.dag` | 55 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.sec.mock_corpus` | `dag/extdeps/sec/mock_corpus.dag` | 23 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.shell.mock_corpus` | `dag/extdeps/shell_mock_corpus.dag` | 31 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.tcgplayer.mock_corpus` | `dag/extdeps/tcgplayer/mock_corpus.dag` | 63 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `gunbc.apply` | `dag/gunbc/apply.dag` | 118 | {'dag': 2} `dag/gunbc/runner/runner_lifecycle.dag` `dag/gunbc/runner/runner_capacity_realize.dag` | -| `gunbc.auth.credentials` | `dag/gunbc/auth/credentials.dag` | 89 | {'dag': 1, 'rs': 1} `dag/gunbc/tailscale_acl_phase2_credential.dag` `src/v1/stage0/src/bin/parse_witness.rs` | -| `gunbc.auth.optional_impersonation` | `dag/gunbc/auth/optional_impersonation.dag` | 20 | {'dag': 2} `dag/test/claim/tailscale_acl_phase2_design_witness_test.dag` `dag/gunbc/tailscale_acl_phase2_credential.dag` | -| `gunbc.auth.patterns` | `dag/gunbc/auth/patterns.dag` | 113 | {'rs': 1} `src/v1/stage0/src/cli_run.rs` | -| `gunbc.bootstrap` | `dag/gunbc/bootstrap.dag` | 126 | {'dag': 2} `dag/gunbc/doc_graph_roots.dag` `src/v2/compiler/self_host/frontier_probe_types.dag` | -| `gunbc.char_at_scaling_probe_support` | `dag/gunbc/char_at_scaling_probe_support.dag` | 58 | {'rs': 1} `src/v1/stage0/src/bin/char_at_scaling_probe.rs` | -| `gunbc.ci_build_job_v1_compiler_unit_receipt` | `dag/gunbc/ci/ci_build_job_v1_compiler_unit_receipt.dag` | 21 | {'dag': 1} `dag/gunbc/ci/ci_spec.dag` | -| `gunbc.ci_input_envelope` | `dag/gunbc/ci/ci_input_envelope.dag` | 86 | {'dag': 3} `dag/gunbc/doc_graph_roots.dag` `dag/gunbc/plans/bounded_input_cost_envelope_scheduling.dag` | -| `gunbc.compile_source_model` | `dag/gunbc/compile_source_model.dag` | 65 | {'dag': 1} `dag/gunbc/plans/seed_debt_bundle_item_2.dag` | -| `gunbc.deployed_intent_v0` | `dag/gunbc/deployed_intent_v0.dag` | 61 | {'dag': 2} `dag/gunbc/host/host_standup.dag` `dag/gunbc/host/host_identity_adopt.dag` | -| `gunbc.deployed_intent_v1` | `dag/gunbc/deployed_intent_v1.dag` | 69 | {'dag': 1} `dag/gunbc/host/host_standup.dag` | -| `gunbc.design_argument` | `dag/gunbc/design_argument.dag` | 93 | {'dag': 1} `dag/gunbc/plans/axiom_syllogism_lens.dag` | -| `gunbc.githooks_pre_push_cli` | `dag/gunbc/githooks/githooks_pre_push_cli.dag` | 10 | {'dag': 2, 'rs': 2} `dag/std/emit_on_demand.dag` `dag/gunbc/githooks/githooks_pre_push_fmt_transport_scaffold.dag` | -| `gunbc.host_authorized_keys_reconcile` | `dag/gunbc/host/host_authorized_keys_reconcile.dag` | 104 | {'dag': 1} `dag/gunbc/build_cache/build_cache_instance.dag` | -| `gunbc.host_build_cache_provision` | `dag/gunbc/host/host_build_cache_provision.dag` | 335 | {'dag': 4} `dag/gunbc/build_cache/build_cache_instance.dag` `dag/gunbc/fleet/fleet_host_budget.dag` | -| `gunbc.host_identity_assimilation` | `dag/gunbc/host/host_identity_assimilation.dag` | 266 | {'dag': 3} `dag/gunbc/host/host_standup.dag` `dag/gunbc/host/host_identity_adopt.dag` | -| `gunbc.host_identity_converge` | `dag/gunbc/host/host_identity_converge.dag` | 250 | {'dag': 1} `dag/gunbc/host/host_standup.dag` | -| `gunbc.host_identity_knob` | `dag/gunbc/host/host_identity_knob.dag` | 55 | {'dag': 1} `dag/gunbc/host/host_standup.dag` | -| `gunbc.host_identity_observation` | `dag/gunbc/host/host_identity_observation.dag` | 89 | {'dag': 1} `dag/gunbc/host/host_standup.dag` | -| `gunbc.host_network_diagnosis` | `dag/gunbc/host/host_network_diagnosis.dag` | 213 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `gunbc.host_toolchain_components` | `dag/gunbc/host/host_toolchain_components.dag` | 195 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `gunbc.interpreter_kernel_model` | `dag/gunbc/interpreter_kernel_model.dag` | 82 | {'dag': 1} `dag/gunbc/plans/interpreter_kernel_d.dag` | -| `gunbc.namespace_census_receipt` | `dag/gunbc/namespace/namespace_census_receipt.dag` | 74 | {'dag': 1} `dag/gunbc/doc_graph_roots.dag` | -| `gunbc.network_identity_subsumption` | `dag/gunbc/network_identity_subsumption.dag` | 134 | {'dag': 5} `dag/test/claim/dgx_spark_witness_test.dag` `dag/test/claim/host_phase_status_witness_test.dag` | -| `gunbc.p1_retention_cohort_receipt` | `dag/gunbc/p1_retention_cohort_receipt.dag` | 8 | {'dag': 1} `dag/gunbc/doc_graph_roots.dag` | -| `gunbc.plans.affected_set_self_confirmation` | `dag/gunbc/plans/affected_set_self_confirmation.dag` | 29 | {'rs': 1} `src/v1/stage0/src/cli_run.rs` | -| `gunbc.plans.branch_merge_admission_model` | `dag/gunbc/plans/branch_merge_admission_model.dag` | 172 | {'dag': 2} `dag/test/claim/merge_lifecycle_interleaving_witness_test.dag` `dag/gunbc/merge_lifecycle.dag` | -| `gunbc.plans.fleet_subsumption_manual_gaps` | `dag/gunbc/plans/fleet_subsumption_manual_gaps.dag` | 196 | {'dag': 12} `dag/test/claim/retained_shell_script_witness_test.dag` `dag/gunbc/build_cache/build_cache_instance.dag` | -| `gunbc.plans.host_convergence_circuit_residue` | `dag/gunbc/plans/host_convergence_circuit_residue.dag` | 75 | {'dag': 1} `dag/gunbc/host/host_converge.dag` | -| `gunbc.plans.merge_admission_gate_shape_proposal` | `dag/gunbc/plans/merge_admission_gate_shape_proposal.dag` | 78 | {'dag': 1} `dag/gunbc/merge_admission.dag` | -| `gunbc.plans.transport_argv_anemia_dissolution` | `dag/gunbc/plans/transport_argv_anemia_dissolution.dag` | 89 | {'dag': 2} `dag/extdeps/git/git.dag` `dag/extdeps/exec/command.dag` | -| `gunbc.process_algebra` | `dag/gunbc/process_algebra.dag` | 147 | {'dag': 3} `dag/gunbc/doc_graph_roots.dag` `dag/gunbc/plans/invert_hand_maintained.dag` | -| `gunbc.runner_slot_enforcement` | `dag/gunbc/runner/runner_slot_enforcement.dag` | 129 | {'dag': 3} `dag/gunbc/host/host_standup.dag` `dag/gunbc/runner/runner_slot_allocation.dag` | -| `gunbc.seed_closed_vocabulary_wildcard_census` | `dag/gunbc/seed_closed_vocabulary_wildcard_census.dag` | 175 | {'rs': 1} `src/v1/stage0/src/cli_run.rs` | -| `gunbc.site.interaction` | `dag/gunbc/site/interaction.dag` | 14 | {'dag': 1} `dag/gunbc/design/interaction.dag` | -| `gunbc.spark.provisioning` | `dag/gunbc/spark/provisioning.dag` | 543 | {'dag': 2} `dag/extdeps/systems/nvidia_dgx_spark_setup.dag` `dag/test/claim/spark_provisioning_witness_test.dag` | -| `gunbc.srv3_os_install_diagnostic` | `dag/gunbc/srv3/srv3_os_install_diagnostic.dag` | 1410 | {'dag': 1} `dag/gunbc/non_fold_residue.dag` | -| `gunbc.tailscale_acl_emit` | `dag/gunbc/tailscale_acl_emit.dag` | 52 | {'dag': 1} `dag/gunbc/host/host_standup.dag` | -| `gunbc.test_node_wall_clock_ratchet` | `dag/gunbc/test_node_wall_clock_ratchet.dag` | 99 | {'dag': 1} `dag/gunbc/plans/structural_quadratic_wall_coverage_audit.dag` | -| `gunbc.tools.review` | `dag/gunbc/tools/review.dag` | 187 | {'dag': 4} `dag/test/claim/workflow_default_field_projection_fold_witness_test.dag` `src/v2/lens/meta_exec_confinement.dag` | -| `gunbc.tools.review_codex` | `dag/gunbc/tools/review_codex.dag` | 205 | {'dag': 2, 'rs': 1} `dag/test/claim/workflow_default_field_projection_fold_witness_test.dag` `dag/gunbc/roadmap/roadmap_belt_actuate.dag` | -| `gunbc.v1_maintenance_standing` | `dag/gunbc/v1/v1_maintenance_standing.dag` | 83 | {'dag': 6, 'rs': 1} `dag/test/claim/match_arm_pattern_identity_emission_witness_test.dag` `dag/test/claim/documentary_refs_witness_test.dag` | -| `gunbc.workflow.types` | `dag/gunbc/workflow/types.dag` | 311 | {'dag': 1} `dag/gunbc/plans/host_effect_orchestration.dag` | -| `std.behavioral` | `dag/std/behavioral.dag` | 51 | {'rs': 1} `src/v1/stage0/src/bin/parse_witness.rs` | -| `std.durable_compare_and_set` | `dag/std/durable_compare_and_set.dag` | 292 | {'dag': 1} `dag/test/claim/durable_compare_and_set_witness_test.dag` | -| `std.methods` | `dag/std/methods.dag` | 67 | {'dag': 1, 'rs': 2} `src/v1/compiler_tests_rust.dag` `src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs` | -| `std.stack` | `dag/std/stack.dag` | 56 | {'dag': 1, 'rs': 1} `dag/gunbc/witness/witness_floor_workflow.dag` `src/v1/stage0/src/bin/parse_witness.rs` | -| `std.verification` | `dag/std/verification.dag` | 34 | {'dag': 3} `dag/gunbc/plans/resolver_type_name_collision_wall.dag` `dag/gunbc/plans/realization_measurement_loop.dag` | -| `tools.gunbc_ci` | `dag/gunbc/instruments/gunbc_ci.dag` | 25 | {'dag': 2, 'rs': 1} `dag/std/emit_on_demand.dag` `src/v2/test/claim/host_language_transport_script/corpus/wall_residue_live_test.dag` | -| `v2.extdeps.languages.ecmascript` | `src/v2/extdeps/languages/ecmascript.dag` | 1340 | {'dag': 1} `dag/gunbc/language_target_registry.dag` | -| `v2.extdeps.languages.machine_code` | `src/v2/extdeps/languages/machine_code.dag` | 559 | {'dag': 4} `dag/extdeps/languages/riscv/subject.dag` `dag/test/claim/language_target_registry_totality_test.dag` | -| `v2.extdeps.languages.ptx` | `src/v2/extdeps/languages/ptx.dag` | 223 | {'dag': 1} `dag/gunbc/language_target_registry.dag` | -| `v2.extdeps.languages.swift` | `src/v2/extdeps/languages/swift.dag` | 2366 | {'dag': 2} `dag/gunbc/language_target_registry.dag` `src/v2/test/claim/complexity/accumulator_copy_roster_gate_swift_test.dag` | -| `v2.extdeps.languages.wasm` | `src/v2/extdeps/languages/wasm.dag` | 2019 | {'dag': 2} `dag/gunbc/language_target_registry.dag` `dag/gunbc/plans/language_target_self_host_frontier.dag` | -| `v2.std.datetime` | `src/v2/std/datetime.dag` | 658 | {'dag': 2, 'rs': 1} `dag/extdeps/pin.dag` `src/v2/test/manual/parse_forensics_scaling_witness.dag` | -| `v2.std.float` | `src/v2/std/float.dag` | 174 | {'dag': 1} `dag/gunbc/non_fold_residue.dag` | -| `v2.std.probe_selector` | `src/v2/std/probe_selector.dag` | 674 | {'dag': 2} `dag/gunbc/non_fold_residue.dag` `dag/gunbc/plans/dag_v2_defork_audit.dag` | -| `v2.test.workflow.glob_discovery_law` | `src/v2/workflow/glob_discovery_law.dag` | 113 | {'dag': 1} `src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag` | -| `v2.workflow.class_b_import_closure_transport` | `src/v2/workflow/class_b_import_closure_transport.dag` | 118 | {'dag': 2, 'rs': 1} `dag/test/claim/long/rust_test_fixtures_import_closure_witness_test.dag` `src/v2/workflow/class_b_import_closure_probe.dag` | -| `v2.workflow.compile_door_ledger` | `src/v2/workflow/compile_door_ledger.dag` | 341 | {'dag': 1} `src/v2/test/claim/long/door_real_module_probe_test.dag` | -| `v2.workflow.compiler_closure_ingest_transport` | `src/v2/workflow/compiler_closure_ingest_transport.dag` | 150 | {'dag': 4} `dag/gunbc/instruments/ci_gates.dag` `dag/gunbc/ci/ci_layer_roots.dag` | -| `v2.workflow.phase_profile_proof_plan` | `src/v2/workflow/phase_profile_proof_plan.dag` | 22 | {'rs': 1} `src/v1/stage0/tests/phase_profile_claim_executor.rs` | -| `v2.workflow.source_root_ingest_gate` | `src/v2/workflow/source_root_ingest_gate.dag` | 18 | {'dag': 3} `dag/test/claim/guarantee_stall_witness_test.dag` `dag/gunbc/instruments/ci_gates.dag` | -| `v2.workflow.source_root_ingest_transport` | `src/v2/workflow/source_root_ingest_transport.dag` | 90 | {'dag': 2} `dag/gunbc/instruments/ci_gates.dag` `src/v2/test/claim/host_language_transport_script/corpus/migrated_transports_clean_test.dag` | - -### CITED-AUTHORITY — 103 modules - -| module | path | lines | live-source mentions | -| --- | --- | --- | --- | -| `extdeps.access.aws_iam` | `dag/extdeps/access/aws_iam.dag` | 45 | {'dag': 1} `dag/gunbc/principal_projection.dag` | -| `extdeps.access.zanzibar` | `dag/extdeps/access/zanzibar.dag` | 62 | {'dag': 1} `dag/gunbc/principal_projection.dag` | -| `extdeps.audit.cloudevents` | `dag/extdeps/audit/cloudevents.dag` | 58 | — | -| `extdeps.bmc.ipmi` | `dag/extdeps/bmc/ipmi.dag` | 48 | — | -| `extdeps.boot.emit` | `dag/extdeps/boot/emit.dag` | 127 | — | -| `extdeps.boot.framebuffer` | `dag/extdeps/boot/framebuffer.dag` | 15 | — | -| `extdeps.boot.freestanding_payload` | `dag/extdeps/boot/freestanding_payload.dag` | 19 | — | -| `extdeps.boot.freestanding_witness` | `dag/extdeps/boot/freestanding_witness.dag` | 198 | — | -| `extdeps.boot.linux_x86_boot` | `dag/extdeps/boot/linux_x86_boot.dag` | 37 | — | -| `extdeps.cloud.gcp.iam_admin` | `dag/extdeps/cloud/gcp/iam_admin.dag` | 114 | — | -| `extdeps.cloud.gcp.serviceusage` | `dag/extdeps/cloud/gcp/serviceusage.dag` | 51 | — | -| `extdeps.cloud.gcp.sts` | `dag/extdeps/cloud/gcp/sts.dag` | 111 | {'rs': 2} `src/v1/stage0/src/cli_run.rs` `src/v1/stage0/src/bin/effects_rest_transport_witness.rs` | -| `extdeps.cloud_init.cloud_init` | `dag/extdeps/cloud_init/cloud_init.dag` | 58 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.colo.centersquare` | `dag/extdeps/colo/centersquare.dag` | 47 | — | -| `extdeps.colo.colocation_america` | `dag/extdeps/colo/colocation_america.dag` | 45 | — | -| `extdeps.colo.coresite` | `dag/extdeps/colo/coresite.dag` | 58 | — | -| `extdeps.colo.dataverge` | `dag/extdeps/colo/dataverge.dag` | 47 | — | -| `extdeps.colo.digital_realty` | `dag/extdeps/colo/digital_realty.dag` | 59 | — | -| `extdeps.colo.equinix` | `dag/extdeps/colo/equinix.dag` | 71 | — | -| `extdeps.colo.evocative` | `dag/extdeps/colo/evocative.dag` | 49 | — | -| `extdeps.colo.h5` | `dag/extdeps/colo/h5.dag` | 59 | — | -| `extdeps.colo.halsey_165` | `dag/extdeps/colo/halsey_165.dag` | 73 | — | -| `extdeps.colo.hivelocity` | `dag/extdeps/colo/hivelocity.dag` | 45 | — | -| `extdeps.colo.interserver` | `dag/extdeps/colo/interserver.dag` | 100 | — | -| `extdeps.colo.iron_mountain` | `dag/extdeps/colo/iron_mountain.dag` | 44 | — | -| `extdeps.colo.natcoweb` | `dag/extdeps/colo/natcoweb.dag` | 123 | — | -| `extdeps.colo.netrality` | `dag/extdeps/colo/netrality.dag` | 45 | — | -| `extdeps.colo.qts` | `dag/extdeps/colo/qts.dag` | 45 | — | -| `extdeps.colo.summit` | `dag/extdeps/colo/summit.dag` | 49 | — | -| `extdeps.colo.three_sixty_five` | `dag/extdeps/colo/three_sixty_five.dag` | 89 | — | -| `extdeps.colo.tierpoint` | `dag/extdeps/colo/tierpoint.dag` | 86 | — | -| `extdeps.colo.types` | `dag/extdeps/colo/types.dag` | 110 | — | -| `extdeps.container.oci.ctrl_session_witness` | `dag/extdeps/container/oci/ctrl_session_witness.dag` | 361 | — | -| `extdeps.container.oci.image_config` | `dag/extdeps/container/oci/image_config.dag` | 100 | — | -| `extdeps.container.oci.linux` | `dag/extdeps/container/oci/linux.dag` | 156 | — | -| `extdeps.container.oci.manifest` | `dag/extdeps/container/oci/manifest.dag` | 98 | — | -| `extdeps.container.oci.runtime_config` | `dag/extdeps/container/oci/runtime_config.dag` | 97 | — | -| `extdeps.currency.currency` | `dag/extdeps/currency/currency.dag` | 24 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.darwin.rusage` | `dag/extdeps/darwin/rusage.dag` | 31 | {'dag': 2, 'rs': 1} `dag/test/claim/peak_resident_measured_witness_test.dag` `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.darwin.sysctl` | `dag/extdeps/darwin/sysctl.dag` | 64 | {'dag': 3, 'rs': 1} `dag/gunbc/extdeps_scope_frontier.dag` `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.ebay.ebay` | `dag/extdeps/ebay/ebay.dag` | 94 | — | -| `extdeps.ebay.ebay_contracts` | `dag/extdeps/ebay/ebay_contracts.dag` | 34 | — | -| `extdeps.ebay.errors` | `dag/extdeps/ebay/errors.dag` | 61 | — | -| `extdeps.ebay.inventory` | `dag/extdeps/ebay/inventory.dag` | 468 | — | -| `extdeps.ebay.oauth` | `dag/extdeps/ebay/oauth.dag` | 151 | — | -| `extdeps.energy.nj_electricity` | `dag/extdeps/energy/nj_electricity.dag` | 61 | — | -| `extdeps.exec.xargs` | `dag/extdeps/exec/xargs.dag` | 31 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.filesystem.ntfs` | `dag/extdeps/filesystem/ntfs.dag` | 28 | — | -| `extdeps.filesystem.posix` | `dag/extdeps/filesystem/posix.dag` | 34 | — | -| `extdeps.formats.elf.encode` | `dag/extdeps/formats/elf/encode.dag` | 201 | — | -| `extdeps.formats.elf.hello_static_witness` | `dag/extdeps/formats/elf/hello_static_witness.dag` | 259 | — | -| `extdeps.formats.elf.primitives` | `dag/extdeps/formats/elf/primitives.dag` | 61 | — | -| `extdeps.formats.elf.relocation` | `dag/extdeps/formats/elf/relocation.dag` | 26 | — | -| `extdeps.formats.elf.sections` | `dag/extdeps/formats/elf/sections.dag` | 31 | — | -| `extdeps.formats.elf.segments` | `dag/extdeps/formats/elf/segments.dag` | 79 | — | -| `extdeps.formats.elf.types` | `dag/extdeps/formats/elf/types.dag` | 190 | — | -| `extdeps.git.versioning` | `dag/extdeps/git/versioning.dag` | 73 | — | -| `extdeps.github.auth` | `dag/extdeps/github/auth.dag` | 61 | {'rs': 1} `src/v1/stage0/src/bin/parse_witness.rs` | -| `extdeps.github.ci` | `dag/extdeps/github/ci.dag` | 30 | {'dag': 1} `dag/gunbc/plans/realization_measurement_loop.dag` | -| `extdeps.github.gists` | `dag/extdeps/github/gists.dag` | 76 | {'rs': 2} `src/v1/stage0/src/bin/parse_witness.rs` `src/v1/stage0/src/bin/effects_rest_transport_witness.rs` | -| `extdeps.github.github_contracts` | `dag/extdeps/github/github_contracts.dag` | 15 | — | -| `extdeps.github.issues` | `dag/extdeps/github/issues.dag` | 202 | {'dag': 1, 'rs': 1} `dag/gunbc/extdeps_scope_frontier.dag` `src/v1/stage0/src/bin/effects_rest_transport_witness.rs` | -| `extdeps.github.mergeable_state` | `dag/extdeps/github/mergeable_state.dag` | 39 | — | -| `extdeps.github.mergeable_state_contracts` | `dag/extdeps/github/mergeable_state_contracts.dag` | 18 | — | -| `extdeps.gitignore` | `dag/extdeps/git/gitignore.dag` | 36 | — | -| `extdeps.languages.go.module` | `dag/extdeps/languages/go/module.dag` | 144 | {'dag': 1} `dag/gunbc/commit_workflow.dag` | -| `extdeps.languages.go.primitives` | `dag/extdeps/languages/go/primitives.dag` | 211 | {'dag': 1, 'rs': 1} `dag/std/checked_arithmetic.dag` `src/v1/stage0/src/std_checked_arithmetic.rs` | -| `extdeps.languages.python.primitives` | `dag/extdeps/languages/python/primitives.dag` | 86 | — | -| `extdeps.languages.rust.primitives` | `dag/extdeps/languages/rust/primitives.dag` | 112 | {'dag': 2, 'rs': 2} `dag/extdeps/languages/rust/types.dag` `dag/std/checked_arithmetic.dag` | -| `extdeps.languages.typescript.primitives` | `dag/extdeps/languages/typescript/primitives.dag` | 103 | — | -| `extdeps.linux.edac` | `dag/extdeps/linux/edac.dag` | 26 | — | -| `extdeps.linux.proc_meminfo` | `dag/extdeps/linux/proc_meminfo.dag` | 41 | {'dag': 1} `dag/extdeps/linux/procfs.dag` | -| `extdeps.linux.rusage` | `dag/extdeps/linux/rusage.dag` | 31 | {'dag': 2, 'rs': 1} `dag/test/claim/peak_resident_measured_witness_test.dag` `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.llm.anthropic_errors` | `dag/extdeps/llm/anthropic_errors.dag` | 80 | — | -| `extdeps.llm.anthropic_rest` | `dag/extdeps/llm/anthropic_rest.dag` | 96 | {'rs': 1} `src/v1/stage0/src/bin/effects_rest_transport_witness.rs` | -| `extdeps.llm.llm` | `dag/extdeps/llm/llm.dag` | 12 | — | -| `extdeps.llm.openai_contracts` | `dag/extdeps/llm/openai_contracts.dag` | 32 | — | -| `extdeps.llm.openai_errors` | `dag/extdeps/llm/openai_errors.dag` | 66 | — | -| `extdeps.llm.openai_rest` | `dag/extdeps/llm/openai_rest.dag` | 92 | {'rs': 1} `src/v1/stage0/src/bin/effects_rest_transport_witness.rs` | -| `extdeps.netplan.netplan` | `dag/extdeps/netplan/netplan.dag` | 69 | {'dag': 1} `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.observability` | `dag/extdeps/observability.dag` | 49 | — | -| `extdeps.posix.rusage` | `dag/extdeps/posix/rusage.dag` | 66 | {'dag': 3, 'rs': 2} `dag/test/claim/peak_resident_measured_witness_test.dag` `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.provisioning.ubuntu_seeded_install_media_toolchain` | `dag/extdeps/provisioning/ubuntu_seeded_install_media_toolchain.dag` | 39 | — | -| `extdeps.realestate.nj_industrial` | `dag/extdeps/realestate/nj_industrial.dag` | 70 | — | -| `extdeps.render.surface` | `dag/extdeps/render/surface.dag` | 79 | {'rs': 2} `src/v1/stage0/src/cli_run.rs` `src/v1/stage0/src/v1_interpreter.rs` | -| `extdeps.render.terminal_capability` | `dag/extdeps/render/terminal_capability.dag` | 60 | {'rs': 1} `src/v1/stage0/src/v1_interpreter.rs` | -| `extdeps.runtime.api.darwin` | `dag/extdeps/runtime/api/darwin.dag` | 59 | — | -| `extdeps.runtime.api.windows` | `dag/extdeps/runtime/api/windows.dag` | 114 | — | -| `extdeps.runtime.local` | `dag/extdeps/runtime/local.dag` | 22 | — | -| `extdeps.sec.edgar_rest` | `dag/extdeps/sec/edgar_rest.dag` | 71 | {'dag': 2} `dag/extdeps/sec/edgar.dag` `dag/gunbc/prose_row_frontier.dag` | -| `extdeps.shell.credentials` | `dag/extdeps/shell/credentials.dag` | 19 | — | -| `extdeps.tailscale.acl` | `dag/extdeps/tailscale/acl.dag` | 312 | {'dag': 2} `dag/test/claim/host_standup_spine_witness_test.dag` `dag/gunbc/host/host_standup.dag` | -| `extdeps.tailscale.acl_api` | `dag/extdeps/tailscale/acl_api.dag` | 70 | — | -| `extdeps.tcgplayer.catalog` | `dag/extdeps/tcgplayer/catalog.dag` | 180 | — | -| `extdeps.tcgplayer.pricing` | `dag/extdeps/tcgplayer/pricing.dag` | 78 | — | -| `extdeps.tcgplayer.store` | `dag/extdeps/tcgplayer/store.dag` | 97 | — | -| `extdeps.tcgplayer.tcgplayer` | `dag/extdeps/tcgplayer/tcgplayer.dag` | 82 | — | -| `extdeps.tools.diffutils` | `dag/extdeps/tools/diffutils.dag` | 29 | {'dag': 1} `dag/extdeps/tools/gnu_coreutils.dag` | -| `extdeps.transports.sql` | `dag/extdeps/transports/sql.dag` | 41 | {'dag': 1} `src/v2/test/fixture/frontier_probe_elision_boundary_overlay.dag` | -| `extdeps.vendor.arm` | `dag/extdeps/vendor/arm.dag` | 31 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.vendor.qualcomm` | `dag/extdeps/vendor/qualcomm.dag` | 31 | {'dag': 1} `dag/gunbc/extdeps_scope_frontier.dag` | -| `extdeps.version.pep440` | `dag/extdeps/version/pep440.dag` | 310 | — | -| `gunbc.hand_lens_host_bridge_scaffold_index` | `dag/gunbc/hand_lens_host_bridge_scaffold_index.dag` | 23 | — | \ No newline at end of file diff --git a/docs/plans/unconsumed-module-residue-disposition.md b/docs/plans/unconsumed-module-residue-disposition.md deleted file mode 100644 index 7dcb8c6000e..00000000000 --- a/docs/plans/unconsumed-module-residue-disposition.md +++ /dev/null @@ -1,805 +0,0 @@ -# Disposition of the unconsumed-module residue - -**This is the execution record for the population -[`unconsumed-module-census.md`](unconsumed-module-census.md) measured. The census stays the -authority on how the population is derived; this records what was done to it and, for every -row not deleted, the typed reason it survived.** Separate facts on separate clocks: the census -answers *what is unconsumed*, this answers *what was dispositioned, and what refused*. - -Operator directive both serve (2026-08-21, verbatim): *"yes please make sure to clean up -anything without consumers that we don't need, or get them actually consumed."* - -## 1. What was deleted, and what the number is - -The census's reconciled cleanup list is **131 modules** -- 96 STILL-UNCONSUMED plus 35 -DEAD-CONSUMER-ONLY, the two buckets that are residue on all three decoded surfaces. Measured -2026-08-22 against a tree that has since moved. **Re-deriving the instrument on this branch's -base (`90986d19469`) reproduces it within the drift:** - -| bucket | census (2026-08-22) | re-derived here | delta | -| --- | --- | --- | --- | -| population (unreachable) | 298 | 302 | +4 | -| CONSUMED-DECISIVE | 91 | 89 | -2 | -| DEAD-CONSUMER-ONLY | 35 | 32 | -3 | -| AMBIGUOUS-SHARED-ONLY | 75 | 74 | -1 | -| STILL-UNCONSUMED | 96 | 107 | +11 | -| **residue (the cleanup list)** | **131** | **139** | **+8** | - -The instrument is re-derived, not inherited, on purpose: the census records that the population -*has a clock on it* -- one of its own appendix rows already pointed at a deleted file -- so a -lane trusting a four-day-old row list deletes against a tree that no longer exists. Re-deriving -also re-runs the census's controls: `v2.compiler.compile`, `gunbc.spark.serving_desired`, -`gunbc.clock_read`, `v2.std.node` and `gunbc.accelerator_demo_gpu` all score reachable here, -and RESIDUE-EMPTY again scores 0 consumed, so the instrument finds consumption neither -everywhere nor nowhere. - -**Of the corrected 114-row residue, this change deletes 55.** The deleted rows carry no -obligation to anything outside themselves, on every surface the instrument decodes *and* every -surface it does not: - -- unreachable from every root -- discovery path, entry row, or v1 seed mirror; -- no uniquely-owned symbol -- **including coproduct variant constructors and flat-namespace - `operation`/`service`/`resource` declarations**, see 4d -- named bare by any `.dag` file; -- **no mention of the module name or its path in any `.dag`, `.rs`, `.yml`, `.yaml`, - `.toml`, `.sh` or `.txt` file** in the tree; -- no `test fn` declared, so no assertion stops executing; -- no `src/v1/stage0/src` mirror, so the seed population is unchanged; -- **and its whole island is in the set** -- eligibility is computed as a fixed point over the - deletion set, not per module (4g). - - - -## 2. The deletion as the census - -DESIGN §3 says the deletion *is* the census. Measured against the corrected instrument (§4d), -before and after the cut: - -| | before | after | delta | -| --- | --- | --- | --- | -| modules | 3851 | 3796 | -55 | -| **reachable** | **3549** | **3549** | **0** | -| population | 302 | 247 | -55 | -| **CONSUMED-DECISIVE** | **91** | **91** | **0** | -| AMBIGUOUS-SHARED-ONLY | 97 | 97 | 0 | -| STILL-UNCONSUMED | 80 | 29 | -51 | -| DEAD-CONSUMER-ONLY | 34 | 29 | -5 | - -Reachable, CONSUMED-DECISIVE and AMBIGUOUS are unchanged; the whole movement is inside the two -residue buckets and equals the file count exactly. - -**These counters are necessary and NOT sufficient, and §4d is why.** An earlier revision offered -this table as proof the cut was safe. It was computed by an instrument carrying an extraction -defect, so it could not surface the case that defect was blind to -- and read as reassurance -precisely because it was consistent. The table is retained because it still falsifies a whole -class of error (a cut catching a *reachable* module would move the top row), and demoted because -it cannot falsify the class that actually occurred. The independent instrument is the floor, §8. - -## 3. The one row the census deferred, now answered by its author -- and the cause it gave is not the cause the census guessed - -`gunbc.scm.commit_closure_store` was deleted by the batch rule, restored while the question -was open, and is **deleted again** on the answer from `gentle-eagle-360`, who authored #8820. -The verdict is recorded here rather than in the module, because a citation into a deleted -module dies with it. - -**Both arms the census offered rest on a false premise, and the correction is load-bearing.** -The census framed this as *the envelope grew save/load and superseded it* versus *the envelope -should consume it as its persistence layer*. - -- *Not superseded.* `gunbc.scm.repository_envelope` contains **zero `Filesystem` operations - and not one `func`** -- a pure codec, `RepositoryEnvelope` to `JsonValue` and back. Nothing - took over this module's job; recording the deletion as "replaced by the envelope" would be - false. -- *And the envelope should NOT consume it.* `commit_closure_store` persists **one root and - its closure**. A repository is a different subject: an empty initialized state with **no - root at all**, several commits over one shared node population, a checked-out selection. - Wiring the envelope to a carrier that demands a root would force `init` to invent a phantom - commit -- a grain mismatch dressed as a fix. - -**The correct disposition row: DELETE, cause = STAGED ORPHAN AT THE WRONG GRAIN.** Not -superseded, not replaced, not merely unconsumed. It was never wired, and the layer that will -do this job is repository-grain and will be written that way whether or not this module stands. - -**Why delete rather than hold it for that layer** -- the author's reasoning and DESIGN §3's: a -surviving X is an **attractor**; while it stands, every nearby persistence question is answered -in commit-closure vocabulary already scheduled to die. They declined to hold a lane open for a -speculative "export one commit" operation with no consumer -- §6 experimental residue, applied -by an author to their own module. - -**A rung-honesty defect in its own carrier, volunteered by that author, strengthens the -warrant.** The module's header claims its persistence is *"verified by direct execution in Wet -mode"* and points at `commit_closure_round_trip_probe`. True when run by hand, not now: the -probe is **enrolled nowhere and nothing executes it**. The module carries an overclaim about -its own evidence -- a stronger reason to delete than unreachability, and exactly the -specification-without-execution shape DESIGN §5 names. - -**What must survive into the replacement, recorded because this deletion removes the prompt -for it** (not this lane's to carry): the host's success/content/error triple folded into a -coproduct **at** the boundary, so no downstream consumer picks which member to believe or reads -content from a failed read; and encode adjudicated **before** the write, so a successful write -cannot upgrade an encoding failure into `Saved`. - -## 4. One finding the deletion surfaced, and it is not a deletion - -**`gunbc.v1_maintenance_standing` is unreachable from every root and named bare by nothing, -and DESIGN.md names it as an authority.** DESIGN's 3 standing rule on the v1 seed states: -*"The authority is `gunbc.v1_maintenance_standing` `v1_seed_standing`"* -- the carrier for the -semantics-frozen / maintenance-active reclassification, its admission test, and its five-class -refusal vocabulary. - -It is held, not proposed for deletion. The finding runs the other way: **the carrier DESIGN -designates as the authority for the v1 freeze standing executes nowhere and is reached by -nothing.** Its four in-tree `.dag` mentions (`ci_layer_roots`, `documentary_refs`, -`roadmap_serve`, `whole_corpus_compile_admission`, plus two witnesses) are string citations, -not calls. Consistent with the standing's own declared rung -- *"is consumed by review -diligence, not by any gate, so it sits at mitigatable"* -- and this measurement is its -independent corroboration: not merely ungated, unreachable. Recorded as a rung-honesty datum -for whoever climbs it, not as a disposition. - -## 4b. A strand that appeared, then dissolved when its module came back - -A record of the surface, not of a repair. Widening the mention scan past source extensions to -`.txt` found `docs/probes/census_extra_excludes.txt` and its seeds file naming -`dag/examples/gunbhub_serve_program/gunbhub_serve_program.dag`, which the first cut deleted. -`v1_compiler.census_exclude_derive` loads both -- the seeds drive the derived exclude closure -and the pinned oracle is its drift witness -- so a row naming a deleted path skews the -symmetric diff toward *drift* rather than *staleness*. - -**Both the row removal and the two count literals it forced are reverted**, because -`gunbhub_serve_program` left the deletion set in §4d's re-derivation and the row is correct -again. **This PR touches no file under `src/v1`.** - -The reusable finding: **a source-extension mention scan is not a complete consumption -surface.** Authored data files carry references, and this one was load-bearing. The census's -decoded-surface list should include them. The check is now part of the §1 precondition and -stays there whether or not any row trips it. - -## 4c. One deleted row was a committed copy of a DO-NOT-COMMIT artifact - -Raised by review as two surviving references to a deleted filename -(`gunbc.ci_layer_roots`'s `WitnessExclusionRow` pattern and `discover_owned_data`'s -`exclude_subpaths` default), cleared there as harmless string patterns. Correct -- and checking -*why* turned up the sharper fact and reversed the tidy-up it invites. - -`v1_compiler.cli_run` `discover_owned_data_decls` **generates** a module named -`v2.test.claim.workflow.host_discovered_owned_data_manifest` under -`src/v2/test/claim/workflow/`, stamped `GENERATED by discover_owned_data -- ephemeral -host transport. DO NOT COMMIT.` What this change deleted is -`v2.test.workflow.host_discovered_owned_data_manifest` at `src/v2/workflow/` -- a -**committed instance of that ephemeral artifact under a second module path, carrying all-zero -counts**. A stale committed copy of a file the generator says must not be committed is a -better reason to delete than unreachability. - -**And the two exclusion rows must stay.** `path_excluded` is a substring match over the full -path, so `host_discovered_owned_data_manifest.dag` still matches the *generated* file whenever -`discover_owned_data` produces it. They are live exclusions of an ephemeral artifact, not dead -rows. Removing them as tidy-up -- what "a pattern that no longer matches anything" invites -- -would have re-admitted a generated transport into discovery. - -Recorded because the wrong reading is the natural one for the next person to grep these names. - -## 4e. One row restored on disposition, not on measurement - -`gunbc.generic_binder_field_projection_deficit` scores STILL-UNCONSUMED correctly and is -restored anyway: dispositioned KEEP-WITH-REASON in #8851 as a DESIGN 4b **deficit filing** -- -a declared rung, a ceiling, a next-rung trigger. - -**An unconsumed deficit filing is 4b(2) working, not residue.** A class below its ceiling is -*required* to name its trigger, and nothing consumes that filing by design; unreferenced is its -normal state. Deleting it would remove a safety-ledger row and lower a rung with none of the -reason, bounded population and restoration trigger 4b(3) demands. Its sibling -`gunbc.empty_decl_file_checkpoint_bypass` carries the same disposition and was never in this -batch. - -The general point, stated by the census and learned here twice: **unreachability is the wrong -predicate for a row whose purpose is to be read by a human ledger.** This row and section 3's -`commit_closure_store` are that shape; neither was caught by a mechanical rule -- both came -from a per-row disposition someone had already written down. - -## 4d. Four extraction defects, one mechanism: under-extraction creates false uniqueness - -**The floor refused the first cut**, with `unresolved type 'MergeReadinessVerdict'` and eight -`undefined variable 'Ready'` in `gunbc.code_change_workflow`. `gunbc.pr_digests` had been -deleted and should not have been. - -The defect-6 re-score asks whether another `.dag` file names a candidate's declared symbol -*bare*. That needs what a module **declares** -- and four ways of getting it wrong were found, -all under-extraction, all with the same consequence: - -| # | defect | effect | -| --- | --- | --- | -| 7 | coproduct **variant constructors** not extracted (`= Ready \| NotReady`) | found here, by the floor | -| a | a **generic header** `type X

= A \| B` skips the block | relayed by `silent-deer-368` | -| b | a **multi-line variant record** truncates a start-of-line scan | relayed by `silent-deer-368` | -| c | `operation`/`service`/`resource` are **declarations**, read as references | relayed by `silent-deer-368` | -| d | a **same-line coproduct** `type X = A \| B` | found by their discriminator, in *my* extractor | - -**Why this direction is the dangerous one.** Under-extraction makes a module own fewer symbols -than it does, so a *shared* name looks **uniquely owned** -- false uniqueness. A module whose -declarations all sit behind an unparsed header owns nothing, so a live consumer naming its -symbols bare is invisible and the module scores residue and **gets deleted**. The opposite -direction from the `pr_digests` case, which merely kept a dead-looking module alive. - -**Fixed at the root, not per bug.** Adopted `silent-deer-368`'s region-based extractor: a -type's region runs to the next top-level declaration (so (b) cannot truncate it), a leading -`<...>` is stripped before testing for `=` (so (a) parses), and `operation`/`service`/ -`resource` names are declarations (so (c) does not misattribute). Verified against their four -discriminators -- `Apply` in `std.upsert_decision`, `Select` in `v2.extdeps.languages.llvm_ir`, -`Ready` in `gunbc.pr_digests`, `Capability` in `std.behavioral` -- **my extractor passed three -and failed the fourth**, which is how (d) was found. Their (c) tell also passes: -`extdeps.transports.sql` scores AMBIGUOUS rather than consumed-by-`filesystem_io`, which linked -to it only through `operation Delete {`. - -**What re-deriving cost.** **13 modules left the deletion set** -- nine formatters, the -`language_model` rust root, `generic_instantiation`, `roadmap_dispatch`, -`gunbhub_serve_program` -- and **every one landed in AMBIGUOUS-SHARED-ONLY, not CONSUMED.** -That distinction is why the bucket exists: they are *no longer provably unconsumed* and *not -proven consumed*, so none may be deleted on this evidence, and reading the shrinking residue as -"more live modules found" collapses exactly the gap the bucket holds. - -**The general lesson, which outlives every row above:** - -> **A control derived from the measurement it controls does not discriminate that -> measurement's blind spot.** - -Both counters §2 offers came from the instrument carrying defect 7. The floor caught it because -it is an independent instrument sharing none of the method. That is delete-first's actual -mechanism (DESIGN §3): not that the rule is trustworthy, but that the substrate refuses on an -authority the rule had no part in building. Defects (a)-(d) were found by **hand-reading rows** -while counters stayed consistent throughout -- the same lesson from the other direction. - -## 4h. Six discriminators, and the count/set rule that resolves two instruments disagreeing - -The extraction defects were settled by **discriminating cases, not by descriptions**, and that -is the reusable result. Bug *descriptions* relayed between lanes found one bug in this lane's -extractor; the *test cases* that came with them found two more nobody had named -- and one of -those still failed here after its own description had been implemented faithfully, which is -(f2) below. **A discriminator travels; a description does not.** - -The roster, each failing on exactly the defect it names: - -| case | specimen | catches | -| --- | --- | --- | -| `Apply` | `std.upsert_decision` | (a) generic header | -| `Select` | `v2.extdeps.languages.llvm_ir` | (b) multi-line variant record | -| `Ready` | `gunbc.pr_digests` | defect 7, variant constructors | -| `Capability` | `std.behavioral` | (d) same-line coproduct | -| `Run` | `gunbc.cli_services` | (e) service-namespace-only module | -| `v2.std.projection` NOT consumed by `v2.lens.common.graph_invariant` | (f) generic parameter counted as a reference | - -**(f) is the mirror of (a), with a second half the obvious fix does not reach.** A generic -parameter is a **binder, not a reference**: `type GraphInvariant` binds -`Projection`, and `v2.lens.common.graph_invariant` imports no projection module at all. One -construct yields two distinct false references: - -- **(f1)** the parameter list in the declaration header; -- **(f2)** *uses of the bound name inside that declaration's body* -- `projection: Projection` - on the very next line. - -Implementing (f1) alone left `v2.std.projection` scoring CONSUMED-DECISIVE; only region-scoped -shadowing of the bound name moved it to AMBIGUOUS. So the construct touches the instrument -three ways in two directions: a generic header **defeats declaration extraction** in (a) and -**inflates reference extraction** in (f1) and (f2). - -**The scoping is the safety-critical part.** Dropping the bound name file-wide is the obvious -implementation and the *delete-a-live-module* direction: removing references moves a live -module into the residue, so a name bound as a parameter in one declaration and genuinely -referenced in another would silently lose its real reference. The shadow is scoped to the -binding declaration's region. - -**The `service` short-name credit: dropped, then restored, and the second reason is the one -that holds.** First dropped citing a false-consumption claim measured on the *other* lane's -instrument -- never true here, since their specimen `service gcp.Metadata` is **dotted** and -this lane's pattern never matched it. Then justified as *"no measured effect on this -population"*, also wrong as a basis: a nil effect argues for either choice and settles nothing. - -**The basis that holds is correctness of ownership.** `extdeps.filesystem.filesystem_io` -declares `service Filesystem`; an instrument saying otherwise is wrong whether or not a row -moves today. And withholding the credit is not merely a missing fact -- `Filesystem` is declared -**twice**, as a `service` there and as a `resource` in `std.resources`, so dropping the service -side hands `std.resources` **false sole ownership** of a name two modules declare. That is -*manufactured uniqueness*: the mechanism behind every confident wrong attribution in this -chain, created by the fix meant to avoid it. - -The evidence is the seventh discriminator: `v2.extdeps.realization.artifact_store_fs` uses -`Filesystem.Write`, `.Read` and `.Delete` as a qualifier with **no import of `Filesystem` in -its import block**. A consumer does bare-resolve an undotted service short name through -whole-pool resolution. Credited, dotted forms included by last segment (`service gcp.Metadata` -declares `Metadata`); zero modules in this corpus own nothing without it, but the same basis -applies, since under-extraction is the delete-a-live-module direction. - -**How the real cause was located, the transferable part.** This lane could not reproduce the -other's false positive *at all* -- the patterns differ on dotted names. That **inability to -reproduce, with a stated reason**, identified (f) as the actual cause and the service credit as -merely adjacent. A cross-check that agrees tells you less than one that fails to reproduce and -can say why. - -**Direction check, which is what makes (f) safe to have found late:** (f) and the service -short-name credit both *inflate* consumption, so neither can cause a wrong deletion -- they can -only hold a dead module. Both were fixed anyway; the deletion set did not move. That is also -the count/set rule predicting which lane had to care about which defect before either -measured it: an inflated-consumption defect moves a residue *count* and leaves a conservative -deletion *set* alone. - -**(e) is contributed back and is the sharpest of the five**: its specimen's declared set is -*empty* under an unfixed extractor rather than merely incomplete -- `gunbc.cli_services` is a -`service` block with three `operation`s and **zero** `fn`/`data`/`type`/`const`. Owning nothing -is the exact precondition for the delete-a-live-module failure. Measured exposure today: none --- reported as that lane reported theirs, **in the bucket where it would be dangerous** rather -than over the population: of the operation-bearing rows, none sits in STILL-UNCONSUMED. - -**The two lanes' (c) fixes are not equivalent, and reconciling them would be wrong.** -`silent-deer-368` subtracts declaration-keyword names from the *consuming* file's reference -set; this lane adds them to the *declaring* module's owned set, making the symbol -non-uniquely-owned and parking the row in AMBIGUOUS instead of removing the reference. -Subtraction is more precise; addition is more conservative. Their rule, verbatim, because it -settles what looked like a discrepancy: - -> **A residue COUNT should come from the precise implementation, a deletion SET from the -> conservative one. Our numbers disagreeing in a known direction is a property to state, not -> a discrepancy to reconcile.** - -**Two independent populations, same skew -- the finding, not the bookkeeping.** 13 of this -lane's 69 left the residue on re-derivation and 12 of 14 left theirs, in both cases **almost -entirely to AMBIGUOUS and near-zero to CONSUMED**. The residue shrank far more than the -consumption claim grew. That gap is the whole reason the ambiguous bucket exists, and the -natural summary -- *"the residue shrank, so more modules are live"* -- is the reading that -collapses it into unevidenced deletions. - -## 4g. Eligibility is a fixed point over the set, not a property of a module - -31 residue rows are named bare only from *inside* the population. A per-module verdict over a -mutually-referencing island returns "consumed" for every member and the island never becomes -eligible; splitting one across batches reds the first batch. - -So the deletion set is computed as a **fixed point**: repeatedly drop any module with a -surviving in-population consumer until nothing drops. This ran twice for real, in both -directions: - -- **A deletion created a violation.** The first cut deleted `gunbc.pr_digests` while keeping - `gunbc.code_change_workflow` -- one island, split. That is the row the floor refused on. -- **A restore created one.** Putting the 13 AMBIGUOUS rows back re-created an in-population - consumer for `v2.std.rust_leaf_model_claim` (via `v2.test.language_model.rust`), eligible - until its neighbour returned. - -**This is why the change is one PR** and not six cluster PRs: the property is over the set, so -six PRs merging in arbitrary order re-open this class unless each is re-derived against the -others' merged state -- strictly more work and risk than keeping the set closed. The reviewer's -seams are the commits. - -## 4f. A finding walked past while editing the pin, filed rather than fixed - -Repairing `census_extra_excludes.txt` meant moving two count literals in -`v1_compiler.census_exclude_derive` (83 to 82, 27 to 26). **Those literals are a change -detector, not a check**, and this lane can say so with a receipt rather than a style objection. - -A hand-maintained count beside a hand-maintained list measures whether someone remembered to -update both. It has no independent referent: not a controlled fixture, not an external or -versioned authority, not a policy budget, not a monotone debt contract over a closed universe --- the four grounds DESIGN §5 admits for a numeric literal in a merge-blocking test. The -section's review tell applies exactly: automating the literal's update collapses the assertion -to `measure() == measure()`, so the manual update was the assertion's entire content. - -The receipt is this change: one row moved for an unrelated reason, both literals had to be -hand-followed, and nothing about the list's *correctness* was checked by either. Had the row -moved without them, the failure would have reported as a count mismatch rather than as what it -was. - -**Filed, not fixed.** The pin belongs to the exclude-closure lane -- `census_exclude_derive`'s -own header names its dissolution (the pin retires when strict whole-tree resolve greens without -host fixed-point closure) -- and repairing an oracle inside a deletion PR would be this -document's own §6 objection turned on itself. The person who paid the cost is the right person -to report it and the wrong person to fix it. - -**Postscript: the edit that prompted this is reverted.** `gunbhub_serve_program` left the -deletion set in §4d, so the pin's row is correct again and the literals return to 83 and 27. -The finding stands on its own merits -- a property of the pin, not of this deletion. - -## 4i. The floor's second refusal, and it is the same module telling the truth again - -The required floor refused a second time, on one witness: -`v2.test.lens_inert_carrier.inert_carrier_test.inert_carrier_no_unrostered_or_stale`. The cause -is real, caused by this change, and is the census working rather than a defect. - -Deleting `v2.test.workflow.host_discovered_owned_data_manifest` (§4c -- the committed copy of a -`DO NOT COMMIT` ephemeral artifact) removed the **only non-test reference** to -`OwnedDataDiscoveryReceipt`, declared in `v2.compiler.discovery_enumeration`. Verified directly, -not through any re-implementation: before the cut that carrier had two non-test `.dag` -references, its own declaration and that manifest's construction of it; after, only its own -declaration. It is self-tested by `discovery_enumeration_test`. Declared once, self-tested, -zero consumers outside its own block is exactly the inert-carrier definition, so the lens -refused. - -**Fixed by rostering it, not by restoring the module.** The roster is the declared frontier for -precisely this state: the carrier's real consumer is *generated at runtime and deliberately -never committed*, so in the committed corpus it is inert **by construction** and no corpus scan -can see otherwise. Restoring the deleted module would re-commit a file its own generator stamps -`DO NOT COMMIT` to satisfy a hygiene lens -- the tail wagging the dog. - -**How this was diagnosed, because the first attempt was the error this document keeps -recording.** I re-implemented the inert-carrier rule from the Rust to locate the moved name. -That re-implementation was **not faithful** -- its absolute output shared *zero* names with the -live roster, twice, under two different test-predicate readings. Its *delta* was stable across -both, which is suggestive and not evidence. The claim above rests on a direct, checkable -observation about one symbol's references before and after. An unfaithful instrument does not -become trustworthy because its delta looks stable. - -## 4j. A finding this change is a specimen of, and it is not about this change - -**A defect keyed on a specific `OccurrenceId` value stops reproducing under any change that -perturbs occurrence allocation.** Stated as a property of the class, because the next such -defect will have it and nobody will be watching. - -The specimen is this branch, usable *precisely because it contains no fix*: - -| | tree | failed | planned | -| --- | --- | --- | --- | -| base `90986d19469` | as-is | **16** | 10425 | -| this branch | base minus 55 deleted modules | **1** (unrelated) | **10425** | - -The 16 are the `fold_lowering` / `body_lowering.statement_let_bind` family, every one -reporting `non-exhaustive pattern match on: OccurrenceId { value: 79 }`. All 16 identities are -absent -- passing -- on a tree that differs from their red base *only* by deleted modules. No -test file was deleted, no lowering code touched, and both runs reported the same discovery -figures (`offered=11798 routed=10425`). - -**That last control is weaker than an earlier revision of this section claimed.** It read -*"`planned` is identical at 10425, which rules out roster change"*. Identical counts are -consistent with an unchanged roster and do not establish one. Not a theoretical caveat: on the -very next run, `offered` moved **11798 → 11812** and `routed` **10425 → 10439** across two -commits that changed **only a markdown file and a single `inert_row` data row**, declines -unchanged. Fourteen witnesses entered *discovery* with no corpus change, cause **not yet -attributed**. So the discovered roster is not a pure function of the committed tree, and a -matching count is weaker evidence than it looks. - -The comparison still stands as a controlled one -- base and post-deletion runs agree on every -reported discovery figure, and the tree difference is exactly the 55 deletions -- but it is -corroboration, not proof, and the isolated-variable claim is downgraded accordingly. The same -failure this document records elsewhere, committed in its own write-up: **a count treated as a -check.** - -**Consequence for method:** a bisect, or any before/after comparison, across trees of different -module counts measures allocation luck rather than the defect. A green obtained that way is not -evidence of a repair. - -**What this is NOT a claim about.** Not that this change fixed anything -- it plainly did not, -and saying so would be the rung inflation DESIGN §4b names. Nor about the original defect's -status: that was root-caused separately at the declaration (`v2.extdeps.languages.dag`'s -int-literal constructors declared `OccurrenceId` while every caller passed -`SyntheticOccurrence`, repaired with a 17-site caller census), and main is green on that -repair. Two greens, two causes; this branch has the luck, main has the fix. - -## 4k. What the static instrument was actually worth, measured - -Across **seven instrument defects**, four extractor corrections, and a population that moved -298 → 131 → 117 → 114, **the static census never once identified a live module.** It -identified *unknowns*, correctly, and stopped this lane from acting on them. The one true -positive -- `gunbc.pr_digests` -- was found by **one execution**, the required floor; no static -bucket caught it. - -DESIGN §3 as a measurement rather than a doctrine: *in a fail-closed substrate the deletion is -the census*, demonstrated against a static alternative that got four rounds of sharpening and -still could not do the job. - -**The "zero near-misses" reading, stated so a later reader does not relax at it.** Of the 14 -rows that left the deletion set, **none became CONSUMED**. That reads as reassurance and is -not. Thirteen went to AMBIGUOUS -- **unresolved**, neither proven consumed nor unconsumed -- -and **a live module among them would present exactly as they do; the bucket cannot distinguish -it.** The honest statement is *thirteen rows of unknown status that I would have deleted*, and -the near-miss count is **1, found by execution**. - -**The same standard applied to the 55 being deleted.** They are STILL-UNCONSUMED or -island-closed DEAD-CONSUMER-ONLY: zero on three *decoded* surfaces whose declared standing is -`LowerBoundOnly`. The best-evidenced set this instrument can produce. **It is not proof** -- -which is the argument *for* landing it and reading the refusals, not a risk run in spite of -them. - -## 4l. Editing this set means re-running the fixed point, not adjusting the count - -`69 − 13 + 1 = 56` reconciled (a fourth term lands in §4m, making it 55) arithmetically **by -the wrong route**: the 13 left on *evidential* grounds (the corrected extractor moved them to -AMBIGUOUS) and the 14th, `v2.std.rust_leaf_model_claim`, on a *structural* one -- restoring -`v2.test.language_model.rust` re-created its in-population consumer, so deleting it would have -split an island the restore had just re-formed. It is still residue. Two unrelated mechanisms -that happen to sum correctly: coincidence in the totals wearing the appearance of -reconciliation -- a control derived from the measurement it controls, one level up. - -**So: anyone who later adds or removes a row from this set must RE-RUN THE FIXED POINT, not -adjust the count.** Eligibility is a property of the set (§4g), a restore creates a violation -as readily as a deletion, and the failure is silent -- the arithmetic keeps agreeing. - -## 4m. The merge was the third census, and it refused one module - -**`v2.workflow.floor2_prepared_subject` was deleted by this branch and is restored here.** -Not because the measurement was wrong -- it was STILL-UNCONSUMED at base `90986d19469` on every -decoded surface, and still is at that commit. It acquired a consumer *while this branch was -open*: #8896 landed `src/v2/workflow/floor_terminal_ledger.dag`, which imports -`WitnessIdentity` and `witness_identity_qualified_name` from it by name, as does its test. - -**Worth a section for the shape of the failure, which no measurement in this document could -have had.** The two branches touch disjoint files: #8896 adds two files this branch never saw, -this branch deletes one file #8896 never saw. **git merged them clean.** No textual conflict, -no marker, no driver refusal -- and the merged tree does not compile. A three-way textual merge -answers *did the same bytes change twice*; the deciding question is *does the merged tree still -resolve*, which is not about bytes at all. **A merge preserves bytes, not measurements**: every -count in this document is a statement about a tree, and the merge produced a tree none of them -were measured against. - -The correction is the doctrine's own mechanism arriving one stage later than expected. §3 says -the deletion is the census and the refusals are the instrument; here the refusal is a -*merge-time* one, and the instrument that would have reported it is the floor on the merged -tree -- which did not exist until the merge created it. **The re-score against the merged tree -found exactly one**, and the §4l rule was followed rather than the count adjusted: the -restore's own imports were checked for in-population consumers, the fixed point closed at one, -no cascade followed. - -**The near-miss ledger now reads 2, both found by execution and neither by the census.** -`gunbc.pr_digests` (defect 7, the floor refusing on an unread declaration surface) and this one -(the merge refusing on a tree that did not exist when the surface was read). The same finding -from two directions: **a static census is a statement about the tree it read, and both of its -blind spots are trees -- the part it decoded wrongly, and the part not yet written.** The -second is not fixable by improving the instrument. - -**And the arithmetic breaks the way §4l says it does, for a third unrelated reason.** -`69 - 13 + 1 - 1 = 55` now reconciles, and the fourth term shares no mechanism with the other -three: 13 evidential, 1 structural, 1 temporal. Anyone tempted to read that chain as a -derivation should read §4l instead. - - -### 4m.1 The rule this produced was wrong on its dangerous half, and the correction is unilateral - -**First statement of the consequence: "a PR that deletes a module and merges main cleanly is not -verified by that clean merge, so deleters owe a resolve pass."** True, and it told a sibling -lane it was clear when it was not. `bright-ferret-335` checked its subtree on that rule, found -no deletion lanes -- #8877, #8909 and #8882 are purely additive -- and concluded it had nothing -to check. #8909 adds three files that all import `v2.workflow.floor2_prepared_subject`, the -module §4m restores. - -**The hazard is symmetric and the ADDER is the worse side.** A deleter that merges main -discovers an added consumer in its own resolve. An adder that merges main is *already green* -when the deletion lands later and breaks it -- and an additive PR feels safe by construction, -so its author has no reason to ask whether anything it imports is scheduled for deletion in -someone else's open branch. The party in danger is the party without a prompt. - -**The proposed repair was an intersection requiring publication** -- the deleter cannot see -unmerged additions, the adder cannot see unmerged deletions, so one side publishes a list. -**The first clause is false**, falsified by running it: open PR heads are on the remote, -`gh pr list` enumerates them, `git fetch origin ` retrieves them, and the intersection was -run against all three sibling PRs from this side with no list. Zero real exposure: no qualified -import of any of the 55, and of the 664 symbols declared solely by the 55 with no surviving -declarer, one token hit -- `render_footer` in #8909, which declares its own and never depended -on the deleted `tools.readme`. Deleting it *removes* a two-declarer whole-pool ambiguity rather -than creating a break. - -**One result from that intersection has the opposite sign from a normal token hit, and is -worth its own class.** `render_footer` read as a hazard -- a name declared by a module being -deleted and used in an open PR -- and inverted on inspection: the consumer declares its own, -and while `tools.readme` stands the name has TWO whole-pool declarers. It is AMBIGUOUS, and the -deletion *removes* the ambiguity. **So a deletion can improve resolution safety, and the same -intersection that finds breaks also finds these.** A reviewer reading every token hit as danger -misclassifies this direction. - -It matters beyond one row because it is the ambient-pool defect in miniature: a name resolving -by whole-pool uniqueness is **one deletion away from binding differently**, so its meaning is a -function of the corpus denominator rather than of anything written at the declaration or the -use. This deletion happens to move that name from two declarers to one. **Nothing structural -guarantees the next one moves that way** -- the same mechanism can, elsewhere, silently rebind -a use from one surviving declarer to another. That class is neither created nor closed by this -change; it is visible from here because an intersection over deletions is one of the few -operations that looks directly at it. - -**So the obligation is unilateral, which is the point of restating it.** A rule requiring -publication requires coordination, which fails silently when a lane is busy, blocked or -archived; a rule one party can execute alone has no such failure mode. **The deleter owes the -intersection against every open PR head at merge time. The adder owes nothing but pushing.** -That places the duty on the party that knows it is dangerous -- the sibling lane's own argument -for why the adder cannot be trusted to look, one step further: an adder who cannot be expected -to look cannot be expected to publish either. - -The residue is real but strictly smaller than the version needing coordination: this reaches -*pushed* work only. Work in a never-pushed worktree is unreachable by any mechanism, and that -alone is what publication would buy. - -## 5. The 55 deleted - -| module | path | bucket | -| --- | --- | --- | -| `gunbc.assimilate.bmc_wif_canary_bootstrap` | `dag/gunbc/assimilate/bmc_wif_canary_bootstrap.dag` | STILL-UNCONSUMED | -| `gunbc.cursor_sdk_secure_api_key` | `dag/gunbc/cursor_sdk_secure_api_key.dag` | STILL-UNCONSUMED | -| `gunbc.devboot.vertical_receipt` | `dag/gunbc/devboot/vertical_receipt.dag` | STILL-UNCONSUMED | -| `gunbc.floor_resolve_realization` | `dag/gunbc/floor_resolve_realization.dag` | STILL-UNCONSUMED | -| `gunbc.language_subject_scope_scaffold` | `dag/gunbc/language_subject_scope_scaffold.dag` | STILL-UNCONSUMED | -| `gunbc.p3a1_self_fork_homonym_disposition` | `dag/gunbc/p3a1_self_fork_homonym_disposition.dag` | STILL-UNCONSUMED | -| `gunbc.parse_allowlist` | `dag/gunbc/parse_allowlist.dag` | STILL-UNCONSUMED | -| `gunbc.provider_standing_live_probes` | `dag/gunbc/provider_standing_live_probes.dag` | STILL-UNCONSUMED | -| `gunbc.scm.commit_closure_store` | `dag/gunbc/scm/commit_closure_store.dag` | STILL-UNCONSUMED | -| `gunbc.site.register_principles` | `dag/gunbc/site/register_principles.dag` | STILL-UNCONSUMED | -| `gunbc.srv4_seeded_install_media_artifact` | `dag/gunbc/srv4_seeded_install_media_artifact.dag` | STILL-UNCONSUMED | -| `gunbc.tools.bmc_onboard_validate` | `dag/gunbc/tools/bmc_onboard_validate.dag` | STILL-UNCONSUMED | -| `gunbc.tools.grounding_confirm` | `dag/gunbc/tools/grounding_confirm.dag` | STILL-UNCONSUMED | -| `gunbc.tools.roadmap_spawn_request` | `dag/gunbc/tools/roadmap_spawn_request.dag` | STILL-UNCONSUMED | -| `gunbc.witness_family_fanout` | `dag/gunbc/witness_family_fanout.dag` | STILL-UNCONSUMED | -| `shared.dag_util` | `dag/shared/dag_util.dag` | DEAD-CONSUMER-ONLY | -| `std.binding` | `dag/std/binding.dag` | STILL-UNCONSUMED | -| `std.containers` | `dag/std/containers.dag` | STILL-UNCONSUMED | -| `std.list` | `dag/std/list.dag` | STILL-UNCONSUMED | -| `tools.build` | `dag/gunbc/instruments/build.dag` | STILL-UNCONSUMED | -| `tools.codegen` | `dag/gunbc/instruments/codegen.dag` | STILL-UNCONSUMED | -| `tools.readme` | `dag/gunbc/instruments/readme.dag` | STILL-UNCONSUMED | -| `v2.bin.main` | `src/v2/bin/main.dag` | STILL-UNCONSUMED | -| `v2.extdeps.formats.csv` | `src/v2/extdeps/formats/csv.dag` | STILL-UNCONSUMED | -| `v2.extdeps.formats.json_schema` | `src/v2/extdeps/formats/json_schema.dag` | DEAD-CONSUMER-ONLY | -| `v2.extdeps.formats.openapi` | `src/v2/extdeps/formats/openapi.dag` | STILL-UNCONSUMED | -| `v2.extdeps.formats.toml` | `src/v2/extdeps/formats/toml.dag` | STILL-UNCONSUMED | -| `v2.extdeps.formats.yaml` | `src/v2/extdeps/formats/yaml.dag` | DEAD-CONSUMER-ONLY | -| `v2.test.language_model.go_r1` | `src/v2/extdeps/language_model/go_r1.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.go_r2a` | `src/v2/extdeps/language_model/go_r2a.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.go_r2b` | `src/v2/extdeps/language_model/go_r2b.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.go_r3_external` | `src/v2/extdeps/language_model/go_r3_external.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.python_cross_runtime_drift` | `src/v2/extdeps/language_model/python_cross_runtime_drift.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.python_l2_cross_target_parity` | `src/v2/extdeps/language_model/python_l2_cross_target_parity.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.python_r2a` | `src/v2/extdeps/language_model/python_r2a.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.python_r2b` | `src/v2/extdeps/language_model/python_r2b.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.python_r3_external` | `src/v2/extdeps/language_model/python_r3_external.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.rust_r2a` | `src/v2/extdeps/language_model/rust_r2a.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.rust_r2b` | `src/v2/extdeps/language_model/rust_r2b.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.rust_r3_external` | `src/v2/extdeps/language_model/rust_r3_external.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.typescript_r2a` | `src/v2/extdeps/language_model/typescript_r2a.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.typescript_r2b` | `src/v2/extdeps/language_model/typescript_r2b.dag` | STILL-UNCONSUMED | -| `v2.test.language_model.typescript_r3_external` | `src/v2/extdeps/language_model/typescript_r3_external.dag` | STILL-UNCONSUMED | -| `v2.extdeps.typecheckers.mypy` | `src/v2/extdeps/typecheckers/mypy.dag` | STILL-UNCONSUMED | -| `v2.extdeps.typecheckers.pyright` | `src/v2/extdeps/typecheckers/pyright.dag` | STILL-UNCONSUMED | -| `v2.test.algebra_laws.zip_eq_list_equality` | `src/v2/std/algebra_laws/zip_eq_list_equality.dag` | STILL-UNCONSUMED | -| `v2.std.inhabitant_bridge` | `src/v2/std/inhabitant_bridge.dag` | STILL-UNCONSUMED | -| `v2.test.nat_semiring.rung_l1_go_compiler_slice` | `src/v2/std/nat_semiring/rung_l1_go_compiler_slice.dag` | STILL-UNCONSUMED | -| `v2.test.nat_semiring.rung_l1_python_runtime` | `src/v2/std/nat_semiring/rung_l1_python_runtime.dag` | STILL-UNCONSUMED | -| `v2.test.qualified_name.from_node` | `src/v2/std/qualified_name/from_node.dag` | STILL-UNCONSUMED | -| `v2.std.type_expr_projection_row_schema` | `src/v2/std/type_expr_projection_row_schema.dag` | STILL-UNCONSUMED | -| `v2.workflow.ci_stage0_partition_compile_gate_emit` | `src/v2/workflow/ci_stage0_partition_compile_gate_emit.dag` | STILL-UNCONSUMED | -| `v2.workflow.ci_v1_compiler_test_targets_compile_gate_emit` | `src/v2/workflow/ci_v1_compiler_test_targets_compile_gate_emit.dag` | STILL-UNCONSUMED | -| `v2.test.workflow.host_discovered_owned_data_manifest` | `src/v2/workflow/host_discovered_owned_data_manifest.dag` | STILL-UNCONSUMED | -| `v2.workflow.probe_selector_host_health` | `src/v2/workflow/probe_selector_host_health.dag` | STILL-UNCONSUMED | - -## 6. The 58 held, with the reason each survived - -**Held is not keep.** Every row carries a typed reason and an owed next step; none is -dispositioned as "consumed". The census's instruction is that a refusal is data, and 58 -refusals over a 114-row residue is the measurement this lane produces. - -Separately, **97 rows sit in AMBIGUOUS-SHARED-ONLY**, outside the residue entirely: not -provably unconsumed, not proven consumed, each needing a per-row read. 13 arrived there from -this lane's own re-derivation (§4d). - -### Residue, but named somewhere this cut would strand (35) - -Named by a live `.dag`, `.rs` or data file, or otherwise carrying an obligation the deletion precondition refuses to guess at. Weak evidence both ways: several are receipt prose and ordinary residue; at least one (`v2.workflow.product_receipt_stage`) is a live transport named via an `--entry` path held in a *variable*, the dynamically-composed-argv blind spot the census declares. - -| module | path | bucket | -| --- | --- | --- | -| `direct_rust_door_ingest_fixture` | `src/v2/compiler/self_host/direct_rust_door_ingest_fixture.dag` | STILL-UNCONSUMED | -| `extdeps.bmc.mock_corpus` | `dag/extdeps/bmc/mock_corpus.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.ebay.mock_corpus` | `dag/extdeps/ebay/mock_corpus.dag` | STILL-UNCONSUMED | -| `extdeps.linux.mock_corpus` | `dag/extdeps/linux/mock_corpus.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.tcgplayer.mock_corpus` | `dag/extdeps/tcgplayer/mock_corpus.dag` | STILL-UNCONSUMED | -| `gunbc.auth.optional_impersonation` | `dag/gunbc/auth/optional_impersonation.dag` | STILL-UNCONSUMED | -| `gunbc.auth.patterns` | `dag/gunbc/auth/patterns.dag` | STILL-UNCONSUMED | -| `gunbc.char_at_scaling_probe_support` | `dag/gunbc/char_at_scaling_probe_support.dag` | STILL-UNCONSUMED | -| `gunbc.ci_build_job_v1_compiler_unit_receipt` | `dag/gunbc/ci/ci_build_job_v1_compiler_unit_receipt.dag` | STILL-UNCONSUMED | -| `gunbc.generic_binder_field_projection_deficit` | `dag/gunbc/generic_binder_field_projection_deficit.dag` | STILL-UNCONSUMED | -| `gunbc.githooks_pre_push_cli` | `dag/gunbc/githooks/githooks_pre_push_cli.dag` | STILL-UNCONSUMED | -| `gunbc.namespace_census_receipt` | `dag/gunbc/namespace/namespace_census_receipt.dag` | STILL-UNCONSUMED | -| `gunbc.p1_retention_cohort_receipt` | `dag/gunbc/p1_retention_cohort_receipt.dag` | STILL-UNCONSUMED | -| `gunbc.plans.affected_set_self_confirmation` | `dag/gunbc/plans/affected_set_self_confirmation.dag` | STILL-UNCONSUMED | -| `gunbc.plans.fleet_subsumption_manual_gaps` | `dag/gunbc/plans/fleet_subsumption_manual_gaps.dag` | STILL-UNCONSUMED | -| `gunbc.plans.host_convergence_circuit_residue` | `dag/gunbc/plans/host_convergence_circuit_residue.dag` | STILL-UNCONSUMED | -| `gunbc.plans.transport_argv_anemia_dissolution` | `dag/gunbc/plans/transport_argv_anemia_dissolution.dag` | STILL-UNCONSUMED | -| `gunbc.seed_closed_vocabulary_wildcard_census` | `dag/gunbc/seed_closed_vocabulary_wildcard_census.dag` | STILL-UNCONSUMED | -| `gunbc.site.interaction` | `dag/gunbc/site/interaction.dag` | STILL-UNCONSUMED | -| `gunbc.test_node_wall_clock_ratchet` | `dag/gunbc/test_node_wall_clock_ratchet.dag` | STILL-UNCONSUMED | -| `gunbc.v1_maintenance_standing` | `dag/gunbc/v1/v1_maintenance_standing.dag` | STILL-UNCONSUMED | -| `std.exec_format` | `dag/std/exec_format.dag` | DEAD-CONSUMER-ONLY | -| `std.import` | `dag/std/import.dag` | STILL-UNCONSUMED | -| `std.methods` | `dag/std/methods.dag` | STILL-UNCONSUMED | -| `std.verification` | `dag/std/verification.dag` | STILL-UNCONSUMED | -| `v2.std.datetime` | `src/v2/std/datetime.dag` | DEAD-CONSUMER-ONLY | -| `v2.std.probe_selector` | `src/v2/std/probe_selector.dag` | DEAD-CONSUMER-ONLY | -| `v2.std.rust_leaf_model_claim` | `src/v2/std/rust_leaf_model_claim.dag` | DEAD-CONSUMER-ONLY | -| `v2.test.workflow.glob_discovery_law` | `src/v2/workflow/glob_discovery_law.dag` | STILL-UNCONSUMED | -| `v2.workflow.class_b_import_closure_transport` | `src/v2/workflow/class_b_import_closure_transport.dag` | DEAD-CONSUMER-ONLY | -| `v2.workflow.compiler_closure_ingest_transport` | `src/v2/workflow/compiler_closure_ingest_transport.dag` | DEAD-CONSUMER-ONLY | -| `v2.workflow.phase_profile_proof_plan` | `src/v2/workflow/phase_profile_proof_plan.dag` | STILL-UNCONSUMED | -| `v2.workflow.product_receipt_stage` | `src/v2/workflow/product_receipt_stage.dag` | STILL-UNCONSUMED | -| `v2.workflow.source_root_ingest_gate` | `src/v2/workflow/source_root_ingest_gate.dag` | STILL-UNCONSUMED | -| `v2.workflow.source_root_ingest_transport` | `src/v2/workflow/source_root_ingest_transport.dag` | DEAD-CONSUMER-ONLY | - -### Declares an `ExternalAuthority` anchor (21) - -DESIGN §3's extdeps citation duty: the value may be the citation, not a call, so unreachability is not evidence of residue. Mostly `DEAD-CONSUMER-ONLY` — the island shape — so they delete as connected components or not at all (census B6+, one island per PR, each gated on whether the citation is the deliverable). - -| module | path | bucket | -| --- | --- | --- | -| `extdeps.boot.emit` | `dag/extdeps/boot/emit.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.boot.framebuffer` | `dag/extdeps/boot/framebuffer.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.boot.freestanding_payload` | `dag/extdeps/boot/freestanding_payload.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.boot.linux_x86_boot` | `dag/extdeps/boot/linux_x86_boot.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.cloud.gcp.sts` | `dag/extdeps/cloud/gcp/sts.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.colo.types` | `dag/extdeps/colo/types.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.container.oci.image_config` | `dag/extdeps/container/oci/image_config.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.container.oci.linux` | `dag/extdeps/container/oci/linux.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.container.oci.manifest` | `dag/extdeps/container/oci/manifest.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.container.oci.runtime_config` | `dag/extdeps/container/oci/runtime_config.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.ebay.ebay_contracts` | `dag/extdeps/ebay/ebay_contracts.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.formats.elf.encode` | `dag/extdeps/formats/elf/encode.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.formats.elf.hello_static_witness` | `dag/extdeps/formats/elf/hello_static_witness.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.formats.elf.primitives` | `dag/extdeps/formats/elf/primitives.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.formats.elf.segments` | `dag/extdeps/formats/elf/segments.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.formats.elf.types` | `dag/extdeps/formats/elf/types.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.github.mergeable_state_contracts` | `dag/extdeps/github/mergeable_state_contracts.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.posix.rusage` | `dag/extdeps/posix/rusage.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.runtime.local` | `dag/extdeps/runtime/local.dag` | DEAD-CONSUMER-ONLY | -| `extdeps.tcgplayer.tcgplayer` | `dag/extdeps/tcgplayer/tcgplayer.dag` | DEAD-CONSUMER-ONLY | -| `gunbc.hand_lens_host_bridge_scaffold_index` | `dag/gunbc/hand_lens_host_bridge_scaffold_index.dag` | DEAD-CONSUMER-ONLY | - -### Frozen against a named re-add (2) - -DESIGN §3 frozen-X. Deleting these deletes what the re-add queue exists to re-attach. - -| module | path | bucket | -| --- | --- | --- | -| `tools.dag_compile_clean_seam_transport` | `dag/gunbc/instruments/dag_compile_clean_seam_transport.dag` | DEAD-CONSUMER-ONLY | -| `tools.merge_admission_capture_transport` | `dag/gunbc/instruments/merge_admission_capture_transport.dag` | STILL-UNCONSUMED | - -## 7. What this change does NOT claim - -- **Not that 55 is the maximum safe cut.** It is the subset provable clean on every decoded - surface, islands closed. The 35 rows held for a per-row read are held on a *string mention*, - weak evidence in both directions. -- **Not that the residue is now 58.** Five modules moved DEAD-CONSUMER-ONLY to - STILL-UNCONSUMED as their island neighbours went. **The residue is a fixed point reached by - iteration, not a set reached in one pass**, and a follow-on pass is owed. -- **Not that the AMBIGUOUS bucket is cleared.** 97 rows are unresolvable at identity grain. - 13 got there from this lane's own correction; reading that as "13 live modules found" would - be exactly wrong: they are *not proven consumed either*. -- **Not that the extractor is now correct.** Four defects were found in it, three by someone - else hand-reading rows while every counter stayed consistent. The honest claim is that it - passes four discriminators it previously failed one of, not that a fifth defect does not - exist. -- **Not that the directive's second arm was served.** Every row was deleted or held; **none - was wired up.** The candidates are the 21 `CITED-AUTHORITY` islands and the 22 modules the - census found named `v2.test.*` while declaring no test and sitting outside every test path - -- a name asserting enrolment that floor discovery, which keys on the file suffix, never - grants. - -## 8. Evidence - -The deletion's discriminating check is not "the tree still parses" -- 71 modules nothing -imports would parse-clean whether or not they were load-bearing. It is the pair of counters in -section 2 that must not move (`reachable`, `CONSUMED-DECISIVE`) beside the one that must -(`population`, by exactly the number of files removed), plus the required run: - -`claim_executor --required-ci --source-root dag --source-root src/v2` -- the three-phase -required mode (src/v1 `.dag` parse sweep, `--required-regen`, witness floor). Its result on -this branch is reported on the PR. - -**The instrument is not committed, by repository rule:** `.gitignore` excludes `*.py` -tree-wide, so the audit script that produced these numbers has no home here. It is fully -specified instead -- the census's own section-2 method without deviation; the two arms that -matter, exactly: - -- **Roots** are the discovery paths (`/test/`, `*_test.dag`, `/lens/`, `/manual/`, - `/fixture*`), every `--entry` argv literal and `*entry*:` path field found in any `.dag`, - `.rs`, `.md`, `.yml`, `.sh` or `.toml` file, and every module whose name-with-underscores - matches a `src/v1/stage0/src/*.rs` seed mirror. Edges are `import` lines plus - fully-qualified `module.symbol` references resolved by longest-prefix against the module - index. The population is what no root reaches. -- **The defect-6 re-score** takes each population member's declared symbols -- not its name, - not its path -- keeps only those declared by exactly one module corpus-wide, and asks - whether any other `.dag` file names one bare. Comments and string literals are removed - with a character scanner, never a regex, because a regex terminates early on the `\{` - interpolation escapes real `.dag` prose contains. An identifier preceded by `.` or - followed by `:` is not a reference. - -That specification makes the numbers disagreeable: re-implementing it is a half-page, and any -reader who does so and gets a different answer has found a defect in one of us. Beside it, the -two counters in section 2 are checkable without the instrument -- `reachable` and -`CONSUMED-DECISIVE` not moving is a claim about the tree, not about the -script. \ No newline at end of file diff --git a/docs/plans/uses-occurrence-census.md b/docs/plans/uses-occurrence-census.md deleted file mode 100644 index c9b81926ee6..00000000000 --- a/docs/plans/uses-occurrence-census.md +++ /dev/null @@ -1,364 +0,0 @@ -# USES-0 census: authored `uses` joined to body-derived demand - -Authority: operator D13 (2026-09-18), recorded in `gunbc.plans.demand_engine_program`. This analysis is the identity-grain join. It does not delete or add any `uses` row. - -## Method - -Two independently produced populations: - -1. **Authored `uses` rows** — every parsed function/pattern header clause `uses alias: Type` in committed `.dag` source (not AST field `uses:`, not GHA `uses:`, not comments). Identity is `(module file, declaration, alias, type)`. -2. **Body-derived demand (session approximation of DependencyDemand)** — capitalized dotted operation references in the visible body that inhabit the authored resource, plus `file_compare_and_set` (Filesystem) and `clock_now_probed_at` (Clock); plus calls to other declarations that themselves author a `uses` of the same normalized resource (`Network` = `std.resources.Network`). - -A dotted call is **not** Network demand merely because it is a host operation. `Filesystem.*` is Filesystem. `shell.Mktemp`, `shell.Remove`, `shell.Move`, `shell.Env`, `os.Hostname`, and `git.Inspect` / `git.Core` are local and do not establish Network. Classifying `read_docker_hub_image_config_at_digest` as DirectRestatement from `shell.Mktemp` was that error (review 67467): the Network work is `docker_hub_bearer_token` → `http.Client.GetBounded` with no `uses` on those helpers, which is Unresolved. - -This is **not** the compiler `DependencyDemand` carrier. Whole-corpus resolve that would produce that carrier is CI-sized (the seed cannot hold the v2 closure under a session cap). Rows the source join cannot decide are `Unresolved` — that class is the CI-side remainder, not a count to close by hand. No cardinality is an oracle; membership is the table. A helper call that is not itself a `uses` declaration, and is not a known pure combinator, is insufficient call relation: class Unresolved, even if the body also shows a *different* resource. - -## Class dispositions (D13) - -| Class | When | Disposition | -| --- | --- | --- | -| DirectRestatement | body directly references a resolved operation of this resource | delete/derive | -| TransitiveRestatement | body only calls another declaration that requires it | delete/derive over call relations | -| NamedDependencyBinder | a distinct logical provider identity actually used in the body | move to dependency-subject or capability param | -| OpaqueContract | no body; requirement not derivable | keep as contract | -| PolicyEnvelope | constrains what may be used, not actual use | move to policy | -| UnusedRequirement | no body path consumes this authored identity | delete/refuse | -| Unresolved | identity/call relations insufficient (typically a helper without `uses` that still performs host work) | typed refusal until resolution names the relation | - -## Empty classes in the live parsed population - -- **OpaqueContract** — every live `uses` clause is followed by a visible body. The interesting inverse is `gunbc.clock_read` `clock_now_probed_at`, which calls `Clock.Now()` with **no** authored `uses`. -- **PolicyEnvelope** — no header constrains a derived set (no may-not-use / upper-bound form exists in source). -- **NamedDependencyBinder** — every live alias is the generic interface (`net: Network`, `fs: Filesystem`, `clock: Clock`). No row distinguishes `source_tree` from `output_tree`. Fixture strings that bind `net: EaNet` / `GewNet` / `ZpeNet` and read `net.id` are the binder-shaped specimens, listed separately. - -Likely outcome per the ruling: the function-level `uses` clause disappears for transparent bodies; what would survive is opaque/subject/policy, of which the live corpus currently has none. - -## DirectRestatement - -| File | Declaration | Alias | Authored type | Evidence | -| --- | --- | --- | --- | --- | -| `dag/gunbc/assimilate/bmc_bootstrap_provision.dag` | `bmc_bootstrap_provision_srv3` | `net` | `std.resources.Network` | ops shell.GCloud.AuthPrintAccessToken, gcp.ServiceUsage.BatchEnableServices, gcp.IamAdmin.CreateServiceAccount, gcp.SecretManager.GetSecretIamPolicy, gcp.SecretManager.SetSecretIamPolicy | -| `dag/gunbc/auth/access_token_source.dag` | `ensure_access_token` | `net` | `Network` | ops shell.GCloud.AuthPrintAccessToken; local-ops shell.Env.Get | -| `dag/gunbc/auth/credentials.dag` | `gcp_secret_credential` | `net` | `std.resources.Network` | ops shell.GCloud.AuthPrintAccessToken, gcp.SecretManager.AccessVersion | -| `dag/gunbc/auth/credentials.dag` | `gcp_adc_token_from_credentials` | `net` | `std.resources.Network` | ops oauth2.Google.Refresh | -| `dag/gunbc/auth/gcp_secret_access.dag` | `secret_access_ensure_for` | `net` | `Network` | ops gcp.SecretManager.GetSecretIamPolicy, gcp.SecretManager.SetSecretIamPolicy; callees ensure_access_token | -| `dag/gunbc/auth/patterns.dag` | `credential_chain` | `net` | `std.resources.Network` | ops gcp.STS.Exchange, gcp.SecretManager.AccessVersion | -| `dag/gunbc/auth/patterns.dag` | `github_oidc` | `net` | `std.resources.Network` | ops github.OIDC.GetToken; local-ops shell.Env.Get, shell.Env.Get | -| `dag/gunbc/auth/patterns.dag` | `metadata_oidc` | `net` | `std.resources.Network` | ops gcp.Metadata.GetIdentityToken | -| `dag/gunbc/auth/secret_ref_credential.dag` | `fetch_secret_ref_credential_with_token` | `net` | `Network` | ops gcp.SecretManager.AccessVersion | -| `dag/gunbc/command_runner.dag` | `run_shell_command` | `net` | `Network` | ops shell.Exec.RunArgv | -| `dag/gunbc/command_runner.dag` | `run_shell_command_observe` | `net` | `Network` | ops shell.Exec.RunArgv | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_is_reachable` | `net` | `std.resources.Network` | ops ssh.Session.Reachability | -| `dag/gunbc/machine_intake/megarac_media_attach.dag` | `megarac_attach_remote_image` | `net` | `std.resources.Network` | ops megarac.Media.OpenSession; callees megarac_attach_then_release, megarac_release_unreadable_session | -| `dag/gunbc/machine_intake/megarac_media_attach.dag` | `megarac_release_unreadable_session` | `net` | `std.resources.Network` | ops megarac.Media.CloseSession | -| `dag/gunbc/machine_intake/megarac_media_attach.dag` | `megarac_attach_then_release` | `net` | `std.resources.Network` | ops megarac.Media.CloseSession; callees megarac_attach_with_token | -| `dag/gunbc/machine_intake/megarac_media_attach.dag` | `megarac_attach_with_token` | `net` | `std.resources.Network` | ops megarac.Media.GetRemoteConfigurations, megarac.Media.GetRemoteImages; callees megarac_start_and_confirm | -| `dag/gunbc/machine_intake/megarac_media_attach.dag` | `megarac_start_and_confirm` | `net` | `std.resources.Network` | ops megarac.Media.StartMedia, megarac.Media.GetRemoteConfigurations | -| `dag/gunbc/machine_intake/oob_boot_handoff.dag` | `oob_power_cycle_after_confirmed_selection` | `net` | `std.resources.Network` | ops diagnostic.ipmi.Tool.ChassisPowerControl | -| `dag/gunbc/machine_intake/oob_boot_handoff.dag` | `oob_boot_handoff` | `net` | `std.resources.Network` | ops diagnostic.ipmi.Tool.ChassisBootDev, diagnostic.ipmi.Tool.ChassisBootParamGet; callees oob_power_cycle_after_confirmed_selection | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `rlm_predecessor_run` | `net` | `Network` | ops github.WorkflowRuns.GetRun | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd_gated` | `clock` | `std.resources.Clock` | ops clock_now_probed_at | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_claim_boot_once_cd_authorization` | `fs` | `std.resources.Filesystem` | ops file_compare_and_set | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_settle_boot_once_cd_claim` | `fs` | `std.resources.Filesystem` | ops file_compare_and_set | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd_resolved` | `net` | `std.resources.Network` | ops Filesystem.Write, redfish.Http.SetBootSourceOverride, Filesystem.Write, redfish.Http.ResetSystem; callees materialize_bmc_login_password; other-demand Filesystem | -| `dag/gunbc/tools/bmc_first_contact.dag` | `bmc_first_contact` | `net` | `std.resources.Network` | ops redfish.Http.GetSystem; local-ops shell.Env.Get | -| `dag/gunbc/tools/bmc_onboard.dag` | `bmc_rotate_credential` | `net` | `Network` | ops Filesystem.Write, redfish.Http.SetAccountPassword, redfish.Http.GetSystem; other-demand Filesystem | -| `dag/gunbc/tools/bmc_onboard.dag` | `bmc_store_and_rotate` | `net` | `Network` | ops gcp.SecretManager.AddVersion, gcp.SecretManager.AccessVersion; callees ensure_access_token, bmc_rotate_credential | -| `dag/gunbc/tools/bmc_onboard.dag` | `bmc_assimilate_credential` | `net` | `Network` | ops redfish.Http.GetServiceRoot, redfish.Http.GetSystem; callees bmc_store_and_rotate | -| `dag/gunbc/tools/bmc_onboard.dag` | `bmc_probe_credential_phase` | `net` | `Network` | ops redfish.Http.GetSystem, gcp.SecretManager.AccessVersion, redfish.Http.GetSystem; callees ensure_access_token | -| `dag/gunbc/tools/bmc_onboard.dag` | `bmc_converge_credential` | `net` | `Network` | ops redfish.Http.GetServiceRoot; callees bmc_store_and_rotate, bmc_probe_credential_phase | -| `dag/gunbc/tools/bmc_read_telemetry.dag` | `bmc_read_telemetry` | `net` | `std.resources.Network` | ops redfish.Http.GetSystem, redfish.Http.GetChassisSensors; local-ops shell.Env.Get, shell.Env.Get | -| `dag/test/ctl/pos_emit.dag` | `emits_a_service_call` | `net` | `std.resources.Network` | ops diagnostic.ipmi.Tool.SensorList | -| `dag/test/ctl/pos_emit2.dag` | `emits_a_two_segment_call` | `net` | `std.resources.Network` | ops cargo.Build.Run | -| `dag/test/ctl/pos_service.dag` | `call_three_segment` | `net` | `std.resources.Network` | ops diagnostic.ipmi.Tool.SensorList | - -## TransitiveRestatement - -| File | Declaration | Alias | Authored type | Evidence | -| --- | --- | --- | --- | --- | -| `dag/gunbc/auth/credentials.dag` | `gcp_oauth_access_token_via_adc_refresh` | `net` | `std.resources.Network` | ops Filesystem.Read; callees gcp_adc_token_from_credentials; other-demand Filesystem | -| `dag/gunbc/auth/credentials.dag` | `gcp_oauth_access_token_adc_for_path` | `net` | `std.resources.Network` | callees gcp_oauth_access_token_via_adc_refresh | -| `dag/gunbc/auth/secret_access_admission.dag` | `secret_access_ensure_admitted` | `net` | `Network` | callees secret_access_ensure_for | -| `dag/gunbc/auth/secret_ref_credential.dag` | `fetch_secret_ref_credential` | `net` | `Network` | callees fetch_secret_ref_credential_with_token, ensure_access_token | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `bmc_fan_run` | `net` | `Network` | callees fetch_secret_ref_credential | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `srv3_bmc_fan_observe` | `net` | `Network` | callees bmc_fan_run | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `srv3_bmc_fan_converge` | `net` | `Network` | callees bmc_fan_run | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `srv4_bmc_fan_observe` | `net` | `Network` | callees bmc_fan_run | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `srv4_bmc_fan_converge` | `net` | `Network` | callees bmc_fan_run | -| `dag/gunbc/bmc/bmc_fan_converge.dag` | `srv4_bmc_fan_rollback_previous` | `net` | `Network` | callees bmc_fan_run | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `bmc_netboot_run_bmc` | `net` | `Network` | callees run_shell_command | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `bmc_netboot_put_path` | `net` | `Network` | callees run_shell_command | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `bmc_netboot_stage_file` | `net` | `Network` | callees bmc_netboot_stage_inline, bmc_netboot_put_path | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `bmc_netboot_stage_inline` | `net` | `Network` | ops Filesystem.Write; callees bmc_netboot_put_path; other-demand Filesystem | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `bmc_netboot_provision` | `net` | `Network` | callees bmc_netboot_run_bmc, bmc_netboot_stage_file | -| `dag/gunbc/bmc/bmc_netboot_serve.dag` | `srv4_bmc_netboot_provision` | `net` | `Network` | callees bmc_netboot_provision | -| `dag/gunbc/busybox_bmc_build.dag` | `busybox_bmc_build_run` | `net` | `Network` | callees run_shell_commands | -| `dag/gunbc/command_runner.dag` | `run_shell_command_capture` | `net` | `Network` | callees run_shell_command_observe | -| `dag/gunbc/command_runner.dag` | `run_shell_commands` | `net` | `Network` | callees run_shell_command | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_effective_user_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_host_short_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_timer_members_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_fleet_converge_request_wet` | `net` | `Network` | callees observe_runner_unit_standings_wet, observe_slot_retirement_evidence_wet, observe_runner_slot_members_with_provenance_wet, observe_timer_members_wet, observe_cap_members_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_slot_retirement_evidence_wet` | `net` | `Network` | callees observe_transition_retirement_evidence | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_runner_unit_standings_wet` | `net` | `Network` | callees observe_runner_unit_standing_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `write_fleet_converge_plan_artifact_wet` | `net` | `Network` | ops Filesystem.Write, Filesystem.Write, Filesystem.Write, Filesystem.Write, Filesystem.Write; callees run_shell_commands; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_allocation_store_plan_wet` | `net` | `Network` | callees write_fleet_converge_plan_artifact_wet, observe_generation_store_wet, observe_host_short_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_plan_wet` | `net` | `Network` | callees fleet_converge_scoped_plan_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_launch_environment_plan_wet` | `net` | `Network` | callees fleet_converge_scoped_plan_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_scoped_plan_wet` | `net` | `Network` | callees fleet_converge_plan_scoped_bound_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_plan_scoped_bound_wet` | `net` | `Network` | ops Filesystem.Write, Filesystem.Write, Filesystem.Write, Filesystem.Write, Filesystem.Write; callees run_shell_commands, observe_generation_store_wet, observe_host_short_wet, observe_fleet_converge_request_wet; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_apply_wet` | `net` | `Network` | callees fleet_converge_apply_bound_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_apply_bound_wet` | `net` | `Network` | ops Filesystem.Read, Filesystem.Read, Filesystem.Read, Filesystem.Read, Filesystem.Read; callees run_shell_commands, observe_effective_user_wet, observe_host_short_wet, observe_fleet_converge_request_wet; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_local_plan_wet` | `net` | `Network` | callees observe_host_short_wet, observe_operator_local_run_binding, fleet_converge_plan_scoped_bound_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_operator_cli_entry` | `net` | `Network` | ops Filesystem.Read, Filesystem.Write; callees fleet_converge_operator_cli_dispatch, observe_operator_local_run_binding; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_operator_cli_dispatch` | `net` | `Network` | local-ops git.Core.CurrentBranch; callees fleet_converge_local_apply_wet, fleet_converge_local_plan_wet, observe_host_short_wet | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `fleet_converge_local_apply_wet` | `net` | `Network` | ops Filesystem.Read; callees observe_operator_local_run_binding, fleet_converge_apply_bound_wet; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_runner_slot_provenance_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_runner_slot_members_with_provenance_wet` | `net` | `Network` | callees observe_runner_slot_provenance_wet, observe_runner_slot_members_wet | -| `dag/gunbc/fleet/fleet_host_key_enrollment.dag` | `verify_enrollment_outcome` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_host_key_enrollment.dag` | `fleet_host_key_enroll_apply` | `net` | `Network` | ops Filesystem.Write; callees run_shell_commands, run_shell_command_capture; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_host_key_enrollment.dag` | `fleet_host_key_enroll_wet` | `net` | `Network` | ops Filesystem.Write; callees fleet_host_key_enroll_apply; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_host_key_enrollment.dag` | `fleet_authorized_key_self_enroll_wet` | `net` | `Network` | ops Filesystem.Write, Filesystem.Write; callees verify_enrollment_outcome, run_shell_command_capture; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `probe_agent_credential_verification` | `net` | `Network` | callees verify_fleet_ssh_agent_credential, materialize_agent_public_selector | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `fleet_multi_principal_probe_with_attempt` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor, run_with_materialized_fleet_ssh_binding; other-demand Clock,Filesystem | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `spark_exact_grant_probe_wet` | `net` | `Network` | ops Filesystem.Write; callees materialize_fleet_known_hosts_anchor, run_with_materialized_fleet_ssh_binding; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `spark_managed_access_standing_for_attempt` | `net` | `Network` | callees spark_managed_access_standing_with_verified_credential, probe_agent_credential_verification | -| `dag/gunbc/fleet/fleet_probe_identity_observe.dag` | `fleet_probe_identity_observe_wet` | `net` | `Network` | ops Filesystem.Write; callees run_shell_command_capture; other-demand Filesystem | -| `dag/gunbc/fleet/fleet_ssh_credential_verify.dag` | `derive_identity_file_public_key_line` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_ssh_credential_verify.dag` | `verify_fleet_ssh_credential` | `net` | `Network` | callees derive_identity_file_public_key_line | -| `dag/gunbc/fleet/fleet_ssh_credential_verify.dag` | `list_agent_identities` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/fleet/fleet_ssh_credential_verify.dag` | `verify_fleet_ssh_agent_credential` | `net` | `Network` | callees list_agent_identities | -| `dag/gunbc/fleet/fleet_ssh_locus.dag` | `prepare_fleet_ssh_agent_context` | `net` | `Network` | callees fleet_ssh_context_from_verified_binding, verify_fleet_ssh_agent_credential, materialize_agent_public_selector | -| `dag/gunbc/fleet/fleet_ssh_locus.dag` | `fleet_ssh_context_from_verified_binding` | `net` | `Network` | callees materialize_fleet_known_hosts_anchor | -| `dag/gunbc/fleet/org_actions_converge.dag` | `org_admin_app_key_access_converge_with_supplied_token` | `net` | `Network` | callees secret_access_ensure_for | -| `dag/gunbc/fleet/printer_job_start.dag` | `acquire_pinned_ca_bundle` | `net` | `Network` | local-ops shell.Mktemp.Dir; callees run_shell_command_capture | -| `dag/gunbc/fleet/printer_job_start.dag` | `start_print_on_printer` | `net` | `Network` | callees publish_start_with_credential, fetch_secret_ref_credential | -| `dag/gunbc/fleet/printer_job_start.dag` | `publish_start_with_credential` | `net` | `Network` | local-ops shell.Remove.RecursiveForce, shell.Remove.RecursiveForce, shell.Remove.RecursiveForce; callees acquire_pinned_ca_bundle, run_shell_command_capture | -| `dag/gunbc/fleet/printer_project_delivery.dag` | `deliver_project_to_printer` | `net` | `Network` | local-ops shell.Remove.FileForce, shell.Remove.FileForce; callees fetch_secret_ref_credential, run_shell_command_capture | -| `dag/gunbc/host/host_build_cache_provision.dag` | `provision_build_cache` | `net` | `Network` | callees host_toolchain_ensure | -| `dag/gunbc/host/host_codex_runtime_provision.dag` | `provision_codex_runtime` | `net` | `Network` | callees host_toolchain_ensure | -| `dag/gunbc/host/host_compile_pool_provision.dag` | `provision_compile_pool` | `net` | `Network` | callees host_toolchain_ensure | -| `dag/gunbc/host/host_reset_return_run.dag` | `watch_step` | `net` | `std.resources.Network` | callees host_is_reachable | -| `dag/gunbc/host/host_reset_return_run.dag` | `watch_for` | `net` | `std.resources.Network` | callees watch_step | -| `dag/gunbc/host/host_reset_return_run.dag` | `observe_reset_return` | `net` | `std.resources.Network` | callees watch_for | -| `dag/gunbc/host/host_reset_return_run.dag` | `subject_baseline` | `net` | `std.resources.Network` | callees host_is_reachable | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_attempt` | `net` | `std.resources.Network` | callees host_reset_return_issue_and_watch, observer_self_corroboration, subject_baseline | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_issue_and_watch` | `net` | `std.resources.Network` | callees oob_boot_handoff, observe_reset_return | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_wet` | `net` | `std.resources.Network` | local-ops shell.Env.Get, shell.Env.Get; callees host_reset_return_exit, host_reset_return_attempt, host_reset_return_preflight_exit, host_reset_return_converge_route_read | -| `dag/gunbc/instruments/pair_incumbent_identity_standing.dag` | `group_identity_standing` | `net` | `Network` | callees prepare_fleet_ssh_agent_context | -| `dag/gunbc/instruments/pair_incumbent_identity_standing.dag` | `pair_incumbent_identity_standing` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees group_identity_standing; other-demand Clock,Filesystem | -| `dag/gunbc/live_deploy/deployed_tree_report.dag` | `deployed_tree_report_ci_wet` | `net` | `Network` | callees deployed_tree_report_for_context, prepare_fleet_ssh_agent_context | -| `dag/gunbc/machine_intake/boot_image_fetch.dag` | `fetch_verified_boot_image` | `net` | `std.resources.Network` | callees run_shell_command, verify_and_publish_boot_image | -| `dag/gunbc/machine_intake/boot_image_fetch.dag` | `verify_and_publish_boot_image` | `net` | `std.resources.Network` | callees admit_then_publish_boot_image | -| `dag/gunbc/machine_intake/boot_image_fetch.dag` | `admit_then_publish_boot_image` | `net` | `std.resources.Network` | callees publish_verified_boot_image | -| `dag/gunbc/machine_intake/boot_image_fetch.dag` | `publish_verified_boot_image` | `net` | `std.resources.Network` | local-ops shell.Move.File; callees confirm_published_boot_image | -| `dag/gunbc/machine_intake/mtcollins1_actuate.dag` | `mtcollins1_drive_handoff` | `net` | `std.resources.Network` | callees oob_boot_handoff | -| `dag/gunbc/machine_intake/mtcollins1_actuate.dag` | `mtcollins1_netboot_handoff` | `net` | `std.resources.Network` | callees mtcollins1_drive_handoff | -| `dag/gunbc/machine_intake/mtcollins1_actuate.dag` | `mtcollins1_cdrom_handoff` | `net` | `std.resources.Network` | callees mtcollins1_drive_handoff | -| `dag/gunbc/machine_intake/mtcollins1_actuate.dag` | `mtcollins1_denied_admission_control` | `net` | `std.resources.Network` | callees mtcollins1_drive_handoff | -| `dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag` | `mtcollins1_fetch_boot_image` | `net` | `std.resources.Network` | callees fetch_verified_boot_image | -| `dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag` | `mtcollins1_fetch_boot_image_wrong_pin_control` | `net` | `std.resources.Network` | callees fetch_verified_boot_image | -| `dag/gunbc/machine_intake/mtcollins1_boot_image_fetch.dag` | `mtcollins1_fetch_boot_image_unowned_export_control` | `net` | `std.resources.Network` | callees fetch_verified_boot_image | -| `dag/gunbc/machine_intake/mtcollins1_media_attach.dag` | `mtcollins1_attach_absent_image_control` | `net` | `std.resources.Network` | callees megarac_attach_remote_image | -| `dag/gunbc/machine_intake/mtcollins1_media_attach.dag` | `mtcollins1_attach_diskless_image` | `net` | `std.resources.Network` | callees megarac_attach_remote_image | -| `dag/gunbc/product/printed_chassis/slicing_toolchain.dag` | `attempt_orca_execution` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `persist_receipt` | `net` | `Network` | ops Filesystem.Write, Filesystem.Write, Filesystem.WriteOwnerOnly; callees run_shell_commands; other-demand Filesystem | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `rlm_launch_deployment_receipt_wet` | `net` | `Network` | callees rlm_launch_deployment_receipt_bound, prepare_fleet_ssh_agent_context | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `rlm_run_provenance_fact` | `net` | `Network` | callees rlm_predecessor_run | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `rlm_launch_deployment_receipt_bound` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at_or_unknown; callees window_snapshot, observe_host_short_wet, rlm_run_provenance_fact, persist_receipt, unit_standing_fact; other-demand Clock,Filesystem | -| `dag/gunbc/runner/runner_guest_image.dag` | `runner_guest_image_observe_wet` | `net` | `Network` | callees run_shell_commands | -| `dag/gunbc/runner/runner_guest_image.dag` | `runner_guest_image_converge_wet` | `net` | `Network` | callees run_shell_commands | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `observe_runner_host_file` | `net` | `Network` | callees observe_runner_host_file_content, observe_runner_host_paths | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `apply_runner_host_file` | `net` | `Network` | callees observe_runner_host_paths | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `plan_runner_host_file` | `net` | `Network` | callees observe_runner_host_file | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `runner_host_manager_reload_step` | `net` | `Network` | callees reload_runner_host_manager | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `run_runner_host_files` | `net` | `Network` | callees apply_runner_host_file, plan_runner_host_file, observe_runner_slot_population, runner_host_manager_reload_step, observe_runner_host_administrator_privilege | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `runner_host_file_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees prepare_fleet_ssh_agent_context, run_runner_host_files; other-demand Clock,Filesystem | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `runner_host_file_observe_ci_wet` | `net` | `Network` | callees runner_host_file_ci_wet | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `runner_host_file_converge_ci_wet` | `net` | `Network` | callees runner_host_file_ci_wet | -| `dag/gunbc/runner/runner_host_kernel_config.dag` | `observe_built_kernel_config` | `net` | `Network` | callees run_shell_command_observe | -| `dag/gunbc/runner/runner_host_kernel_config.dag` | `verify_built_kernel_config` | `net` | `Network` | callees observe_built_kernel_config | -| `dag/gunbc/runner/runner_host_kernel_config.dag` | `verify_runner_host_kernel_config` | `net` | `Network` | callees verify_built_kernel_config | -| `dag/gunbc/runner/runner_microvm_boot_probe.dag` | `runner_microvm_boot_probe_wet` | `net` | `Network` | ops Filesystem.Write; callees run_shell_commands, run_shell_command_observe, observe_firecracker_host_standing_wet; other-demand Filesystem | -| `dag/gunbc/runner/runner_microvm_host_ready.dag` | `observe_firecracker_host_standing_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/runner/runner_microvm_host_ready.dag` | `runner_microvm_host_observe_wet` | `net` | `Network` | ops Filesystem.Write; callees run_shell_commands, observe_firecracker_host_standing_wet; other-demand Filesystem | -| `dag/gunbc/runner/runner_microvm_host_ready.dag` | `runner_microvm_host_converge_wet` | `net` | `Network` | ops Filesystem.Write; callees run_shell_commands, observe_firecracker_host_standing_wet; other-demand Filesystem | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `observe_captured_console` | `net` | `Network` | callees run_shell_command_observe | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `check_captured_console` | `net` | `Network` | callees observe_captured_console | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `verify_captured_console` | `net` | `Network` | callees check_captured_console | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `verify_bound_attempt_runner_version` | `net` | `Network` | callees verify_captured_console | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `verify_two_attempt_runner_version` | `net` | `Network` | callees verify_captured_console | -| `dag/gunbc/runner/runner_observed_version_check.dag` | `verify_jit_absent_capture_lacks_runner_version` | `net` | `Network` | callees verify_captured_console | -| `dag/gunbc/runner/runner_password_session_tool_converge.dag` | `runner_password_session_tool_converge_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees prepare_fleet_ssh_agent_context; other-demand Clock,Filesystem | -| `dag/gunbc/runner/runner_slot_provision.dag` | `observe_runner_slot_members_wet` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/spark/credential_workflow.dag` | `observe_spark_credential_consumer_evidence` | `net` | `Network` | callees fetch_secret_ref_credential | -| `dag/gunbc/spark/credential_workflow.dag` | `run_with_materialized_fleet_ssh_binding` | `net` | `Network` | callees fetch_secret_ref_credential, verify_fleet_ssh_credential | -| `dag/gunbc/spark/credential_workflow.dag` | `resolve_spark_administrator_credential` | `net` | `Network` | callees fetch_secret_ref_credential | -| `dag/gunbc/spark/fabric_rail_apply.dag` | `fabric_rail_apply_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor; other-demand Clock,Filesystem | -| `dag/gunbc/spark/fabric_reach.dag` | `observe_executor_reach` | `net` | `Network` | callees run_shell_command_capture | -| `dag/gunbc/spark/glm_canary_converge.dag` | `glm_canary_cutover_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor, observe_executor_reach; other-demand Clock,Filesystem | -| `dag/gunbc/spark/glm_canary_converge.dag` | `glm_canary_converge_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor, observe_executor_reach; other-demand Clock,Filesystem | -| `dag/gunbc/spark/glm_group_b_relaunch_cli.dag` | `rank_startup_facet` | `net` | `Network` | callees observe_rank_vllm_revision | -| `dag/gunbc/spark/glm_group_b_relaunch_cli.dag` | `observe_rank_report` | `net` | `Network` | callees observe_rank_container, rank_startup_facet, capture_rank_log | -| `dag/gunbc/spark/glm_group_b_relaunch_cli.dag` | `glm_group_b_observe_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees with_group_b_access, observe_rank_report; other-demand Clock,Filesystem | -| `dag/gunbc/spark/glm_group_b_relaunch_cli.dag` | `glm_group_b_relaunch_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees relaunch_arm, with_group_b_access; other-demand Clock,Filesystem | -| `dag/gunbc/spark/glm_group_b_relaunch_cli.dag` | `with_group_b_access` | `net` | `Network` | callees materialize_fleet_known_hosts_anchor, observe_executor_reach | -| `dag/gunbc/spark/managed_access_apply.dag` | `spark_managed_access_apply_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees resolve_spark_administrator_credential, run_with_materialized_fleet_ssh_binding; other-demand Clock,Filesystem | -| `dag/gunbc/spark/managed_access_apply.dag` | `spark_managed_access_bootstrap_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees prepare_fleet_ssh_agent_context; other-demand Clock,Filesystem | -| `dag/gunbc/spark/managed_grant_install.dag` | `spark_grant_install_execute` | `net` | `Network` | callees run_remote_step_sequence, spark_grant_read_back, run_privileged_step | -| `dag/gunbc/spark/managed_grant_install.dag` | `spark_grant_read_back` | `net` | `Network` | callees spark_grant_probe_outcome | -| `dag/gunbc/spark/managed_grant_install.dag` | `spark_grant_install_host` | `net` | `Network` | callees spark_grant_probe_outcome, spark_grant_install_execute | -| `dag/gunbc/spark/managed_grant_install.dag` | `spark_grant_install_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees prepare_fleet_ssh_agent_context, spark_grant_install_host; other-demand Clock,Filesystem | -| `dag/gunbc/spark/pair_serving_apply.dag` | `spark_pair_serving_apply_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees observe_executor_reach, materialize_fleet_known_hosts_anchor; other-demand Clock,Filesystem | -| `dag/gunbc/spark/pinned_base_env_classification.dag` | `sparkrun_pinned_base_env_read` | `net` | `Network` | callees read_docker_hub_image_config_at_digest | -| `dag/gunbc/spark/pinned_base_env_classification.dag` | `sparkrun_pinned_base_env_classification_wet` | `net` | `Network` | callees sparkrun_pinned_base_env_read | -| `dag/gunbc/spark/remote_step_sequence.dag` | `remote_step_sequence_step` | `net` | `Network` | callees run_remote_operation | -| `dag/gunbc/spark/remote_step_sequence.dag` | `run_remote_step_sequence` | `net` | `Network` | callees remote_step_sequence_step | -| `dag/gunbc/spark/secret_access_ensure.dag` | `spark_secret_access_ensure` | `net` | `Network` | callees secret_access_ensure_for | -| `dag/gunbc/spark/secret_access_ensure.dag` | `spark_secret_access_converge` | `net` | `Network` | callees spark_secret_access_ensure | -| `dag/gunbc/spark/secret_access_ensure.dag` | `spark_secret_access_converge_with_supplied_token` | `net` | `Network` | callees spark_secret_access_ensure | -| `dag/gunbc/spark/serving_rank_observe.dag` | `observe_container_presence` | `net` | `Network` | callees rank_leg | -| `dag/gunbc/spark/serving_rank_observe.dag` | `observe_rank_container` | `net` | `Network` | callees observe_container_presence | -| `dag/gunbc/spark/serving_rank_observe.dag` | `capture_rank_log` | `net` | `Network` | callees rank_leg | -| `dag/gunbc/spark/serving_rank_observe.dag` | `observe_rank_transport` | `net` | `Network` | callees capture_rank_log | -| `dag/gunbc/spark/serving_rank_observe.dag` | `observe_rank_vllm_revision` | `net` | `Network` | callees rank_leg | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_restart_incumbent` | `net` | `Network` | callees relaunch_leg | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `capture_rank` | `net` | `Network` | callees observe_rank_container, preserved_name_is_free, observe_rank_transport | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `preserved_name_is_free` | `net` | `Network` | callees observe_container_presence | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `capture_arm` | `net` | `Network` | callees capture_rank | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `advance_rank` | `net` | `Network` | callees relaunch_leg | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `stop_and_preserve_arm` | `net` | `Network` | callees advance_rank | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `create_and_start_arm` | `net` | `Network` | callees advance_rank | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_restore_names` | `net` | `Network` | callees rollback_restore_one | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_restore_one` | `net` | `Network` | callees rollback_rename_back, rollback_after_observing | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_after_observing` | `net` | `Network` | callees rollback_rename_back, observe_container_presence | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_rename_back` | `net` | `Network` | callees relaunch_leg | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_start_restored` | `net` | `Network` | callees rollback_start_one | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_start_one` | `net` | `Network` | callees rollback_restart_incumbent | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `rollback_arm` | `net` | `Network` | callees rollback_start_restored, rollback_restore_names | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `await_rank_announcement` | `net` | `Network` | callees observe_rank_container, observe_rank_transport, await_after_interval | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `await_after_interval` | `net` | `Network` | callees await_rank_announcement | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `verify_arm` | `net` | `Network` | callees await_rank_announcement | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `relaunch_arm` | `net` | `Network` | callees capture_arm, rollback_arm, stop_and_preserve_arm, create_and_start_arm, verify_arm | -| `dag/gunbc/spark/v41_runtime_image_probe.dag` | `v41_published_image_probe_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor, observe_executor_reach, v41_probe_published_image; other-demand Clock,Filesystem | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_observe_member` | `net` | `Network` | callees v41_check_applicable, v41_place_declared_patch | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_observe_members` | `net` | `Network` | callees v41_observe_member | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_observe_tree` | `net` | `Network` | callees v41_observe_members | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `converge_vllm_source_with_patches` | `net` | `Network` | callees v41_observe_tree, converge_vllm_source_to_revision, v41_apply_plan | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `converge_vllm_runtime_image_with_patches` | `net` | `Network` | callees converge_vllm_image_keyed, converge_vllm_source_with_patches | -| `dag/gunbc/spark/v41_source_patch_converge_wet.dag` | `v41_patched_source_acquire_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees converge_vllm_source_with_patches, probe_agent_credential_verification, materialize_fleet_known_hosts_anchor; other-demand Clock,Filesystem | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `vllm_source_after_revision` | `net` | `Network` | callees vllm_source_tree_clean | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `converge_vllm_source_to_revision` | `net` | `Network` | callees vllm_source_after_revision | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `converge_vllm_source` | `net` | `Network` | callees converge_vllm_source_to_revision | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `converge_vllm_image` | `net` | `Network` | callees converge_vllm_image_keyed | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `converge_vllm_image_keyed` | `net` | `Network` | callees vllm_docker_leg | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `converge_vllm_runtime_image` | `net` | `Network` | callees converge_vllm_image, converge_vllm_source | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `vllm_runtime_image_build_ci_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees observe_executor_reach, materialize_fleet_known_hosts_anchor, converge_vllm_runtime_image, probe_agent_credential_verification; other-demand Clock,Filesystem | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `vllm_source_acquire_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees converge_vllm_source, probe_agent_credential_verification, materialize_fleet_known_hosts_anchor; other-demand Clock,Filesystem | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `spark_build_host_reachability_wet` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; callees materialize_fleet_known_hosts_anchor, probe_spark_host_reachability, probe_agent_credential_verification; other-demand Clock,Filesystem | -| `dag/gunbc/srv3/srv3_bmc_credential_resolve.dag` | `materialize_bmc_login_password` | `net` | `Network` | callees fetch_secret_ref_credential | -| `dag/gunbc/srv3/srv3_bmc_credential_resolve.dag` | `observe_and_resolve_srv3_bmc_credential` | `net` | `Network` | callees srv3_probe_credential_phase | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd` | `clock` | `std.resources.Clock` | callees srv3_boot_once_cd_gated | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd` | `net` | `std.resources.Network` | callees srv3_boot_once_cd_gated | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd_operator_approved` | `clock` | `std.resources.Clock` | callees srv3_boot_once_cd_gated | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd_operator_approved` | `net` | `std.resources.Network` | callees srv3_boot_once_cd_gated | -| `dag/gunbc/srv3/srv3_boot_once_cd.dag` | `srv3_boot_once_cd_gated` | `net` | `std.resources.Network` | ops clock_now_probed_at; callees srv3_boot_once_cd_resolved, observe_and_resolve_srv3_bmc_credential; other-demand Clock | -| `dag/gunbc/srv3/srv3_os_install_actuate.dag` | `srv3_bmcweb_session_login` | `net` | `std.resources.Network` | callees srv3_bmcweb_session_login_resolved, observe_and_resolve_srv3_bmc_credential | -| `dag/gunbc/srv3/srv3_os_install_actuate.dag` | `srv3_bmcweb_session_login_resolved` | `net` | `std.resources.Network` | ops Filesystem.Write; callees materialize_bmc_login_password; other-demand Filesystem | -| `dag/gunbc/srv3/srv3_os_install_actuator_toolchain_ensure.dag` | `srv3_os_install_actuator_toolchain_ensure` | `net` | `Network` | callees host_toolchain_ensure | -| `dag/gunbc/tailscale_acl_phase2_credential.dag` | `tailscale_acl_operator_ephemeral_credential` | `net` | `std.resources.Network` | callees tailscale_acl_credential_with_operator_token | -| `dag/gunbc/tailscale_acl_phase2_credential.dag` | `tailscale_acl_credential_with_operator_token` | `net` | `std.resources.Network` | callees gcp_secret_credential | -| `dag/gunbc/tools/bmc_onboard.dag` | `srv3_probe_credential_phase` | `net` | `Network` | callees bmc_probe_credential_phase | -| `dag/gunbc/tools/bmc_onboard.dag` | `srv3_converge_credential` | `net` | `Network` | callees bmc_converge_credential | -| `dag/gunbc/tools/bmc_onboard.dag` | `srv4_converge_credential` | `net` | `Network` | callees bmc_converge_credential | -| `dag/test/ctl/pos_emit.dag` | `forwards_to_a_service_call` | `net` | `std.resources.Network` | callees emits_a_service_call | - -## UnusedRequirement - -(empty in the live parsed population) - -## Unresolved - -| File | Declaration | Alias | Authored type | Evidence | -| --- | --- | --- | --- | --- | -| `dag/gunbc/container/registry_image_config.dag` | `read_docker_hub_image_config_at_digest` | `net` | `Network` | ops Filesystem.WriteOwnerOnly; local-ops shell.Mktemp.Dir, shell.Remove.RecursiveForce, shell.Remove.RecursiveForce, shell.Remove.RecursiveForce; other-demand Filesystem; calls check_sha256_hex, container_image_config_env_from_json, container_image_config_from_env, digest_hex_from_sha256_wire, docker_hub_bearer_token, docker_hub_blob_url, docker_hub_manifest_url, manifest_config_digest_wire | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_generation_store_wet` | `net` | `Network` | ops Filesystem.Read; other-demand Filesystem; calls fleet_converge_generation_admission, fleet_converge_generation_observation, fleet_converge_generation_store_path | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_cap_members_wet` | `net` | `Network` | ops Filesystem.List, Filesystem.Read; other-demand Filesystem; calls FilesystemFileAbsent, cap_members_observation, filesystem_file_observation, filesystem_listing_observation, filesystem_read_outcome | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_runner_unit_standing_wet` | `net` | `Network` | calls read_unit_property | -| `dag/gunbc/fleet/fleet_converge_plan_cli.dag` | `observe_operator_local_run_binding` | `net` | `Network` | ops clock_now_probed_at; local-ops git.Inspect.HeadCommit; other-demand Clock; calls clock_now_probed_at, release_revision_text_valid | -| `dag/gunbc/fleet/fleet_host_key_enrollment.dag` | `fleet_host_key_scan_wet` | `net` | `Network` | ops Filesystem.Write; other-demand Filesystem; calls scan_host_key_lines | -| `dag/gunbc/fleet/fleet_known_hosts_anchor.dag` | `materialize_fleet_known_hosts_anchor` | `net` | `Network` | ops Filesystem.Read, Filesystem.Write, Filesystem.Read; other-demand Filesystem; calls compare_content_hash, content_hash_of_value, fleet_known_hosts_anchor_path, fleet_known_hosts_fragment, fleet_known_hosts_registry_identity, fleet_known_hosts_rows, host_key_provenance_label, known_hosts_anchor_seal | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `spark_managed_access_standing_wet` | `net` | `Network` | calls fleet_ssh_attempt_raw_from_env, spark_managed_access_standing_with_attempt | -| `dag/gunbc/fleet/fleet_multi_principal_probe.dag` | `spark_managed_access_standing_with_verified_credential` | `net` | `Network` | ops Filesystem.Write, clock_now_probed_at; other-demand Clock,Filesystem; calls agent_backed_principal_observation, clock_now_probed_at, derive_spark_managed_access_standing, probed_at_word, serialize_content_hash, spark_authorization_lines, spark_standing_line, spark_standing_unobserved_host | -| `dag/gunbc/fleet/fleet_ssh_credential_verify.dag` | `materialize_agent_public_selector` | `net` | `Network` | ops Filesystem.Read, Filesystem.Write, Filesystem.Read; other-demand Filesystem; calls agent_public_selector_content, agent_public_selector_path, agent_selector_file_seal, compare_content_hash, content_hash_of_value | -| `dag/gunbc/host/host_codex_runtime_provision.dag` | `materialize_codex_runtime_on_local_host` | `net` | `Network` | calls codex_runtime_materialization_refusal_reason, committed_admitted_codex_runtime_source, materialize_codex_runtime_bundle, provider_runtime_placement_for_host | -| `dag/gunbc/host/host_reset_return_run.dag` | `observer_self_corroboration` | `net` | `std.resources.Network` | local-ops os.Hostname.ReadShort; calls canonical_hostname_for_fleet_slot, observer_self_identity | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_preflight_exit` | `net` | `std.resources.Network` | ops Filesystem.Write; other-demand Filesystem; calls reset_preflight_refusal_wire | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_dispatch_admission_wet` | `net` | `std.resources.Network` | local-ops shell.Env.Get, shell.Env.Get; calls admit_reset_dispatch | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_converge_route_read` | `net` | `std.resources.Network` | calls actions_variable_read | -| `dag/gunbc/host/host_reset_return_run.dag` | `host_reset_return_exit` | `net` | `std.resources.Network` | ops Filesystem.Write; other-demand Filesystem; calls host_reset_return_receipt_line, reset_return_refusal_cause_wire | -| `dag/gunbc/host/host_runner_memory_provision.dag` | `provision_runner_memory_caps` | `net` | `Network` | calls fleet_compute_host_for_identity, host_effect_apply_gated, host_toolchain_direct_access, runner_memory_converge_effect | -| `dag/gunbc/host/host_toolchain_ensure.dag` | `host_toolchain_ensure` | `net` | `Network` | calls host_effect_apply_gated, host_toolchain_ensure_effect, host_toolchain_ensure_reach | -| `dag/gunbc/live_deploy/deployed_tree_report.dag` | `deployed_tree_report_for_context` | `net` | `Network` | calls converge_target_revision, deployed_tree_locus, deployed_tree_report_emit, deployed_tree_standing, fleet_locus_ssh_target, observe_deployed_tree, observe_fleet_desired_revision | -| `dag/gunbc/machine_intake/boot_image_fetch.dag` | `confirm_published_boot_image` | `net` | `std.resources.Network` | calls boot_image_digest_agrees, boot_image_observed_hex, sha256sum_file_digest_via_shell | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `window_snapshot` | `net` | `Network` | calls active_instance_text_routed, deployed_tree_hexes, deployed_tree_locus, fleet_locus_ssh_target, observe_deployed_tree, read_desired_hex, read_remote_main_hex, remote_ref_text | -| `dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag` | `unit_standing_fact` | `net` | `Network` | calls deployment_belt_service_unit_name, deployment_belt_timer_unit_name, deployment_spec_srv1, live_deploy_belt_service_unit_file, live_deploy_belt_timer_unit_file, live_deploy_serve_unit_file, observe_belt_oneshot_standing, observe_belt_timer_standing | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `observe_runner_host_administrator_privilege` | `net` | `Network` | calls elevated_argv, typed_argv_exec_over_fleet_ssh | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `observe_runner_host_paths` | `net` | `Network` | calls classify_root_only_paths, runner_host_root_only_find_argv, typed_argv_exec_over_fleet_ssh | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `observe_runner_host_file_content` | `net` | `Network` | calls classify_host_file_presence, classify_typed_remote_file_comparison, runner_host_file_standing_of_report, runner_host_file_write_admission, typed_argv_exec_over_fleet_ssh, typed_remote_file_compare_argv, typed_remote_file_step | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `observe_runner_slot_population` | `net` | `Network` | calls classify_runner_slot_population, runner_slot_population_units, runner_slot_units_from_list_units, systemctl_list_all_service_units_argv, systemctl_show_properties_argv, typed_argv_exec_over_fleet_ssh | -| `dag/gunbc/runner/runner_host_file_converge.dag` | `reload_runner_host_manager` | `net` | `Network` | calls elevated_argv, systemctl_daemon_reload_argv, typed_argv_exec_over_fleet_ssh | -| `dag/gunbc/runner/runner_slot_population_census.dag` | `observe_slot_population_over_ssh` | `net` | `Network` | calls slot_census_from_command_outcome, systemctl_list_all_service_units_argv, typed_argv_exec_over_fleet_ssh | -| `dag/gunbc/runner/runner_slot_retirement.dag` | `observe_transition_retirement_evidence` | `net` | `Network` | calls observe_retirement_standings, transition_required_retiring | -| `dag/gunbc/spark/glm_canary_converge.dag` | `glm_canary_cutover` | `net` | `Network` | calls glm_canary_replace_container, glm_canary_run_argv | -| `dag/gunbc/spark/grant_privileged_operation.dag` | `run_privileged_step` | `net` | `Network` | calls admitted_root_argv_words, exec_as_fleet_principal_with_stdin, fleet_probe_endpoint_for, portable_remote_words, read_bootstrap_credential, sudo_rejected_the_credential | -| `dag/gunbc/spark/managed_grant_install.dag` | `spark_grant_probe_outcome` | `net` | `Network` | calls run_typed_argv_transport, spark_grant_install_transport | -| `dag/gunbc/spark/remote_step_sequence.dag` | `run_remote_operation` | `net` | `Network` | calls SshShell, converge_typed_remote_file, run_typed_argv_transport | -| `dag/gunbc/spark/serving_rank_observe.dag` | `rank_leg` | `net` | `Network` | calls argv_words, rank_observation_refusal, spark_remote_run | -| `dag/gunbc/spark/serving_relaunch_transaction.dag` | `relaunch_leg` | `net` | `Network` | calls argv_words, relaunch_step_wire, spark_remote_run | -| `dag/gunbc/spark/v41_runtime_image_probe.dag` | `v41_probe_published_image` | `net` | `Network` | calls docker_image_inspect_id_from_stdout, docker_image_inspect_names_no_such_image, image, spark_remote_run, v41_capability_reading, v41_privileged_outcome, v41_privileged_stdout, v41_probe_config_id_argv | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_place_declared_patch` | `net` | `Network` | ops Filesystem.Read; other-demand Filesystem; calls converge_typed_remote_file, filesystem_read_outcome, typed_remote_file_write_seal, v41_remote_declared_patch_path, vllm_build_root | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_check_applicable` | `net` | `Network` | calls git_apply_check_command, git_apply_reverse_check_command, v41_remote_declared_patch_path, vllm_image_leg_unchecked, vllm_source_dir | -| `dag/gunbc/spark/v41_source_patch_converge.dag` | `v41_apply_plan` | `net` | `Network` | calls git_apply_command, v41_remote_declared_patch_path, vllm_image_leg, vllm_source_dir | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `vllm_docker_leg` | `net` | `Network` | calls argv_words, spark_remote_run | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `vllm_source_tree_clean` | `net` | `Network` | calls git_status_porcelain_command, vllm_image_leg, vllm_source_dir, vllm_source_is_clean | -| `dag/gunbc/spark/vllm_runtime_image_build.dag` | `probe_spark_host_reachability` | `net` | `Network` | calls classify_reported_hostname, hostname_short_read_command, spark_reach_endpoint, vllm_image_leg | -| `dag/gunbc/srv3/srv3_install_media_fetch.dag` | `srv3_install_media_fetch` | `net` | `Network` | calls srv3_apply_to_process_exit, srv3_install_media_fetch_apply | -| `dag/gunbc/srv3/srv3_os_install_actuate.dag` | `srv3_nbd_proxy_serve` | `net` | `std.resources.Network` | calls srv3_apply_to_process_exit, srv3_nbd_proxy_serve_apply | -| `dag/gunbc/srv3/srv3_seeded_install_media.dag` | `srv3_seeded_install_media_toolchain_ensure` | `net` | `std.resources.Network` | calls srv3_apply_to_process_exit, srv3_seeded_install_media_toolchain_ensure_apply | -| `dag/gunbc/srv3/srv3_seeded_install_media.dag` | `srv3_seeded_install_media_remaster` | `net` | `std.resources.Network` | calls srv3_apply_to_process_exit, srv3_seeded_install_media_remaster_apply, srv3_seeded_install_media_toolchain_ensure_apply | - -## NamedDependencyBinder - -(empty in the live parsed population) - -## OpaqueContract - -(empty in the live parsed population) - -## PolicyEnvelope - -(empty in the live parsed population) - -## Fixture-string population (not live declarations) - -Witness modules embed programs as `data *_source: String`. Those `uses` rows are specimens, not production headers. - -| Host | Embedded decl data | Alias | Type | Class in the specimen | -| --- | --- | --- | --- | --- | -| `dag/test/claim/data_class_refusal_probe_witness_test.dag` | `durable_write_literal_control_source` | `net` | `Network` | `UnusedRequirement` | -| `dag/test/claim/effectful_item_kind_collapse_witness_test.dag` | `efr_control_source` | `net` | `EfrNet` | `NamedDependencyBinder` | -| `dag/test/claim/effectful_item_kind_collapse_witness_test.dag` | `efr_non_tail_source` | `net` | `EfrNet` | `NamedDependencyBinder` | -| `dag/test/claim/effectful_item_kind_collapse_witness_test.dag` | `efr_tail_source` | `net` | `EfrNet` | `NamedDependencyBinder` | -| `dag/test/claim/effectful_item_kind_collapse_witness_test.dag` | `zpe_reads_resource_source` | `net` | `ZpeNet` | `NamedDependencyBinder` | -| `dag/test/claim/entry_authority_witness_test.dag` | `ea_one_entry_source` | `net` | `EaNet` | `NamedDependencyBinder` | -| `dag/test/claim/entry_authority_witness_test.dag` | `ea_uses_library_source` | `net` | `EaNet` | `NamedDependencyBinder` | -| `dag/test/claim/generic_effectful_declaration_wall_witness_test.dag` | `gew_generic_effectful_source` | `net` | `GewNet` | `UnusedRequirement` | -| `dag/test/claim/generic_effectful_declaration_wall_witness_test.dag` | `gew_plain_effectful_source` | `net` | `GewNet2` | `NamedDependencyBinder` | - -## Inverse (derived demand without an authored `uses` on that declaration) - -Not a `uses` occurrence, so not a USES-0 class member. Named because D13 derives demand from operations: `gunbc.clock_read` `clock_now_probed_at` calls `Clock.Now()` with no header contract. Callers then restate `uses clock` transitively. That is the derive-once-carry gap: the Clock demand is produced at `Clock.Now` and should be carried, not re-authored up the chain. - -HOLD: no `uses` row added or deleted by this census. - diff --git a/docs/plans/v1-closeout-pr-accounting.md b/docs/plans/v1-closeout-pr-accounting.md new file mode 100644 index 00000000000..488f0850f18 --- /dev/null +++ b/docs/plans/v1-closeout-pr-accounting.md @@ -0,0 +1,354 @@ +# v1 closeout: open PR accounting + +As of 2026-10-09; it includes the operator's side-chat dispositions. There are 162 open PRs on gunb-ai/gunbc. Apart from #13203 (blackjack, kept up by request), every one of them ends either in the mega branch #13641 (`integration/v1-closeout`) or closed with a stated reason. The owners come from the dashboard's PR-to-session records and the side chats. The dispositions come from each manager's closeout report. Merge state is GitHub's `mergeStateStatus` at the time of writing. + +| Disposition | PRs | What happens | +| --- | --- | --- | +| In the mega branch | 33 | Lands when #13641 lands | +| Pending fold | 64 | On a manager's integration branch, with the conflict worker, or waiting for CI; folded into #13641 next | +| Blocked | 4 | Needs an operator or host action first | +| Left out | 47 | Not in the snapshot. Its remaining work is recorded under Outstanding work before the PR is closed; nothing is dropped | +| Unreported, owned by a live lane | 0 | Waiting on that lane's report | +| Orphans: no live owner | 13 | Triaged by the closeout: fold if done and clean, otherwise close | +| Kept up by request | 1 | #13203 blackjack | + +## In the mega branch + +These are merged into `integration/v1-closeout`. + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | +| [#13641](https://github.com/gunb-ai/gunbc/pull/13641) | v1 closeout mega branch (wave 1) | ready / BLOCKED | neat-wolf-604 | +| [#13626](https://github.com/gunb-ai/gunbc/pull/13626) | host_reach_identity_probe: the identity probe's argv is spelled by its | ready / CLEAN | stern-boar-596 | +| [#13622](https://github.com/gunb-ai/gunbc/pull/13622) | dispatch-actuator witness: supply the unrooted specimen instead of bor | ready / CLEAN | bold-bee-114 | +| [#13616](https://github.com/gunb-ai/gunbc/pull/13616) | Belt SCM capture: read each re-bound object shard once (recapture 30x | ready / CLEAN | bold-bee-114 | +| [#13610](https://github.com/gunb-ai/gunbc/pull/13610) | walk_cgroup, entry_presence: tail-position self-recursion so both modu | ready / CLEAN | silent-lark-156 | +| [#13602](https://github.com/gunb-ai/gunbc/pull/13602) | Runner obligation and census read committed identities; Spark training | ready / CLEAN | side chat | +| [#13601](https://github.com/gunb-ai/gunbc/pull/13601) | Fabric cell witnesses: least-cause refusal join; carry #11625/#11716 i | ready / CLEAN | side chat | +| [#13600](https://github.com/gunb-ai/gunbc/pull/13600) | witnesses: match optional results instead of comparing T? == T (3 file | ready / CLEAN | side chat | +| [#13599](https://github.com/gunb-ai/gunbc/pull/13599) | srv1 lab follows main instead of a July session branch | ready / CLEAN | side chat | +| [#13587](https://github.com/gunb-ai/gunbc/pull/13587) | Bind gunbc run --arg against declared parameter types | ready / DIRTY | bold-bee-114 | +| [#13577](https://github.com/gunb-ai/gunbc/pull/13577) | Price the parse memo below the cost floor: the production door admits | ready / CLEAN | side chat | +| [#13570](https://github.com/gunb-ai/gunbc/pull/13570) | BMC onboarding: Redfish PasswordChangeRequired is the factory phase; s | ready / CLEAN | side chat | +| [#13569](https://github.com/gunb-ai/gunbc/pull/13569) | store: held-session op + O(1) named hold-slot reads | ready / CLEAN | royal-moth-86 | +| [#13568](https://github.com/gunb-ai/gunbc/pull/13568) | Delete the pasted-operator-token refusal (operator ruling 2026-10-08) | ready / CLEAN | side chat | +| [#13566](https://github.com/gunb-ai/gunbc/pull/13566) | Skip module-surface members in reference-derived residency reading | ready / CLEAN | bold-bee-114 | +| [#13563](https://github.com/gunb-ai/gunbc/pull/13563) | Altra firmware library: beta BIOS 4.01 + SCP archives, flashable membe | ready / CLEAN | side chat | +| [#13560](https://github.com/gunb-ai/gunbc/pull/13560) | Identity-only skip for module-surface members in record-field lenses | ready / CLEAN | bold-bee-114 | +| [#13554](https://github.com/gunb-ai/gunbc/pull/13554) | srv13 first contact: factory BIOS defaults (PXE disabled by default) | ready / CLEAN | side chat | +| [#13550](https://github.com/gunb-ai/gunbc/pull/13550) | Bankrupt docs/plans: delete hand-written plans, keep DESIGN-linked and | ready / DIRTY | side chat | +| [#13516](https://github.com/gunb-ai/gunbc/pull/13516) | Managed-host cut O1c-3a: ManagedHostAdmission population with drop-arm | ready / CLEAN | eager-gull-22 | +| [#13513](https://github.com/gunb-ai/gunbc/pull/13513) | mandatory_tag gate: read the lowered data-declaration carrier (clean f | ready / CLEAN | bold-bee-114 | +| [#13472](https://github.com/gunb-ai/gunbc/pull/13472) | Realize rust shell stderr-capture channels (unblocks #13217 floor) | ready / DIRTY | silent-lark-156 | +| [#13460](https://github.com/gunb-ai/gunbc/pull/13460) | map_get fork: the declared projection renames to map_get_checked | ready / CLEAN | lively-ram-153 | +| [#13442](https://github.com/gunb-ai/gunbc/pull/13442) | Workspace transcript append rate: size-and-mtime instrument on srv1 (s | ready / CLEAN | neat-wolf-604 | +| [#13412](https://github.com/gunb-ai/gunbc/pull/13412) | semver: route the version scheme's identity compare through the cited | ready / CLEAN | lively-ram-153 | +| [#13359](https://github.com/gunb-ai/gunbc/pull/13359) | Codex + Cursor worker turns on one gunbai-secrets credential each (cus | ready / CLEAN | neat-wolf-604 | +| [#13357](https://github.com/gunb-ai/gunbc/pull/13357) | Onboard OpenRouter Nemotron 3 Ultra (free) as a quota-leased harness b | ready / CLEAN | neat-wolf-604 | +| [#13351](https://github.com/gunb-ai/gunbc/pull/13351) | Oracle OCI Always Free: network + instance converge (stacked on #13335 | ready / CLEAN | nimble-heron-805 | +| [#13346](https://github.com/gunb-ai/gunbc/pull/13346) | Remove quadratic remainder copying from code-point/octet slicing in bo | ready / CLEAN | neat-wolf-604 | +| [#13210](https://github.com/gunb-ai/gunbc/pull/13210) | v2 native infer accepts variant-scrutinee matches (payload binder typi | ready / CLEAN | | +| [#13125](https://github.com/gunb-ai/gunbc/pull/13125) | belt: demote the monolithic tick into event-driven attempt obligations | ready / BLOCKED | bold-bee-114 | +| [#13072](https://github.com/gunb-ai/gunbc/pull/13072) | Compute work is a request family of std.materialization_provider; work | ready / CLEAN | bold-bee-114 | +| [#12942](https://github.com/gunb-ai/gunbc/pull/12942) | MQ-5 cut: producers seal what they lower (BodyTerm via sealed #12799 m | ready / CLEAN | neat-boar-16 | + +## Pending fold + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | +| [#13609](https://github.com/gunb-ai/gunbc/pull/13609) | Derive ledger roster modules without mutating source roots | ready / CLEAN | conflict worker | +| [#13108](https://github.com/gunb-ai/gunbc/pull/13108) | admit_callers: restore the native-route caller-admission wall (DP-M6 i | ready / CLEAN | conflict worker (04_infer.dag, body_lowering_fold.dag; regen-confirm ROADMAP/rung-drops) | +| [#13583](https://github.com/gunb-ai/gunbc/pull/13583) | One directory authority for dashboard instances (D14 OwnedDirectory sl | ready / CLEAN | conflict worker (keep 13583 spec.dag) | +| [#13582](https://github.com/gunb-ai/gunbc/pull/13582) | Delete the v2 packrat parse memo carrier (R2, stacked on #13577) | draft / CLEAN | conflict worker (stack on 13577) | +| [#13615](https://github.com/gunb-ai/gunbc/pull/13615) | Delete unused memo and preparation carriers (native-memory-inert-carri | draft / CLEAN | conflict worker (stack; keep floor_preparation_witness_test.dag deleted) | +| [#13608](https://github.com/gunb-ai/gunbc/pull/13608) | RFM: fixture-closure-union-emit suspected superlinear cost | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13596](https://github.com/gunb-ai/gunbc/pull/13596) | target_invocation witness: repair two false claims; file the unplanned | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13590](https://github.com/gunb-ai/gunbc/pull/13590) | Native ingest: refuse two files claiming one module path | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13588](https://github.com/gunb-ai/gunbc/pull/13588) | std.change: import any and Present so its own names resolve natively | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13586](https://github.com/gunb-ai/gunbc/pull/13586) | guarantee_stall: correct accumulator-copy native-resolve residue recei | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13585](https://github.com/gunb-ai/gunbc/pull/13585) | Enroll native-resolve coverage for fold handler binders | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13580](https://github.com/gunb-ai/gunbc/pull/13580) | regen-round-cost: re-exec once when the seed build replaces the runnin | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13579](https://github.com/gunb-ai/gunbc/pull/13579) | Climb import-less UniqueBare from proximity to lexical on-chain bindin | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13575](https://github.com/gunb-ai/gunbc/pull/13575) | Isolate fixture census on a scratch MultiEntryIndex | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13571](https://github.com/gunb-ai/gunbc/pull/13571) | Fallout from #13179: latent T? == T sites and stale grounded_principal | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13520](https://github.com/gunb-ai/gunbc/pull/13520) | Delete dead emit_resolve_transitively_fn stamp from generated main | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13519](https://github.com/gunb-ai/gunbc/pull/13519) | stage0 witness bins: close compile subjects through the closure author | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13517](https://github.com/gunb-ai/gunbc/pull/13517) | Managed-host cut O1c-3b: BmcSecure gated through the convergence fold | ready / DIRTY | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13507](https://github.com/gunb-ai/gunbc/pull/13507) | Board identity: bound host's FRU board reaches its per-board authority | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13503](https://github.com/gunb-ai/gunbc/pull/13503) | Move AMI-bundle-derived MegaRAC content out of public gunbc | ready / BLOCKED | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13497](https://github.com/gunb-ai/gunbc/pull/13497) | Emit arrival_converge; IAM pair and pinned accessor (msg_f03558d1) | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13341](https://github.com/gunb-ai/gunbc/pull/13341) | v2 parse: occurrence ids minted once per parse across a rejected attem | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13333](https://github.com/gunb-ai/gunbc/pull/13333) | v2 resolve: the single-tree namespace binds its own variants and recor | ready / CLEAN | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13247](https://github.com/gunb-ai/gunbc/pull/13247) | mtcollins1 runner: boot leg, runner-host medium + runner-host-up termi | ready / BLOCKED | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13225](https://github.com/gunb-ai/gunbc/pull/13225) | mtcollins1 runner: extract qualification instruments from the run (leg | ready / DIRTY | eager-gull-22 @54f257a3f5 (with the sharp-raven/eager-gull fold worker; 19-file conflict) | +| [#13598](https://github.com/gunb-ai/gunbc/pull/13598) | Retire native_serve_request_budget_unrealized: native-serve holds the | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13593](https://github.com/gunb-ai/gunbc/pull/13593) | File emitter defect: variant pattern nested inside Present drops the R | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13584](https://github.com/gunb-ai/gunbc/pull/13584) | Seed eval: empty list is not Unit | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13567](https://github.com/gunb-ai/gunbc/pull/13567) | Emit algebra length on host String as string_length | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13549](https://github.com/gunb-ai/gunbc/pull/13549) | Fix every T? == T site the whole-population census found | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13454](https://github.com/gunb-ai/gunbc/pull/13454) | Native broker 2K-b: identity-keyed type_summaries; Host-Option decided | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13260](https://github.com/gunb-ai/gunbc/pull/13260) | emit_rust: a type parameter carries its own cardinality (K? emits Opti | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13255](https://github.com/gunb-ai/gunbc/pull/13255) | Emitter: a cloned Rc match scrutinee owes Clone on its generics wherev | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13243](https://github.com/gunb-ai/gunbc/pull/13243) | Inference: a variant literal of a generic coproduct takes the expected | ready / DIRTY | gentle-dove-36 (with the conflict worker) | +| [#13224](https://github.com/gunb-ai/gunbc/pull/13224) | Native effect realization admission; native mains bind admitted handle | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13202](https://github.com/gunb-ai/gunbc/pull/13202) | Native broker 2C/G: optional data rows keep their ? (signature and JSO | ready / CLEAN | gentle-dove-36 (with the conflict worker) | +| [#13621](https://github.com/gunb-ai/gunbc/pull/13621) | A data reference uses its value: ConstantMemberEdge marks the lowered | draft / BLOCKED | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13618](https://github.com/gunb-ai/gunbc/pull/13618) | workflow scripts: a refused bash emission refuses, never emits an empt | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13545](https://github.com/gunb-ai/gunbc/pull/13545) | Compare an Optional with an Optional at 46 latent sites no gated closu | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13511](https://github.com/gunb-ai/gunbc/pull/13511) | Join corpus Int and kernel Int at inhabitance (N7 g_tokenize_parse) | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13502](https://github.com/gunb-ai/gunbc/pull/13502) | Pair identity-cast native with the real emit artifact | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13501](https://github.com/gunb-ai/gunbc/pull/13501) | v2 tokenize: bind EmptyPattern on 01_tokenize's import chain (N7 resol | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13496](https://github.com/gunb-ai/gunbc/pull/13496) | Bool-native rust_logic/variant emit after BindingRef octet de-fork | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13469](https://github.com/gunb-ai/gunbc/pull/13469) | Out-of-gate emit census: binder types, A4 order on rust fixtures, refi | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13453](https://github.com/gunb-ai/gunbc/pull/13453) | Floor judges base-revision facts with the base revision's own compiler | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13438](https://github.com/gunb-ai/gunbc/pull/13438) | Precedence-climbing tree contract for dag binary expressions | ready / DIRTY | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13436](https://github.com/gunb-ai/gunbc/pull/13436) | XL-2: named conversions for every non-String interpolation hole (std.n | ready / BLOCKED | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13425](https://github.com/gunb-ai/gunbc/pull/13425) | Gate the foreign-language grammar claims (census phase 4): runnable te | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13406](https://github.com/gunb-ai/gunbc/pull/13406) | Claims tokenize .dag text through tokenize_prepared(dag_prepared_lex() | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13380](https://github.com/gunb-ai/gunbc/pull/13380) | RFM: 'first element of a list' nickname fork (roster first / list_head | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13379](https://github.com/gunb-ai/gunbc/pull/13379) | v2.std.grammar: qualify the GrammarExpr Optional arm (N7 resolve link) | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13325](https://github.com/gunb-ai/gunbc/pull/13325) | v2 infer: derive a call's result type from a declared return (instanti | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13320](https://github.com/gunb-ai/gunbc/pull/13320) | v2 infer: an integer literal elaborates at its expected type through s | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13308](https://github.com/gunb-ai/gunbc/pull/13308) | Census part 2: the native infer census on v2-native-census (census-inf | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13307](https://github.com/gunb-ai/gunbc/pull/13307) | v2: bare count over lists binds the std.algebra collection size row, a | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13126](https://github.com/gunb-ai/gunbc/pull/13126) | Grammar overlap: validation refuses every overlap row; required zero-c | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13123](https://github.com/gunb-ai/gunbc/pull/13123) | C4: native route enforces constructor confinement (sole_constructor + | ready / CLEAN | sharp-raven-357 (~200-file conflict; separate worker after the conflict worker lands) | +| [#13625](https://github.com/gunb-ai/gunbc/pull/13625) | CAX41 guest onboarding; fleet-converge from a branch after a per-run n | draft / BLOCKED | side chat, draft; folds when CI green (touches fleet-converge.yml) | +| [#13614](https://github.com/gunb-ai/gunbc/pull/13614) | seed interpreter: withdraw the spelling-admitted, label-blind PureCall | ready / CLEAN | side chat, folds when CI green | +| [#13611](https://github.com/gunb-ai/gunbc/pull/13611) | demand engine: a request under a different nature is a durable key con | ready / CLEAN | side chat, folds when CI green | +| [#13607](https://github.com/gunb-ai/gunbc/pull/13607) | Connect srv1 workspace VM lifecycle and kernel prerequisites | draft / BLOCKED | side chat, in progress: kernel activation/readback, VM commissioning + SSH, release/reuse/expiry checks, CI | +| [#13597](https://github.com/gunb-ai/gunbc/pull/13597) | Make the crate partitioner the only emitted-Rust layout; delete single | draft / DIRTY | side chat, not yet done: needs a main merge + regen, the remaining acceptance runs, and a PR body update; folds once marked ready | +| [#13428](https://github.com/gunb-ai/gunbc/pull/13428) | Carry the alias declaration: its name resolves to its target (ruling B | ready / CLEAN | with the sharp-raven fold worker (green, approved; conflicts in 03_normalize/body_lowering_fold) | +| [#13643](https://github.com/gunb-ai/gunbc/pull/13643) | Census: cross-module record field values are judged (rcf pin rewritten | draft / BLOCKED | with the sharp-raven fold worker if its last check is green (census, approved) | + +## Blocked + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | +| [#13544](https://github.com/gunb-ai/gunbc/pull/13544) | Decommission Group A (srv5-srv8, sold): remove it from fleet config an | ready / DIRTY | DIRTY vs main | +| [#13411](https://github.com/gunb-ai/gunbc/pull/13411) | Accepting wildcards cut 3: name the two wet-step exit gates | ready / CLEAN | stacked 13383 | +| [#13383](https://github.com/gunb-ai/gunbc/pull/13383) | Accepting wildcards cut 2: name the vocabulary walls, the grounding ga | ready / CLEAN | stacked 13382 | +| [#13382](https://github.com/gunb-ai/gunbc/pull/13382) | Accepting wildcards cut 1: name the verdict classifier and the two rou | ready / BLOCKED | browser toolchain | + +## Left out + +These are drafts, red, superseded or WIP, per the manager's report or a side chat. Closing one never drops its work: what is left gets a row under Outstanding work first. + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | +| [#13097](https://github.com/gunb-ai/gunbc/pull/13097) | G1: belt verify through the materialization provider; compute outcomes | ready / BLOCKED | bold-bee: red on T?==T sites until #13549; draft | +| [#13591](https://github.com/gunb-ai/gunbc/pull/13591) | Resolve free kernel calls to std.primitives identity; infer refuses th | draft / CLEAN | eager-gull: APPROVED (review 78380); stacked on #13588 (in the snapshot via eager-gull); fold once v2-native-cli is confirmed | +| [#13574](https://github.com/gunb-ai/gunbc/pull/13574) | Interim check-time refusal of list-read == Present{..} | ready / CLEAN | eager-gull: OPERATOR: reverted out (review 78262) | +| [#13604](https://github.com/gunb-ai/gunbc/pull/13604) | Prepare the required-floor gate once; project policy from that subject | draft / BLOCKED | eager-gull: REQUEST_CHANGES review 78326 (hand .dag walker in seed); reverted | +| [#13391](https://github.com/gunb-ai/gunbc/pull/13391) | Refuse a product value at a kernel-scalar (or refined) declared type | ready / DIRTY | eager-gull: REQUEST_CHANGES review 78347 (census missing); conflicts main | +| [#13595](https://github.com/gunb-ai/gunbc/pull/13595) | Reclassify the match-arm proven-disjoint InternalError as a located Ty | draft / CLEAN | eager-gull: depends on #13574; reverted out | +| [#13576](https://github.com/gunb-ai/gunbc/pull/13576) | DRAFT: floor-control for #13575 (plan forged-probe census) | draft / UNSTABLE | eager-gull: red draft control for #13575 | +| [#13211](https://github.com/gunb-ai/gunbc/pull/13211) | mtcollins1 runner: dispatch the floor to the attempt's slot and read t | ready / BLOCKED | eager-gull: red: floor 90m cap in fixture-closure-union-emit; needs #13608 newer head 8ff94ca471 | +| [#13420](https://github.com/gunb-ai/gunbc/pull/13420) | Managed-host cut 5: fan, served-UI and KVM observations over ManagedHo | draft / BLOCKED | eager-gull: red: floor stuck-floor pin; waits #13575 | +| [#13432](https://github.com/gunb-ai/gunbc/pull/13432) | [DO NOT MERGE] baseline control: fixture-closure union with only a KVM | draft / BLOCKED | eager-gull: scratch baseline, DO NOT MERGE | +| [#13288](https://github.com/gunb-ai/gunbc/pull/13288) | mtcollins1 runner: the dedicated runner-qualification group, ensured o | ready / CLEAN | eager-gull: stacked on #13211 | +| [#13640](https://github.com/gunb-ai/gunbc/pull/13640) | mtcollins1 runner: dispatch floor to ephemeral slot + collect instrume | draft / DIRTY | eager-gull: superseded quiet-cat-583 WIP flush | +| [#13252](https://github.com/gunb-ai/gunbc/pull/13252) | mtcollins1 runner offer at measured one-socket shape + census meminfo | ready / CLEAN | eager-gull: waits on census boot measurement | +| [#13475](https://github.com/gunb-ai/gunbc/pull/13475) | fold_list empty: [] no longer locks the accumulator as List | draft / BLOCKED | gentle-dove: APPROVED (review 78364), fix judged sound; only CI red (4 failing). Closest to revivable | +| [#13330](https://github.com/gunb-ai/gunbc/pull/13330) | Derived-node identity step 2, shape 2: unify_generics reads a containe | ready / BLOCKED | gentle-dove: REQUEST_CHANGES review 76999 (Bool predicate over DeclField in std/decl_ref.dag; inline it at the caller) | +| [#13633](https://github.com/gunb-ai/gunbc/pull/13633) | Re-land #13255 (3rd conflict) + propose de-hotspotting native_emission | draft / DIRTY | gentle-dove: left out | +| [#13488](https://github.com/gunb-ai/gunbc/pull/13488) | Design: nested optionality census + layer-count carrier (no code) | ready / BLOCKED | gentle-dove: left out | +| [#13265](https://github.com/gunb-ai/gunbc/pull/13265) | Type parameters bind only inside their own declaration (rule 2 / A'; s | draft / DIRTY | gentle-dove: left out | +| [#13632](https://github.com/gunb-ai/gunbc/pull/13632) | dusk qwen 3 | ready / BLOCKED | qwen: jq length(null)->0 fabricated default (DESIGN §5) | +| [#13634](https://github.com/gunb-ai/gunbc/pull/13634) | C2 #13482: the seven review fixes (own-interface entries, lookup-first | draft / BLOCKED | royal-moth: left out | +| [#13482](https://github.com/gunb-ai/gunbc/pull/13482) | C2: typecheck materialization through the local store | draft / CLEAN | royal-moth: left out | +| [#13399](https://github.com/gunb-ai/gunbc/pull/13399) | Move the bottom seam to the leaf std.error_primitives; trim diverges t | draft / DIRTY | sharp-raven: APPROVED (review 76752); needs rebase on main + CI fix | +| [#13426](https://github.com/gunb-ai/gunbc/pull/13426) | Foreign lexer reds: rust ingest bare +, TS single-quoted string body; | draft / CLEAN | sharp-raven: APPROVED (review 76863); base #13425, never ran CI; easy pickup once sharp-raven is folded | +| [#13284](https://github.com/gunb-ai/gunbc/pull/13284) | v2: kernel-String concat through a free-monoid structure bound on the | ready / DIRTY | sharp-raven: APPROVED (review 76877); needs rebase + CI fix; unowned since XL-2 was cancelled | +| [#13630](https://github.com/gunb-ai/gunbc/pull/13630) | Recover callee Arrow through Instantiation-grounding refusal | ready / CLEAN | sharp-raven: BLOCKING §5 fail-open: refused callee reaches eval body (reviews 78331/78359) | +| [#13558](https://github.com/gunb-ai/gunbc/pull/13558) | N7: derive payload-binder facts for construct-field consumption | ready / CLEAN | sharp-raven: base n7/payload-binder-combined-base, no CI (approved) | +| [#13642](https://github.com/gunb-ai/gunbc/pull/13642) | Census: quoted-key brace is not a record field at resolve, not lowerin | ready / BLOCKED | sharp-raven: census, approved but CI red (2 failing) | +| [#13541](https://github.com/gunb-ai/gunbc/pull/13541) | Retire v2.std.algebra filter/any onto the collection roster's callback | ready / DIRTY | sharp-raven: conflicts; 15 root scratch scripts (review 78343) | +| [#13637](https://github.com/gunb-ai/gunbc/pull/13637) | XL-2 3c: enumerate ServiceSetAside consumers before delete-first | ready / BLOCKED | sharp-raven: docs-only 3c census (approved); not merged per docs-only preference; input to unowned XL-2 3c | +| [#13635](https://github.com/gunb-ai/gunbc/pull/13635) | XL-2 cleanup mgr | draft / BLOCKED | sharp-raven: duplicate of #13621 | +| [#13548](https://github.com/gunb-ai/gunbc/pull/13548) | DRAFT: unimported-type import migration (source_reference_repoint) | ready / CLEAN | sharp-raven: plan only, empty roster, REQUEST_CHANGES 78338; reverted from the branch | +| [#13377](https://github.com/gunb-ai/gunbc/pull/13377) | node_query: qualify the Cardinality/Optional references (import-level | ready / BLOCKED | sharp-raven: red: DependencyView import turns off the bare channel (main defect from #13500) | +| [#13430](https://github.com/gunb-ai/gunbc/pull/13430) | XL-2 PR2b: string templates — disjoint lexer, one ^dag_string_template | ready / BLOCKED | sharp-raven: red; tplscan scratch + WIP head (review 78344); waits on #13284 | +| [#13378](https://github.com/gunb-ai/gunbc/pull/13378) | std.unicode.scalar: partial from_code_point (typed refusal) + char_tex | ready / DIRTY | sharp-raven: stack APPROVED (review 76866) but red/stale vs #13453; revive from prep/13378-no-rung-drop | +| [#13623](https://github.com/gunb-ai/gunbc/pull/13623) | Type undeclared lambda actuals from preceding application formals (N7 | ready / CLEAN | sharp-raven: stacked on #13558, red on target (approved) | +| [#13392](https://github.com/gunb-ai/gunbc/pull/13392) | Manual git R0 fixture: request the default -z records its decoders mod | draft / CLEAN | sharp-raven: stacked on red #13378 (approved) | +| [#13390](https://github.com/gunb-ai/gunbc/pull/13390) | Split RFC 3986 §2.1 percent-coding into extdeps.uri.percent_encoding; | draft / CLEAN | sharp-raven: stacked on red #13378 (approved) | +| [#13557](https://github.com/gunb-ai/gunbc/pull/13557) | plans: tie native memory rulings to resumable roadmap milestones | draft / CLEAN | side chat: plans/roadmap rows only; optional | +| [#13613](https://github.com/gunb-ai/gunbc/pull/13613) | Tailscale ACL: admit tag:dashboard to the dusk-1 serving API (tcp:8080 | ready / BLOCKED | side chat: red (AmbiguousBareNameRead in tailscale_acl_witness); unrelated to cutover | +| [#13639](https://github.com/gunb-ai/gunbc/pull/13639) | Deployment risk conformance: one environment model for the repo | draft / BLOCKED | silent-lark: D2 respawn, REQUEST_CHANGES review 78345 (unstated Absent->srv1 fallbacks) | +| [#13217](https://github.com/gunb-ai/gunbc/pull/13217) | Deployment risk D2: role-following singletons resolve the prod-role ho | ready / DIRTY | silent-lark: D2, red floor + DIRTY | +| [#13339](https://github.com/gunb-ai/gunbc/pull/13339) | D2 follow-up: role-singleton observation by host self-report (ssh prob | draft / CLEAN | silent-lark: stacked on #13217 | +| [#13429](https://github.com/gunb-ai/gunbc/pull/13429) | Workspace pack: same-size synthetic transcript placeholders; withhold | ready / DIRTY | superseded by 13442 | +| [#13617](https://github.com/gunb-ai/gunbc/pull/13617) | Key native producer_compiler on the running seed executable | draft / BLOCKED | swift-bat-828 (error): seed producer_compiler provenance; author paused it (v1/CI programme); legacy | +| [#13629](https://github.com/gunb-ai/gunbc/pull/13629) | Floor: reach differential reuses the prepared required_floor authority | draft / BLOCKED | swift-bat-828 (error): v1 floor reach-differential reuse; legacy (the floor is retired by the v1 withdrawal) | +| [#13578](https://github.com/gunb-ai/gunbc/pull/13578) | Generalize hosted OpenAI-compat failures (one classifier, no retry) | ready / CLEAN | swift-ibex: left out (RC) | +| [#13212](https://github.com/gunb-ai/gunbc/pull/13212) | Starter eval set: .dag modeling and DESIGN.md adherence, graded mechan | ready / CLEAN | valiant-crab (unreported): eval set, green and CLEAN, but no PR body and no completed review (its one review failed). Write a body, get a review, then it is an easy fold | + +## Unreported, owned by a live lane + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | + +## Orphans + +No live session owns these. The proposed disposition: a CLEAN, non-draft PR from the last week gets reviewed for folding, and anything DIRTY, drafted, or older than a week is closed. Each one is checked before it's closed. + +| PR | Title | Draft / merge state | Note | +| --- | --- | --- | --- | +| [#13603](https://github.com/gunb-ai/gunbc/pull/13603) | Fix fleet convergence pool display name length | draft / CLEAN | opened 2026-10-09; proposed: close (record remaining work first) | +| [#13565](https://github.com/gunb-ai/gunbc/pull/13565) | ROADMAP #117: Realize node HTTP serve smoke as typed ops, not heredoc | ready / CLEAN | opened 2026-10-08; proposed: review for fold | +| [#13564](https://github.com/gunb-ai/gunbc/pull/13564) | argv dissolution: replace hand-typed id/hostname argv with typed build | ready / CLEAN | opened 2026-10-08; proposed: review for fold | +| [#13328](https://github.com/gunb-ai/gunbc/pull/13328) | recurring_failure_mode: re-scope the native-route row to the package-p | ready / CLEAN | opened 2026-10-05; proposed: review for fold | +| [#13295](https://github.com/gunb-ai/gunbc/pull/13295) | Consolidate cassette joinery, power and shared platform profiles | ready / DIRTY | opened 2026-10-04; proposed: close (record remaining work first) | +| [#13223](https://github.com/gunb-ai/gunbc/pull/13223) | Separate pinned Orca fit-prototype preparation from printer execution | draft / DIRTY | opened 2026-10-04; proposed: close (record remaining work first) | +| [#13218](https://github.com/gunb-ai/gunbc/pull/13218) | Route approval clients to the active store writer | draft / DIRTY | opened 2026-10-04; proposed: close (record remaining work first) | +| [#13149](https://github.com/gunb-ai/gunbc/pull/13149) | Unify printer starts behind ntfy approval, idle checks and durable cla | draft / DIRTY | opened 2026-10-03; proposed: close (record remaining work first) | +| [#12917](https://github.com/gunb-ai/gunbc/pull/12917) | DS4.1 | ready / DIRTY | opened 2026-10-01; proposed: close (record remaining work first) | +| [#12749](https://github.com/gunb-ai/gunbc/pull/12749) | Observe mode for the approval broker: unit, journal, slice cgroup, /li | ready / DIRTY | opened 2026-09-30; proposed: close (record remaining work first) | +| [#12737](https://github.com/gunb-ai/gunbc/pull/12737) | kimi k3 low | ready / DIRTY | opened 2026-09-30; proposed: close (record remaining work first) | +| [#12707](https://github.com/gunb-ai/gunbc/pull/12707) | luna pro low qual | ready / DIRTY | opened 2026-09-29; proposed: close (record remaining work first) | +| [#12691](https://github.com/gunb-ai/gunbc/pull/12691) | Journal initial commissioning and gate workspace supply on committed r | draft / CLEAN | opened 2026-09-29; proposed: close (record remaining work first) | + +## Closed by the closeout + +| PR | Why | +| --- | --- | +| #13648 | Auto-opened on neat-boar-16's archive from stale branch pkg11b-fix: no merge base with main, and the only new commit is .probe_tmp scratch. Its rung drops and projection roster are already on main | +| #13649, #13650, #13651, #13652, #13654 | Auto-opened or re-opened when managers were archived; content is in #13641, or a superseded duplicate (#13654 duplicates #13560) | +| #13656 | deep-crab-89 wind-down WIP of the FQDN follow-up; self-contradictory (review 78372) | +| #13636, #13644, #13646, #13647 | Auto-opened integration PRs. Their content is in #13641 (or, for #13647, deliberately left out) | +| #13638 | Duplicate of #13430 | +| #13552, #13555 | They build the BMC-hosted boot route, which was dropped; the srv13-16 naming is carried by #13570 | +| #13631 | Doesn't compile; the credential model is unresolved; to be restarted from scratch | + +Every branch was kept. + +## Outstanding work that isn't a mergeable PR + +This is work that has to go somewhere even though none of it merges now. Each row names its owner today and what unblocks it. + +### Follow-up code (no PR yet, or the PR is out of the snapshot) + +| Item | Source | What's needed | +| --- | --- | --- | +| #13125 lifecycle fixes: drain disable on retract; drain After/Wants user@.service | bold-bee-114 (WIP head 9f206434, excluded) | A fresh branch without the scratch_tc test; 2 oracle tests fail today | +| #13125 event-driven units are not deployed on srv2 | bold-bee-114 | Cut over from the legacy user timer gunbc-srv2-deploy-belt.timer to the emitted system units | +| #13097 G1 store/belt cutover | bold-bee-114 | Unblocked when #13549 (the T?==T sweep) lands | +| #13513 door-binding stall | bold-bee-114 | A door-grain claim. #13560's door control refuses an isolated module at TranslateTo | +| #13616 real-path receipt | bold-bee-114 | Capture the first srv2 tick after deploy; per-shard reads should be 1 | +| #13632 jq fan-config migration | stern-boar-596 | Fix length(null)->0 with select(type==...); add a pure request seam and a route test | +| Redfish/http.Client onto transport rest (was #13631) | stern-boar-596 | Restart from scratch; decide the credential model first | +| #13626 follow-up: FQDN vs short slot label in the identity probe | stern-boar-596 / deep-crab-89 | DECISION NEEDED: short (-s) or bare hostname. A WIP attempt (#13656, closed; branch session/deep-crab-89-followup @8255a8f1bd) contradicted itself (review 78372). Today the probe refuses fail-closed | +| #13578 dusk-1 Qwen replica + failure classifier | swift-ibex-601 | Fix review 78337: a third placement arm, refusing an absent clock, the stale capacity claim | +| #13430 XL-2 string templates (PR2b) | XL-2 lane (closed) | Unowned. Its REQUEST_CHANGES is stale; it needs #13436's named conversions | +| #13613 Tailscale ACL grant for dusk-1 | side chat | Find the ambiguous name in tailscale_acl_witness | +| #13597 crate partitioner | side chat | Main merge + regen, the remaining acceptance runs, and a PR body update | +| Unit-modeling fix (review 78356): RetryAfterSeconds and QuotaTermSeconds as Second | closeout worker adhoc-1bfd89a3-3eb | In progress; its PR targets #13641 | +| Wave-2 conflicts (#13583, #13582, #13615, #13609) + full regen | closeout worker adhoc-ac536796-f2c | In progress; its PR targets #13641 | +| #13617 seed binary provenance (paused, legacy) | swift-bat-828 (archived) | If revived, fix review 78362: the prose String row v1_seed_binary_provenance_admission_note becomes a typed admission row; retire/repoint gunbc_cli_build_identity_seed_dissolve_trigger and the ROADMAP boundary text that still describe build.rs rustc-env | +| #13595 String-vs-Int control | gentle-dove-36 | Switch #13574's equality control to a TypeMismatch count, then fold | +| Nested-Present Rc-deref emitter fix; derive the expected identities from the cases; the option-C closure-by-prefix note | gentle-dove-36 | Never started (row and specimen are in #13593) | +| #13482 / #13634 C2 cross-run typecheck store | royal-moth-86 | Review 78363: delete the scratch t_tmp.sh re-added on #13634's tip (19c41daf). Then: Switch to local_store_held_session; a real-route control (store_openings=1, hit/miss/committed>0); unset/cold/warm timings; observed_largest_entry_bytes; the live_pool_thread_tests red; the List-completeness question at ~757 files | +| Fleet-converge mint modes for run_cache_object_read/_write | royal-moth-86 | Never started | +| neat-boar-16 unowned follow-ups (7) | neat-boar-16 | (1) The cost shape of the cost_debt_* / reach_base_standings tests; (2) the typed-store snapshot grain, where symbol_index ~1.71 GB per TypeEnv (calm-pike-525/calm-boar-904); (3) the floor_cost_debt_edit conjunct gap; (4) the reach-differential Err-discard; (5) the CI classifier counting cargo network warnings as structural; (6) the Int/Symbol/Char/Filesystem de-forks; (7) the LoweredShape arm frontier. Detail is in memory pending/neat-boar-16-capacity-freeze.txt | +| OCI UsagePart.micros -> e2_micro_instances rename | nimble-heron-805 | Verified locally, never pushed; land it after #13641 | +| OCI A1 shape | nimble-heron-805 | Stays off until the month-to-date meter is observed | +| #13330 derived-node identity step 2 | gentle-dove-36 (transferred from neat-boar-16) | Fix review 76999: inline declaration_ref_is_type_parameter at its one caller in 04_resolve; otherwise sound | +| #13179 checker message prescribes `== Present{..}`, which executes false at runtime | lively-ram-153 | Change the prescribed remedy to match-on-Present or whole-value equality (#13581 has the receipts) | +| lively-ram recorded next units | lively-ram-153 | Deep substitution in infer_frame_instantiated (derivation on #13210); the sole_constructor native wall; route-gap class (b); DP-M2 residue (4 retained-shell one-offs); the argv census recount (WIP session/witty-tern-54, needs a partitioned emitter); the #13257 follow-up | +| silent-lark D2 stack (#13217 -> #13339; respawn #13639) | silent-lark-156 | #13217 needs a main merge (import union in fleet_converge_workflow.dag, regen fleet-converge.yml keeping #13334 lines), then floor phases_failed=0. #13639 needs the three Absent->srv1 fallbacks fixed (review 78345). After D2: a one-time srv1 ownership-marker write | +| silent-lark gaps | silent-lark-156 | walk_cgroup_pending quadratic concat (§6); fabric_storage_file_store has no head listing; rename srv1_gunbc_approval_broker_root to be host-neutral; python/go still refuse capture channels | +| #13488 nested-optionality carrier | gentle-dove-36 | Review 78334 (REQUEST_CHANGES): the body says 'no code' but it changes 31 compiler files; the plan's own 'go' gate is not in evidence; CardOptional { layers: Int } admits 0/-1 (the plan requires OptionalLayers); it silently saturates at 8 layers; peeling Required succeeds instead of refusing. Restart from the plan's OptionalLayers design after an explicit go | +| #13574 interim T?==T wall | gentle-dove-36 / eager-gull-22 | Review 78262: the approval cites escalation msg_66f62924, which can't be found; it recognizes List reads by leaf name, not declaration (§4). Keep or revert is with the operator | +| #13265 type-param scope | gentle-dove-36 | Review 78367: without module-scope params, Result's `ok` binds to an unrelated fn ok (fabricated type). Don't land until binders carry the instantiated field type | +| sharp-raven integration fold | closeout | integration/sharp-raven-357 @82d7624f5a conflicts with #13641 in ~200 files; a dedicated worker folds it after the conflict worker's PR lands | +| srv2 v2-native census/frontier run on main d0f2067 | sharp-raven-357, owed to the operator | Relaunched detached as srv2 user unit v2native-census.service (invocation 43a0c64d4e45, MemoryMax=60G, ~25 min emit+build, then the corpus walk, 3h cap per instrument). Check: `ssh srv2 'systemctl --user status v2native-census; cat ~/rmain/v2native.exits'`; logs ~/rmain/v2-native-census.log and v2-native-frontier.log. Someone must read the result and report the stage-by-stage counts (swift-bat-828 is archived) | +| LIVE MAIN DEFECT: #13500 made bare DependencyView ambiguous | sharp-raven-357 | Any closure with gunbc.roadmap.roadmap_hierarchy_view plus src/v2/lens/unused_parameters.dag or structural_resolution.dag refuses. Fix: full explicit import lists in those two lens files (#13377's partial import made it worse) | +| N7 filesystem_io refuses in normalize (service_realization_unreachable) | sharp-raven-357 | Needs XL-2 3c (3b-ii is not sufficient). Census in #13637; forwarded to smart-gull-336. NOTE: the XL-2 lane is cancelled, so 3c is unowned | +| v2-infer PRs validated only by the v1 floor | sharp-raven-357 | Only #13502 and #13308 execute native binaries. Cutting the floor removes the validator unless the claims move to a native job (smart-gull-336 has been told) | +| Logged follow-ups | sharp-raven-357 | #13436 decimal_digit_of_reduced_units accepts out-of-range Int; #13320 ViaHomomorphism arm admits without installing a conversion; bash_orch_if Rejected=>"" fail-open (partly fixed by #13618) | +| #13608 newer head 8ff94ca471 (deep-ram-343's emit fix: is_known_variant scan -> carried variant_to_enum; predicted emit 1500s -> 100-300s) | eager-gull-22 | Review 78369 APPROVES the newer head (behaviour-preserving). Its CI hasn't run. The snapshot takes c7c3ef7744; fold the delta to the newer head once it's green. #13211 needs it | +| eager-gull closed children | eager-gull-22 | bold-moth, warm-crane, zesty-wren, sleek-koi, sharp-ant and quiet-stag were closed before the no-close directive; their PRs are in this accounting via their re-homing | +| #13548 unimported-type import migrate | sharp-raven-357 | Review 78338: a hand-listed roster of native bare spellings duplicates is_kernel_type/is_container_type; a second import-list scanner duplicates source_reference_repoint's. Consume both instead | +| #13630 callee facts on the underived arm | sharp-raven-357 | Review 78359 (§5 fail-open): a refused contract keeps a denotation, so eval dispatches into the body. Gate eval on DerivedGrounding, or carry the recovered Arrow in a separate field | +| #13541 filter/any retirement (337 files) | sharp-raven-357 | Delete the 15 root scratch scripts (review 78343), rebase, then give it a fresh full review | +| #13637 XL-2 3c consumer census (docs-only, approved) | sharp-raven-357 | Not merged (docs-only preference; plans bankruptcy). It is the input to the unowned 3c deletion that clears N7 filesystem_io | +| #13212 starter eval set (49 cases + grader, about 2.1k lines) | valiant-crab-775 / smart-bear-46 | Green and CLEAN. Needs a real PR body and a completed review; its consumer (who runs the eval) should be named | +| #13579 ancestor-chain binding of import-less bare names | eager-gull-22 | Folded as strictly narrower than main. Under the operator ruling of 2026-10-04 (no ancestor-import leak), the remaining ancestor-chain case should also refuse; that belongs to XL-5's namespace cut | +| #13640 WorkflowRunArtifact.size_in_bytes: Int | eager-gull-22 / quiet-cat-583 | Review 78346: type it as std.measure ByteSize if revived | +| #13391 product-at-scalar refusal climb | eager-gull-22 / sharp-ant-136 | Review 78347: add the whole-corpus census of newly refused sites (including dag/ outside the gate) with dispositions, or narrow the CLIMBED claim to the gated population and declare the rest under §4b(3) | +| session/lively-wren-411 @6194f56012 | bold-bee-114 | Duplicate member_not_a_binder fix, superseded by #13560. No PR; delete the branch | +| wise-koi-396 (shell-dag live-deploy restart-tailscale) | stern-boar-596 | Nothing was ever pushed; the work is unstarted | + +### Operator steps + +| Step | Source | +| --- | --- | +| OpenRouter accessor grant + a live harness_hosted_probe_cli run | swift-ibex-601 | +| claude-code-oauth-harness: the secret, the grant, srv1 custody, a live turn | swift-ibex-601 | +| cursor-api-key-harness and codex-auth-harness: secrets, grants, srv2 custody, a live turn each | swift-ibex-601 | +| Royal-moth credential checklist: R2 cache tokens, boot SA grant, fleet-desired WIF + census row | royal-moth-86 | +| Consume or delete the unused secrets: openrouter -2/-3, groq, opencode | swift-ibex-601 | +| OCI srv1: accessor grant (phone approval) -> host_credential_custody_converge srv1 oracle_oci_api_signing_key -> as root, `gunbc run ... always_free_ensure.dag --function ensure` until it exits 0. A stale lock in /etc/gunbc/oracle-oci is never broken automatically | nimble-heron-805 | +| The browser toolchain on the srv1/srv3/srv4 floor runners (unblocks #13382/#13383/#13411) | lively-ram-153 | +| Fabric storage measurement: transcript_rate_observe, source_pack, checkpoint_measure, restore | warm-badger-442 | +| Native lane records no verdicts on main; a terminal Failed renders as PlannedWithoutTerminalVerdict; the shared FleetSsh host-effect arm | lively-ram-153 | +| srv1-09 floor runner: no cgroup memory limit (HostBudgetUnreadable); floor_class mislabels it 'structural' | stern-boar-596 | + +### Open decisions + +| Decision | Source | +| --- | --- | +| Host self-report transport: A (tailnet /instance.json + WhoIs, 2 grants) or B | silent-lark-156 | +| Fabric-store Move mechanism (A: fenced pull via :10000, exact readback, retire) | silent-lark-156 | +| Remove Group B (srv9-12) from config/convergence: the repo models a fleet that was sold and wiped | valiant-crab-775 | +| Control-plane platform (#13084/#13161) | warm-badger-442 | +| r2_bucket_ensure skips lifecycle convergence when a purpose's rules are [] | warm-badger-442 | +| external_model_scope 2 red on main; quarantine_probe_disposition 2/9 red | warm-badger-442 | +| ObservedReceipt next rungs: in-substrate sha256; an executing EEPROM read | warm-badger-442 | +| Close or keep #13382/#13383/#13411 | lively-ram-153 | +| #13544 Group A decommission is DIRTY: rebase it or fold it | valiant-crab-775 | + +### Lanes not yet reported + +eager-gull-22 is reverting #13574/#13595 and re-gating their integration branches. swift-bat-828 (state of CI) is in an error state and has 2 PRs. neat-boar-16's #12942 is in the mega branch, and its unowned follow-ups are in its memory note `pending/neat-boar-16-capacity-freeze.txt`, which still needs reading. + +### swift-bat-828 branches (checked by a side chat) + +#13505, #13542, #13551, #13562 and #13619 are merged. #13628 is closed. `swift-bat-828/lens-explicit-imports` is identical to main and has no PR, so the branch can be deleted. + +### Falsified defects (do not refile) + +The marshal projected-let drop; negative-literal verdict loss (#13289). Both from lively-ram-153. + +### Archive flush residue + +Archiving a session commits its leftover worktree files as a 'WIP' commit and pushes them. That happened on #13648 (.probe_tmp scratch) and #13634 (t_tmp.sh). Any archived manager's branch may carry such a tip commit. Check the tip before reviving or merging one of these branches. + +## Mega branch status and plan + +- #13653 has been folded (wave-2 conflicts, the #13574 revert, regen; v1_rt.rs is regenerated). #13516 has been folded. +- calm-bee-170 is folding sharp-raven-357's branch, then #13428, #13643, then eager-gull-22's branch, then the final srv1 regen. +- Then: the gate cut (smart-gull-336's new single 'witnesses' job), then #13657 re-verified by sharp-eagle-256. + +Notes for the remaining merges: + +- #13545 (46 T?==T rewrites) overlaps #13600 and #13549 in witness files; either form is fine, keep one that compiles. +- grounded_principal_witness: #13549 derives counts from ci_runner_sudo_binaries (keep that); then DROP #13545's floor_expected_red_chunk_grounded_principal_transcribed_counts (3 entries) if those claims pass. +- #13428 (session/lively-bear-843, green, approved) also folds here; conflicts in 03_normalize.dag and body_lowering_fold.dag. +- #13126 is the grammar-overlap final step; after the fold, the overlap count should be zero. +- #13453 adds base-fact (claim_executor --base-fact) steps to witnesses.yml via compiler_gate_workflow. smart-gull-336's gate cut owns those files; when the cut is merged, its side wins and the v1-floor base-fact steps go with the floor. +- #13643 (census, approved review 78351): fold if its pending check is green at fold time. +- GATE CUT #13658 (smart-gull-336, head e1b851e5a8fb, base main d0f2067): once it's green on its own head, merge it into integration/v1-closeout; its side wins on dag/gunbc/witness/compiler_gate_workflow.dag and .github/workflows/witnesses.yml. OPERATOR RULING (via smart-gull-336): do NOT delete repo_self_slice_cost_control_command. Keep it and the v1_rt list_slice_tests RED/GREEN pair as a LOCAL-ONLY check, named in DESIGN.md's Building & checks local-checks row beside clippy and the v1 unit tests, preserved for later native migration. +- #13658 head e1b851e5a8fb is SUPERSEDED: the operator wants emit-build + the aggregate collapsed into one required job named 'witnesses' that emits and builds both products fresh (restored native-product hits refused). Wait for smart-gull-336's new green head. Merge it into the mega branch AFTER #13653 (or via main if the operator lands it first); take its side whole on compiler_gate_workflow.dag + witnesses.yml, then regen. +- After the cut, no required lane checks generated-artifact drift: run tools.generated_artifact_gate main (remotely) before pushing any wave. +- v1 regen: one emission round (operator 2026-10-04); no fixed-point loop. + +## Notes + +- The plans bankruptcy (#13550) deleted `buganizer-visual-contract.md` and `printed-chassis-program.md`, and that deletion won over edits on our side. Some comments and `sites/README.md` still mention them. +- #13602's green checks are on its current head `f867e9d670`, which is later than the srv1 authorization push. + +## Kept up by request + +- [#13203](https://github.com/gunb-ai/gunbc/pull/13203) Complete Blackjack simulation milestone (branch `blackjack-milestone`) stays open and outside the closeout. diff --git a/docs/plans/v2-self-host-direct-path-2026-09-06.md b/docs/plans/v2-self-host-direct-path-2026-09-06.md deleted file mode 100644 index 51e680ba943..00000000000 --- a/docs/plans/v2-self-host-direct-path-2026-09-06.md +++ /dev/null @@ -1,67 +0,0 @@ -# The direct path to a clean v2 self-host — orientation, sequencing, autonomy (2026-09-06) - -**What this file is.** An orientation and sequencing read, written at one point in time, plus the autonomy contract under which an agent executes it. It is **not** a work-item roster and not a second authority: every fact about what remains lives in the carriers named below and is re-derived by running the instruments named below, never by reading a number off this page (DESIGN §6 — name the instrument, never transcribe its output). When this file and a carrier disagree, the carrier is right and this file is stale. - -**The authorities this file defers to:** - -- DESIGN §7 — the goal itself, and the honesty boundary around it. -- [v2-self-hosting.md](v2-self-hosting.md) — the operator-signed (2026-07-11) Weak → Strong Self Host program: four waves, dependency-gated exits, `src/v1` deleted only at the end of Wave 4. That is the plan from here to there; this file does not replace it. -- `dag/gunbc/roadmap/roadmap_authority.dag` — the node chain with per-node boundary, first slice, RED control, exclusions, handback. Projected to `ROADMAP.md`; every node requires explicit manual acceptance. -- The three self-host frontier carriers: `v2.compiler.self_host.seed_retention_frontier`, `v2.compiler.self_host.emit_coverage_frontier`, `v2.compiler.self_host.native_routing_frontier`. -- `dag/gunbc/guarantee_stall/` — the typed stall roster; `self_host_candidate_generation_add_slice_stall` is the grounding-frontier row. -- gunbc#9664 ("XL-N") — the native-closure burn-down program with go/no-go gates. - -## 1. What you actually get at the end - -In daily terms: **you edit `.dag` files, and the compiler that compiles them was itself compiled from `.dag` files.** The hand-written Rust seed is gone except a pinned, content-addressed bootstrap kernel (the wave doc sizes it at roughly 8–15k LOC); everything else the compiler does — reading source, resolving names, deriving types, translating, emitting — is emitted from the graph it compiles. - -The proof shape, per the roadmap nodes' own RED controls: - -- The generator builds itself **twice**; the second build is shown never to have reached the old compiler — run with the old one taken away, or with a receipt proving it was never called. Behaving the same is not accepted as proof of independence. -- Every self-emitted module passes the same behavioral corpus as the seed it replaced, including the deliberately-broken controls that must still fail. Byte-identity with the seed is explicitly **not** the goal (operator, 2026-07-08) — the seed's warts are not reproduced. -- Required CI runs `v2.test.*` through the native emitted crate; the interpreter survives only for `src/v1` regen until that too is gone. - -The payoff is DESIGN §7's: **language design opens up.** A new guarantee is a row, not a compiler fork; and because the substrate reads one grammar in both directions over many media, that row applies on top of an existing language through ingest — no adoption problem. This is the economic point of the whole exercise: today every improvement routes through the generator we are trying to delete. - -What you do **not** get (§7's honesty boundary): the compiler does not invent unstated intent, does not prove unmodeled external reality, and does not lift arbitrary predicates to proof. Runtime mechanisms — typed refusal, totality, rollback, budgets — remain real at honest boundaries. - -## 2. Where "here" is — by instrument - -Each line names the producer that re-derives the state. Run it; do not believe this sentence. - -- **Axis A — the real path, slice by slice.** The add slice (the `add` fn of `std.integer`) is green end-to-end on the dag path as of #10673: infer derives grounding by declared-inhabitant roster membership, and the ingest→compile round trip is byte-faithful. Instrument: `gunbc run --source-root dag --source-root src/v2 --entry src/v2/test/claim/execution/self_host_candidate_generation_stage_verdicts_test.dag --function add_slice_stage_verdicts_entry`. The **direct-rust-door** (roadmap `v2-emitter-direct-rust-door`) is **green by execution as of 2026-09-06**: the production walk reaches `ArtifactProduced`, all five leaf projections establish (source fidelity byte-equal to `direct_rust_door_expected_source`, producer identity, seed-emitter absence, `SourceProduced` qualification, missing-rule refusal), and the known-red admission row deleted per its own dissolution, promoting the group root to the lane's permanent regression wall. Instrument: `claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/long/direct_rust_door_production_group_test.dag --functions direct_rust_door_production_group_closing_expectation_holds` (a `long/` row — it reconstructs the pipeline per evaluation; the cost defect is declared in the test module's header). -- **The grounding frontier.** The door specimen's frontier is closed, and the parse-product family followed on 2026-09-07: the declared-inhabitant roster-membership derivation widened from the dag roster to the closed ingest set (dag, python, typescript), closing the python and typescript fixtures' frontiers and greening the four same-language round-trips. The add-slice stall's trigger fired and it retired per §4b(4). The remaining frontier is counted, not transcribed: `v2.workflow.compile_door_cause_ownership` `node_grounding_frontier_retirement` names its causes, and `gunbc.witness_v2_native_route` `native_route_frontier_retirement_standing` joins them to the route's observed population. -- **Axis B — the native closure (XL-N).** The XL-0 gate (`cargo check` = 0 on the emitted compiler closure) is **not satisfied**; the issue's own receipt table records that the closure reached complete rustc type checking for the first time at XL-0A and carries the honest error population there. gunbc#10266 (fabricated runtime panics on representation-identical casts) is filed as an independent compiler slice on this path. Instrument (the issue's own): `gunbc compile --source-root dag --source-root src/v2 --entry src/v2/compiler/00_compile.dag --target rust --output-dir /tmp/v2emit && cd /tmp/v2emit && cargo check --message-format short`. -- **Axis C — the seed dissolves.** `v2.compiler.self_host.seed_retention_frontier` holds every retained seed file as a declared row; `undeclared_reason_rows()` is the prioritizable debt, and the census refuses unrostered retention in three directions. `v2.compiler.self_host.emit_coverage_frontier`'s `interpreter_retained_rows()` and `v2.compiler.self_host.native_routing_frontier`'s roster cover the witness-execution axes. The first InterpreterRetained → SelfEmittedNative promotion after classical_not landed 2026-09-07: the add family's row flipped when `emit_host_native_only_add_holds` (+ its wrong-octet broken control) landed in the file-grain-enrolled native-only verdict entry — the emitted add crate's stdout pinned to its expected octet with `eval()` never called, program-side discrimination staying with the family's equals_eval primitive-five/six pair. **The emit coverage frontier closed 2026-09-08: all fifteen roster rows are SelfEmittedNative, `interpreter_retained_rows()` is empty**, each row backed by a native-only verdict arm (positive + wrong-octet broken control) in the same enrolled entry, and the `retained_via_eval_agreement` row constructor was deleted with the last flip (DESIGN §3c). This closes axis (a) — witness-body-runs-native — only; it says nothing about which engine compiled the compiler itself (axis (b), the regen-grain flip, remains operator-gated below). Instrument: `claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/execution/long/emit_host_native_only_verdict_test.dag` (offline wet legs — real cargo build and native run per leg). -- **Wave position.** Per the wave doc, Wave 1's four exit items all carry landed receipts; Wave 2 (first real flips — emit-surface tracks self-emit, behaviorally verify, and **replace** their v1 counterparts) is the open wave. The wave doc's header records that its roster carrier was deleted and per-module dispositions must be re-derived from the emitter — which is what the door-first sequence below does. - -## 3. The sequence from here - -Ordered by the roadmap chain; each step names its green condition as the node's own contract states it. - -1. **The door** (`v2-emitter-direct-rust-door`) — **LANDED 2026-09-06.** The loop's actual findings, for the record: not missing `TargetModel` rows. (a) The grounding frontier closed by six real derivation rules in `04_infer.dag` (see §2's frontier line). (b) Emission needed the right composition, not more rules: a resolved module is a module shell over named declarations, so the production path is `generate_rust_module_emission_candidate` — collect the produced-decl-shaped nodes, admit exactly one, emit through `v2.compiler.emit_produced` — while the raw whole-tree `generate_rust_emission_candidate` remains the fixture lane's composition with its own dissolution trigger. (c) One representation decode gap: resolution canonicalizes surface operators into canonical-operation wire nodes, so the translate value-expression projection decodes the operator position with `canonical_operation_from_wire_node` first and falls to the surface-token table only on a wire miss. Green condition met: the production observation reaches `ArtifactProduced`, all five leaf projections establish, the known-red expectation row deleted in the same change, and the group root promotes to permanent regression evidence (`v2_emitter_direct_rust_door_contract_dissolution_note`). -2. **The parse-product grounding family** (the stall's named trigger) — **LANDED 2026-09-07.** The roster-membership derivation generalized from the dag language roster to the closed ingest set (dag, python, typescript): `infer_node_declared_in_language_inhabitants` returns the declaring authority's roster root as evidence, with deep subtree membership so a declared inhabitant's leaf fact atoms derive exactly as the inhabitant node itself does. Measured: the python fixture's 19-node frontier and the typescript fixture's 28-node frontier both close to zero; all four stall-population round-trip witnesses green; the python→typescript cross-language compile accepts, byte-identical to `ts_source_text`. One §4b(4) flip: `cross_language_compile_refuses_canonical_underived_holds` → `cross_language_compile_python_to_typescript_round_trip_holds` (the pipeline-level frontier guard became the permanent regression control for the acceptance). The add-slice stall's trigger fired, so it retired per §4b(4) — removed from `all_guarantee_stalls`, row file deleted, witnesses stay enrolled. -3. **First behavioral module** (`v2-emitter-first-behavioral-module`). One already-tested module regenerated by the new generator; its existing tests — including the deliberately-broken one — pass unchanged, and the run is shown never to have reached the old generator. First-slice evidence landed 2026-09-07 at the coverage-frontier grain: the add family (fewest dependencies — integer literals plus one canonical operation) carries a native-only verdict witness, so its behavior is established by the emitted crate's own stdout with `eval()` unreachable, and its coverage row reads SelfEmittedNative. What remains for the node is the regen-grain flip — a corpus module's *production* switching generators — plus operator acceptance. -4. **The XL-N milestones, in the issue's order** — arms → 0; emitter defects C, B → 0; crate compiles; native = interpreted with every divergence typed; swap. This is Wave 4's "pipeline runs on emitted Rust" executed as a burn-down, and its rules bind: fixes land in `.dag` and reach Rust via regen, and each closed class gets a `test.claim` fixture under the required gate. Its no-go gates are real: M2 stops if a fix needs a fact the inferred tree doesn't carry (an `04_infer` modeling gap — file it, don't grind); M3 re-plans if a class needs a language capability we don't have. -5. **Native bootstrap, then fixed point** (`v2-emitter-native-bootstrap`, `v1-emitter-fixed-point`). The two-round self-build for the generator, then the same two-round test extended stage by stage to the rest of the compiler. Then `dag/gunbc/v1/v1_deletion_plan.dag` executes; the seed-retention roster drains as modules flip, and Wave 3's first obligation — a derived denominator for what the compiler consists of — is named in the wave doc. -6. **Parallel track, not a wave gate:** the import-deletion ladder (wave doc §3) — B3's migration alongside Waves 2–3, B4's grammar deletion at Wave 4. - -## 4. The autonomy contract - -An agent can execute, end to end and without supervision, the per-slice loop: run the instrument → diagnose from the typed diagnostics → land the `.dag` fix → flip each affected witness per §4b(4) with per-witness justification → narrow the roster/stall rows in the same change → commit, push, open the PR, and iterate to green CI. #10673 is the worked example of that loop. Declaring seed-retention reasons, landing derivation rules, and burning XL-N error classes are all inside it. - -The operator gates that remain, each with its repo citation: - -- **Merges.** The agent never merges; every slice pauses at a green PR for human review. -- **Roadmap node acceptance.** ROADMAP.md's header states every active row requires explicit manual acceptance; a green node is handed back, not self-accepted. -- **CI job roster growth.** Closed without operator sign-off (DESIGN *Building & checks*, 2026-09-04 ruling). XL-N's M5 swap changes what required CI runs on — it is an operator decision by construction. -- **Scaffold admission.** §5: approval is external to the diff; an author cannot self-approve a scaffold. -- **Declared rung drops.** §4b(3): the agent can author the full declaration (previous rung, temporary rung, reason, population, restoration trigger); landing it is review-gated. -- **Language-semantics rulings** that the tree cannot decide. The Atom-authority question is the template: surfaced de-jargoned, decided by the operator (2026-09-06: the namespacing rules answer it), recorded in the carrier. These are rare but real, and the agent's obligation is to stop and raise them rather than guess. - -## 5. What would make this plan wrong - -- An XL-N no-go gate firing (M2's modeling gap, M3's missing language capability) — re-plan at that gate, per the issue's own rule. -- ~~The door's emission diagnostics turning out to be an `04_infer` modeling gap rather than `TargetModel` rows~~ — discharged 2026-09-06: the door is green; the gaps were derivation rules, one emission composition, and one operator-representation decode, none of them a modeling wall. -- A Wave-2 flip surfacing behavioral divergence that re-sequences the module order — typed as emitter bug or interpreter bug either way, but the sequence absorbs it. -- This file aging. It is a position read, not a carrier; the instruments in §2 are the standing truth. diff --git a/docs/plans/wet-evidence-convergence-plan.md b/docs/plans/wet-evidence-convergence-plan.md deleted file mode 100644 index c99f9cb2928..00000000000 --- a/docs/plans/wet-evidence-convergence-plan.md +++ /dev/null @@ -1,123 +0,0 @@ -# Wet-evidence convergence: the anti-collapse count, taken before the extraction - -Two authorities answer "did the wet evidence hold" for one fold. `v2.workflow.local_repo_wet_terminal` -(landed via #9903) joins a co-resident execution's terminals against a scheduled roster; -`v2.workflow.floor_wet_route` (proposed in #9725) validates a transported receipt envelope against -a subject digest pair. `v2.workflow.floor_changed_witness` imports one of them and, under the union, -would import both — the meaning fork DESIGN §3 forbids. - -This document exists for one reason: **a careless unification silently merges typed refusals**, and -the tell is that the extracted join grows one generic "binding mismatch" cause where six real -remedies used to live. A detail string is not a typed cause and cannot be joined on. So the union of -both refusal vocabularies is counted HERE, BEFORE the extraction, and the integrated join must be -able to emit at least as many. - -## The count: 18 distinct causes, deduplicated by REMEDY - -Where the two modules already agree on a remedy the row is listed once, naming both spellings. - -| # | cause | local_repo_wet_terminal | floor_wet_route | -|---|---|---|---| -| 1 | scheduled identity has no terminal | `TerminalMissing` | inside `ReceiptRosterInexact { detail }` | -| 2 | two terminals claim one identity | `TerminalDuplicated { occurrences }` | Rust reader only, untyped in `.dag` | -| 3 | terminal for an identity nobody scheduled | `TerminalUnscheduled` | inside `ReceiptRosterInexact { detail }` | -| 4 | executed a different entry than scheduled | `TerminalForeignEntry { observed, required }` | inside `ReceiptContractMismatch { detail }` | -| 5 | executed a different function than scheduled | `TerminalForeignFunction { observed, required }` | inside `ReceiptContractMismatch { detail }` | -| 6 | observed verdict is not the expected one | `TerminalVerdictNotExpected` | `wet_receipt_unexpected_red_identities` | -| 7a | co-resident: ran against a different prepared subject | `TerminalForeignCandidate { observed, required }` | — (meaningless for this route) | -| 7b | transported: semantic subject digest differs | — | `ReceiptSubjectMismatch { axis, receipt, computed }` | -| 7c | transported: executor contract digest differs | — | `ReceiptExecutorSnapshotDifferent { receipt, computed }` | -| 8 | receipt aged past its staleness budget | — (meaningless for this route) | `ReceiptExpired { age_secs }` | -| 9 | schema version is not the one this floor reads | — | inside `ReceiptContractMismatch { detail }` | -| 10 | executed_at exceeds published_at | — | inside `ReceiptContractMismatch { detail }` | -| 11 | published_at exceeds tree commit beyond declared skew | — | inside `ReceiptContractMismatch { detail }` | -| 12 | outcome wire outside the closed vocabulary | — | Rust reader only, untyped in `.dag` | -| 13 | envelope unreadable | — | Rust reader only | -| 14 | projection missing or orphaned from its authority | — | Rust reader only | -| 15 | projection is not the canonical projection (hand edit) | — | Rust reader only | -| 16 | enrolled expected-red observed passing | — | `wet_receipt_now_passing_identities` | -| 17 | no subject verdict reached | — | `wet_receipt_no_verdict_identities` | -| 18 | completed past its line — a COST, not a defect | `LocalRepoWetCompletedOverBudget` | `wet_receipt_cost_debt_identities` | - -**Post-extraction obligation: the integrated join emits ≥ 18, and rows 7a/7b/7c stay three.** -Collapsing them into one "binding mismatch" is the exact failure this count exists to catch. - -## What the count found, which was not the expected direction - -The collapse is in **floor_wet_route**, not in main's module. Rows 1, 3, 4, 5, 9, 10 and 11 are -typed causes on one side and `detail: String` payloads on the other — seven remedies behind two -string-carrying arms. Rows 2, 12, 13, 14 and 15 exist only in the Rust reader and have no `.dag` -spelling at all. - -So the extraction WIDENS the transported route to main's grain. It does not narrow main's join to -meet it, and any version of this work that reduces main's seven causes has failed rather than -converged. - -## The vocabulary is not ours to mint: `floor_terminal_ledger` already owns it - -`v2.workflow.floor_terminal_ledger` already declares the raw attempt terminal -(`ClaimAttemptTerminal`), observed-verdict versus unreadable-observation (`ClaimObservation`), -expectation (`ClaimExpectation`), the row (`ClaimTerminalRow`), a binding (`LedgerBinding`), and -thirteen derived dispositions. Neither wet module imports it. `floor_changed_witness` — the -conflicted consumer — does. - -Its `claim_disposition` already maps a completed-past-limit terminal by expectation: -`ExpectedToHold → PassedOverBudget`, `ExpectedRed → KnownRedNowPassing`. That is row 18 of the count -above, derived independently in `local_repo_wet_terminal`, derived independently again in -`floor_wet_route`, and owned all along by a module none of the three imported. - -**Three independent arrivals at one distinction is the proven coincidence DESIGN §6 asks for before -unifying vocabulary** — a better argument for the shared terminal than any reasoning about elegance. -So the terminal half of this convergence is REUSE, not extraction, and minting a third terminal type -between the two would be the same fork wearing a new name. - -## What is genuinely new work - -`LedgerBinding` carries a repository snapshot and a prepared subject: the co-resident case. The -transported case additionally needs the executor-contract digest, because a receipt produced by a -different executor over the same tree is a different evidential fact. That is one typed widening of -an existing type — two arms feeding one validation — and not a new authority: - -- co-resident prepared-subject binding -- transported receipt-subject binding carrying semantic subject and executor contract - -Freshness and executor age are meaningless for an execution running inside the floor, so they do not -move onto the local route; the local route's prepared-subject equality is meaningless for a receipt -produced by another process, so it does not move onto the transported one. One interface and one -exact join, two execution realizations, two route-owned roster policies. - -## Scope boundary - -This is an extraction. It changes no behavior of `local_repo_wet_terminal`: its join, its seven -refusals, its deliberately single-armed expectation and its invocation wall come out intact and land -back on the same population. Anything found wrong in that module during the lift is NAMED here and -routed separately, never repaired under cover of a mechanical extraction. - -## What the migration step changed, recorded because the paragraph above no longer describes the head - -The scope boundary above is stated for the EXTRACTION, and it was true of it. The migration that -followed is a different step with a different boundary, and leaving only the earlier paragraph would -leave a reader with a sentence that reads as current and is not. - -At this head `local_repo_wet_terminal` no longer owns a join. Its verdict, expectation, scheduled, -terminal and join types and its seven join refusals are DELETED; the module retains its roster, the -requirement it derives, the co-resident executor and the invocation wall, and it reaches -`wet_evidence_validate` for everything else. `floor_changed_witness` carries one route-neutral -standing over `WetEvidenceValidation`. - -**All seven refusals remain emittable as typed causes**, six under their own names and -`LocalRepoWetTerminalForeignCandidate { identity, observed, required }` as -`WetBindingPreparedSubjectDiffers { identity, observed, required }` — the same three fields compared -by the same equality, because the local requirement sets `prepared_subject` to the candidate. None -became a detail string, which is the condition this plan set for the work to have succeeded rather -than reduced. - -**One declared widening.** `wet_disposition_is_agreement` treats `KnownRedHeld` as agreement, which -the deleted single-armed expectation could not express. No roster row constructs it, so no landed -behaviour moves; it is recorded here because a widening discovered later reads as a defect. - -**Two refusals had no asserting witness** when the migration reached them — -`WetTerminalForeignEntry` and `WetTerminalForeignFunction`, emittable by the shared validator and -covered by the local route's old acceptance matrix. A refusal an authority can construct and no test -discriminates can be deleted with nothing going red, so they are now asserted in -`v2.test.wet_evidence` with both cross-negatives. diff --git a/docs/plans/wet-witness-host-premise-census.md b/docs/plans/wet-witness-host-premise-census.md deleted file mode 100644 index ccc4ff8fc30..00000000000 --- a/docs/plans/wet-witness-host-premise-census.md +++ /dev/null @@ -1,90 +0,0 @@ -# Wet-witness host-premise census (2026-09-29) - -Scope: every module on the local-repo wet schedule (`v2.workflow.local_repo_wet_terminal` -`local_repo_wet_schedule`, 51 modules at this revision), classified per claim for **assumed host -state** under `gunbc.recurring_failure_mode` `wet_witness_keyed_to_the_runner_not_its_subject` and -`wet_witness_premise_read_from_the_runner_it_happens_to_land_on`. Hermetic claims reach no host -effect and are out of scope. This is a read of the sources at the revision this file landed with, -not an execution receipt; the dispositions below say which rows were executed by a fix PR. - -The runner pool: CI runners are fleet hosts. srv1 hosts several runner instances sharing one `/tmp` -and one network namespace, and declares a dashboard instance; srv3/srv4 do not. User managers, -memory headroom and installed tools vary. - -Severity: **RUNNER-KEYED** — the verdict flips by which runner dequeues the job. **LATENT** — the -premise holds on every current runner but is neither created nor supplied by the claim. -**CLEAN** — the premise is created by the claim (per-claim `shell.Mktemp`), supplied as data, or -both arms are asserted. - -## Runner-keyed - -| witness | claim(s) | assumed premise | disposition | -|---|---|---|---| -| `test.claim.spark.fabric_capacity_standing_wet_witness`, `test.claim.spark.spark_pair_serving_apply_wet_witness` | all | executor is off-fleet (no dashboard instance) | repaired: #12478 supplied the premise, #12532 removed the live-log reads | -| `test.claim.compute.manager_unaskable_wet_witness` | `a_host_without_a_user_manager_reaches_the_unavailable_arm_instead_of_aborting` | no user manager answers | repaired: #12456 asserts the route relation | -| `test.claim.approval_ntfy_access_readback_wet_witness_test` | store-root claim | approval store root absent on the runner | repaired: #12614 asserts unchanged-by-the-verb | -| `test.claim.mtcollins1_census_image_local_wet` | `a_failed_or_partial_or_disagreeing_workload_emits_no_token_by_real_execution`, `a_workload_larger_than_capacity_refuses_at_preflight_by_real_execution` | fixed `/tmp/gunbc-wl-counter`, `/tmp/gunbc-wl-dd-marker` writable by this runner user (shared `/tmp`, sticky bit) | open: #12467 (per-claim temp dir), covers both paths | -| `test.claim.spark.pair_serving_d0_front_door_real_execution` | `a_vacated_port_reads_as_no_response_by_real_execution`, `a_failed_connect_fences_under_an_active_head_unit_and_suspends_only_under_a_quiet_one_by_real_execution`, `a_listener_behind_a_failed_connect_is_read_on_the_host_and_fences_by_real_execution` | a just-vacated loopback port stays unbound; `/proc/net/tcp` shows no other runner's listener on it | **owned by crisp-lynx-364** — not touched here | -| `test.claim.fabric.fabric_storage_file_store_wet_witness` | `a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store` | runner executes as a non-root uid (`shell.Chmod.RecursiveReadOnly` does not bind root) | #12658: uid→outcome relation plus an all-uid refusal claim | -| `test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness` | `a_held_observer_is_admitted_and_its_triggered_still_is_hash_bound`, `a_busy_viewer_slot_refuses_the_observer_and_the_handoff`, `a_connection_lost_mid_boot_is_reported_and_no_still_is_claimed_after_it`, `an_unreadable_canvas_is_acquisition_failed_with_the_browsers_words`, `a_relative_observer_directory_is_resolved_once` | browser toolchain converged for the job user (`gunbc.runner_browser_toolchain` `runner_browser_toolchain_here_wet` reads Ready; observed false on srv1 where `runner_browser_toolchain_converge` had never run) | typed refusal: declared in `v2.workflow.local_repo_wet_terminal` `local_repo_wet_premise_roster`, read before invocation, refused as `WetTerminalHostPremiseUnmet` carrying the toolchain authority's words rather than `WetTerminalVerdictNotExpected`; claims unchanged and still refuse the lane. Restoration trigger: keen-pike-73's floor slot class makes toolchain convergence a floor-pool readiness property | - -## Latent — fixed - -| witness | claim(s) | assumed premise | disposition | -|---|---|---|---| -| `test.claim.compute.host_capacity_wet_witness` | `competing_reservations_cannot_over_reserve_and_a_release_returns_the_capacity`, `a_reused_reference_is_a_ledger_refusal_and_never_reported_as_a_full_pool`, `releasing_one_seat_twice_is_not_a_ledger_failure_and_charges_nothing` | runner has ≥2 GiB `MemAvailable` at that instant | #12652: reading supplied (`reserve_with_room`); real route kept by `the_real_reservation_route_admits_exactly_what_the_reading_backs` (relation to the reading) | -| `test.claim.fabric.fabric_storage_file_store_wet_witness` | `a_put_or_advance_by_a_principal_the_store_areas_do_not_admit_is_refused_by_the_real_file_store` | runner uid ≠ 0 | #12658: uid→outcome relation; refusal route on every runner via `a_put_or_advance_into_store_areas_that_refuse_every_principal_is_a_typed_fault_by_real_execution` | -| `test.claim.long.fleet_release_bins_key_witness` | positive/key claims reaching `fixture_env` | runner exports no `release_bins_credential_pattern` name; no ancestor of `$TMPDIR` holds `.cargo/config*` | #12703: fixture unsets those names, pins the temp root under `/tmp` | -| `test.claim.machine_intake.sol_hold_stdin_wet_witness` | `the_collector_stdin_supplies_zero_bytes_and_no_eof_when_executed`, `a_launch_that_cannot_publish_its_pid_stops_the_child_it_created` | launch + `timeout 3` probe (or the supervisor's exit record) lands inside a fixed 5 s | #12704: bounded poll for the awaited line (`read_until_contains`) | - -## Latent — recorded, not fixed (operator decision via deep-ferret-305, 2026-09-29) - -Each row holds on every current runner, so no row flips a verdict by assignment today. "Flips when" -names the runner condition that would make the claim red for a reason other than its subject. A row -here is picked up when that condition is observed or planned for a runner class. - -| witness | claim | assumed premise | why latent | flips when | -|---|---|---|---|---| -| `test.claim.commit_writer_heal_admission_real_execution` | `clean_staged_index_admits_by_real_execution`, `unmerged_index_with_no_markers_refuses_by_real_execution`, `unclaimed_text_blob_with_markers_refuses_red_control_by_real_execution`, `space_and_tab_named_paths_admit_by_real_execution` | the runner user's global/system git config and inherited `GIT_*` env do not act on a temp repo (`seed_witness_repository` sets only `user.email`/`user.name` locally) | no runner user carries such config today | a runner user gains `commit.gpgsign=true`, a `core.hooksPath` whose hooks refuse, an `init.templateDir` with hooks, or exports `GIT_DIR`/`GIT_INDEX_FILE`/`GIT_CONFIG_*` | -| `test.claim.contract_identity.required_ci_epoch_real_execution_witness` | `a_run_of_the_contract_at_another_epoch_refuses_naming_both_epochs_by_real_execution`, `a_run_of_the_contract_at_the_required_epoch_admits_by_real_execution`, `a_commit_that_is_not_in_the_repository_is_unreadable_not_absent_by_real_execution`, `a_readable_commit_without_the_workflow_is_path_absent_not_commit_unreadable_by_real_execution` | same git-config premise | same | same | -| `test.claim.devboot_text_blob_real_execution` | `a_lease_claim_is_stored_as_exactly_its_bytes_in_the_bare_store_by_real_execution`, `a_staged_entry_is_stored_as_exactly_its_bytes_in_the_worktree_repository_by_real_execution`, `a_path_routed_hash_under_a_clobbered_path_publishes_the_empty_blob_red_control_by_real_execution` | same git-config premise (no local identity is set either, so `user.*` must resolve from global config or not be needed) | same | same, or a runner user with no resolvable git identity if a path commits | -| `test.claim.generated_artifact_merge_driver_real_execution` | `divergent_generated_artifact_merge_refuses_by_real_execution`, `divergent_generated_artifact_merge_under_true_driver_silently_drops_theirs_red_control_by_real_execution`, `one_sided_generated_artifact_change_merges_clean_by_real_execution` | same git-config premise, plus no global `merge.*` driver or attribute file overriding the locally configured driver | same | same, or a global `core.attributesFile` / `merge..driver` naming the witnessed path | -| `test.claim.effect_plan_bash_materialize_real_execution_witness` | `effect_plan_bash_fail_fast_prevents_later_operation_execution`, `effect_plan_bash_if_branch_fail_fast_stops_the_branch_and_the_plan` | `/gunbc-wave-a-definitely-absent` does not exist | nothing creates it | anything on the runner creates that root-level path | -| `test.claim.effect_plan_bash_materialize_real_execution_witness` | `effect_plan_bash_metachar_and_newline_cannot_open_a_statement` | `/tmp/absent; printf PWN` and `/tmp/absent\nid -u` do not exist in shared `/tmp` | only a deliberate write creates them | another runner instance on a shared-`/tmp` host (srv1) leaves such a path behind | -| `test.claim.fabric.fabric_storage_file_store_wet_witness` | `a_declared_entry_mode_is_the_published_mode_of_objects_and_heads` | the runner's umask leaves the undeclared mode ≠ `0444` | runners use the default umask 022/002 | a runner runs with umask `0333` or stricter | -| `test.claim.approval_device_routes_wet_witness_test` | `a_bound_realization_passes_the_read_past_the_gate` | a read of the host-global approval store root answers (present, absent or unreadable) rather than aborting | the verdict accepts every answering state | a permission error on that root aborts the route instead of producing a typed body | -| `test.claim.self_host_logic_behavioral_witness` | `self_host_logic_behavioral_receipt_holds` | cargo/rustc present, crates registry reachable or warm, memory and disk for a release build | every runner has the toolchain and a warm registry | a runner without network and a cold `~/.cargo`, or below the build's memory | -| `test.claim.self_host_logic_seed_unavailable_witness` | `self_host_logic_seed_unavailable_receipt_holds` | same, plus `git` | same | same | -| `test.claim.runner.runner_browser_toolchain_real_execution` | `the_library_readback_runs_ldd_and_reads_a_resolved_binary_by_real_execution` | `ldd` present and `/bin/sh` dynamically linked | glibc runners | a musl/static-shell runner, or one without `ldd` | -| `test.claim.runner.runner_browser_toolchain_real_execution` | `a_verified_archive_installs_and_its_retained_bytes_read_back_verified_by_real_execution`, `a_sha256_mismatch_refuses_and_runs_no_install_by_real_execution`, `an_npm_integrity_mismatch_refuses_and_runs_no_install_by_real_execution`, `a_retained_archive_altered_after_install_reads_back_mismatched_by_real_execution` | `curl` on PATH | present everywhere | a runner without `curl` | -| `test.claim.spark.pair_serving_authority_log_real_execution` | `a_release_needs_the_host_observed_quiet_by_real_execution` | `python3` and `pgrep` on PATH | present everywhere | a runner without either | -| `test.claim.spark.spark_pair_head_unit_digest_witness`, `test.claim.spark.spark_serving_offer_route_witness_test` (the claims reaching `spark_pair_declared_head_digest`), `test.claim.spark.v41_row_store_encode_witness` `the_receipt_was_produced_by_the_rendered_program_the_mode_ships`, `test.claim.srv3_install_media_fetch_real_execution`, `test.claim.srv3_seeded_install_media_real_execution` `install_media_remaster_ensure_grub_cmdline_inserts_by_real_execution` | digest claims | `sha256sum` on PATH | coreutils everywhere | a runner without coreutils `sha256sum` | -| `test.claim.spark.v41_engram_runtime_witness` (`the_live_storage_backed_patches_are_digestfile_of_committed_bytes`, `overlay_magic_cites_the_row_store_authority`), `test.claim.spark.v41_source_patch_converge_witness` `digestfile_inhabits_the_committed_patch_population` | committed-patch reads | cwd is the repo root | the wet executor runs from the checkout root | the executor's cwd changes | - -| `test.claim.machine_intake.sol_hold_stdin_wet_witness` | the watch, establishment, notice, release and publication claims #12434 added (`an_end_written_before_exit_is_judged_on_the_final_bytes`, `a_collector_gone_with_the_envelope_open_is_channel_lost_and_frozen`, `an_authentic_client_exit_is_classified_by_its_own_words`, `the_watcher_publishes_a_collector_exit_without_the_boot_watch`, `an_adopted_collector_needs_a_receipt_naming_its_instance`, `the_capture_watch_stops_when_its_observer_dies_and_waits_while_it_lives`, `the_release_signals_only_the_recorded_instance`, `a_publication_failure_stops_even_a_term_ignoring_child`, and the claims reaching `observer_alive` / `owned_release`) | a launched probe publishes its pid or reaches its state within a fixed 1–7 s sleep | runners start a `sh` probe in well under a second | a runner loaded enough that spawn + publication exceeds the fixed sleep; remedy is the bounded `read_until_contains` poll #12704 added for the two stdin claims | -The spark/v41 rows are keyed to their subject (the committed tree) rather than to the runner; they -are listed for completeness. - -## Clean - -`approval_assertion_counter_wet_witness_test`, `approval_device_enrolment_code_wet_witness_test`, -`approval_ntfy_access_readback_wet_witness_test` (decode claim), `compute.attempt_lifecycle_wet_witness`, -`durable_cas_fabric_storage_real_execution`, `durable_cas_file_store_wet_witness`, -`durable_exclusive_hold_file_store_wet_witness`, `eval_model_probe`, -`fabric.fabric_control_plane_wet_witness`, `fabric.fabric_event_log_wet_witness`, -`fabric.fabric_partition_read_wet_witness` (these two leak their temp roots — hygiene, not verdict), -`host_cli_dependency_wet_witness_test` (both arms accepted — vacuous, not keyed), -`live_deploy.approval_broker_helper_readback_real_execution`, `materialization_store_local_wet_witness`, -`megarac_spx_ui_surface_artifact_integrity_witness`, `memory_capture_real_execution_witness`, -`pre_os_capture_replay_witness`, `provenance_calibration_report_real_execution`, -`review_sheet_legacy_declaration_wet_witness`, `self_host_logic_seed_unavailable_check_fixture`, -`serving.serving_availability_bind_wet_witness`, `shell_spawn_refused_real_execution_witness`, -`spark.pair_incumbent_identity_wet_witness`, `spark.pair_serving_d0_authorization_witness`, -`spark.pair_serving_d0_real_execution`, `spark_snapshot_verify_wrapper_local_wet`, -`spark.v41_runtime_candidate_witness`. - -## Absorbing-fallback finding (not a host premise) - -`test.claim.fabric.fabric_storage_file_store_wet_witness` `fresh_store` drops a -`FabricStorageRootRefused` and returns the root anyway. §5 names this an absorbing fallback; queued -with the recorded rows; not scheduled. diff --git a/docs/plans/witness-cost-first-touch-attribution.md b/docs/plans/witness-cost-first-touch-attribution.md deleted file mode 100644 index e3e87c6a6ec..00000000000 --- a/docs/plans/witness-cost-first-touch-attribution.md +++ /dev/null @@ -1,892 +0,0 @@ -# The floor's per-row budget charges shared entry cost to whichever row touches it first - -Finding from the 2026-08-18 runaway census -([measurement](measurements/floor-slow-rows-2026-08-18.md)). It changes what the -six runaways are, and so what may be done about them. - -## The claim - -The budget attributes per row; much of the cost is INCURRED per entry: the first -row to reach an expensive shared computation pays all of it, later rows pay -nothing, and the ceiling refuses the first row for a cost not its own. - -## The receipt — two rows, same computation, 1ms apart - -`dag/test/claim/g2_data_reference_under_selection_witness_test.dag` declares a -deliberate pair: both rows call the same `specimen_projection()`, both are -enrolled, both EXECUTED in run 32172125816, and both are reported because both -are expected-red (failures report regardless of duration): - -``` -18:59:48.653 specimen_classifies_as_runtime_read FAILED in 12 seconds -18:59:48.688 specimen_does_not_classify_as_local_read FAILED in 31ms -``` - -12197ms and 31ms, 393x apart, same call, 35ms apart on the clock. The second row -is not cheaper; it is free because the first already paid. - -Not the only instance: in `test.claim.extdeps_scope_placement_gate_loudness_witness` -exactly one of seven rows is over the warn line at 71244ms; the siblings calling -the same `scope_placement_membership_verdict` on the same fixture are all under -100ms. That row is also the run's worst memory event, `grew rss by 1.00GB` — the -shape of a corpus-scale structure built once, not work repeated per row. - -Precisely: the g2 pair shows directly that a SECOND call is free, which makes the -first row's charge shared rather than its own. The extdeps_scope entry is -consistent with that but does not independently establish it — the floor reports -duration only for rows that fail or cross the warn line, so the six siblings are -known under 100ms but their execution order relative to the runaway is not -recorded, and this document does not assume it. - -## Why this matters for the lane - -The lane's framing is to decompose over-budget rows into small complete -segments. For this class decomposition cannot work, and the repository says so: -`gunbc.witness_row_cost` `witness_decomposition_does_not_reduce_entry_cost_note` -(operator, 2026-08-05) rules that splitting a witness does not divide the entry's -cost but amortizes it across more rows, so the per-row drop is an artifact of the -split. This finding is that prediction observed: splitting one of these rows -moves the charge to whichever fragment runs first and changes no real cost. - -The honest remedies are two, neither a split: - -1. Reduce what the shared computation reaches for — a real cost reduction, - which moves the entry total and not just the attribution. -2. Attribute shared cost to the entry rather than its first row, so the ceiling - stops refusing a row for cost it did not incur — a change to the floor's - measurement, not to any witness. - -## What is NOT available, and why it is worth stating - -The third remedy the refusal text offers — "move it to a designated `long/` -home that CI does not run" — is not available today, and taking it would be a -coverage deletion, not a fix. No cadence executes the long home: the repository -carries exactly two workflows, `witnesses.yml` and `fleet-converge.yml`, and the -falsifier cadence was deleted in the floor cut. The 2026-08-04 operator ruling -(DESIGN, "witness cost derives from purpose") names this exact move as its -originating specimen — a witness relocated under `long/`, removed from discovery, -no cadence row, executed by nothing, admission still green. Relocation would -reproduce it. - -Two files' notes still claim a path-based exclusion that no longer exists. -`required_floor.dag` excludes the long home by AUTHORED MODULE NAME -(`long_home_prefixes`: `test.claim.long.`, `v2.test.long.`, -`v2.test.claim.long.`), deliberately off path because a directory deciding -admission was the defect. `test.claim.rust_test_fixtures_import_closure_witness` -declares no long prefix and no `ReadsLiveTree`, so it is fully enrolled and pays -50.4s per run while its note says it does not run; -`v2.test.manual.grounding_lens_whole_tree` is enrolled the same way, its -`v2.test.manual.` prefix not being excluded. The notes were corrected in -gunbc#8430; the enrollment is real and is counted here. - -## Scope of this document - -A measured finding and its consequences; no mechanism changes. The attribution -repair (remedy 2) belongs to whoever owns the floor's measurement and is not -taken here. - -## Follow-up: the 71-second row is not expensive in isolation - -Measured because the row is 13% of all slow time alone, and "first-toucher or -genuinely expensive?" decides whether paring it is aimed at anything. - -Same head, same two source roots CI uses (`--source-root dag --source-root -src/v2`), `claim_batch` on that entry alone, reading the per-row `[witness]` -CPU line — the quantity the floor's budget compares: - -``` -row run alone red_seed_runner_failure_detail_projects_located_receipt cpu=70ms -cheap sibling alone red_membership_fixture_names_change_and_path cpu=0ms -both, in order red_membership 0ms, then red_seed_runner 85ms -``` - -Against CI's `cost is exactly 71060ms against 1552ms` for the same row. Both are -per-row CPU at the same head, so comparable: **70-85ms isolated, 71060ms on the -floor — about a thousandfold.** - -The difference is evaluation context. The sandbox resolves this entry's own -closure (67 modules, 1425 resolved items); the floor evaluates every row against -one whole-corpus prepared subject (2376 modules). The row's cost is CONTRIBUTED -BY THE CONTEXT, not by what the witness computes — it computes almost nothing, -and the sibling reaching the same `scope_placement_membership_verdict` costs 0ms. - -Consequence for the lane: remedy 1, "reduce what the shared computation reaches -for", has almost nothing to bite on here — paring would aim at 70ms of work to -move a 71-second bill not made of it. - -A candidate mechanism, not asserted: this row reaches its companion by NAME -through the seed runner (`run_claim_failure_receipt` → `run_in_context` → -`ctx.lookup_fn`), and the prepared-floor scope is a flat whole-corpus namespace -in which 378 helper names collide and bind last-write-wins (gunbc#8437). A -name-keyed lookup is the one thing this row does that its cheap siblings do not. -A lead for whoever holds the prepared-floor scope, untested here; the measurement -stands without it. - -Sandbox caveat, so the number is not reused wrongly: single-entry `claim_batch` -is fixed-cost dominated — its whole-tree graph-facts phase alone is 18-21s and -its resolve-split load term ~25s — and the repository already rules that such a -harness characterizes itself rather than the floor. Hence the comparison is on -the per-row `[witness]` CPU line, not process wall. An earlier attempt compared -process wall, made the cheap sibling look like an 82-second row, and established -nothing. - -## The eight-sibling test: identity does not predict cost, position does - -`v2.test.claim.effect_reach_test` carries eight rows landing within a 346ms band -just past the ceiling on the floor (1954-2122ms). Two obvious readings — one -shared fill with seven free riders, or eight genuinely expensive witnesses — -predict opposite things under isolation. Measured, with the order-reversal -control that distinguishes them: - -``` -row alone fwd(pos) rev(pos) -path_data_init_derived_host_reading 246 252(1) 511(8) -path_data_init_red_when_import_severed 217 441(2) 488(7) -path_touch_selects_on_normalize_path 225 497(3) 459(6) -unrelated_path_does_not_select 244 517(4) 518(5) -hermetic_fixture_stays_local 241 589(5) 446(4) -prose_string_path_does_not_classify 251 498(6) 421(3) -concat_built_path_frontier_not_classified 247 496(7) 385(2) -live_03_normalize_witness_derived_host_reading 234 514(8) 231(1) - -by position, mean of both orders: - pos 1: 242 pos 2: 413 pos 3: 459 pos 4: 482 - pos 5: 554 pos 6: 478 pos 7: 492 pos 8: 512 -``` - -**Neither reading is right.** Alone, all eight cost 217-251ms — a 34ms spread -around a 238ms mean, as uniform as this harness measures — so not eight -independently expensive witnesses. But the first row in a batch is the CHEAPEST, -the opposite of a shared fill with free riders. Cost tracks position, not -identity: `path_data_init_derived_host_reading` costs 252ms at position 1 and -511ms at position 8; `live_03_normalize_witness_derived_host_reading` 514ms at -position 8 and 231ms at position 1. Same rows, closure, head. - -### The step is a claim_batch artifact, not a floor cost — corrected - -An earlier revision called that step "a second attribution defect", implying the -production path. It does not belong to the floor; three independent checks say so. - -**Source.** `claim_batch` `run_entry_group` builds ONE ctx per entry group, runs -every function in the group against it, and calls `eval_call_memo_frame_exit` -after each — position 1 meets a virgin context, positions 2..N one that has had -an eviction pass. The floor's claim-evaluation fold in `cli_run.rs` builds -`evaluation_frame` FRESH INSIDE the per-claim loop, stating why in source: claims -sharing one immutable scope must not share a context's mutable evaluation caches. -The floor deliberately lacks the mechanism that produces a once-off step. - -**Cross-entry measurement.** Batching two different entries shows no step — each -row costs its alone figure in either order, since each entry gets its own ctx: - -``` - effect_reach row root_d row -alone 239 1774 -batch [effect_reach, root_d] 229 1802 -batch [root_d, effect_reach] 219 1732 -``` - -Within one entry the step reproduces (235ms then 396ms for two rows of one file), -so the effect is bounded by the entry group — a property of the shared ctx, -exactly where the source says. - -**Floor data.** With the same step, later rows within an entry would cost about -twice the first. Across the 32 modules in this run with five or more uncensored -slow rows, the mean rest-to-first ratio is **1.27**, spanning 0.75 to 3.48 — -several modules have a first row MORE expensive than its siblings, the -first-touch shape and the opposite of the step. A shared mechanism would cluster -near 2.0. It does not reproduce. - -So the position step is a fact about `claim_batch` and says nothing about floor -attribution; the additive-versus-multiplicative question it raises is not about -the production path. - -### What this did establish: the first confirmed paring target - -`root_d_checkpoint_scalar_declared_arity_witness_holds` costs **1774ms of its -own work** alone, in a closure of 5 modules and 160 resolved items — far too -small for the cost to be context; it is the witness's own evaluation. The floor -attributes 2792ms, so unlike every other row examined here the gap is modest — -about 1.6x, not a thousandfold. - -It is the first row in this lane whose expense survives isolation, hence the -first genuine subject for the operator's decomposition brief. It is also the -most stable over-budget row across runs (2792 / 2849 / 2855, a 2.3% spread, -against 22-30% for the large ones) — what a real cost looks like and an -attribution artifact does not. - -## The stability screen: a sound negative filter, not a paring worklist - -`root_d`, the one row whose cost survived isolation, was also the most stable -across runs (2.3% spread where the large rows swung 22-30%). That suggests a -cheap screen over all 720 slow rows — rank by cross-run variance rather than -magnitude — since a witness's own cost should not care what else ran, while -context, fill and position costs vary with everything around them. Magnitude -sorts mostly by who touched a cache first, so a principled screen would be the -first way to build the paring worklist without 720 isolation runs. - -Built over four runs (32172125816, 32177951514, 32185058245, 32187164199), -joined by identity, 650 rows with three or more usable observations. - -**Censoring rule, and it decides the result.** An observation is dropped when -the row was explicitly interrupted (`cost is at least`) or sits in the -poll-pinned band — those values are the deadline, not a cost. Budget-refused -rows that COMPLETED are kept: they carry a real measurement. An earlier cut -dropped every budget-refused row and thereby excluded `root_d`, the 71s row and -all eight `effect_reach` rows — every row worth screening — which is over-broad -censoring when the expensive population is exactly the refused one. - -**Validation.** Three rows with known ground truth from isolation: - -``` -row CV mean percentile known truth -root_d 1.5% 2854ms 18 real own work (1774ms isolated) -71s extdeps_scope row 7.8% 76988ms 86 context (70-85ms isolated) -g2 specimen row 7.4% 13292ms 84 first-touch -``` - -Two of three land correctly: the context-dominated rows in the variable head, -`root_d` in the stable tail. - -**But it fails the fourth case, and the failure is the finding.** The eight -`effect_reach` rows come out STABLE — CV 1.9% to 6.4% around a ~2050ms mean — -though isolation established they do about 238ms of their own work. A row can be -highly stable and still carry an order of magnitude of cost not its own, because -the context it carries is itself reproducible run to run. - -So the screen measures REPRODUCIBILITY, not ownership; only the second is what -paring needs: - -- **Sound as a negative filter.** A high-variance row is certainly not measuring - its own work, so the variable head can be excluded from a paring worklist - without isolation — a real saving over 720 rows. -- **Unsound as a positive test.** A low-variance row may be real work (`root_d`) - or stable context (`effect_reach`). Only isolation separates them; the screen - must not be presented as producing the worklist directly. - -The honest procedure is two-stage: screen out the variable head for free, then -isolate the stable tail. The stable tail above 1000ms is small enough for that -to be affordable — the actual saving on offer. - -## Cluster-tightness: also suggestive, also not decisive - -If N distinct witnesses — different names, assertions, modules — land within a -few percent of each other, the figure looks like it belongs to something shared, -since independent work has no reason to coincide. Over the four-run join, rows -above 800ms fall into 19 clusters at 3% width; one is remarkable: **18 -identities across 4 unrelated modules** (roadmap static site, running-release -identity, deploy readiness, site-surface readiness) inside a 2.3% band at -1213-1242ms. - -It correctly flags the `effect_reach` six at 2016-2067ms, which isolation showed -collapse to ~238ms — one confirmed true positive. - -**But the 18-row cluster does not collapse.** One member isolated from each of -its four modules: - -``` -module floor isolated closure -roadmap_static_site_witness 1213 888ms - -running_release_identity 1216 791ms 630 modules / 15657 items -live_deploy.readiness 1218 819ms 632 modules / 15656 items -roadmap_site_surface_readiness 1218 827ms 632 modules / 15640 items -``` - -Two-thirds of each floor figure survives isolation: not eighteen rows riding one -fill — each still pays most of its cost alone. So cluster-tightness is a -suggestive prior, not a decisive test — the same standing as the variance -screen, for the same reason: both observe the SHAPE of a cost distribution, and -neither can see whose work it is. - -**The run did surface a better discriminator than either.** Cost against closure -size: - -``` -root_d 1774ms over 160 resolved items dense -cluster members ~820ms over 15,650 resolved items sparse -effect_reach 238ms over 2,694 resolved items sparse -``` - -`root_d` does an order of magnitude more work per resolved item than anything -else examined. That density — not magnitude, not stability — marks its cost as -its own evaluation rather than its surroundings. - -**Not settled by this run, stated so it is not assumed:** whether the cluster's -residual ~820ms is assertion work or per-process warm-up of a 15,650-item -closure the single running row pays alone. Distinguishing them needs two rows of -one module on a harness without `claim_batch`'s shared-ctx step, which does not -exist today. Until then the honest reading is that ~820ms is *not shared across -the eighteen* — not that it is assertion work. - -## root_d: the cost is one claim's fixture import, not the witness - -`root_d_checkpoint_scalar_declared_arity_witness_holds` is one `test fn` -conjoining three claims, two calling `compile_dag_rust_emit_check` on a small -virtual module. Measured by splitting them temporarily and running each in -isolation (5 modules, 163 resolved items): - -``` -claim verdict cpu -positive fixture (declared-arity leaf) PASS 36ms -arity-0 scalar still strips phantom arguments PASS 1782ms -authority answers for both fixtures PASS 0ms -``` - -**All three pass**, and one claim carries essentially the entire 1774ms. Two -things follow; the first corrects a pre-measurement assumption. - -**The expensive claim is not the one carrying the defect.** The row is enrolled -in `floor_expected_red`, inviting the reading that its positive fixture is red -and expensive. It is neither: the positive fixture costs 36ms and passes. The -row passes as a whole and is a stale quarantine, consistent with the floor's -`stale_quarantine=5`. - -**The cost is an import in a fixture string, not an assertion.** Both -expensive-looking claims call the same checker on a two-line module. The -difference between 36ms and 1782ms is that the second fixture's source begins -`import std.integer { Int8 }`, so the emit check compiles that closure, while -the first declares its `Witness` inline and imports nothing. The 1774ms is -compiling `std.integer` inside a virtual fixture — not proving anything about -checkpoint-scalar arity. - -**Why no split is proposed here.** Splitting would move the 1782ms onto a new -identity not on the expected-red roster, turning a budget-refused quarantined -row into an ordinary over-budget failure — worse than before. The decomposition -is only correct together with the fixture fix, and that fix is a judgement -about whether the arity-0 wall can be witnessed by a scalar that does not drag -`std.integer` in — Root D's owner's call, not a cost lane's. Recorded with the -measurement so that decision rests on facts, not the row's size. - -So the corpus's one row that survived every screen is also not a -witness-decomposition subject: its expense is a fixture-authoring choice with a -localised cause and a plausibly cheap fix. - -## Reconciled against the floor-path fill ledger (gunbc#8464) - -The shared-fill ledger instruments corpus caches on the floor path and reports, -per fill, cost, payer, and readers. Its run 32192150969 (9425 claims, 19 fills) -settles two of this document's rows at identity grain; one corrects a claim here. - -**First-touch, confirmed on the production path.** `unbound_dissolution_empty_literal_refuses` -— one of the six runaways — paid a 51508ms `module_path_index` fill that **139 -claims across 29 modules** then read: 99.8% of the row's measured cost belongs to -a computation twenty-nine other modules consume, observed on the floor rather -than inferred from a sandbox. The g2 pair's shape at corpus scale, and the run's -only shared fill. - -**But the 71-second row is the opposite, which refines what this document said.** -It paid a 29017ms `module_graph_facts` fill and a 24965ms `reference_edges` -fill, **both exclusive** — no other module reads either. Its cost is genuinely -its own, and the earlier framing, "a cheap witness billed for the context it -runs in", is not quite right. - -The measurements reconcile once the variable is named. Isolated in a 67-module -closure the row costs 70-85ms; on the floor's 2376-module subject, 71s. The -witness did not change — it asks for whole-corpus facts, so its bill is a -function of corpus size. Neither a cheap row wearing someone else's cost nor an -expensive assertion: a witness whose subject IS the corpus, small in isolation -and large on the floor. Isolation missed this because shrinking the corpus is -exactly what makes such a row look cheap. - -That distinction decides the remedy, and it inverts between the two rows: - -- the 51.5s `module_path_index` payer must NOT be quarantined — moving it - recovers ~112ms and hands 51.5s to whichever of the other 29 modules runs - first. The fix is to hoist that fill into preparation, where its consumers - already are in spirit and it stops being any witness's bill. -- the 71s row's fills are wanted by nothing else, so moving or reducing it - recovers the whole amount. - -Per-row wall time does not distinguish these cases; nothing in this document's -census does — the strongest argument that the ledger is the instrument the -population needed, and the honest limit of a census built from timings alone. - -**And a floor of ~108s no paring can reach:** three fills — a 55392ms module -graph, a 35060ms path index, a 17646ms reference-edge scan — are paid before any -claim runs. Quarantining every witness would not move them. - -## Retraction: the variance screen measured four different trees - -External review on the subtree rollup caught a defect in the screen above, -unchecked by author and reviewer, fatal to the statistic as stated. - -**The four runs have four different heads, on four different branches:** - -``` -32172125816 6aa80c79 work/last-two -32177951514 59653f56 main -32185058245 178db9ad session/crisp-wren-479 -32187164199 ccf21913 floor/cost-is-not-a-defect -``` - -No pair is a repeat measurement of one subject. Repeatability requires the same -subject digest, claim code, manifest and order, budget and poll policy, and -source universe; these share none. So every coefficient of variation above is -**agreement across four different worlds, not repeatability under identical -input** — and this lane already established that changing the source population -moves both instrument sensitivity and first-touch ownership, exactly the -variable left uncontrolled. - -`root_d`'s 1.5% figure therefore did not establish what it was used for. -Isolation vindicated the row independently; the statistic did not earn that -agreement and should not be cited as if it had. - -**The negative direction fails too.** The screen was demoted to a "sound -negative filter" after `effect_reach` came out stable. Review rejects that half -as well: an intrinsically expensive claim can swing 20-30% from host scheduling -and frequency variation, page-cache state, allocator behaviour, subprocess -startup, lock contention, input-dependent branch and allocation shape, or a -nested shared fill reached during otherwise genuine work. High variance does not -establish context-only, and the variable head cannot be dropped for free. - -And stable context cost is not the accident this document treated it as. Claim -order is deterministic, so the same identity always first-touches a given cache; -corpus and cache key are deterministic; every process starts cold; the same host -class recurs; a cooperative deadline pins observations near one polling boundary. -Under those conditions a shared cost is *expected* to look stable. -`effect_reach` is the normal case, not a freak counterexample. - -**What survives.** Variance is legitimate PRIORITISATION — high variance: investigate -order and context sensitivity early; low variance: repeatable-cost candidate — -and never evidence of ownership either way. The minimum evidence stack for -ownership is three things together: an uncensored completion rather than an -interrupt-point value, repeated runs on the SAME subject, and an order -perturbation showing whether the charge moves to a different first toucher. The -reversal control earlier in this document is the third; the second this lane has -never performed. - -## The error bar on a single floor observation, measured - -The lane had never measured one subject twice. A GitHub re-run replays the -pinned merge ref, so re-running a completed run yields two observations of one -world — every row measured twice. Run 32182126916, attempts 1 and 2. - -**Same world, verified:** identical subject digest `b0eb3f2be1a200e0` and -identical counters on both attempts (planned 9420, executed 9420, passed 8951, -failed 0, budget_refused 102). *(Capture the log BEFORE triggering the re-run: -GitHub serves only the latest attempt and the earlier archive is unrecoverable. -This pair exists only because attempt 1 had been downloaded hours earlier for an -unrelated check.)* - -**Paired per-identity delta**, on the 673 rows uncensored in both attempts — -not marginal distributions, because opposing moves cancel and two histograms can -look identical while every row moved: - -``` -median |delta| 3.8% -p95 |delta| 12.7% -p99 |delta| 16.4% -largest moves -5% on the 93.8s row, +12% on the 16.6s row -``` - -So a single floor observation carries a real error bar: about 4% typically, over -12% at the tail, on an identical subject. - -**Crossings of the 1552ms ceiling: zero.** Spread and crossings are different -facts; only the second is the enforcement question — a 200ms move on a 400ms row -is irrelevant, at 1500ms it flips a build. Across 673 genuine rows measured -twice, none changed sides. - -**Not because the instrument is precise — because the danger band is nearly -empty.** Only **4** genuine rows sit within the p95 flip-radius (191ms) of the -ceiling; the closest: - -``` -margin 29ms (1.9%) 1523 -> 1517 where_refinement_cast_literal_oci_other_digest_algorithm_accepts -``` - -That margin is **below the median noise** of 3.8%, so it is the corpus's one -materially flip-prone identity. It is also, independently, the exact row -sharp-raven-273 observed at 1505ms then 1274ms on a re-run of one frozen ref in -the 1500ms-ceiling era — a 15.4% move, near this distribution's p99. Their flip -is the predicted behaviour of the closest-to-threshold row, not an anomaly; two -lanes located one identity by different routes. - -**What this settles.** The ceiling is enforceable as a per-run threshold on -today's population: enforcement risk is concentrated in a handful of rows, not -diffuse across the corpus. It is not safe in general — any row resting within -~200ms of the ceiling inherits a pass/fail partly a property of the runner, and -so does a single-run quarantine decision. - -**Caveat that changes the reading, nearly unstated.** The rows *closest* to the -ceiling overall sit at 1553ms and did not move between attempts — poll-pinned -censored values, not measurements, so they cannot cross by construction. -Including them makes the instrument look far steadier than it is. Every figure -above excludes them. - -### Rule, not caveat: exclude censored values before computing any distribution - -Three shapes of one class turned up in one day on this log — `cost is at least` -vs `cost is exactly`, the `BUDGET-REFUSED` third channel, and the poll-pinned -1553ms band inside a paired distribution. Each flatters: the instrument looks -steadier, or the population smaller, than it is. So this is a standing step, not -a thing to remember when suspicious: **before computing any statistic over floor -timings, drop every right-censored value, say how many were dropped, and name -the channel each value came from.** The channel is part of the datum because -the three are not equally trustworthy and no field distinguishes them once the -number is extracted: - -``` -BUDGET-REFUSED an interrupt -> right-censored, no upper bound -cost is at least an interrupt -> right-censored, lower bound only -cost is exactly a completion -> a measurement -[floor-witness-slow] elapsed -> a measurement (fires at the 100ms warn) -a poll-pinned 1553ms with 1ms margin -> neither; it cannot move by construction -``` - -(The first three spellings are the pre-#9599 rendering, kept because this -document's figures were parsed from runs that emitted them. Current spelling: -`cost=UNMEASURED` with a separate `interrupt_point=` field for an interrupt, and -`cost={n}ms EXACT` for a completion — `ClaimOutcome::budget_figure_phrase`. That -is this rule made structural: the bound no longer occupies a measurement's field, -so it cannot be extracted as one.) - -(The channel refinement is sharp-raven-273's, and the right generalization: "how -many were dropped" hides that the dropped rows failed in different ways.) An -interrupt is not a measurement and a pinned value cannot move by construction, -so neither answers a question about movement in either direction — they are -unmeasured, a third state from pass and fail. - -## Is the noise relative or absolute? It is relative — so a flip radius scales - -Decides whether the 1552ms result transfers to a lower ceiling. Mean paired -movement, bucketed by row magnitude over the 672 genuine rows: - -``` -magnitude n mean |rel| mean |abs| -100-250ms 391 4.9% 7.9ms -250-500ms 142 5.4% 19.3ms -500-1000ms 109 4.3% 29.7ms -1000-2000ms 30 2.6% 30.4ms -``` - -Absolute movement grows nearly 4x across the range while relative movement stays -flat. The noise is **proportional**, so a flip radius must be a fraction of the -ceiling, not a fixed millisecond figure carried across. Carrying the 1552ms p95 -radius (210ms) down to a 500ms ceiling would overstate that band by more than 3x. - -## What each candidate ceiling costs, measured - -p95 relative movement is 13.5%, so the flip radius at ceiling C is 0.135*C. -Over the 672 genuine paired rows: - -| ceiling | p95 radius | rows in band | CROSSINGS observed | rows already over | -|---|---|---|---|---| -| 500ms | 67.7ms | 53 | **11** | 139 | -| 750ms | 101.5ms | 40 | **9** | 62 | -| 1000ms | 135.3ms | 24 | **3** | 30 | -| 1552ms | 210.0ms | 4 | **0** | 0 | - -Crossings are observed, not modelled: each is a row that landed on opposite -sides of the ceiling in two runs of one identical subject. - -**Not scattered coin flips — they cluster by module.** Six of the eleven 500ms -crossings are one module (`generic_item_clone_bound_witness`, every row -476-491ms then 502-528ms), two more `where_refinement_enforcement_witness`, two -the python round-trip pair. Rows in a module share a closure and move together: -a ceiling in a crowded band refuses a whole module at a time on a runner's luck. - -**So a 500ms ceiling is not a marginal adjustment.** It refuses 139 rows on the -first run for being over, and puts 53 more in a band where 11 were observed -changing sides between two runs of one tree — a build's outcome partly a -property of which runner picked it up. 1000ms is the first candidate where the -band thins (24 rows, 3 crossings); 1552ms has no crossings at all. - -This does not say the corpus should stay slow — the ceiling cannot lead the cost -work. Rows must leave the band before the ceiling descends onto it, the ordering -#8470 demonstrated: the 45941ms row became 844ms by moving a shared fill into -preparation, with no change to the ceiling. - -## Audit of the two quarantine-decision runs - -Both logs retrieved complete (terminal counter line present in each — not the -CLI-truncation class): - -``` -run 32189985063 planned=8995 executed=8995 passed=8636 failed=1 budget_refused=0 - FAIL emitted_lib_rs_module_declaration_witness_test.small_crate_lib_rs_omits_unemitted_dispatch_module - cost is exactly 42545ms against 1552ms (27.4x the ceiling) - -run 32193032348 planned=8997 executed=8997 passed=8638 failed=1 budget_refused=0 - FAIL emitter_nested_refinement_cast_witness_test.single_refinement_carrier_emits_no_unsupported_cast - cost is exactly 45826ms against 1552ms (29.5x the ceiling) -``` - -Neither decision is marginal: flipping a 42545ms observation needs a 96.4% drop -against a measured p99 of 17.7%. Both quarantines are clean on the evidence, and -both rows are the first-touch fill carriers this note is about — the second is -the row #8470 later brought to 844ms without touching the witness. - -One discrepancy recorded, not resolved: each run's terminal counters report -`failed=1, budget_refused=0`, so these logs contain two measured verdicts, not -the eight-plus-one this audit was scoped to. The remaining identities are not in -these runs' counters; their provenance needs naming before they can be audited -or cleared. - -## The ownership instrument now exists, and this document's whole method is superseded - -Everything above infers ownership from timings — variance screens, cluster -tightness, isolation runs, order reversals — and concludes, correctly, that a -census built from timings alone cannot see whose work a cost is. That limit is -now lifted, and by an instrument the floor already emits on every required run. - -`claim_executor` turns `GUNBC_RECOMPUTE_TRACE` on unconditionally, and the -required floor writes **`required_floor_cross_claim_demand.tsv`** beside -`required_floor_claim_cost.tsv`. Its row is a producer identity with the count of -claims that demanded it, the count of times it was actually evaluated, the -modules it spans, and its declaration site. A producer with claims equal to evals -was re-derived once per claim and shared with nobody — the question every screen -in this document was a proxy for, answered directly and at identity grain rather -than inferred from a distribution's shape. - -So the standing procedure for this class is: read the two artifacts, not the log -and not a sandbox. Both are named in the log's own announcements, with their -caveats in band — `[cross-claim-demand]` states that its cost column is -inclusive of callees and must not be summed, and that its printed head is a -preview ordered by recomputation rather than a candidate roster. - -**Both log renderings are capped at 25 rows and both say so in a trailer.** That -trailer is load-bearing: on run 33668368846 the `[over-cost]` head carries 25 -rows while `required_floor_claim_cost.tsv` carries 295 over the 100ms line. A -census taken from the log is the log's ranking read as the population — -`instrument_output_read_as_subject_content`, observed twice on this lane. - -**And a third truncation lives in the ARTIFACT, not the log, which is why it is -the dangerous one.** `required_floor_cross_claim_demand.tsv`'s `module_sample` -column caps at **8 modules**; on run 33668368846, **1044 of 26317 rows** carry a -`modules` count above that cap, so their consumer list is cut. Unlike the two -print caps this one has no trailer — the row simply lists eight modules beside a -`modules=71`, and a reader joining producers to consumer modules gets a silently -partial answer. - -It bites exactly where the join matters: the producers whose sample is truncated -are the WIDELY SHARED ones, which are precisely the producers most likely to -explain why a whole module's rows cluster. So **a corpus-scale join from module -to shared producer is not performable from this artifact today** — the per-module -reading below is sound only where the join was performed by hand, one producer at -a time, and a 73%-of-over-line-CPU classification reported off the step ratio -alone is a step-cluster inference at that strength rather than an ownership -result. Widening or inverting that column is named here as an obligation and -deliberately not undertaken in this lane. - -Three truncated renderings on one lane is a pattern rather than three accidents, -and the shape they share is the reusable part: **an instrument that caps a -collection reports the cap honestly at the top level (`modules=71`) while the -column a consumer actually reads silently answers for eight.** Check the cap -against the count before joining on any list-valued column. - -### Eval steps, not milliseconds, is the within-module discriminator - -`required_floor_claim_cost.tsv` carries `eval_steps` beside the two clocks. Steps -are deterministic where a millisecond is not, so a step figure cannot be explained -by a quiet runner — which is exactly the confound that demoted the variance screen -and the cluster-tightness prior above. - -**What `eval_steps` measures is WORK, and work is not OWNERSHIP.** Rows landing at -near-identical step counts are not thereby shown to share a producer; that is a -step-cluster inference, and the same total can arise from unrelated derivations of -similar size. Ownership is established by the cross-claim demand artifact, which is -keyed by PRODUCER IDENTITY and says which claims actually reach the same producer. -The shared-derivation conclusion is a JOIN of the two instruments: demand identifies -the shared producer, and `eval_steps` then characterises how much evaluation work -the claims reaching it perform. - -Read that way on run 33668368846's over-line population, the majority of the CPU -sits in modules where the demand artifact names a common producer AND the rows -agree on steps to within a few percent: shared derivation, where -splitting re-attributes the fold and changes no real cost -(`witness_row_cost` `witness_decomposition_does_not_reduce_entry_cost_note`). - -The remedy for that bucket is enrolment on -`v2.workflow.floor_pure_producer_share`, whose admission criterion is measured -serve cost below measured recompute cost — the demand census answers only the -recompute half, and a present-versus-absent join of `required_floor_claim_cost.tsv` -across two runs answers the other. A row that fails the serve half comes back out; -the roster's header already records one such exclusion with the capability that -would retire it. - -### What this screening is worth, measured elsewhere and not by this document - -gunbc#10143 landed on `main` after this document's tested base and measures the population this -screening ranks. Two of its results bound what any screening here can buy, so they belong beside -the method rather than discovered after acting on it. - -**The band is dense and the tail is a plateau — there is no gap below the ceiling.** On its -post-repair run, 5 rows sit at or above 500ms, 27 in 400–500, 55 in 300–400, against 3,174 below -100ms. Repairing modules top-down, the first three buy 43ms and the next seven buy 61ms — roughly -**10ms per module**, about one fiftieth of the ceiling. That is the treadmill, quantified. So a -screening that surfaces the next-most-expensive producer is answering a question whose payoff is -small by construction, and the document's own ordering — safety-relevant rows ahead of -milliseconds — is the more defensible one for reasons that are now measured rather than argued. - -**And the crossers do not generally share a root.** gunbc#10143 records that the modules newly near -the ceiling do NOT share a root the way gunbc#10133's three did. The shared-derivation bucket this -document describes is therefore the exception rather than the common case, which sharpens the -warning already stated above: splitting re-attributes a shared fold and changes no real cost, and -the shared fold is not what most near-ceiling rows have. - -**A worked counterexample was claimed here and then REFUTED by this document's own discriminator, -which is worth more than the example would have been.** The claim was that gunbc#10170 relocated -five canonical fixtures out of a ~5k-line module and took four `rust_body_add_emit` rows from -506/425/416/362ms to 180/174/172/168ms, with the spread collapsing 144ms → 12ms. - -Two floor artifacts refute it. Pre-relocation (run `33707763185`) and post-relocation (run -`33711894017`), same four identities: - -| | cpu_ms | eval_steps | -| --- | --- | --- | -| pre | 314 · 305 · 290 · 318 | 171091 · 166669 · 162507 · 171124 | -| post | 339 · 327 · 317 · 344 | 171052 · 166630 · 162468 · 171085 | - -**`eval_steps` moved 0.02%** — 39 steps out of 171091. The work did not change, so the relocation -removed no evaluation work from these claims. CPU is *higher* after, which is within noise and makes -the honest reading "no measured effect in either direction". The 506 baseline was a contended-run -outlier: those rows were already at 314/305/290/318 with a spread of 28 before anything was touched, -so both the "drop" and the "spread collapse" were artifacts of the baseline. And 180/174/172/168 -came from a targeted `claim_batch` — a fraction of the corpus, a different execution envelope, and -not a floor result. - -**The mechanism that caught it is the one this section argues for.** Steps are deterministic where -milliseconds are not; a cost story that milliseconds support and steps refute is the clock talking. -The same discriminator that says work is not ownership says here that a millisecond drop is not a -work reduction. - -The census does name where the family's cost sits, and it is worth recording which instrument found -it: the cross-claim demand artifact keyed by PRODUCER IDENTITY reports -`target_project_arrow_body_to_value_expression` at 95 claims / 117 evals, 4396ms total with -**4350ms cross-claim** across the emit family including all four rows. That is a producer named by -identity, not a cluster inferred from step totals. - -**It is NOT a candidate, and this document disqualifies it below.** That function carries -`handle_transform: fn(Node, Node, TargetModel) -> …`, so it meets the fn-typed-parameter exclusion: -reification refuses the argument, the key cannot be formed, and the store refuses AT PUBLICATION. -A controlled present-versus-absent pair would measure noise against nothing, because the absent arm -is the only arm — the row never stores. **The demand it names is real; the route to serving it is -closed until the key can be formed.** - -Recorded rather than dropped because the reading is the useful part: the largest cross-claim -producer in this family is unreachable by the serve mechanism, which is a fact about the mechanism's -coverage and not a gap in the census. - -### Screening candidates from the census, by reading a declaration - -The demand census ranks by cost, and cost is the wrong sort key: three large -classes on it cannot be shared at all, and one of them refuses at publication -rather than losing a measurement. Screen with the signature before spending a -controlled pair, because a row that never stores cannot have its serve measured — -the resulting pair is noise, not a negative result. - -**The rule is a ratio, and it screens OUT — it never admits.** Serve cost scales -with the size of the ARGUMENT plus the VALUE; recompute cost scales with the -DERIVATION between them, so a candidate is worth measuring only when the -derivation is large relative to what must be hashed and verified. That is a -necessary condition and emphatically not a sufficient one: gunbc#10094 enrolled -three candidates on exactly such reasoning and withdrew all three on measurement, -and gunbc#10108's note withdraws a fourth — `compile_phase_frontier_standing` -claim-forced at its general declaration — on single-authority grounds despite a -measured win. Nothing below replaces the roster's present-vs-absent receipt. - -Three classes are disqualified without a run: - -- **A fn-typed parameter anywhere in the signature.** Reification refuses a - `Value::Fn` as `OriginBoundNode` and a `Value::Closure` as `Closure`, and - arguments are reified on the same path as values - (`v1_interpreter::portable_args_from_ctx` → `CrossClaimStoreOutcome::RefusedArgsNotPortable`), - so the key cannot be formed and the store refuses at publication. This is the - class that excluded the two rust target models, met on the argument side rather - than the value side. *e.g.* `target_project_arrow_body_to_value_expression` and - `target_project_match`, both carrying - `handle_transform: fn(Node, Node, TargetModel) -> …`. -- **A hot utility — evals far exceeding claims.** A function called many times - per claim is not one derivation shared across claims; per-argument keys with - little reuse are cache population, which is why `formal_production_for_lhs_exact` - was removed from the roster after a run measured 1,635 fills against 113 - consumer claims. *e.g.* `bind_outcome`, `zip_eq`, `fold_grammar_expr`, - `emit_host_list_map`. **claims ≈ evals is the shape that SURVIVES this - exclusion screen** — it is worth measuring, which is not the same as admitted. -- **A constructor.** Its census cost is call volume plus inclusive callees, not a - derivation, so there is nothing to serve. *e.g.* `decl_ref`, - `effect_demand_key`, `formal_productions_catalog_to_node`. - -A fourth shape survives the screen but is usually a loss: a large argument reduced to a -small value with little work between, such as -`target_catalog_contains_node_shape(slot: Node, catalog_targets: List) -> Bool`, -where hashing and verifying the node list per serve costs more than the fold it -replaces. That one is the ratio rule applied, not a separate exclusion. - -Applied to the census, these three classes account for every remaining producer -above a second outside the emit family. The emit family is **not** available, and -the reason is stronger than it was: the roster's re-enrol trigger — a serve that -does not walk the value per consuming frame — HAS FIRED (gunbc#10094 made a serve -O(1) by handing over the reified value), the family was re-measured against that -new serve, and it is still out. All three candidates regressed, showed no effect, -or improved a family total while the rows nearest the ceiling got worse. The -remaining cost is deliberately left unidentified there rather than given a -plausible story. - -Two lessons from that run belong here because they bound this whole method. -**The subject is the per-claim row, not a total** — the ceiling is per claim, so a -family that improves on aggregate while its rows nearest the line get worse is a -loss. And **a discharged capability makes re-enrolment testable, never decided**: -inferring admission from "the serve is cheaper now" is the -specification-without-execution DESIGN §5 names, and it was wrong three times. - -Whether that capability also lifts the fn-in-key limitation is **open and should -not be assumed either way**: one is about the cost of moving a portable value, -the other about whether an origin-bound reference can be represented in one at -all. - -### The rows nearest the deadline are the ones whose refusals are being silenced - -Recorded because it reorders the population by something other than milliseconds. -Observed on main run 33671815204 (head 20caf4eb): the two rows the 500ms CPU -deadline preempted there were not incidental — they were -`self_host_compile_phase_live_gate_witness`'s -discriminating REDs — the planted-identity refusal and the equal-cardinality swap -refusal. A preempted row is not a slow row that still answered; it is a refusal -that did not execute, which is why the floor refuses to call the run green. So -cost work on this population is not traded against safety. Measured: sharing the -fold under those rows (a roster row later withdrawn on gunbc#10108's -single-authority grounds, so the cost figures are not carried here) took the run -to `cpu_deadline=0` and both refusals executed. Whoever makes that fold cheap by -whatever route gets the same effect, because the mechanism is the row reaching a -verdict rather than any particular repair. - -**Prioritise this population ahead of milliseconds — but the safety RANKING it -would need does not exist yet, and this document does not supply one.** A -preempted row is a strong candidate regardless of where it sits in a cost -ranking. What is observable is only the VICTIM IDENTITY: `cpu_deadline` counts -how many rows were cut off, `required_floor_claim_cost.tsv` carries each of them -with `verdict_reached=false`, and both name WHICH claim was preempted. - -Neither names what that claim exists to PROVE. The two rows above are refusal -probes because their source was read and verified one at a time, not because any -instrument said so — and reading names off a diagnostic does not generalise to a -corpus-wide procedure. `std.witness_purpose` is explicit that purpose is -AUTHORED, NOT INFERRED FROM IMPLEMENTATION, and it presently carries no per-row -declarations at all: zero declarers, zero consumers. - -So the honest statement of the method is three separate claims, and only the -first two are discharged here: victim identity is observable; these two victims -were independently verified as refusal probes; ranking the population by safety -relevance REMAINS BLOCKED until an authored purpose can be joined against -`verdict_reached`. Treating the second as if it were the third is how a class -acquires a rung it did not earn. - -**The repair for this class has to travel through the gate the class refuses.** -Recorded because it is a structural property and not a queue accident, and -because it decides what "prioritise the fix" can and cannot buy. A preemption is -runner-dependent — the same rows measure anywhere from ~100ms to over 500ms on -one tree — so any PR can draw a refusal from two rows that are not broken, and -that includes the PR carrying the repair. Observed in situ: a 138-line -documentation-only change with no `.dag`, no Rust and no workflow was refused by -`a_live_tree_that_gained_an_identity_refuses_and_names_it` at -`cpu_at_least=501ms/500ms`, while the consolidation that would make that fold -cheap sat open with its own required floor lane running. - -The tempting exit is a re-run, and it is the one move this document's evidence -forbids most specifically. A re-run does not make the row cheaper; it redraws the -runner. What it buys is a green **over refusals that did not execute** — the -`interrupted_before_verdict` rows are the discriminating REDs themselves, so the -mark asserts a verdict for exactly the claims that were preempted. That is worse -than a slow build, and it is why `cpu_deadline` must be read as a safety counter -rather than a performance one. diff --git a/docs/plans/witness-execution-closure.md b/docs/plans/witness-execution-closure.md deleted file mode 100644 index b2e30331a4b..00000000000 --- a/docs/plans/witness-execution-closure.md +++ /dev/null @@ -1,337 +0,0 @@ -# Witness execution closure: 778 identities discovered, declined, and never run - -**Found and closed 2026-08-20 (`nimble-wolf-645`).** The required floor discovered 778 witness -identities every run, declined all at file grain, and executed none — and since the 2026-08-15 -floor cut left `claim_executor --required-floor` as the tree's only witness-executing consumer, -"declined by the floor" and "not run anywhere" became one fact. - -This document is the finding and the receipt; the change is in `v2.workflow.required_floor`, -`v2.workflow.floor_route_gap`, and the seed's site projection. - ---- - -## The measurement - -Counted with the floor's former Rust scanner (a column-zero -`test fn `, and a column-zero `data … LiveTreeDisposition … ReadsLiveTree`), on `4cec10f66a3`: - -| population | identities | files | -|---|---|---| -| discovered `test fn` sites | 11,115 | — | -| `DeclinedLongModule` (authored `long.` module name) | 538 | 97 | -| `DeclinedLiveTree` | 782 | 112 | -| routed and executed | remainder | — | - -The floor's own run receipt agrees and is the citable number: `gunbc#8638`, run `32337765205` -against `32333231183` at the same base, reported `claims=9782 declined_long=538 declined_live=778` -on the base — the brief's 778 exactly. Quoted in -`dag/test/claim/seed_mirror_constant_lens_witness_test.dag` by a session that had just added four -rows to the declined population and measured the delta. - ---- - -## Three defects, and only the third is about cost - -### 1. The decline rested on a premise that had stopped being true - -`DeclinedLiveTree` excluded any identity whose FILE declared -`data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree`, on the premise that reaching -the live tree implies "cannot run in the hermetic frame this floor folds". - -That premise had been false for some time. Hermetic mode carries the **checkout-input carve-out** -(`v1_interpreter`, the readonly `Filesystem.Read|List` arm guarded by -`hermetic_checkout_input_disposition`): a readonly filesystem operation whose path the disposition -*confirms* sits under the checkout root, with no `.git` or `target` component below it, -**dispatches to real input access** — the commit is the run's deterministic input, so this is -input access, not a host effect. Reading committed `.dag` and `.rs` sources is exactly that case, -and is what most of the 778 do. - -So the floor ran a **file-grain prediction of an answer the interpreter already decides exactly, -per identity, at the effect boundary** — and predicted it wrong, in the direction that silently -removed coverage. - -Independent confirmation: another session reached the opposite conclusion in writing one day -earlier — *"the required floor folds one hermetic prepared subject, so a live-tree reader cannot -participate by construction"* — followed by a §4b rung drop to *mitigatable* whose next-rung -trigger was *"an executing consumer for the declined-live population"*. The trigger needed no new -lane, only the stale prediction deleted. - -### 2. One fact, two computations, in one binary - -`reads_live_tree` was derived twice, by methods that cannot agree except by coincidence: - -- the former `cli_run` floor scanner — a **syntactic text scan** for the declaration line. -- `cli_run` `reads_live_tree_effective` — reads the same declaration, then falls through to - `effect_reach_derived_reads_live_tree_for_entry`, a **semantic effect-reachability derivation** - over the entry's import closure. - -These disagree as a function of the import graph (DESIGN §3). The resolution is not to pick one: -the two consumers asked **different questions**, and only one was entitled to the name. -Affected-set selection asks *does this entry's result depend on live tree state* — -`reads_live_tree_effective` keeps that question, untouched. The floor asked *can this identity -execute*, which no authored file-level boolean can answer. The floor's copy is deleted; nothing -replaces it, because execution answers it. - -### 3. The run's own honesty check could not see what it dropped - -`claims_planned` was the **post-decline** number. The terminal invariant -(`ClaimIdentityCountsDisagree`) compares `planned == executed == receipted`, all measured *after* -the projection dropped whatever it dropped: a projection declining a thousand identities and one -declining none produce identical healthy triples. - -And the per-identity receipt (`RequiredFloorDispositionRow` TSV) was gated on -`GUNBC_REQUIRED_FLOOR_DISPOSITION`, which `witnesses.yml` did not set — so on CI the whole -declined population was two integers in a `[floor-phase]` line. - ---- - -## The measurement, and what it settles - -The decline arm was deleted on a branch and the whole population executed — floor run -**32345970386**, sha `c812b9fb6d0`, the first run in which every discovered identity was routed: - -``` -offered=11103 routed=10565 declined_long=538 (partition exact) -planned=10565 executed=10565 terminal=10565 passed=10116 -known_red_held=207 failed=36 route_gap=157 -interrupted_before_verdict=47 completed_over_cost_requirement=2 -``` - -**Of ~783 identities admitted, 626 pass.** 157 route-gap, naming exactly the operations this -corpus's exclusion notes have named in prose for months — `Mktemp.Dir` 54, `IsExecutable` 26, -`Run` 17, `emit_host_native_cache_evict` 10, `Check` 10, `Write` 8, `git.Inspect.HeadCommit` 5, -then a tail. **Not one is a committed-source read.** The premise was stale for the large majority -of the excluded population, not an interesting minority. - -The cost worry did not materialise: 49 of 783 in the cost tail (~6%), 32 minutes wall against a -180-minute timeout. - -### The finding nobody was looking for: a third of the expected-red roster was never red - -That run carried **308** enrolled expected-red identities and held **207**. The difference is -**101** — and the 101 enrolled identities that route-gapped are *precisely* that set, by -intersection, not by count coincidence. - -Those 101 were already executing on main every run and already reaching a hermetic refusal. Since -that refusal arrived as a `TypeError` carrying prose, `ExpectedRedArm` could only read it as an -ordinary failure and **held it as agreement**. The debt ledger recorded "this witness runs and -fails and someone is fixing it" about 101 rows that never reached their subject; nothing was -fixing them because there was nothing to fix: **they needed a route, not a repair.** - -This is the state-space conflation §5 names, inside the mechanism whose job is making debt -visible. It also means main's `passed=8702 / known_red_held=306` counted something narrower than -executed coverage, by 101 identities, independently of the 778. - -### The cost tail is two mechanisms, not one budget - -The CPU histogram over the 49 is bimodal with a **14.4-second empty gap**: - -| band | rows | -|---|---| -| 5001–5013 ms | 23 | -| 6682 ms | 1 | -| *(nothing from 6.7s to 21.1s)* | 0 | -| 21109–53301 ms | 25 | - -A 12ms spread across 23 rows is **the interrupt firing**, not 23 witnesses each needing five -seconds — and an interrupted claim's cost is a *lower bound*, so those rows' true cost is -**unmeasured**. The low mode cannot argue the budget is too tight. - -24 of the high-mode 25 are *also* interrupted, having accrued 21–53s against a 5000ms deadline — -only possible where the stride poll cannot land: `ClaimPreemptionReachability::OpaqueHostCallUnbounded`, -already modeled in `v2.workflow.required_floor`. **Exactly one of the 25 is the known member** of -`opaque_host_call_grandfather_population()`, which declares itself *exhaustive at one member* and -"grows only when a DIFFERENT operation is found to share this shape, which is a new finding, not -a declaration." This is that finding: the other 24 reach host-fed `*_live` builtins in -`enforcement_live`, `cost_coverage`, `grammar_coverage`, `lifecycle_survivor_corpus_census`, and -`realization_vocabulary_containment`. The "bounded population" was bounded by what anyone had -observed on a floor not executing the population where the others live. - -## What replaced it - -**Delete-first at the root** (DESIGN §3 replacement migration). `DeclinedLiveTree` and the text -scan feeding it are gone; no intermediate representation was built beside them. - -- **One execution route per identity.** Every non-long identity is routed and executed. The route - is `RequiredFloorClaim.execution_mode`, already per-claim — no new vocabulary restates it. -- **The interpreter's refusal is the classifier, and it is typed.** - `InterpError::HermeticHostEffectRefused { operation, ground }` replaces three - `TypeError`-with-prose sites and reaches the floor as `ClaimOutcome::HostEffectRefused` — the - precedent `TimedOut` and `HostToolUnresolved` set against substring-matching prose. A route gap - is **not a verdict**: the witness never reached its subject, so it is neither pass nor failure, - and is reported apart from both because its remedy is a route. -- **`HermeticEffectGround` is closed and names the remedy**: `UnpublishedMockCase`, - `NoMockResponse`, `FilesystemRemoval`. One `String` would have collapsed three fixes into one - sentence. -- **The offered population is checked, not just reported.** `SitePartitionInexact` refuses - unless `offered == routed + declined_long`, stated at the loop that could violate it. -- **The residue is identity-grain debt, not a category.** `v2.workflow.floor_route_gap` reuses - the `floor_expected_red` contract rather than re-coining it: enrolled-and-gapped is agreement, - unenrolled-and-gapped reds, enrolled-and-did-not-gap reds as stale, enrolled-and-did-not-execute - reds. Monotone, identity grain, over an independently discovered closed universe — the four - conditions DESIGN §5's 2026-08-01 oracle ruling requires of a debt contract. No count assertion - anywhere. -- **The receipts are emitted on CI**, unconditionally: the disposition TSV and the long-home - storage agreement TSV are named in `gunbc.witness_floor_workflow`. - -Nothing gets a wet route — no live shell, write, or network. That is a separate lane with its own -admission question, deliberately not opened here. - ---- - -## What landed here, and what is staged - -This change carries the **mechanism** and the **101 reclassification**; it does **not** delete -the decline arm. - -The seam is exact, not chosen: the full-closure run surfaced **55 blockers — 6 witnesses that do -not RESOLVE** (`undefined variable`, `no such function`; never caught because nothing evaluated -them) **and 49 in the cost tail — all 55 newly-admitted, none previously routed.** So the -blocker-free part is a complete change that happens to be smaller, not a partial one. The 101 -route-gap on main today and need nothing deleted to be reclassified. - -Staged behind their owners: the 6 broken artifacts (route to author or delete — enrolling a -non-resolving witness as expected-red would assert it runs and fails, re-minting the conflation -this change removes), and the cost-tail policy call, which the histogram says is two questions. - -## What is NOT claimed - -The `long.` decline is untouched: an operator-ruled cost quarantine on a different axis, with -538 identities and no executing consumer — the *other* half of the hidden population, not closed -here. - -## The adjacent residue, named rather than left to be rediscovered - -`run_required_floor` consults `floor_prepared_subject_exclusions()` **and nothing else** — the -`witness_exclusion_frontier` rosters have no consumer on this path, which that function's own -comment records after a session added rows to them and measured `modules_excluded=2` unchanged. - -Three of its five entries exist for **exactly the route-gap reason** — an operation with no -`mock_response`, so "the hermetic floor refuses it and one refusing member fails the run." The -same defect this change removes, one layer over: a **file-grain, uncounted exclusion** where an -identity-grain typed disposition belongs. Strictly worse than a route gap: an excluded module -leaves the prepared subject entirely (not even typechecked), whereas a route-gapped identity -stays, executes, and reports. - -Converting those entries into `floor_route_gap` rows is the obvious next step and is deliberately -**not** here: it should follow, not precede, the measurement proving the route-gap mechanism -behaves as designed on its own population. - ---- - -## The same failure one level up: the projection's denominator (2026-08-29, `nimble-ibex-902`) - -The population above was lost *inside* the projection — discovered, declined, never run. The -sequel is the population lost *before* it: identities that never reached the projection, so no -partition over "offered" could speak for them. - -### The two mechanisms that removed them - -`assemble_prepared_subject_closure` drops modules two ways, and a witness declared in a dropped -module was neither planned nor declined: - -- **the exclusion substrings**, whose entry-grain receipt (`collect_deferred_discovery_rows`) - existed but was never joined to the disposition population at identity grain; and -- **the gate closure** introduced by the 2026-08-29 gate cut (§4b rung drop *Required gate - reduced to the compiler floor*). Only modules in the transitive closure of the gate seeds are - prepared, so `declined_outside_required_gate` — which reads two figures — can only ever speak - for modules that *reached* the index. - -### The subject universe, measured - -Counted with the floor's own rule (a column-zero `test fn ` / `test data `) over `dag/` and -`src/v2`: **13,975 distinct declared identities** in 1,759 files, **zero** duplicate identities, -**zero** module-name collisions. So the gap between the declared corpus and the floor's `offered` -was never duplication but two silent removals. The live figures are the run's own: -`required-floor: declared=… offered=… declined_outside_gate_closure=…`. - -### What changed - -**Hand-Rust receipt.** The receipt is a roster row, not this paragraph: -`gunbc.floor_population_projection_seed_growth`, enrolled in -`gunbc.seed_growth_admission seed_growth_justification_roster` — item delta, admitted -modifications, owning lane (`v1-hand-queue-drain`), dissolution trigger, and boundary chain live -there, where the census reads them. **Full-index retention (review 57430):** the discovery fold -consumes the prepared full-index views by value inside its own phase (the intermediate -`FloorDiscoverySource` vector is deleted), and the phase's completion line prints -`full_inventory_release_rss_kb_before` / `_trim_reclaimed_kb` / `_rss_kb_after` through the -floor's existing statm/malloc_trim instruments, so the run itself states that outside-closure -bytes end with the phase rather than surviving into claim execution. - -1. **The universe is the declared population, answered by one authority.** The floor folds - `v2.workflow.floor_discovery_producer` (`discover_floor_rows_for_source`) over preparation's - FULL module index rather than over the prepared closure alone, and classifies each returned - identity against the prepared closure and the exclusion map: an identity whose module - preparation dropped carries `DeclinedOutsideGateClosure` or - `DeclinedDiscoveryExcluded { matched_substring }` — two arms on the existing authority, not - a second status vocabulary. No Rust rescan stands beside the producer; gunbc#9685's - single-discovery-authority cut holds for the whole corpus, not only the prepared subject. The - gate-closure population keeps its own arm rather than folding into - `DeclinedOutsideRequiredGate`: the two are removed by different mechanisms, restored by - different triggers, differ by two orders of magnitude, and the first is the rung drop's subject. - - **The consequence, stated now:** the producer's per-file refusals — misplaced `test` decl, - barren sidecar, misplaced wire contract, malformed `live_tree_disposition` row — now stop the - required floor for ANY module under the source roots, not only one the gate closure admitted. - A real widening of what this lane refuses over, survivable today because the corpus carries - none of those violations (measured 2026-08-29: zero `test`-marked decls outside a `*_test.dag` - sidecar, zero barren sidecars); the first violation authored anywhere in the tree will red - this lane rather than the one owning the file. -2. **The partition is an identity join, not a count equality.** The old check was - `offered == routed + declined_long + declined_fixture + declined_outside_gate + - declined_cost_debt`, which DESIGN §5 names by shape: green over a projection that drops one - identity and writes another twice, since every count still agrees. It is now the terminal - ledger's own reconciliation, through the same function (`reconcile_identity_population`), - refusing as `FloorDispositionJoinInexact` with the offending identities named. Its calibration - pair is enrolled beside the terminal-ledger one. -3. **The counters are derived from the rows** instead of accumulated beside them, and duplicate - detection moved from the *planned* subset to the *whole declared* population — a duplicate - whose first site declined used to pass unnoticed. -4. **The artifact carries two axes, never folded.** The disposition TSV gains an `outcome` - column, joined from the terminal ledger through `claim_disposition`; an identity that never - ran reads `not_executed` — a statement, not a blank. - -### Two axes deliberately NOT built, and what each needs first - -Both were in the commissioning brief and both would have meant *authoring* the authority they -claim to join to (§3), so they are named as triggers rather than improvised: - -- **semantic producer / retire-with-producer.** No authority in the tree maps a witness identity - to the producer whose behavior it witnesses. Trigger: a modeled producer binding on the witness - carrier itself, at which point the join is a lookup rather than a classification. -- **the Rust `#[ignore]` roster.** A different universe with no roster authority and no identity - grain shared with `.dag` witnesses. Trigger: an enumerated, typed roster of ignored Rust tests - with a stated reason per row — the same shape `floor_cost_debt` already has for its population. - -### One inert wall found on the way - -`gunbc.discovery_census` claims, twice and in prose, that its wildcard-free matches make a new -`RequiredFloorDisposition` arm *fail to compile*. `DeclinedOutsideRequiredGate` had been added -without either match acquiring an arm, and nothing refused — the module's own witness is outside -the gate closure, so no executing path typechecks it. The arms are added here and the claim -restated at its honest rung, with the trigger recorded in the module. - -### A false specification selected out of the nonexecuting population (2026-09-02, gunbc#10092) - -The changed-witness sublane selected -`test.claim.discovery_census_witness.w_unrostered_sibling_in_the_same_module_is_planned`, an -identity the ordinary required floor did not execute. The claim called -`required_floor_site_disposition` with module path -`dag.test.claim.lifecycle_survivor_corpus_census` and asserted `Planned`, although that path -matches none of `required_gate_prefixes` and the function therefore answers -`DeclinedOutsideRequiredGate`. The specification was false and stayed green because it did not -run — this document's class at one identity, not a new failure class. - -Run `33656005986` is the discriminating receipt. Its identity-grain changed-witness ledger reports -the claim as `planned-without-terminal-verdict`, `outcome=failed`; the aggregate reports -`changed_witnesses=12 changed_witness_blocking=1 -changed_witness_declined_in_declared_nonexecuting_root=0`, so the row executed rather than being -declined again. The same run measured the containing universe as 15,383 declared identities, -3,499 routed and 11,277 declined outside the gate closure, and ended with exactly one semantic -failure and zero interrupted verdicts. Those population figures are bound to that run and tree: -declared, offered and routed populations move with the tree, so figures from another head are -neither approximations of nor superseded by this receipt. The repair renames the claim to -`w_unrostered_sibling_in_the_same_module_reaches_the_gate_decline` and asserts the disposition its -own input entails; direct claim execution then passes. The reusable fact is not the spelling fix: -selection of a normally nonexecuting identity is an executable falsifier for specifications that -ordinary floor green cannot adjudicate. diff --git a/docs/plans/workspace-control-plane.md b/docs/plans/workspace-control-plane.md deleted file mode 100644 index a29b6f8cce1..00000000000 --- a/docs/plans/workspace-control-plane.md +++ /dev/null @@ -1,179 +0,0 @@ -# Workspace control plane: off-rack, serverless, queue-driven - -Design for the off-rack control plane of the recoverable workspace. Status: **design only. Nothing here is built, and no store is minted.** - -The plan rows it serves live in the private `strategy.year_end_plan`: - -- `control-plane-state-store` (its recovery half) -- `volume-durability-contract` (the one global epoch authority) -- `checkpointed-workspace` -- `instance-host-evacuation` -- `instance-reconciliation` -- `instance-endpoint-continuity` - -The demo that exercises it is the private `docs/plans/2026-10-02-recoverable-workspace-demo.md`, stages 2–5. The state machine it realizes is the private `docs/plans/2026-09-25-fabric-architecture.md` §2 `resume(instance, target)`. The storage interface it stands on is [fabric-storage](fabric-storage.md), steps 4–5. - -Operator rulings taken as inputs, not reopened here: - -1. The off-rack control plane is serverless and low-QPS, with a queue. -2. Rack loss is a placement failure: restart elsewhere from the last committed revision. -3. Overflow is ordinary fabric placement by cost. Owned capacity has zero marginal cost and wins; OCI is the probable overflow target; work moves back when owned capacity returns. -4. R2 and B2 are fabric options. -5. CPU and memory are requested independently. -6. The operator is in the US northeast. - -Claims are stated as **requirement and confidence**, never as dates. Every external figure carries its standing in the sense of DESIGN §4d (`extdeps.external_authority` `CitedFigureStanding`). A figure marked *transcribed* was copied from the vendor page named beside it, at authoring time, and owes a re-read before any decision is priced on it. - -## 1. The state the control plane carries, and its existing homes - -The control plane holds five kinds of fact. Every one already has a home in the corpus. The design adds **realizations and placements of those homes, not new vocabulary** (DESIGN §3b conformance: each row below conforms or names its divergence). - -| State | Meaning | Home authority | Conformance | -| --- | --- | --- | --- | -| **Intent** | The desired instance: class, CPU and memory requested independently, volume quota, endpoint name, placement constraints. | Requirements: `product.fabric.work` `ExecutionRequirements`. The desired record is a `std.fabric_storage` object under a per-instance head. | Conforms. Intent is an immutable object; "the current intent" is a head. No second desired-state table. | -| **Writer epoch** | Which realization may advance the workspace head and serve the endpoint. A monotone integer per workspace. | `std.temporal_effect` `LeaseEpoch` (`generation` = epoch, `owner_fingerprint` = realization). **The epoch is not a separate record:** it is carried by the `WorkspaceFence` entries on the workspace head, in `std.checkpointed_workspace` (still-dove-673, branch `workspace-revision-model`). | Conforms. There is no second epoch store. Consumer: `std.checkpointed_workspace` `workspace_commit_classify` (§3). | -| **Committed revision head** | The last committed checkpoint of the workspace, as a chain of `WorkspaceRevision` and `WorkspaceFence` entries. | `std.fabric_storage` `FabricHeadReading` / `FabricHeadAdvance`, as **one head per workspace**, `workspace..committed`. Its R2 realization is fabric-storage step 4B (ETag-conditioned put), stacked on gunbc#13080 `gunbc.cloudflare.r2_staged_transfer`. | Conforms. No per-epoch head namespace: a per-epoch namespace would make a stale commit unreadable but still *acknowledged* (§3). | -| **Host lease** | Which host holds a realization, until when, and as observed. | `std.temporal_effect` `HeldLease` (`HeldLeaseObservedState`: `LeaseAbsent` / `LeaseRunningExpected` / `LeaseRunningStale` / `LeasePortForeign` / `LeaseInaccessible`). The cell hold is `gunbc.fabric_control_plane` `CellReservation` / `reserve_selected_cell` / `end_cell_hold` over `std.durable_exclusive_hold`. | Conforms. The reconciler's observation *is* `HeldLeaseObservedState`, so detection reuses its verdict fold (`held_lease_observed_verdict`, `plan_for_held_lease_observation`). | -| **Endpoint binding** | Which realization the workspace's stable name routes to, and at which epoch. | No consumed home yet: the private rows `fabric-network-contract` and `instance-endpoint-continuity` own it. | **Modeling obligation (§6), not a conformance row.** This design only requires that the binding carry the epoch it was bound at (§3). Consumer: the overlay's route programming, named in `instance-endpoint-continuity`. | -| **Credentials per epoch** | Scoped storage write credential and overlay identity for one realization. | `std.effect_grant` `Grant` / `Envelope` (verb `Write`, namespace position = the epoch's chunk prefix). The minting pattern follows `gunbc.auth.authorization_pattern_selection`. | Conforms. **This is a cost and blast-radius control, not the fence.** Chunk writes go under `workspace//e/`, so a revoked epoch-N grant stops a stale writer from spending money. Correctness never depends on revocation. | -| **Placement choice** | Where a realization goes and why. | `product.fabric.selection` `select_supply` → `std.decision` `SelectionReceipt`. | Conforms (§4). | - -No parallel store: intent, epochs, heads and leases are all **objects and heads in one `std.fabric_storage` realization**. The control-plane service that §5 chooses is that realization's *linearization point for heads*. It is not a second database beside it. - -## 2. The reconciler: level-triggered and idempotent - -The reconciler is one pure fold, `reconcile(desired, observed) -> List`. It is wrapped by one effectful step that reads, decides, applies **at most one CAS-guarded transition**, and returns. - -- **Level-triggered.** Its input is the whole current desired state and the whole current observation for one instance (heads plus `HeldLeaseObservedState` plus endpoint binding). It is never the event that woke it. A queue message carries only an instance id ("look at X"), so losing, duplicating or reordering messages costs latency and never correctness. A periodic sweep enqueues every instance, which bounds detection by the sweep period even if every edge-triggered message is lost. -- **Idempotent.** Each action is guarded by the expectation it was computed against: - - an epoch bump is a CAS from `generation = N` to `N+1`; - - a placement is a `CellReservation` keyed by `(instance, epoch)`, via `fabric_cell_allocation_key`; - - an endpoint rebind names its target epoch. - - A retried or duplicated run observes the transition already made and computes `Noop`. This is `ensure` (DESIGN §3d): Noop, Apply or Refuse over a caller-supplied requirement, with independent readback. Selection happens before it, never inside it. -- **One transition per run.** `resume` from fabric-architecture §2 is driven as a sequence of states, not one long transaction: - - `Placed(e) → Fencing(e→e+1) → Fenced(e+1) → Restoring(e+1, target) → Realized(e+1) → Bound(e+1)` - - The current state is a function of the heads, never a flag. Each step is one queue delivery. A worker that dies mid-step leaves the heads unchanged or advanced, and the next delivery continues from what it reads. The receipt chain is `std.temporal_effect` `EffectStepReceipt` / `plan_next_step_from_prior_receipt_and_lease`. Stall detection is `EffectStallBudget`, which escalates as a typed outcome rather than retrying forever (DESIGN §5: refuse, never widen). -- **Detection.** A host lease reads as `LeaseRunningStale` or `LeaseInaccessible` past the lease term. That reading plus the instance's intent "should be running" yields `Fence`. There is no failure detector beyond lease expiry. A partitioned host that still runs is handled by fencing (§3), not by trying to tell "dead" from "unreachable". That is why the demo's harder case, rack alive but unreachable, needs no special arm. - -## 3. Epoch fencing: a stale writer can neither commit nor receive traffic - -**Requirement:** after the epoch authority records epoch N+1 for a volume, no action by a holder of epoch N reaches durable state or a client. **Confidence:** high for commit; medium for traffic, until the endpoint binding has a home. - -**The fence is the same compare-and-set as the commit.** There is no separate epoch record read before the head advance. With "read record == N, then advance `e/head`", a writer fenced *between* the two steps commits after the bump. If restore had already read that head, the stale commit returns success to the guest, an acknowledged barrier, and is then silently lost. A per-epoch namespace makes such a commit unreadable, not unacknowledged, and that breaks the contract's strongest promise. - -The model, `std.checkpointed_workspace` (still-dove-673): - -1. **One head per workspace:** `workspace..committed`. -2. **A fence is a head entry.** An epoch transition is the reconciler advancing that head to a `WorkspaceFence { epoch: N+1, parent: }`. It does so through the same `FabricHeadAdvance` every writer uses, **before** any restore starts. -3. **Every commit expects the entry it built on.** A writer at epoch N advances with `ExpectHeadAt { object: }`. If a fence landed in between, the advance is `FabricHeadMoved`. `workspace_commit_classify` classifies that as `WorkspaceCommitStaleEpoch` (observed entry epoch > writer epoch), and the guest never sees success. The fence and the commit linearize on one compare-and-set, so the gap no longer exists. -4. **Attach** reads the head and refuses unless the newest fence names this realization's epoch. -5. **Restore** walks the chain from the head past any fences to the last `WorkspaceRevision`, then advances from there under the new epoch. -6. **Coverage invariant.** The contract (private `volume-durability-contract`) sells one arm, the asynchronous checkpointed workspace: guest `fsync` is acknowledged locally, and durability is the manifest commit. `oldest_uncommitted_write_age` stays under the published window. A commit refused for unreachable storage is not retried silently: once the oldest uncommitted write would exceed the window, the instance enters the **declared storage outage** and refuses writes. An idle workspace is never refused. A commit refused as `WorkspaceCommitStaleEpoch` makes the realization quarantine itself. - -Two defense-in-depth lines stand beside the fence; neither is the fence: - -- **Per-epoch credentials** (state table): revocation is best-effort at a provider and bounds cost and blast radius only. -- **Traffic follows the binding.** The endpoint binding names `(instance, epoch)`, and the overlay routes only to the realization carrying the bound epoch. Its per-epoch overlay credential is revoked at the fence. A returning rack (demo stage 5) finds its binding gone, its grants dead, and its next commit `WorkspaceCommitStaleEpoch`; it quarantines, and the instance is destroyed. - -**Can R2 conditional puts alone be the epoch authority? Yes.** The epoch authority *is* the workspace head, so R2 `If-Match` on that one object is the only linearization needed: commits and fences both use it, and creation is `If-None-Match: *`. Multi-provider placement does not break this, provided **B2, or any second backend, holds chunks only**, which are immutable and replicate freely. **The head stays in one place.** The fabric-architecture §2 hazard, a partitioned writer advancing a second provider's own head, arises only if a second provider holds a live head, so that is refused by design rather than fenced. - -The residual requirement is to verify R2's conditional put as a strong read-after-write CAS against the cited doc (https://developers.cloudflare.com/r2/api/s3/extensions/ and the conditional-operations reference). Confidence is medium until verified. - -A Durable Object is not needed for fencing. It stays as the fallback **only** if that verification fails, or if one transition must change the head and the endpoint binding atomically. - -## 4. Cost-based placement and move-back - -Placement is `product.fabric.selection` `select_supply` over a candidate field built from: - -- owned cells (`gunbc.fabric_control_plane` `fabric_cell_roster_candidates`) at zero marginal cost; -- overflow offers (OCI Ampere drafted; AWS Graviton the alternative), as priced `CandidateOffer`s. - -`ExecutionRequirements` carries CPU and memory as **independent** requirements. A target is admissible when both fit, never by a bundled shape. The result is a `SelectionReceipt`. A different field (rack lost, rack back) is a different decision, so staleness is mechanical (DESIGN §3d). - -- **Rack loss** removes every owned candidate from the field, and selection lands on overflow. Same `resume`, a different target adapter. -- **Move-back** is not a special path. The sweep re-runs selection for every running instance. When the receipt's selected candidate differs from the current placement and the cost delta exceeds the move cost (the restore transfer plus the endpoint downtime, as a `delay_valuation`), the reconciler plans a planned `resume` to the cheaper target. -- **Thrash guard:** a move is planned only when the delta covers the move cost over a declared hold horizon (`GrantDuration`). This is a selection policy input, not a timer in the reconciler. -- **Overflow quota** is a candidate's availability observation (`AvailabilityObservation`). An unverified quota is `SelectionNeedsEvidence`, never an assumed admit. This follows the demo's "overflow quota" risk. - -## 5. Realization choice - -The load is tiny: one record per instance, transitions on failure or move, and a sweep. The figures below decide which choices are admissible, not which is cheapest; at this QPS every option costs a few dollars a month. - -### (a) Cloudflare Workers + Durable Objects + Queues, beside R2 - -- **Consistency:** - - A Durable Object is a single-threaded actor with transactional, strongly consistent storage. One DO per instance (or per volume) makes every epoch transition serial by construction, and the CAS is a plain read-compare-write inside the object. - - Location hints (`enam`, eastern North America) place it near the operator. - - Docs: https://developers.cloudflare.com/durable-objects/ ; https://developers.cloudflare.com/durable-objects/reference/data-location/ -- **Queues:** - - Delivery is at-least-once, which is what a level-triggered reconciler wants. - - Consumers are Workers, and there are dead-letter queues. - - Docs: https://developers.cloudflare.com/queues/reference/delivery-guarantees/ -- **R2 is the same vendor and the same account.** The credential scope and the step 4B realization are already modeled in `extdeps.cloudflare.r2` / gunbc#13080. There is no egress charge from R2 (https://developers.cloudflare.com/r2/pricing/). -- **Price** (*transcribed*; owes a re-read of https://developers.cloudflare.com/workers/platform/pricing/, https://developers.cloudflare.com/durable-objects/platform/pricing/, https://developers.cloudflare.com/queues/platform/pricing/): - - Workers Paid is $5/month, which includes Workers, DO and Queues allowances: about 10M Worker requests, 1M DO requests and 1M Queue operations per month. - - This load sits inside the included tier. -- **Weakness:** - - A DO's storage lives in one location. A Cloudflare-wide or DO-wide outage takes the control plane down; see §6 for what survives. - - Single vendor for objects and control. That couples their failures, but either one down already stops commits, because the head lives in R2. - -### (b) AWS Lambda + DynamoDB conditional writes + SQS - -- **Consistency:** - - DynamoDB `PutItem`/`UpdateItem` with a `ConditionExpression` is a linearizable per-item CAS: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html - - Strongly consistent reads within a region. Global tables are multi-region but **last-writer-wins**, so they are *not* a CAS across regions: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2globaltables_HowItWorks.html - - The fence therefore lives in one region (us-east-1 or us-east-2, for the operator). -- **SQS** standard queues are at-least-once; there is a Lambda event source and DLQs. Docs: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html -- **Price** (*transcribed*; owes a re-read of https://aws.amazon.com/dynamodb/pricing/on-demand/, https://aws.amazon.com/lambda/pricing/, https://aws.amazon.com/sqs/pricing/): - - On-demand writes are on the order of a dollar per million (reduced in late 2024; re-read). - - Lambda's free tier is 1M requests per month, and SQS's is 1M requests per month. - - This load is near zero. -- **Weakness:** - - A second vendor, a second credential pattern (`gunbc.auth` would need an AWS WIF row) and a second extdeps surface to model, for no consistency gain over a DO. - - Egress from R2 is free, but every control-plane call crosses vendors. - - Its real advantage, independence from Cloudflare, is worth only what §6 says survives anyway. - -### Recommendation - -**Confidence:** medium. - -- **Phase 1 (minimal):** one head per workspace as an R2 object under step-4B conditional puts, carrying commits and fences alike (§3). One Cloudflare Worker is the reconciler, Cloudflare Queues carries "look at X", and a Cron Trigger runs the sweep. No DO and no new store. -- **Phase 2, only if needed:** a Durable Object per workspace holding the head. It is needed if R2's conditional-put semantics fail verification as a strong CAS, or if transitions need multi-key atomicity (epoch + binding in one step). - -Prefer (a) over (b): it puts the control plane on the same vendor whose object store is already the commit dependency. Its correlated failure adds no new outage mode beyond "R2 down", which already stops commits. - -## 6. When the control plane itself is down - -**Requirement:** a control-plane outage degrades to "no *changes* of placement", never to data loss, split brain or a silent wrong answer. - -| Still works | Stops | -| --- | --- | -| Running instances keep running and serving at their bound endpoint (bindings are programmed into the overlay, not looked up per packet). | Detection and recovery: a host that dies during the outage is not re-placed until the control plane returns. Stated in the guarantee set as a compound outage, never hidden. | -| Manifest commits continue **iff** R2 is up: the commit *is* the head compare-and-set and needs no reconciler. | With R2 unreachable, manifest commits refuse. `oldest_uncommitted_write_age` then grows, and at the window the instance enters the declared storage outage. Commits never proceed unchecked. | -| — | A host lost during that outage loses its pending writes. That is receipted as the contract's typed **compound failure** (sole volatile copy lost while coverage could not be kept), never as an ordinary recovery. | -| Restore by hand is possible from the last committed head, because heads and objects are plain storage. | Move-back and overflow placement. Cost drift accumulates; correctness does not. | -| Nothing can split brain: no actor except the reconciler bumps epochs, and it is down. | New instance creation. | - -The rule is the fabric-storage rule applied one layer up: **an unreachable linearization point refuses, it does not fail over.** There is no "proceed without the fence" arm (DESIGN §5, no escape hatches). - -## 7. Consumers and frontier - -Every model named here is consumed by a named row; none lands before its consumer: - -- **Fence-as-head-entry and stale-commit classification:** `std.checkpointed_workspace` `workspace_commit_classify` (still-dove-673). Rows: `checkpointed-workspace`, `instance-reconciliation`, `volume-durability-contract` (demo stages 1 and 5). -- **Per-epoch chunk credential prefix:** the storage grant minting under `instance-reconciliation`. -- **Reconciler fold over `HeldLeaseObservedState`** → `instance-host-evacuation` (demo stage 2). -- **Epoch-carrying endpoint binding** → `instance-endpoint-continuity` (demo stages 2, 5). -- **Overflow candidates in `select_supply`** → `alternate-target-adapter` (demo stage 4). - -## 8. Open decisions (operator) - -1. **Realization:** (a) Cloudflare Workers + Queues (+ DO later), recommended; or (b) AWS Lambda + DynamoDB + SQS. -2. **Epoch authority.** The recommendation is the workspace head itself, on R2 `If-Match`. This is minimal, but R2's availability bounds every commit. The alternative is a Durable Object holding the head. Gated on verifying R2's conditional put as a strong CAS. -3. **Where the epoch is checked:** settled by the model, not open. Fences and commits are the one head CAS; attach reads the head. Nothing is checked on guest `fsync`. (Kept as a numbered item so the list keeps its numbering.) -4. **Sweep period and lease term.** Together they bound detection time, and the demo receipts it. Drafted as same order as the published 60 s window, pending stage 0. -5. **Move-back hold horizon:** how long a cheaper owned slot must be expected to persist before a planned move is worth its downtime. -6. **Second storage provider (B2) timing.** B2 can join as a chunk replica at any time. It never holds a live head, so promotion means moving the one head, which is a planned operator action and not a failover. diff --git a/docs/probes/mtcollins1_bmc_observation_capability_2026-09-12.md b/docs/probes/mtcollins1_bmc_observation_capability_2026-09-12.md deleted file mode 100644 index 7d01cc7f5a4..00000000000 --- a/docs/probes/mtcollins1_bmc_observation_capability_2026-09-12.md +++ /dev/null @@ -1,113 +0,0 @@ -# Mt. Collins BMC — what this controller can and cannot do, read from its own UI source - -Answers two questions that were being guessed at: whether UEFI settings are reachable, and -whether any mechanical console capture exists. All from the controller's own `source.min.js`, -not from probing endpoint names. - -A THIRD QUESTION WAS ASKED HERE AND SHOULD NOT HAVE BEEN: "why is SOL silent". It presupposed its -own answer. SOL was never silent — a broken collector was — so the section that answered it is -withdrawn below rather than revised, and nothing in this document may be read as establishing a -cause for an absence that did not exist. - -## Provenance - -`https://192.168.1.228/source.min.js`, fetched 2026-09-12, 6,597,873 bytes, -sha256 `8b5ef64f5bf1df88a0f497887540d198a677e7a6f63f1bc8fc9bbc79e4f54fcd`. -255 distinct `api/` paths extracted to `artifacts/bmc/mtcollins1-megarac-api-surface.txt`, -sha256 `4fc53fea3fe49bc3ed3b967511654223578d59d963546117ab524df11b419bcd`. - -This is the file whose ABSENCE forced a retraction on 2026-09-11: a `redirection_status` decoder -was read from it and then withdrawn from the receipt because the bytes were never committed. The -bytes are now committed, so any future reading of this controller's behaviour has a resolvable -source. - -## SOL IS NOT SILENT — the collector was, and this section's original claim is withdrawn - -**SUPERSEDED BY MEASUREMENT.** This section was headed "Why SOL is silent — not a BMC defect" and -stated flatly that "the cause is host-side console routing". Both the premise and the cause are -withdrawn. SOL on this controller delivers host console bytes: a repaired collector captured 3764 -bytes of firmware output across a failing DRAM training boot, including a 16-row per-DIMM table and -typed failure lines. Review 64316 found that this section contradicted the ledger row added in the -same change, and it was right — the corpus cannot answer "what causes SOL silence" two ways. - -**THE MEASURED CAUSE, which is not this one.** The collector ran `ipmitool sol activate` with stdin -not a terminal, which warns, then its receive loop reached stdin EOF and it exited `rc=0` after -roughly three seconds of a four-hundred-and-twenty second window, while the wrapper slept out the -remainder and reported the unchanged file. Eight captures across eight hardware configurations were -86 bytes of the instrument's own warning and banner, seven of them byte-identical. Held under a pty -with stdin open, the same client stayed connected 645 seconds and captured host output on the first -attempt. Authority: `gunbc.recurring_failure_mode` `empty_capture_read_as_clean_result`, seventh -instance. - -**WHAT SURVIVES, AT ITS ACTUAL STRENGTH.** The BMC-side readings stand and were never in dispute: -`sol info` reports `Enabled: true`, 115.2 kbps volatile and non-volatile, channel 1, port 623, and -sessions establish and accept keystrokes. The boot-parameter readback also stands as a READING — -every actuation carried `Console Redirection control : Console redirection occurs per BIOS -configuration setting`, i.e. request byte `00`, defer to BIOS. - -What does NOT follow from that reading is that it explains anything, because there was no silence -to explain. "The BIOS evidently routes console to video" was an inference consumed as a cause -(DESIGN section 4d), built to account for an absence that the instrument manufactured. It is -withdrawn, not merely softened: byte `00` is consistent with a console that prints over SOL -perfectly well, which is what was subsequently observed while that byte's successor was in effect. - -IPMI boot parameter 5 data byte 3, bits 1:0 control this: `00` defer, `01` suppress, `10` -request enabled. Set to `0x02` and verified: the readback now renders -`Request console redirection be enabled`. Whether the firmware HONOURS the request is untested -and is a separate fact from the BMC being able to ask. - -## UEFI / BIOS settings — NOT REACHABLE - -Across all 255 endpoints the only BIOS-adjacent ones are -`maintenance/firmware/upgrade`, `maintenance/hpm/getbiosversions`, -`maintenance/ValidBootfwSignImage` — flashing and version reads. There is no settings surface. -`/redfish/v1/` returns 404; this controller serves no Redfish at all. - -CONSEQUENCE FOR CONVERGENCE: UEFI configuration on this platform is reachable ONLY through an -interactive console. Any workflow step requiring a firmware setting is a human-attended barrier -on Mt. Collins, not an automatable action, and should be modelled as one rather than assumed. - -## The mechanical console capture that existed all along - -``` -settings/video/triggers settings/video/sol-triggers -settings/video/pre-event settings/sol-recorded-video -logs/video logs/video-data logs/video-log logs/video-log/delete -sol/session settings/javasol remote_control/get/kvm/launch -``` - -Every trigger was `0` on arrival — the feature shipped disabled and had never been used. The -controller can record the host console ITSELF on events, which survives the resets that kill a -live `ipmitool sol activate` session. That is the difference between an instrument that needs a -babysitter and one that does not. - -ARMED 2026-09-12, verified by readback on both objects: -- video and SOL recording: chassis power-on, chassis reset, critical, non-recoverable, watchdog -- `video/pre-event`: enable 1, fps 1, duration 10 - -TWO RECORDERS, DIFFERENT SUBJECTS, and the distinction is load-bearing: -- `video/*` records the FRAMEBUFFER — the content the KVM renders, which works today -- `sol-triggers` records the SOL STREAM — empty until console redirection actually takes effect - -So video capture is useful immediately; SOL capture becomes useful only if the firmware honours -the redirection request. - -## The observability lesson this session paid for - -SOL returned an identical 86-byte banner across eight configurations, four power states and -every reset, while the KVM rendered real output throughout. Inferences were drawn from that -silence — including that failures occurred before console initialisation. THE INSTRUMENT WAS -POINTED ELSEWHERE. A capability model that records `sol_stream: present` without recording -`delivering: no` will mislead the next platform the same way. - -## What a 20-unit intake needs before it starts - -1. Console capture that does not depend on a live session — now armed here, and it should be - part of unit bring-up rather than discovered per unit. -2. A diagnostic image whose output does NOT depend on the console. Tonight's census image - reports over serial only; it produced nothing retrievable because it died at initrd load and - because serial carries nothing. The NFS share it boots from is writable and was empty. -3. Console redirection requested explicitly at every actuation, not deferred to BIOS. -4. Per-unit capability observation, because none of this is uniform: this controller serves no - Redfish, exposes no BIOS settings, no per-DIMM telemetry, and cannot decode its own OEM SEL - records. diff --git a/docs/probes/mtcollins1_memory_census_and_boot_delivery_2026-09-11.md b/docs/probes/mtcollins1_memory_census_and_boot_delivery_2026-09-11.md index 63a96beb633..65ec67d6616 100644 --- a/docs/probes/mtcollins1_memory_census_and_boot_delivery_2026-09-11.md +++ b/docs/probes/mtcollins1_memory_census_and_boot_delivery_2026-09-11.md @@ -350,13 +350,8 @@ brief said "PXE netboot" and I took it literally. ## 8. Controller-side observations -**The decoded status enum and the readiness rule that were here have been removed.** They -reconstructed the complete MegaRAC state machine from `source.min.js` as served by this -controller — and that file **is not a committed artifact**. This document opens by claiming -every number is artifact-backed or re-derived, so a table whose source was never preserved -was the one thing in here that could not honour that claim. It was also reusable protocol -policy, which belongs to `extdeps.bmc.megarac` keyed to a firmware identity, not to a probe -record. +**No decoded status enum or readiness rule is carried here.** Protocol policy belongs to +`extdeps.bmc.megarac` keyed to a firmware identity, not to a probe record. What survives is what was measured against the live controller and is backed by this session's own observations: @@ -384,8 +379,8 @@ difference I could find. I could not explain the variation and did not invent a **The readback comparator is the entire verdict**; the HTTP status is diagnostic only. Also observed: `start-media` requests without the undocumented key `image_redirection: 1` -were refused with errors 13410/13460; the requests carrying it were accepted. How far that -generalizes is the status/API catalog's to say, not this record's. +were refused; the requests carrying it were accepted. How far that generalizes is not this +record's to say. ### BMC controller reset timings (3 resets, consistent) diff --git a/fixtures/algebra_length_homonym/probe.dag b/fixtures/algebra_length_homonym/probe.dag new file mode 100644 index 00000000000..76c634ea902 --- /dev/null +++ b/fixtures/algebra_length_homonym/probe.dag @@ -0,0 +1,13 @@ +module algebra_length_homonym_probe + +import std.types { String, Int } + +// A user `length` whose leaf is the algebra op's leaf and whose owner is not v2.std.algebra. +// Lives under fixtures/ so the accepted corpus (dag + src/v2) never grows a second `fn length` +// (floor: every bare `length` would then be an unimported two-declarer). Compiled only by +// test.claim.infer_algebra_length_homonym_witness. +fn length(xs: String) -> Int { 99 } + +fn probe() -> Int { + length(xs: "abc") +} diff --git a/fixtures/native_cli_door/door_probe.dag b/fixtures/native_cli_door/dag/door_probe.dag similarity index 94% rename from fixtures/native_cli_door/door_probe.dag rename to fixtures/native_cli_door/dag/door_probe.dag index c441246f4f7..7c20b4831c5 100644 --- a/fixtures/native_cli_door/door_probe.dag +++ b/fixtures/native_cli_door/dag/door_probe.dag @@ -7,7 +7,7 @@ module fixture.native_cli_door.door_probe // walked through, which DESIGN section 5 names as specification-without-execution: a green that // establishes the closure typechecks and nothing about whether the program runs. This file is the // argv the built binary is handed so that it does run: `emit --entry -// fixture.native_cli_door.door_probe --source-root fixtures/native_cli_door`. +// fixture.native_cli_door.door_probe --source-root fixtures/native_cli_door/dag`. // // IT HAS NO IMPORTS, for the same reason `fixture.closure.rustc_green_probe` has none: the // admission's subject resolves against the validated roots the ingest produced, so a module whose @@ -17,7 +17,8 @@ module fixture.native_cli_door.door_probe // // IT IS ITS OWN DIRECTORY AND NOT A FILE IN AN EXISTING FIXTURE ROOT, because the CLI's source // root is walked RECURSIVELY for every `.dag` file under it; sharing a directory would make the -// control's ingest a function of what some other fixture's author adds next to it. +// control's ingest a function of what some other fixture's author adds next to it. The walk +// argument is the dag tree (`.../dag`), not a parent of mixed trees. // // THE DECLARATION'S NAME AND VALUE ARE WHAT THE CONTROL READS. v2 lowers this `data` declaration to // a named member whose body is the literal, and the door renders it through the Rust target's diff --git a/fixtures/native_ingest_copied_single_root/dag/copied_dag.dag b/fixtures/native_ingest_copied_single_root/dag/copied_dag.dag new file mode 100644 index 00000000000..8d779e29278 --- /dev/null +++ b/fixtures/native_ingest_copied_single_root/dag/copied_dag.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.copied_dag + +data native_ingest_copied_dag_value: Int = 1 diff --git a/fixtures/native_ingest_copied_single_root/src/v2/copied_v2.dag b/fixtures/native_ingest_copied_single_root/src/v2/copied_v2.dag new file mode 100644 index 00000000000..4d259ce7c09 --- /dev/null +++ b/fixtures/native_ingest_copied_single_root/src/v2/copied_v2.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.copied_v2 + +data native_ingest_copied_v2_value: Int = 2 diff --git a/fixtures/native_ingest_duplicate_module/dag/collision_a.dag b/fixtures/native_ingest_duplicate_module/dag/collision_a.dag new file mode 100644 index 00000000000..1538ef552a1 --- /dev/null +++ b/fixtures/native_ingest_duplicate_module/dag/collision_a.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.collision + +data native_ingest_collision_a_value: Int = 1 diff --git a/fixtures/native_ingest_duplicate_module/dag/collision_b.dag b/fixtures/native_ingest_duplicate_module/dag/collision_b.dag new file mode 100644 index 00000000000..6e4a6710d0f --- /dev/null +++ b/fixtures/native_ingest_duplicate_module/dag/collision_b.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.collision + +data native_ingest_collision_b_value: Int = 2 diff --git a/fixtures/native_ingest_src_in_dag/dag/pkg/src/leaf.dag b/fixtures/native_ingest_src_in_dag/dag/pkg/src/leaf.dag new file mode 100644 index 00000000000..9761f2f9c71 --- /dev/null +++ b/fixtures/native_ingest_src_in_dag/dag/pkg/src/leaf.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.src_in_dag + +data native_ingest_src_in_dag_value: Int = 1 diff --git a/fixtures/native_ingest_two_roots/dag/dag_entry.dag b/fixtures/native_ingest_two_roots/dag/dag_entry.dag new file mode 100644 index 00000000000..1e2fd471527 --- /dev/null +++ b/fixtures/native_ingest_two_roots/dag/dag_entry.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.dag_entry + +data native_ingest_dag_entry_value: Int = 1 diff --git a/fixtures/native_ingest_two_roots/src/v2/v2_other.dag b/fixtures/native_ingest_two_roots/src/v2/v2_other.dag new file mode 100644 index 00000000000..69338a89ba1 --- /dev/null +++ b/fixtures/native_ingest_two_roots/src/v2/v2_other.dag @@ -0,0 +1,3 @@ +module fixture.native_ingest.v2_other + +data native_ingest_v2_other_value: Int = 2 diff --git a/provisioning/srv1/gunbc-ghrunner.sudoers b/provisioning/srv1/gunbc-ghrunner.sudoers index b816df1ea36..914bac30f00 100644 --- a/provisioning/srv1/gunbc-ghrunner.sudoers +++ b/provisioning/srv1/gunbc-ghrunner.sudoers @@ -118,3 +118,33 @@ ghrunner ALL=(root) NOPASSWD: /usr/sbin/nft list ruleset ghrunner ALL=(root) NOPASSWD: /usr/sbin/iptables-legacy-save ghrunner ALL=(root) NOPASSWD: /usr/sbin/nfct helper list ghrunner ALL=(root) NOPASSWD: /usr/sbin/conntrack -L expect +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/dispatch-worktrees +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/attempt-state +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /home/briansrls/.codex@gunbc-roadmap +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/auth-sessions +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/auth-login-transactions +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/auth-jwks +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/auth-profiles +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/issue-view-preferences +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/roadmap-events +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/compute +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage/objects +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage/heads +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage/state +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage/state/objects +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage/state/heads +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/fabric-storage-door +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /var/lib/gunbc/artifacts.git +ghrunner ALL=(root) NOPASSWD: /usr/bin/stat -c %F|%U|%G|%a -- /opt/gunbc/provider-runtime/codex +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /opt/gunbc +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /opt +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- / +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /home/briansrls +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /home +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /opt/gunbc/fabric-storage +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /opt/gunbc/fabric-storage/state +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /var/lib/gunbc +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /var/lib +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /var +ghrunner ALL=(root) NOPASSWD: /usr/bin/ls -1A -- /opt/gunbc/provider-runtime diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index c0c3bd7f0da..d610428f065 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -432,6 +432,7 @@ type CompilerDiagnostic | EffectfulSelfRecursionUnrealized { name: String, span: SourceSpan } | ModuleFilenameCollision { filename: String, modules: List, span: SourceSpan } | EmittedSymbolCollision { symbol: String, identities: List, span: SourceSpan } + | NativeEffectRealizationRefused { entry: String, refusals: List, span: SourceSpan } | EffectSummaryIncompleteAtFunctionValue { caller: String, span: SourceSpan } | EffectSummaryIncompleteAtLocalBinding { caller: String, name: String, span: SourceSpan } | CallArgumentNameUnknown { callee: String, argument: String, declared: List, span: SourceSpan } @@ -666,6 +667,7 @@ fn diagnostic_to_span(d: CompilerDiagnostic) -> SourceSpan { EffectfulSelfRecursionUnrealized { name: _, span: s } => s ModuleFilenameCollision { filename: _, modules: _, span: s } => s EmittedSymbolCollision { symbol: _, identities: _, span: s } => s + NativeEffectRealizationRefused { entry: _, refusals: _, span: s } => s EffectSummaryIncompleteAtFunctionValue { caller: _, span: s } => s EffectSummaryIncompleteAtLocalBinding { caller: _, name: _, span: s } => s CallArgumentNameUnknown { callee: _, argument: _, declared: _, span: s } => s @@ -776,6 +778,7 @@ fn diagnostic_to_message(d: CompilerDiagnostic) -> String { EffectfulSelfRecursionUnrealized { name: n, span: _ } => concat("effectful declaration '", n, "' calls itself outside tail position: the Rust realization lowers tail calls to a loop, but non-tail async recursion has no realization. Put the recursive call in tail position or move it into a pure helper.") ModuleFilenameCollision { filename: f, modules: ms, span: _ } => concat("module filename collision: ", to_string(value: ms |> count), " modules render one emitted file name '", f, "': ", join(ms, separator: ", "), " — module_to_filename maps '.' to '_', so these names are indistinguishable at the emitted path; rename one module segment") EmittedSymbolCollision { symbol: s, identities: ids, span: _ } => concat("emitted symbol collision: ", to_string(value: ids |> count), " authored identities render one emitted symbol '", s, "': ", join(ids, separator: ", "), " — two authored identities one emitted scope cannot hold apart (an undotted service with a type's name, a type spelled with `__`, dotted services differing only in first-letter case or in a segment's leading or trailing `_`); rename one") + NativeEffectRealizationRefused { entry: e, refusals: rs, span: _ } => concat("native effect realization refused for entry '", e, "': ", join(rs, separator: "; "), " (v1.compiler.emit NativeEffectRealizationAdmission: the rendered main binds only admitted handlers, so an unadmitted effect refuses here rather than failing to build or link)") EffectSummaryIncompleteAtFunctionValue { caller: c, span: _ } => concat("effect summary incomplete: ", c, " calls through a function value, whose callee is chosen at runtime, so its effects are unknown rather than empty — the caller's summary is a lower bound, not the answer") EffectSummaryIncompleteAtLocalBinding { caller: c, name: n, span: _ } => concat(concat(concat(concat("effect summary incomplete: ", c), " calls the local binding '"), n), "', whose effects this pass cannot join through the registry, so the caller's summary is a lower bound rather than the answer") CallArgumentNameUnknown { callee: c, argument: a, declared: ds, span: _ } => @@ -922,6 +925,7 @@ fn diagnostic_disposition(d: CompilerDiagnostic) -> DiagnosticDisposition { EffectfulSelfRecursionUnrealized { name: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } ModuleFilenameCollision { filename: _, modules: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } EmittedSymbolCollision { symbol: _, identities: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } + NativeEffectRealizationRefused { entry: _, refusals: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } EffectSummaryIncompleteAtFunctionValue { caller: _, span: _ } => DiagnosticDisposition { severity: SeverityNonError, gate: GateAdvisoryTypecheck } EffectSummaryIncompleteAtLocalBinding { caller: _, name: _, span: _ } => DiagnosticDisposition { severity: SeverityNonError, gate: GateAdvisoryTypecheck } CallArgumentNameUnknown { callee: _, argument: _, declared: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } diff --git a/src/v1/04_emit_info.dag b/src/v1/04_emit_info.dag index bd401957bea..70c649484d2 100644 --- a/src/v1/04_emit_info.dag +++ b/src/v1/04_emit_info.dag @@ -21,7 +21,9 @@ import v1.std.core { } import std.decl_ref { DeclarationRef, WholeDeclaration, NamedField, TypeParameter } import std.types { is_kernel_type } -import v1.compiler.infer_env { TypeEnv, SymbolIndex, empty_symbol_index, empty_type_env, symbol_index_lookup } +import std.literal_elaboration { kernel_mint_declaration_for, KernelMintDeclarationFound, KernelMintDeclarationAbsent, KernelMintDeclarationAmbiguous } +import gunbc.structural_realization_bindings { kernel_mint_declaration_rows } +import v1.compiler.infer_env { TypeEnv, SymbolIndex, empty_symbol_index, empty_type_env, symbol_index_lookup, BareOccurrenceBinding, BareOccurrenceDeclaredInItsFile, BareOccurrenceImportedByName, BareOccurrenceIsKernelName, BareOccurrenceUndecided, bare_occurrence_binding } import v1.compiler.infer_types { TextJudgment, TextJudgedIn, TextNotAsked, TextNotAskedInVariantFieldSummary, TextNotAskedForCallableComponentResidue, normalize_access_type_node, resolved_type, @@ -40,6 +42,7 @@ type TypeRepr = StructRepr | EnumRepr { unit_only: Bool } type TypeSummary { name: String + key: String repr: TypeRepr field_summaries: Map field_type_map: Map @@ -49,6 +52,122 @@ type TypeSummary { has_fn_fields: Bool } +// THE TYPE SUMMARIES, KEYED BY DECLARATION. A summary is stored under its declaration's identity +// (type_decl_identity: owner module and declared name, the key TypeDeclIndex uses), and a +// variant-payload summary under `::`. The leaf-keyed map this replaces kept ONE +// summary per leaf -- whichever module was folded last -- so a second `Optional` anywhere in a +// closure replaced std.optional's summary for every reader in every module +// (gunbc.recurring_failure_mode emitter_index_keyed_on_a_type_leaf_takes_the_last_declarer). +type TypeSummaryIndex { + by_key: Map + keys_by_leaf: Map> +} + +// A NAME HOLDER'S ANSWER HAS THREE ARMS, as TypeDeclResolution's does: a key (an identity, or an +// identity-qualified variant key) is exact; a bare leaf one declaration owns resolves to it; a bare +// leaf two declarations own is TypeSummaryLeafAmbiguous and names neither. +type TypeSummaryLookup + = TypeSummaryFound { summary: TypeSummary } + | TypeSummaryLeafAmbiguous { leaf: String } + | TypeSummaryNotDeclared + +fn empty_type_summary_index() -> TypeSummaryIndex { + TypeSummaryIndex { by_key: empty_map(), keys_by_leaf: empty_map() } +} + +fn type_summary_index_insert(index: TypeSummaryIndex, key: String, leaf: String, summary: TypeSummary) -> TypeSummaryIndex { + TypeSummaryIndex { + by_key: map_insert(index.by_key, key, summary), + keys_by_leaf: map_insert(index.keys_by_leaf, leaf, type_decl_leaf_identities_with(prior: map_get(index.keys_by_leaf, leaf), identity: key)) + } +} + +fn type_summary_keys_of_leaf(index: TypeSummaryIndex, leaf: String) -> List { + match map_get(index.keys_by_leaf, leaf) { + Present { value: keys } => keys + Absent => [] + } +} + +fn type_summary_at_key(index: TypeSummaryIndex, key: String) -> TypeSummaryLookup { + match map_get(index.by_key, key) { + Present { value: summary } => TypeSummaryFound { summary: summary } + Absent => TypeSummaryNotDeclared + } +} + +fn type_summary_by_leaf(index: TypeSummaryIndex, leaf: String) -> TypeSummaryLookup { + let keys = type_summary_keys_of_leaf(index: index, leaf: leaf) + if (keys |> count) == 1 { + keys |> fold(init: TypeSummaryNotDeclared, f: (_acc, key) => type_summary_at_key(index: index, key: key)) + } else if (keys |> count) > 1 { + TypeSummaryLeafAmbiguous { leaf: leaf } + } else { + TypeSummaryNotDeclared + } +} + +// A string key, as the readers that hold only a name hand it over: an exact key first (an identity, +// or `::`), then a leaf-qualified variant key (`Enum::Variant`, whose enum part +// resolves by its leaf), then a bare leaf. A dotted name that is no key is not resolved by spelling. +fn type_summary_lookup(index: TypeSummaryIndex, key: String) -> TypeSummaryLookup { + match map_get(index.by_key, key) { + Present { value: summary } => TypeSummaryFound { summary: summary } + Absent => + if contains(key, "::") { + let parts = key |> split(delimiter: "::") + let enum_part = parts |> take((parts |> count) - 1) |> join(separator: "::") + let variant_part = match parts |> last { + Present { value: v } => v + Absent => "" + } + match type_summary_lookup(index: index, key: enum_part) { + TypeSummaryFound { summary: owner } => type_summary_at_key(index: index, key: variant_summary_key(enum_name: owner.key, variant_name: variant_part)) + TypeSummaryLeafAmbiguous { leaf: l } => TypeSummaryLeafAmbiguous { leaf: l } + TypeSummaryNotDeclared => TypeSummaryNotDeclared + } + } else if contains(key, ".") { + TypeSummaryNotDeclared + } else { + type_summary_by_leaf(index: index, leaf: key) + } + } +} + +// A reader holding a type REFERENCE resolves it the way every emitter reader of a declaration does +// (resolve_type_decl_reference: carried identity, declaring span, the counted interim arms, then the +// spelling), and reads the summary stored under that identity. +fn type_summary_of_reference(index: TypeSummaryIndex, decls: TypeDeclIndex, type_expr: Node, source_indices: Map) -> TypeSummaryLookup { + match resolve_type_decl_reference(index: decls, type_expr: type_expr, source_indices: source_indices) { + TypeDeclResolved { identity: identity, decl: _ } => type_summary_at_key(index: index, key: identity) + TypeDeclLeafAmbiguous { leaf: l } => TypeSummaryLeafAmbiguous { leaf: l } + TypeDeclKernelSpellingUndecided { leaf: l } => TypeSummaryLeafAmbiguous { leaf: l } + TypeDeclNotDeclared => TypeSummaryNotDeclared + } +} + +// THE OPTIONAL PROJECTION, for readers that hold only a NAME and ask which summary it denotes in order +// to collect use-surfaces or read a field map. It answers a summary only when the name decides one +// (an exact key, or a leaf one declaration owns); a leaf two declarations own answers Absent and no +// summary is picked for it -- the leaf-keyed map this replaces answered with the LAST declarer here. +// The readers that consume it collect names for import lines or check a repr, so an undecided name +// contributes nothing and the item it would have served refuses at rustc, located at its use. +// RESIDUE, declared: these name-holding readers dissolve when the names they hold are keys -- a +// TypeSummary's field_type_map and field_import_surface_names store the field type's declaration +// identity rather than its spelling -- on gunbc.recurring_failure_mode +// emitter_index_keyed_on_a_type_leaf_takes_the_last_declarer's trigger. +fn type_summary_decided(index: TypeSummaryIndex, key: String) -> TypeSummary? { + match type_summary_lookup(index: index, key: key) { + TypeSummaryFound { summary: summary } => Present { value: summary } + TypeSummaryLeafAmbiguous { leaf: _ } => none + TypeSummaryNotDeclared => none + } +} + +fn type_summary_values(index: TypeSummaryIndex) -> List { + map_values(index.by_key) +} + type FieldTypeMapBuild { field_types: Map import_surface_names: List @@ -158,6 +277,15 @@ type TypeDeclIndex { qualified_names: SymbolIndex } +// WHAT A BARE LEAF DENOTES AT ONE OCCURRENCE: v1.compiler.infer_env bare_occurrence_binding (own file, +// then named import, then kernel -- the typechecker's lookup_binding order), joined here to the +// declaration identity this index keys on. +type TypeDeclOccurrenceBinding + = OccurrenceDeclaredHere { identity: String } + | OccurrenceImported { identity: String } + | OccurrenceIsKernelName + | OccurrenceUndecided + // TypeDeclKernelSpellingUndecided IS ITS OWN ARM BECAUSE IT IS A DIFFERENT FACT FROM AMBIGUITY. // `String` and `Int` are kernel spellings (std.types is_kernel_type), and resolve leaves a // kernel-spelled reference unstamped on purpose (v1.compiler.infer_resolve @@ -263,6 +391,8 @@ type TypeDeclReferenceRoute | RouteDeclaringSpan | RouteReferencingModuleDeclarer | RouteQualifiedSpelling + | RouteReferencingModuleImport + | RouteKernelMint | RouteLeafSpelling type TypeDeclRoutedResolution { @@ -292,13 +422,15 @@ fn resolve_type_decl_routed(index: TypeDeclIndex, type_expr: Node, source_indice let leaf = qualified_last_segment(name: authored) let by_spelling = resolve_type_decl_leaf(index: index, leaf: leaf) let referencing_module_identity = match by_spelling { - TypeDeclLeafAmbiguous { leaf: l } => if authored == l { type_decl_referencing_module_declarer(index: index, leaf: l, reference: type_expr) } else { type_decl_qualifier_declarer(index: index, authored: authored) } + TypeDeclLeafAmbiguous { leaf: l } => + if authored == l { type_decl_occurrence_identity(index: index, leaf: l, reference: type_expr) } + else { type_decl_qualifier_declarer(index: index, authored: authored) } TypeDeclResolved { identity: _, decl: _ } => none TypeDeclKernelSpellingUndecided { leaf: _ } => none TypeDeclNotDeclared => none } match referencing_module_identity { - Present { value: identity } => TypeDeclRoutedResolution { route: if authored == leaf { RouteReferencingModuleDeclarer } else { RouteQualifiedSpelling }, resolution: type_decl_resolution_at_identity(index: index, identity: identity) } + Present { value: identity } => TypeDeclRoutedResolution { route: type_decl_spelling_route(index: index, authored: authored, leaf: leaf, reference: type_expr), resolution: type_decl_resolution_at_identity(index: index, identity: identity) } Absent => TypeDeclRoutedResolution { route: RouteLeafSpelling, resolution: by_spelling } } } @@ -341,22 +473,84 @@ fn type_decl_qualifier_declarer(index: TypeDeclIndex, authored: String) -> Strin } } -fn type_decl_index_with_qualified_names(index: TypeDeclIndex, qualified_names: SymbolIndex) -> TypeDeclIndex { - TypeDeclIndex { by_identity: index.by_identity, identity_by_declaring_span: index.identity_by_declaring_span, leaf_owners: index.leaf_owners, identities_by_leaf: index.identities_by_leaf, qualified_names: qualified_names } +// THE KERNEL MINT'S DECLARER. Which corpus declaration the kernel type IS is a modeled fact +// (gunbc.structural_realization_bindings kernel_mint_declaration_rows, the row the pattern side reads +// through kernel_mint_ownership), but that answers WHICH declaration a mint stands for, not WHETHER an +// occurrence denotes it. So this arm is reached only from type_decl_occurrence_identity, and only when +// the occurrence is on the kernel arm of the resolver's order (v1.compiler.infer_env bare_occurrence_binding). A spelling no row mints, or two rows +// mint, stays LeafAmbiguous and refuses. Counted as kernel_mint; interim under +// referencing_module_declarer_route_dissolves_on. +fn type_decl_kernel_mint_declarer(index: TypeDeclIndex, leaf: String) -> String? { + match kernel_mint_declaration_for(rows: kernel_mint_declaration_rows, minted_name: leaf) { + KernelMintDeclarationFound { declaration: d } => + let identity = type_decl_identity(owner_module: d.module_path as String, decl_name: d.decl_name as String) + match map_get(index.by_identity, identity) { + Present { value: _ } => Present { value: identity } + Absent => none + } + KernelMintDeclarationAbsent => none + KernelMintDeclarationAmbiguous { row_count: _ } => none + } } -data referencing_module_declarer_route_dissolves_on: DissolutionCondition = unbound_dissolution(description: "RouteReferencingModuleDeclarer and RouteQualifiedSpelling dissolve when the type environment's declaration bodies carry Node.declaration on every member type reference, stamped by v1.compiler.infer_resolve reference_with_declaration in the DECLARING module's scope, so a bare reference resolved through lookup_type_for hands the emitter stamped members: v1.tests.claim.carrier_realization_census reports zero emitter_decl_route referencing_module_declarer or qualified_spelling rows over the self-host and dag closures. Stamping one declaration, or one closure, is not that capability. Then both arms, type_decl_referencing_module_declarer and type_decl_qualifier_declarer are deleted and such a reference refuses unless carried.") - -fn type_decl_referencing_module_declarer(index: TypeDeclIndex, leaf: String, reference: Node) -> String? { - let local = type_decl_identities_of_leaf(index: index, leaf: leaf) |> filter(identity => - match map_get(index.by_identity, identity) { - Present { value: decl } => decl.span.file == reference.span.file - Absent => false +fn type_decl_spelling_route(index: TypeDeclIndex, authored: String, leaf: String, reference: Node) -> TypeDeclReferenceRoute { + if authored != leaf { RouteQualifiedSpelling } + else { + match type_decl_occurrence_binding(index: index, leaf: leaf, reference: reference) { + OccurrenceDeclaredHere { identity: _ } => RouteReferencingModuleDeclarer + OccurrenceIsKernelName => RouteKernelMint + OccurrenceImported { identity: _ } => RouteReferencingModuleImport + OccurrenceUndecided => RouteLeafSpelling } - ) - if (local |> fold(init: 0, f: (n, _identity) => n + 1)) == 1 { local |> fold(init: none, f: (acc, identity) => Present { value: identity }) } else { none } + } +} + +// THE IDENTITY A BARE AMBIGUOUS LEAF DENOTES AT THIS OCCURRENCE, or none (it stays LeafAmbiguous and +// refuses). The kernel mint is consulted only on the kernel arm of the resolver's order. +fn type_decl_occurrence_identity(index: TypeDeclIndex, leaf: String, reference: Node) -> String? { + match type_decl_occurrence_binding(index: index, leaf: leaf, reference: reference) { + OccurrenceDeclaredHere { identity: identity } => Present { value: identity } + OccurrenceIsKernelName => type_decl_kernel_mint_declarer(index: index, leaf: leaf) + OccurrenceImported { identity: identity } => Present { value: identity } + OccurrenceUndecided => none + } } +fn type_decl_occurrence_binding(index: TypeDeclIndex, leaf: String, reference: Node) -> TypeDeclOccurrenceBinding { + match bare_occurrence_binding(index: index.qualified_names, name: leaf, reference: reference) { + BareOccurrenceDeclaredInItsFile { declaration: decl } => + match type_decl_identity_of_declaration(index: index, decl: decl) { + Present { value: identity } => OccurrenceDeclaredHere { identity: identity } + Absent => OccurrenceUndecided + } + BareOccurrenceImportedByName { module_path: from_module, declaration: decl } => + match type_decl_identity_of_declaration(index: index, decl: decl) { + Present { value: identity } => OccurrenceImported { identity: identity } + Absent => + let imported = type_decl_identity(owner_module: from_module, decl_name: leaf) + match map_get(index.by_identity, imported) { + Present { value: _ } => OccurrenceImported { identity: imported } + Absent => OccurrenceUndecided + } + } + BareOccurrenceIsKernelName => OccurrenceIsKernelName + BareOccurrenceUndecided => OccurrenceUndecided + } +} + +fn type_decl_identity_of_declaration(index: TypeDeclIndex, decl: Node) -> String? { + match decl.ident_span { + Present { value: sp } => map_get(index.identity_by_declaring_span, declaring_span_key(sp: sp)) + Absent => none + } +} + +fn type_decl_index_with_qualified_names(index: TypeDeclIndex, qualified_names: SymbolIndex) -> TypeDeclIndex { + TypeDeclIndex { by_identity: index.by_identity, identity_by_declaring_span: index.identity_by_declaring_span, leaf_owners: index.leaf_owners, identities_by_leaf: index.identities_by_leaf, qualified_names: qualified_names } +} + +data referencing_module_declarer_route_dissolves_on: DissolutionCondition = unbound_dissolution(description: "RouteReferencingModuleDeclarer, RouteReferencingModuleImport, RouteQualifiedSpelling and RouteKernelMint dissolve when the type environment's declaration bodies carry Node.declaration on every member type reference, stamped by v1.compiler.infer_resolve reference_with_declaration in the DECLARING module's scope, so a bare reference resolved through lookup_type_for hands the emitter stamped members: v1.tests.claim.carrier_realization_census reports zero emitter_decl_route referencing_module_declarer, referencing_module_import, qualified_spelling or kernel_mint rows over the self-host and dag closures. Stamping one declaration, or one closure, is not that capability. Then type_decl_occurrence_binding and type_decl_qualifier_declarer are deleted and such a reference refuses unless carried.") + fn resolve_type_decl_reference(index: TypeDeclIndex, type_expr: Node, source_indices: Map) -> TypeDeclResolution { resolve_type_decl_routed(index: index, type_expr: type_expr, source_indices: source_indices).resolution } @@ -368,6 +562,8 @@ fn type_decl_routed_resolution_label(routed: TypeDeclRoutedResolution) -> String RouteDeclaringSpan => "declaring_span" RouteReferencingModuleDeclarer => "referencing_module_declarer" RouteQualifiedSpelling => "qualified_spelling" + RouteReferencingModuleImport => "referencing_module_import" + RouteKernelMint => "kernel_mint" RouteLeafSpelling => "leaf_spelling" } match routed.resolution { @@ -379,7 +575,7 @@ fn type_decl_routed_resolution_label(routed: TypeDeclRoutedResolution) -> String } type EmitGraphInfo { - type_summaries: Map + type_summaries: TypeSummaryIndex type_decl_items: TypeDeclIndex data_variant_wire_spellings: Map fn_decl_items: Map @@ -409,7 +605,7 @@ type EmitGraphInfo { } type EmitInfoBuildState { - type_summaries: Map + type_summaries: TypeSummaryIndex type_decl_items: TypeDeclIndex fn_decl_items: Map structural_alias_fn_surface_names: Map> @@ -419,7 +615,7 @@ type EmitInfoBuildState { fn empty_emit_graph_info() -> EmitGraphInfo { EmitGraphInfo { item_leaf_owner_modules: empty_map(), - type_summaries: empty_map(), + type_summaries: empty_type_summary_index(), type_decl_items: empty_type_decl_index(), data_variant_wire_spellings: empty_map(), fn_decl_items: empty_map(), @@ -540,7 +736,34 @@ fn variant_summary_key(enum_name: String, variant_name: String) -> String { } fn lookup_emit_type_summary(emit_info: EmitGraphInfo, type_name: String) -> TypeSummary? { - map_get(emit_info.type_summaries, type_name) + type_summary_decided(index: emit_info.type_summaries, key: type_name) +} + +// A YES/NO QUESTION ABOUT A NAME. Unanimous answers from every declaration of a leaf are decided. +// Disagreement is not "no": collapsing it to false made `is this an enum?` emit the non-enum arm +// for a name two declarations own, which rustc can accept. NotDeclared is a separate arm so a +// missing name is not an ambiguous one. +type TypeSummaryQuestion + = QuestionDecided { value: Bool } + | QuestionNameAmbiguous { leaf: String } + | QuestionNotDeclared + +fn type_summary_answer(index: TypeSummaryIndex, key: String, question: fn(TypeSummary) -> Bool) -> TypeSummaryQuestion { + match type_summary_lookup(index: index, key: key) { + TypeSummaryFound { summary: summary } => QuestionDecided { value: question(summary) } + TypeSummaryNotDeclared => QuestionNotDeclared + TypeSummaryLeafAmbiguous { leaf: leaf } => + let answers = type_summary_keys_of_leaf(index: index, leaf: leaf) |> fold(init: [], f: (acc, k) => + match map_get(index.by_key, k) { + Present { value: summary } => list_push(acc, question(summary)) + Absent => acc + } + ) + if (answers |> count) == 0 { QuestionNotDeclared } + else if answers |> all(a => a) { QuestionDecided { value: true } } + else if answers |> all(a => !a) { QuestionDecided { value: false } } + else { QuestionNameAmbiguous { leaf: leaf } } + } } fn emit_graph_records_type_decl(item: Node, source_indices: Map) -> Bool { @@ -565,14 +788,17 @@ fn emit_graph_records_fn_decl(item: Node) -> Bool { item.module_item_kind == ModuleItemFunction } -fn derive_variant_to_enum(type_summaries: Map) -> Map { - map_values(type_summaries) |> fold(init: empty_map(), f: (acc, summary) => +// variant leaf -> the KEY of the enum that declares it, or "" when two enums declare that variant. +// The value is the owner's identity, not its leaf: two same-leaf enums own different variants, and a +// leaf value would send every reader back through the ambiguous spelling. +fn derive_variant_to_enum(type_summaries: TypeSummaryIndex) -> Map { + type_summary_values(index: type_summaries) |> fold(init: empty_map(), f: (acc, summary) => match summary.repr { EnumRepr { unit_only: _ } => fold(map_keys(summary.variant_name_set), init: acc, f: (inner, vn) => match map_get(inner, vn) { Present { value: _ } => map_insert(inner, vn, "") - Absent => map_insert(inner, vn, summary.name) + Absent => map_insert(inner, vn, summary.key) } ) _ => acc @@ -580,8 +806,8 @@ fn derive_variant_to_enum(type_summaries: Map) -> Map, name: String) -> Bool { - map_values(type_summaries) |> any(summary => +fn is_known_variant(type_summaries: TypeSummaryIndex, name: String) -> Bool { + type_summary_values(index: type_summaries) |> any(summary => match summary.repr { EnumRepr { unit_only: _ } => emit_map_has(m: summary.variant_name_set, key: name) _ => false @@ -589,32 +815,44 @@ fn is_known_variant(type_summaries: Map, name: String) -> B ) } -fn variant_belongs_to_enum(type_summaries: Map, variant_name: String, enum_name: String) -> Bool { - match map_get(type_summaries, enum_name) { - Present { value: summary } => - match summary.repr { - EnumRepr { unit_only: _ } => - emit_map_has(m: summary.variant_name_set, key: variant_name) - _ => false - } - Absent => false +fn summary_is_enum_with_variant(summary: TypeSummary, variant_name: String) -> Bool { + match summary.repr { + EnumRepr { unit_only: _ } => emit_map_has(m: summary.variant_name_set, key: variant_name) + _ => false } } -fn is_enum_in_summaries(type_summaries: Map, type_name: String) -> Bool { - match map_get(type_summaries, type_name) { - Present { value: summary } => match summary.repr { - EnumRepr { unit_only: _ } => true - _ => false - } - Absent => false +fn summary_is_enum(summary: TypeSummary) -> Bool { + match summary.repr { + EnumRepr { unit_only: _ } => true + _ => false } } -fn find_variant_parent(type_summaries: Map, variant_name: String, scope_enums: List) -> String? { - scope_enums |> filter(en => - variant_belongs_to_enum(type_summaries: type_summaries, variant_name: variant_name, enum_name: en) - ) |> first +fn variant_belongs_to_enum(type_summaries: TypeSummaryIndex, variant_name: String, enum_name: String) -> TypeSummaryQuestion { + type_summary_answer(index: type_summaries, key: enum_name, question: summary => summary_is_enum_with_variant(summary: summary, variant_name: variant_name)) +} + +fn is_enum_in_summaries(type_summaries: TypeSummaryIndex, type_name: String) -> TypeSummaryQuestion { + type_summary_answer(index: type_summaries, key: type_name, question: summary => summary_is_enum(summary: summary)) +} + +fn find_variant_parent(type_summaries: TypeSummaryIndex, variant_name: String, scope_enums: List) -> String? { + if scope_enums |> any(en => + match variant_belongs_to_enum(type_summaries: type_summaries, variant_name: variant_name, enum_name: en) { + QuestionNameAmbiguous { leaf: _ } => true + _ => false + } + ) { + none + } else { + scope_enums |> filter(en => + match variant_belongs_to_enum(type_summaries: type_summaries, variant_name: variant_name, enum_name: en) { + QuestionDecided { value: v } => v + _ => false + } + ) |> first + } } fn field_value_shape_from_type_node(type_node: Node) -> FieldValueShape { @@ -735,6 +973,7 @@ fn build_type_summary(item: Node, source_indices: Map) -> let field_types = build_field_type_map(children: item.children, source_indices: source_indices) Present { value: TypeSummary { name: authored_name_at(source_indices: source_indices, node: item), + key: "", repr: StructRepr, field_summaries: build_struct_field_summaries(parent: item, source_indices: source_indices), field_type_map: field_types.field_types, @@ -747,6 +986,7 @@ fn build_type_summary(item: Node, source_indices: Map) -> let unit_only = item.children |> all(child => child.children |> count == 0) Present { value: TypeSummary { name: authored_name_at(source_indices: source_indices, node: item), + key: "", repr: EnumRepr { unit_only: unit_only }, field_summaries: build_enum_field_summaries(variants: item.children, source_indices: source_indices), field_type_map: empty_map(), @@ -805,15 +1045,26 @@ type FnReachRound { grew: Bool } -fn fn_reach_holds_directly(name: String, summaries: Map, structural_alias_direct_fn_names: Set) -> Bool { - match map_get(summaries, name) { +// The walk is over summary KEYS. A field names its type by spelling, so a target resolves to the +// key it names exactly, or to EVERY declarer of that leaf: reachability is then an over-approximation +// computed as the answer (a type reaching a function value through either same-leaf declaration is +// marked), and the spelling itself is kept for the structural aliases, which this index does not hold. +fn fn_reach_target_keys(index: TypeSummaryIndex, target: String) -> List { + match map_get(index.by_key, target) { + Present { value: _ } => [target] + Absent => concat(type_summary_keys_of_leaf(index: index, leaf: target), [target]) + } +} + +fn fn_reach_holds_directly(name: String, summaries: TypeSummaryIndex, structural_alias_direct_fn_names: Set) -> Bool { + match map_get(summaries.by_key, name) { Present { value: s } => s.has_fn_fields Absent => set_contains(structural_alias_direct_fn_names, name) } } -fn fn_reach_targets(name: String, summaries: Map, structural_alias_fn_surface_names: Map>) -> List { - match map_get(summaries, name) { +fn fn_reach_targets(name: String, summaries: TypeSummaryIndex, structural_alias_fn_surface_names: Map>) -> List { + let spelled = match map_get(summaries.by_key, name) { Present { value: s } => concat(map_values(s.field_type_map), s.field_import_surface_names) Absent => match map_get(structural_alias_fn_surface_names, name) { @@ -821,9 +1072,10 @@ fn fn_reach_targets(name: String, summaries: Map, structura Absent => [] } } + spelled |> fold(init: [], f: (acc, t) => concat(acc, fn_reach_target_keys(index: summaries, target: t))) } -fn fn_reaching_names_fixpoint(candidates: List, summaries: Map, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set, reached: Set) -> Set { +fn fn_reaching_names_fixpoint(candidates: List, summaries: TypeSummaryIndex, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set, reached: Set) -> Set { let round = fold(candidates, init: FnReachRound { reached: reached, grew: false }, f: (acc, name) => if set_contains(acc.reached, name) { acc } else if fn_reach_holds_directly(name: name, summaries: summaries, structural_alias_direct_fn_names: structural_alias_direct_fn_names) @@ -837,9 +1089,9 @@ fn fn_reaching_names_fixpoint(candidates: List, summaries: Map, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set) -> Set { +fn fn_reaching_names(summaries: TypeSummaryIndex, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set) -> Set { fn_reaching_names_fixpoint( - candidates: concat(map_keys(summaries), map_keys(structural_alias_fn_surface_names)), + candidates: concat(map_keys(summaries.by_key), map_keys(structural_alias_fn_surface_names)), summaries: summaries, structural_alias_fn_surface_names: structural_alias_fn_surface_names, structural_alias_direct_fn_names: structural_alias_direct_fn_names, @@ -847,26 +1099,30 @@ fn fn_reaching_names(summaries: Map, structural_alias_fn_su ) } -fn close_fn_fields(summaries: Map, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set) -> Map { +fn close_fn_fields(summaries: TypeSummaryIndex, structural_alias_fn_surface_names: Map>, structural_alias_direct_fn_names: Set) -> TypeSummaryIndex { let reaching = fn_reaching_names(summaries: summaries, structural_alias_fn_surface_names: structural_alias_fn_surface_names, structural_alias_direct_fn_names: structural_alias_direct_fn_names) - fold(map_keys(summaries), init: summaries, f: (acc, name) => - match map_get(summaries, name) { - Present { value: s } => - if !s.has_fn_fields && set_contains(reaching, name) { - map_insert(acc, name, TypeSummary { - name: s.name, - repr: s.repr, - field_summaries: s.field_summaries, - field_type_map: s.field_type_map, - field_import_surface_names: s.field_import_surface_names, - variant_name_set: s.variant_name_set, - generic_param_names: s.generic_param_names, - has_fn_fields: true - }) - } else { acc } - Absent => acc - } - ) + TypeSummaryIndex { + by_key: fold(map_keys(summaries.by_key), init: summaries.by_key, f: (acc, key) => + match map_get(summaries.by_key, key) { + Present { value: s } => + if !s.has_fn_fields && set_contains(reaching, key) { + map_insert(acc, key, TypeSummary { + name: s.name, + key: s.key, + repr: s.repr, + field_summaries: s.field_summaries, + field_type_map: s.field_type_map, + field_import_surface_names: s.field_import_surface_names, + variant_name_set: s.variant_name_set, + generic_param_names: s.generic_param_names, + has_fn_fields: true + }) + } else { acc } + Absent => acc + } + ), + keys_by_leaf: summaries.keys_by_leaf + } } fn structural_alias_direct_fn_names_with(names: Set, name: String, include: Bool) -> Set { @@ -912,7 +1168,9 @@ fn add_emit_item_summary(state: EmitInfoBuildState, module_name: String, item: N state } match build_type_summary(item: item, source_indices: source_indices) { - Present { value: summary } => + Present { value: built } => + let key = type_decl_identity(owner_module: module_name, decl_name: built.name) + let summary = type_summary_with_key(summary: built, key: key) let with_variants = match summary.repr { EnumRepr { unit_only: _ } => fold(item.children, init: state.type_summaries, f: (acc, variant) => @@ -925,9 +1183,11 @@ fn add_emit_item_summary(state: EmitInfoBuildState, module_name: String, item: N ) let vname = authored_name_at(source_indices: source_indices, node: variant) let qualified_vname = variant_summary_key(enum_name: summary.name, variant_name: vname) + let variant_key = variant_summary_key(enum_name: key, variant_name: vname) let variant_field_types = build_field_type_map(children: variant.children, source_indices: source_indices) - map_insert(acc, qualified_vname, TypeSummary { + type_summary_index_insert(index: acc, key: variant_key, leaf: qualified_vname, summary: TypeSummary { name: qualified_vname, + key: variant_key, repr: StructRepr, field_summaries: build_struct_field_summaries(parent: variant, source_indices: source_indices), field_type_map: variant_field_types.field_types, @@ -940,7 +1200,7 @@ fn add_emit_item_summary(state: EmitInfoBuildState, module_name: String, item: N ) _ => state.type_summaries } - let next_summaries = map_insert(with_variants, summary.name, summary) + let next_summaries = type_summary_index_insert(index: with_variants, key: key, leaf: summary.name, summary: summary) EmitInfoBuildState { type_summaries: next_summaries, type_decl_items: state.type_decl_items, @@ -952,4 +1212,18 @@ fn add_emit_item_summary(state: EmitInfoBuildState, module_name: String, item: N } } +fn type_summary_with_key(summary: TypeSummary, key: String) -> TypeSummary { + TypeSummary { + name: summary.name, + key: key, + repr: summary.repr, + field_summaries: summary.field_summaries, + field_type_map: summary.field_type_map, + field_import_surface_names: summary.field_import_surface_names, + variant_name_set: summary.variant_name_set, + generic_param_names: summary.generic_param_names, + has_fn_fields: summary.has_fn_fields + } +} + data emit_dependency_registration_dissolve_on_note: DissolutionCondition = unbound_dissolution(description: "dissolve-on (FreeMonoid pass 2026-07-20): collect emitted cross-module type/path refs during emit into one authority; derive (a) use-lines via build_shared_types + reference_derived_use_line_plan and (b) closure pub-mod membership via emit_lib_rs_from_paths from that set. Instance-patch interim: closure projections (v1.compiler.closure_stub_v2_std_text_rust and closure_stub_v2_std_integer_rust) when refs exist but the real v2.std.text or v2.std.integer module is absent from narrow typed.modules — projections are NOT parallel modeling authorities (see each stub module dissolve_on_note); they dissolve when the ref set includes the real module in the closure.") diff --git a/src/v1/04_env.dag b/src/v1/04_env.dag index 2ea38c63c13..7aa1acaedf6 100644 --- a/src/v1/04_env.dag +++ b/src/v1/04_env.dag @@ -186,7 +186,7 @@ data global_bare_fallback_invariant: String = "Corpus-wide bare-name census, res // lookup_binding_by_name after global_bare; v1 patterns: lookup_variant_in_type routes dotted // variant names through symbol_index_lookup. build_symbol_index_census materializes declared-module // item paths plus module-unique Disj variant aliases (qualified path always; corpus-unique variants -// also enter global_bare — mirrors v2 symbol_index_fill_unique_variant_aliases); +// also enter global_bare — mirrors v2 symbol_index_fill_containment_node's alias arm); // lookup_qualified_module_projection runs only when the reference carries a dot (fail-closed on // miss — never widens to field-access semantics). @@ -226,6 +226,8 @@ type SymbolIndex { services: Map transparent_alias_rep: Map type_head_exposures: Map + file_modules: Map + module_named_imports: Map> } type Scope { @@ -233,7 +235,7 @@ type Scope { } fn empty_symbol_index() -> SymbolIndex { - SymbolIndex { entries: empty_map(), global_bare: empty_map(), services: empty_map(), transparent_alias_rep: empty_map(), type_head_exposures: empty_map() } + SymbolIndex { entries: empty_map(), global_bare: empty_map(), services: empty_map(), transparent_alias_rep: empty_map(), type_head_exposures: empty_map(), file_modules: empty_map(), module_named_imports: empty_map() } } fn empty_type_env() -> TypeEnv { @@ -259,6 +261,97 @@ fn symbol_index_lookup(index: SymbolIndex, qualified_name: String) -> Node? { map_get(index.entries, qualified_name) } +// THE KERNEL NAMES NO IMPORT OVERRIDES: the resolver's scope layer for the kernel sits positionally +// above imports (v1.compiler.infer direct_import_export_precedence_note: locals > kernel > +// direct-selected > transitive). It lives here, beside the occurrence decision below, so the +// resolver's overlay and the emitter's unstamped-occurrence route read one predicate. +fn overlay_skips_kernel_name(name: String) -> Bool { + is_kernel_type(name: name) + || is_container_type(name: name) + || name == "Unit" + || name == "Optional" + || name == "Present" + || name == "Absent" +} + +// WHAT A BARE NAME DENOTES AT ONE UNSTAMPED OCCURRENCE, in the order the typechecker binds a +// declaration-field type: lookup_type_for reaches lookup_binding, which answers from the module's own +// env.bindings -- its declarations and the names it imports BY NAME -- before lookup_binding_by_name +// reaches the overlay where the kernel layer sits above imports (overlay_skips_kernel_name). So: a +// declaration in the occurrence's own file, then the file's named import, then the kernel. That the +// overlay's order (v1.compiler.infer direct_import_export_precedence_note) disagrees for an imported +// kernel-named declaration that has no mint row is the residual class +// (gunbc.recurring_failure_mode.an_imported_name_shadowed_by_a_kernel_name); this decision mirrors the +// binding the typechecker actually uses. It is the emitter's COPY of that order for references that +// arrive with no resolution fact -- type-environment declaration bodies whose members were never +// stamped -- and a counted route, not a second authority: v1.compiler.infer_emit_info +// resolve_type_decl_routed and v1.compiler.emit_rust is_host_optional_carrier_type read it, and it is +// deleted when those bodies carry Node.declaration stamped in the DECLARING scope +// (v1.compiler.infer_emit_info referencing_module_declarer_route_dissolves_on). +type BareOccurrenceBinding + = BareOccurrenceDeclaredInItsFile { declaration: Node } + | BareOccurrenceImportedByName { module_path: String, declaration: Node } + | BareOccurrenceIsKernelName + | BareOccurrenceUndecided + +fn symbol_index_bare_declarations(index: SymbolIndex, name: String) -> List { + match map_get(index.global_bare, name) { + Present { value: GlobalBareUniqueBinding { module_path: _, binding: b } } => [b.resolved] + Present { value: GlobalBareAmbiguousBinding { candidates: cs } } => cs |> map(c => c.binding.resolved) + Absent => [] + } +} + +// A NAMED IMPORT is followed the way lookup_binding follows it: a module that itself imports the name +// by name re-exports it (v2.std.collection imports std.optional { Optional }), so the chain is +// walked to the module that DECLARES the name. A chain that ends in a module holding no such +// declaration, or that does not end within the bound, is Undecided and refuses; it never falls +// through to the kernel. +fn bare_occurrence_import(index: SymbolIndex, name: String, reference: Node) -> BareOccurrenceBinding? { + match map_get(index.file_modules, reference.span.file) { + Absent => none + Present { value: referencing_module } => + match symbol_index_named_import_source(index: index, module_path: referencing_module, name: name) { + Absent => none + Present { value: from_module } => Present { value: bare_occurrence_import_terminal(index: index, module_path: from_module, name: name, fuel: 16) } + } + } +} + +fn symbol_index_named_import_source(index: SymbolIndex, module_path: String, name: String) -> String? { + match map_get(index.module_named_imports, module_path) { + Absent => none + Present { value: named } => map_get(named, name) + } +} + +fn bare_occurrence_import_terminal(index: SymbolIndex, module_path: String, name: String, fuel: Int) -> BareOccurrenceBinding { + if fuel == 0 { BareOccurrenceUndecided } + else { + match symbol_index_named_import_source(index: index, module_path: module_path, name: name) { + Present { value: next } => bare_occurrence_import_terminal(index: index, module_path: next, name: name, fuel: fuel - 1) + Absent => + match symbol_index_lookup(index: index, qualified_name: concat(module_path, ".", name)) { + Present { value: decl } => BareOccurrenceImportedByName { module_path: module_path, declaration: decl } + Absent => BareOccurrenceUndecided + } + } + } +} + +fn bare_occurrence_binding(index: SymbolIndex, name: String, reference: Node) -> BareOccurrenceBinding { + let local = symbol_index_bare_declarations(index: index, name: name) |> filter(d => d.span.file == reference.span.file) + let local_count = local |> fold(init: 0, f: (n, _d) => n + 1) + if local_count == 1 { local |> fold(init: BareOccurrenceUndecided, f: (acc, d) => BareOccurrenceDeclaredInItsFile { declaration: d }) } + else if local_count > 1 { BareOccurrenceUndecided } + else { + match bare_occurrence_import(index: index, name: name, reference: reference) { + Present { value: imported } => imported + Absent => if overlay_skips_kernel_name(name: name) { BareOccurrenceIsKernelName } else { BareOccurrenceUndecided } + } + } +} + fn global_bare_candidates_contain(candidates: List, resolved: Node) -> Bool { candidates |> any(c => c.binding.resolved == resolved) } @@ -298,7 +391,9 @@ fn symbol_index_insert(index: SymbolIndex, qualified_name: String, resolved: Nod global_bare: index.global_bare, services: index.services, transparent_alias_rep: index.transparent_alias_rep, - type_head_exposures: index.type_head_exposures + type_head_exposures: index.type_head_exposures, + file_modules: index.file_modules, + module_named_imports: index.module_named_imports } } @@ -308,7 +403,9 @@ fn symbol_index_insert_decl(index: SymbolIndex, module_path: String, binding: Ty global_bare: symbol_index_track_global_bare(global_bare: index.global_bare, module_path: module_path, binding: binding), services: index.services, transparent_alias_rep: index.transparent_alias_rep, - type_head_exposures: index.type_head_exposures + type_head_exposures: index.type_head_exposures, + file_modules: index.file_modules, + module_named_imports: index.module_named_imports } } @@ -318,7 +415,9 @@ fn symbol_index_insert_service(index: SymbolIndex, name: String, module_path: St global_bare: index.global_bare, services: map_insert(index.services, name, ServiceCensusEntry { module_path: module_path, item: item }), transparent_alias_rep: index.transparent_alias_rep, - type_head_exposures: index.type_head_exposures + type_head_exposures: index.type_head_exposures, + file_modules: index.file_modules, + module_named_imports: index.module_named_imports } } diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 0fc07cbcaeb..ed6e2c4706b 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -124,7 +124,7 @@ import v1.compiler.infer_method { } import v1.compiler.infer_cycle { detect_type_cycles_kahn } import v1.compiler.coercion { provenance_realizes_natively } -import v1.compiler.infer_env { +import v1.compiler.infer_env { overlay_skips_kernel_name, text_representation_by_identity, text_crossing_by_identity, host_text_into_structural_sequence, text_representations_cross, text_representation_is_text_arm, text_representation_is_unidentified, TypeEnv, TypeBinding, TypeEnvCache, empty_type_env_cache, merge_type_env_cache, merge_type_env_cache_guarded, GuardedTypeEnvCacheMerge, TypeEnvCacheMergeConflict, @@ -189,7 +189,7 @@ import v1.compiler.infer_sigs { } import v1.compiler.infer_emit_info { TypeRepr, StructRepr, EnumRepr, - TypeSummary, EmitGraphInfo, EmitInfoBuildState, + TypeSummary, EmitGraphInfo, EmitInfoBuildState, TypeSummaryIndex, TypeSummaryLookup, TypeSummaryFound, TypeSummaryLeafAmbiguous, TypeSummaryNotDeclared, TypeSummaryQuestion, QuestionDecided, QuestionNameAmbiguous, QuestionNotDeclared, type_summary_lookup, empty_type_summary_index, is_enum_in_summaries, empty_emit_graph_info, empty_type_env, lookup_emit_type_summary, build_struct_field_summaries, build_enum_field_summaries, add_emit_item_summary, derive_variant_to_enum, close_fn_fields, empty_type_decl_index, type_decl_index_with_qualified_names @@ -746,7 +746,7 @@ fn variant_reference_inferred_node(expected: Node?, name: String, owner_name: St // THE IDENTITY OF AN ARM WHOSE OWNER IS A KERNEL MINT. A mint that a // gunbc.structural_realization_bindings kernel_mint_declaration_rows row binds to a corpus -// declaration answers with THAT declaration (the kernel Optional is v2.std.optional Optional), so +// declaration answers with THAT declaration (the kernel Optional is std.optional Optional), so // the arm has one identity whether it was reached through `T?` or through an import. A mint with // no row is a kernel type or it is unrecovered, as before. fn kernel_mint_parent_identity(minted_name: String) -> VariantParentIdentity { @@ -1853,6 +1853,39 @@ fn record_lit_expanded_from_expected(type_name: String?, expected: Node?, scope: } } +// A VARIANT LITERAL OF A GENERIC OWNER IS AN APPLICATION OF THAT OWNER WHEN ITS CONTEXT SAYS WHICH. +// The arm above widens a variant literal to its owner DECLARATION: the expansion that still carries +// the declaration's formals as params (`Result`). The expected type -- a declared return, a +// typed let, a fold init whose fold sits in result position -- is the authored APPLICATION of the +// same declaration (`Result, DomainNameParseError>`), and every reader before this +// one dropped its arguments by resolving it back to the declaration (record_lit_expected_coproduct). +// So the literal typed as an uninstantiated declaration, and each consumer downstream compensated +// or failed: fold_accumulator_is_unapplied_generic re-derives an accumulator from the step's +// return, which is itself such a literal, and extdeps.dns.domain_name parse_dns_label_list's +// `labels |> list_push(label)` reached the emitter with `labels` typed by the formal `ok` +// (gunbc native broker, E0599). The application is admitted only when the expected's head resolves +// to the SAME binding as the owner (rc identity, never a spelling comparison) and the owner is +// generic; anything else answers none and the literal keeps the declaration exactly as before. +// ONLY A COPRODUCT OWNER. A struct literal never takes this arm: its fields are instantiated from +// the expected type by record_lit_instantiated_fields, and its type stays the declaration, which +// the Rust emitter's shared-carrier decision reads by the declaration's name. Measured: admitting a +// generic STRUCT here (v2.std.artifact generated_artifact_validation, a Validation data +// row) dropped its Rc carrier and was the one new error on the v2 self-host closure (E0308). +fn variant_literal_expected_application(owner: Node, expected: Node?, scope: InferScope) -> Node? { + if (owner.params |> count) == 0 || owner.connective != Disj { return none } + match expected { + Present { value: exp } => + if exp.connective != NoConnective || (exp.children |> count) == 0 { none } + else { + match lookup_type_for(env: scope.type_env, node: exp) { + Present { value: head } => if rc_ptr_eq(head, owner) { Present { value: with_required_cardinality(n: exp) } } else { none } + Absent => none + } + } + Absent => none + } +} + fn record_lit_expected_coproduct(expected: Node?, scope: InferScope) -> Node? { match expected { Present { value: exp } => @@ -7066,16 +7099,15 @@ type UnresolvedMethodFrontierRow { dissolution: DissolutionCondition } +// THE extdeps.dns.domain_name list_push ROW IS DELETED, observed 0 (an equality, so it is deleted, +// not lowered), and its recorded cause was a misattribution corrected here rather than carried: +// `ok` was not the variant's name but the owner's FORMAL. The payload `labels` was bound out of an +// accumulator typed as the uninstantiated declaration Result, because the fold's +// constructor init `Ok { value: .. }` widened to its owner declaration and dropped the expected +// application. variant_literal_expected_application types that literal as the declared return's +// application, so the receiver is List and the method resolves at tier0. fn unresolved_method_frontier() -> List { [ - { - module_name: "extdeps.dns.domain_name", - method: "list_push", - occurrences: 1, - receiver_shape: "Primitive(ok)", - cause: "receiver is a coproduct payload bound by pattern destructuring, which arrives typed as the VARIANT name rather than the field type.", - dissolution: unbound_dissolution(description: "coproduct payload binding typed as the field type, at which point the receiver is List and DECIDABLE") - }, { module_name: "v1.compiler.trace", method: "map", @@ -7231,7 +7263,7 @@ fn validate_cast(source_inferred: InferredNode?, target_name: String, span: Sour // with the absence intact, so it is left to validate_cast. // // WAS `source_name == "Optional"`, and a BARE-SPELLING comparison is the defect: authored -// names reach here qualified as often as not, so a source typed `v2.std.optional.Optional` +// names reach here qualified as often as not, so a source typed `std.optional.Optional` // was not recognised as optional at all and its cast escaped OptionalCastNotEliminated // entirely. qualified_last_segment is the authority this file already uses for exactly this // question (nominal_call_arg_brand_mismatch, is_set_collection); asking it here is using the @@ -11195,7 +11227,12 @@ fn infer_record_lit_structural(occurrence_identity: NodeOccurrenceIdentity, type } let expected_optional_parent = Present { value: "Optional" } let is_present_ctor = type_name.value == "Present" && local_variant_parent == expected_optional_parent - let resolved_node = if is_present_ctor { + let applied_expected_owner = if is_present_ctor { none } else { + variant_literal_expected_application(owner: raw_resolved, expected: expected, scope: scope) + } + let resolved_node = if applied_expected_owner != none { + applied_expected_owner.value + } else if is_present_ctor { let val_field = fi_infer_results |> filter(fir => field_init_node_name_at(n: fir.typed_field, source_indices: scope.type_env.source_indices) == "value") |> first match val_field { Present { value: val_fir } => with_optional_cardinality(n: resolved_type(n: val_fir.infer_result.typed)) @@ -14970,6 +15007,15 @@ fn transparent_alias_identity_agrees(index: SymbolIndex, left: String, right: St } } +// A MODULE'S NAMED IMPORTS, leaf -> the module it is imported from, read from its import nodes: the +// import half of v1.compiler.infer_env bare_occurrence_binding. +fn file_named_imports_of(module_node: Node, source_indices: Map) -> Map { + module_imports(n: module_node) |> filter(imp => !import_is_all(n: imp)) |> fold(init: empty_map(), f: (acc, imp) => + let from_module = import_module_path_at(imp: imp, source_indices: source_indices) + import_specific_names_at(n: imp, source_indices: source_indices) |> fold(init: acc, f: (inner, name) => map_insert(inner, name, from_module)) + ) +} + fn build_symbol_index_census_raw_nodes(module_nodes: List, source_indices: Map) -> SymbolIndex { let corpus_variant_counts = corpus_disj_variant_name_counts_nodes(module_nodes: module_nodes, source_indices: source_indices) let corpus_item_counts = corpus_item_name_counts_nodes(module_nodes: module_nodes, source_indices: source_indices) @@ -14999,7 +15045,11 @@ fn build_symbol_index_census_raw_nodes(module_nodes: List, source_indices: services: folded.services, transparent_alias_rep: transparent_alias_representatives( module_nodes: module_nodes, source_indices: source_indices, item_counts: corpus_item_counts), - type_head_exposures: type_head_exposure_census(module_nodes: module_nodes, source_indices: source_indices) + type_head_exposures: type_head_exposure_census(module_nodes: module_nodes, source_indices: source_indices), + file_modules: module_nodes |> fold(init: empty_map(), f: (acc, module_node) => + map_insert(acc, module_node.span.file, authored_name_at(source_indices: source_indices, node: module_node))), + module_named_imports: module_nodes |> fold(init: empty_map(), f: (acc, module_node) => + map_insert(acc, authored_name_at(source_indices: source_indices, node: module_node), file_named_imports_of(module_node: module_node, source_indices: source_indices))) } } @@ -15209,7 +15259,7 @@ fn census_bare_fill_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Ma Absent => acc } ) - SymbolIndex { entries: index.entries, global_bare: global2, services: services2, transparent_alias_rep: index.transparent_alias_rep, type_head_exposures: index.type_head_exposures } + SymbolIndex { entries: index.entries, global_bare: global2, services: services2, transparent_alias_rep: index.transparent_alias_rep, type_head_exposures: index.type_head_exposures, file_modules: index.file_modules, module_named_imports: index.module_named_imports } } fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map) -> SymbolIndex { @@ -15228,7 +15278,7 @@ fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map, source_indices: Map) -> SymbolIndex { @@ -15268,7 +15318,9 @@ fn symbol_index_with_bare_fill(closure: SymbolIndex, tree: SymbolIndex) -> Symbo SymbolIndex { entries: closure.entries, global_bare: global2, services: services2, transparent_alias_rep: map_merge(tree.transparent_alias_rep, closure.transparent_alias_rep), - type_head_exposures: map_merge(tree.type_head_exposures, closure.type_head_exposures) + type_head_exposures: map_merge(tree.type_head_exposures, closure.type_head_exposures), + file_modules: map_merge(tree.file_modules, closure.file_modules), + module_named_imports: map_merge(tree.module_named_imports, closure.module_named_imports) } } @@ -15307,20 +15359,14 @@ fn symbol_index_with_qualified_fill(closure: SymbolIndex, fill: SymbolIndex) -> SymbolIndex { entries: entries2, global_bare: closure.global_bare, services: closure.services, transparent_alias_rep: map_merge(fill.transparent_alias_rep, closure.transparent_alias_rep), - type_head_exposures: map_merge(fill.type_head_exposures, closure.type_head_exposures) + type_head_exposures: map_merge(fill.type_head_exposures, closure.type_head_exposures), + file_modules: map_merge(fill.file_modules, closure.file_modules), + module_named_imports: map_merge(fill.module_named_imports, closure.module_named_imports) } } data direct_import_export_precedence_note: String = "Direct-selected exports beat transitive leaks (2026-07-16, the #6663 x #6686 main-red). The ancestry union folds each direct import's WHOLE flattened cache (its own exports AND everything it inherited), so a later import's transitively-leaked homonym could overlay an earlier import's OWN export that this module's import statement explicitly selects - import v2.std.algebra { Monoid } lost 'Monoid' to dag-root std.algebra riding v2.std.node's ancestry via std.types, and #6663's field-presence wall read the wrong shape at 28 sites. This overlay re-applies, in import order, each direct import's OWN export surface (interface.env.str_bindings - locals only, never its ancestry) restricted to the names the import statement makes visible (specific_names; is_all = the parent's whole local surface, type-variable names filtered per the std.types precedent). Winner semantics: direct-selected vs transitive leak = direct wins (lexical nearest-wins, the containment ruling one hop out - the sanctioned 1c universe is own declarations UNION direct import lists); direct vs direct = unchanged import-order overlay-wins (the ledgered peer-fork ruling, 2026-07-11: later import wins, conflict stays LEDGERED on binding_forks); leak vs leak = unchanged union winner; KERNEL names are never overridden (overlay_skips_kernel_name: kernel_type_set, the container_type_arity names List/Set/Map/Witness, plus Unit/Optional/Present/Absent) - the kernel scope layer stays positionally above imports per the same ruling, because builtin typing (first, skip, string ops) grounds on kernel identities and the v2 substrate models of those concepts (v2.std.collection List=FreeMonoid, Optional; v2.std.text String) are the known dual-representation debt, resolved corpus-wide by kernel-wins today. KERNEL INSTALLATION IS INDEPENDENT OF IMPORT CARDINALITY (build_ancestry_precedence, 2026-08-31): the kernel overlay applies unconditionally over the import union and both type-env builders consume the one producer - the former shape skipped the overlay when a module had exactly one import, so kernel identity won or lost by ancestry occupancy (a leak-dependent regime: identical imports, different realizations). Full precedence, nearest first: locals > kernel > direct-selected > transitive union. The fork LEDGER is untouched - rows still record every union conflict; this layer only corrects which binding serves lookups. DISSOLVES WHEN namespace Rule-1 lands (containment tree is the naming authority; imports become parse errors) - the union leak itself disappears and this overlay with it. Receipt: direct_import_precedence_over_transitive_leak_test (green arm = selected name resolves the selected module's shape regardless of import order; red control = the field wall still refuses the true shape's missing fields, with that arm's modules placed so the homonym is census-UNAMBIGUOUS by containment (realleaf under the consumer's own parent, leakleaf under a sibling parent) - a census-ambiguous name stands the wall down BY DESIGN (presence_check_census_gate_note), so without that placement the control asserts a diagnostic the gate exists to suppress and silently measures the gate instead of the overlay; ledger arm = the fork row is still recorded)." -fn overlay_skips_kernel_name(name: String) -> Bool { - is_kernel_type(name: name) - || is_container_type(name: name) - || name == "Unit" - || name == "Optional" - || name == "Present" - || name == "Absent" -} fn overlay_direct_import_exports(ancestry_str_bindings: Map, resolved_imports: List, parent_index: Map) -> Map { fold(resolved_imports, init: ancestry_str_bindings, f: (acc, imp) => @@ -15384,7 +15430,7 @@ fn build_ancestry_precedence(resolved_imports: List, parent_inde // THE SEED'S ONE MINT OF THE OPTIONAL COPRODUCT. `T?` resolves to this node in every kernel scope // this module builds; it was written out three times, once per builder. It is a mint and not the -// declaration: a module that uses `T?` need not hold v2.std.optional in its closure. Which +// declaration: a module that uses `T?` need not hold std.optional in its closure. Which // declaration it stands for is gunbc.structural_realization_bindings kernel_mint_declaration_rows, // and kernel_mint_shape_diagnostics refuses if the two stop agreeing. fn kernel_optional_type_node() -> Node { @@ -17326,16 +17372,12 @@ type EnumVariantShapeSets { positional_payload: Set } -fn enum_variant_shape_sets_for_item(acc: EnumVariantShapeSets, item: Node, si: Map, type_summaries: Map) -> EnumVariantShapeSets { +fn enum_variant_shape_sets_for_item(acc: EnumVariantShapeSets, item: Node, si: Map, type_summaries: TypeSummaryIndex) -> EnumVariantShapeSets { let item_name = authored_name_at(source_indices: si, node: item) - let is_enum = match map_get(type_summaries, item_name) { - Present { value: summary } => match summary.repr { - EnumRepr { unit_only: _ } => true - _ => false - } - Absent => false - } - if is_enum { + if match is_enum_in_summaries(type_summaries: type_summaries, type_name: item_name) { + QuestionDecided { value: v } => v + _ => false + } { item.children |> fold(init: acc, f: (vacc, variant) => let qualified = concat(item_name, "::", authored_name_at(source_indices: si, node: variant)) let is_positional_payload = variant_has_positional_payload_shape(variant: variant, source_indices: si) @@ -17351,7 +17393,7 @@ fn enum_variant_shape_sets_for_item(acc: EnumVariantShapeSets, item: Node, si: M } else { acc } } -fn build_enum_variant_shape_sets(modules: List, type_summaries: Map) -> EnumVariantShapeSets { +fn build_enum_variant_shape_sets(modules: List, type_summaries: TypeSummaryIndex) -> EnumVariantShapeSets { modules |> fold(init: EnumVariantShapeSets { fielded: empty_set(), positional_payload: empty_set() }, f: (acc, m) => m.items |> fold(init: acc, f: (inner, item) => enum_variant_shape_sets_for_item(acc: inner, item: item, si: m.type_env.source_indices, type_summaries: type_summaries) @@ -17435,7 +17477,7 @@ fn build_emit_graph_info(modules: List, registry: Map empty_symbol_index() } let init = EmitInfoBuildState { - type_summaries: empty_map(), + type_summaries: empty_type_summary_index(), type_decl_items: type_decl_index_with_qualified_names(index: empty_type_decl_index(), qualified_names: qualified_names), fn_decl_items: empty_map(), structural_alias_fn_surface_names: empty_map(), diff --git a/src/v1/04_method.dag b/src/v1/04_method.dag index 0b422320ffb..c9e94d41d3d 100644 --- a/src/v1/04_method.dag +++ b/src/v1/04_method.dag @@ -85,8 +85,12 @@ fn pair_declared_names(names: List, templates: List }) } -// THE RETURN IS DERIVED TOO, where std.algebra names it by a plain type name. count and length -// return std.nat.Nat there, and the free-call surface must not restate it: a restated return is a +// THE RETURN IS DERIVED TOO, where std.algebra names it by a plain type name. count and length do +// NOT share one return there: the free monoid's count (collection_count_shape) states Int, a declared +// departure (gunbc.rung_drop collection_count_result_stated_int_until_declared_results_are_judged), +// while the other count/length rows state std.nat.Nat. Free-call typing here still takes +// FinitePowerSet's row first, so a free count call is typed Nat. Either way the free-call surface must +// not restate the return: a restated return is a // second authority that one row can drift from while the other stays green -- which is what a // mutation of the restated count row showed, with every claim still passing. A template whose // return is not a named type has no row here that reads it, and answers error_type so a future diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index eb51b348425..2c7619db16e 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1,5 +1,10 @@ module v1.compiler.emit +import std.decl_ref { DeclarationRef, decl_ref } +import std.compiler_entry { + CompilerEntryDriver, RetainedHostCliKernel, DirectIngestDriver, SourceRootEvalDriver, NativeCliDriver, NativeClaimDriver, NativeServeDriver, + NativeEffectClass, NativeEffectDeclaration, NativeOutgoingHttp, NativeEffectScope, NativeAdmitAllExcept, NativeAdmitOnly, NativeEffectDenominator +} import std.occurrence_identity { OccurrenceSynthetic, OccurrenceMinted, OccurrencePending, OccurrenceProjected, OccurrenceId, NodeOccurrenceIdentity, occurrence_id_eq } import std.coercion { TypeRealizationDecision } @@ -34,13 +39,15 @@ import v1.std.core { is_file_transport, is_local_transport, TransportKind, RestTransport, ShellTransport, FileTransport, LocalTransport, classify_transport, transport_verb, transport_base_path, transport_has_auth, field_init_operation_modifier, operation_modifier_name, with_required_cardinality, tuple_type_name, find_child_named, - Connective, Conj, Disj, NoConnective, Arrow, CardOptional + Connective, Conj, Disj, NoConnective, Arrow, Required, CardOptional } -import v1.compiler.infer_env { TypeEnv, TypeBinding, authored_name, lookup_type_for, GlobalBareLookupState, empty_symbol_index } +import v1.compiler.infer_env { TypeEnv, TypeBinding, authored_name, lookup_type_for, GlobalBareLookupState, empty_symbol_index, type_reference_declaration_reading, TypeReferenceNamesDeclaration } import std.induction { InductiveField, SubValueUnknown } +import std.decl_ref { DeclarationRef, decl_ref, declaration_ref_eq } + import v1.compiler.infer_types { resolved_type, is_product_type, child_type_node, emit_map_has, node_is_collection, node_is_keyed_collection, node_is_element_collection, normalize_access_type_node, for_each_element_type_node, is_unit_like, is_declared_container_alias_spelling } import std.types { is_container_type } @@ -64,7 +71,8 @@ import v1.compiler.infer_lookup { lookup_func_sig } import v1.compiler.infer { InferScope, is_where_refinement_type, - build_params_scope, extend_scope, call_param_caller_labels + build_params_scope, extend_scope, call_param_caller_labels, + resolved_type_name } import v1.compiler.infer_emit_info { TypeSummary, EmitGraphInfo } @@ -492,6 +500,49 @@ fn emit_data_fields_json(value: Node, source_indices: Map, ) } +// A DATA ROW DECLARED `T?` IS SPELLED AS THE TARGET'S NATIVE OPTION, selected by the row's DECLARED +// cardinality -- never by the spelling of a variant's parent. Every target realizes `T?` natively +// (Rust Option, Python None, Go nil), and each one's JSON reading of it is transparent: absent is +// `null`, present is the payload itself. The caller (v1.compiler.emit_rust emit_data_def_body) asks +// this only for a row whose declared type carries CardOptional, so an unrelated coproduct that happens +// to be NAMED Optional keeps its own wire policy (review on gunbc#13202: a leaf-name test here was a +// second, unrostered authority that would have serialized such a coproduct as null or a bare payload). +// Within an optional row, a variant literal whose parent IS the declared inner type is a bare T under +// T?: an implicit lift no declared coercion admits. That arm REFUSES, and it is the refusal exhaustive +// matching requires -- NOT coverage. It is unreachable: v1.compiler.infer refuses a bare T data +// initializer under a declared T? (the C5 wall, optional_at_required_obligation_diags at +// PositionDataInitializer), and an accepted row of that shape takes the typed-expression path, never +// this serializer (measured on gunbc#13202). Any other one-field variant literal is the option's +// present arm and is unwrapped to its payload. Control: gunbc.instruments.native_emission_controls optional_data_row_cases +// -- the present and absent rows. A coproduct merely NAMED Optional +// cannot be emitted at all today (type rendering decides host-option by leaf name); that case is +// lane 2K's RED, not covered here. Neither refusal arm (a bare inner-type literal, a present literal +// with more than one field) is coverage of anything; both exist only so the match is exhaustive. +fn emit_declared_optional_row_json(value: Node, inner_type_name: String, source_indices: Map, variant_wire: Map) -> EmitterOutcome { + match value.expr_data { + ExprLiteral { value: LitNull } => Emitted { json: "null" } + ExprRecordLit { parent_enum: pe } => + match pe { + Present { value: parent } => + if qualified_last_segment(name: parent) == qualified_last_segment(name: inner_type_name) { + Refused { reason: concat("a data row declared ", inner_type_name, "? is initialized with a bare ", inner_type_name, " literal: no declared coercion lifts T into T? (DESIGN 4, 2026-09-26), so spell it Present { value: .. } or none") } + } else { + match value.children |> first { + Absent => Emitted { json: "null" } + Present { value: payload_field } => + if (value.children |> count) == 1 { + emit_data_value_json(value: field_init_node_value(n: payload_field), source_indices: source_indices, variant_wire: variant_wire) + } else { + Refused { reason: concat("an optional row's present literal carries ", to_string(value.children |> count), " fields; the option's present arm carries exactly its payload") } + } + } + } + Absent => emit_data_value_json(value: value, source_indices: source_indices, variant_wire: variant_wire) + } + _ => emit_data_value_json(value: value, source_indices: source_indices, variant_wire: variant_wire) + } +} + fn emit_data_value_json(value: Node, source_indices: Map, variant_wire: Map) -> EmitterOutcome { match value.expr_data { ExprLiteral { value: v } => match v { @@ -1327,6 +1378,7 @@ type ShellResultField { type ShellEmissionRefusal = ShellOutputKeyNotModeled { key: String } | ShellChannelNotRealizedByTarget { key: String, target_name: String } + | ShellCapturePolicyInputAbsent { key: String } // The single authority for which declared output keys the shell realization can // answer. Read by the binding; there is no second list. @@ -1336,17 +1388,11 @@ type ShellEmissionRefusal // when no `from` key is authored -- so the two directions of the one procedure // (DESIGN section 4) read the same rule rather than agreeing by coincidence. // -// NOT MODELED, named rather than left to be inferred, because each names a fact -// the emitted realization cannot honestly produce: -// -// the byte-accounting and truncation channels -- stdout/stderr_total_bytes, -// _retained_bytes and _truncated. An operation declaring them also declares a -// capture policy (gunbc.WitnessBin.Run carries a WitnessStderrCapturePolicy -// input), and the emitted `std::process::Command` body implements no policy at -// all. Answering `false` and a raw length would not be a conservative default, -// it would assert that the declared policy ran and did not truncate. The -// next-rung trigger is the emitted realization implementing the declared -// capture policy; until then these refuse as a counted capability gap. +// The byte-accounting and truncation channels are modeled. The rust handler +// answers them from the declared WitnessStderrCapturePolicy (concurrent drain). +// Answering `false` and a raw length without running that policy is still +// forbidden; python and go refuse the three channels until they bind the same +// handler fact. // // the digest channels -- stdout/stderr_digest_hex, which the interpreter // answers and the emitted realization has no digest facility for. The @@ -1381,23 +1427,36 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // WHETHER A TARGET CAN REALIZE A CHANNEL IS A FACT ABOUT THE TARGET, and keeping // it here rather than in the roster is the whole point of the split. // -// The three capture-accounting channels are REAL shell channels -- the interpreter -// answers all three from the declared WitnessStderrCapturePolicy -- and no emitted -// realization implements a capture policy at all. The emitted `Command` body -// captures whole streams, so `truncated` would always be false and total would -// always equal retained: not a conservative default but an assertion that the -// declared policy ran and did not truncate. -// -// NEXT-RUNG TRIGGER, at capability grain: the emitted realization implementing the -// declared capture policy, sufficient to answer truncation and both byte counts -// from the policy the operation declares. Until then every realizing target -// refuses these three, and the refusal is LOCATED AT THE FIELD. -fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> Bool { +// The three capture-accounting channels are REAL shell channels. The rust +// realization handler (v1.compiler.emit_rust emit_shell_call) implements the +// declared WitnessStderrCapturePolicy: concurrent drain, BoundedTail retains a +// tail, Complete retains within the host budget +// (`v1_rt::read_host_budget_bytes` over `v1_rt::resolve_host_budget_join`, +// the one composer `memory_governor::resolve_host_budget` also calls) or +// refuses. Python and go still realize no capture policy, so they refuse these +// three at field grain rather than answering false / raw length (that answer +// would assert the declared policy ran). +fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> Bool { match c { - ShellChanStderrTruncated => false - ShellChanStderrTotalBytes => false - ShellChanStderrRetainedBytes => false - _ => target_renders_shell_transport(target: target) + ShellChanStderrTruncated => true + ShellChanStderrTotalBytes => true + ShellChanStderrRetainedBytes => true + _ => false + } +} + +fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> Bool { + if shell_channel_is_capture_accounting(c: c) { + target_is_rust(target: target) + } else { + target_renders_shell_transport(target: target) + } +} + +fn target_is_rust(target: RenderTarget) -> Bool { + match target { + Rust => true + _ => false } } @@ -1416,6 +1475,9 @@ fn shell_emission_refusal_fact(refusal: ShellEmissionRefusal) -> String { ShellChannelNotRealizedByTarget { key: k, target_name: tn } => concat("shell output channel '", k, "' is a modeled channel that target ", tn, " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran") + ShellCapturePolicyInputAbsent { key: k } => + concat("shell output channel '", k, + "' is a capture-accounting channel and this operation does not declare a required scalar stderr_capture: WitnessStderrCapturePolicy -- answering it would apply a policy nobody declared") } } @@ -1531,6 +1593,201 @@ fn bind_operation_transport(t: Node, op_node: Node, source_indices: Map } + | NativeCapabilityToken + +type NativeEffectUnavailableCause + = NativeOperationBindingRefused { operation: String, refusal: TransportBindingRefusal } + | NativeResourceTraitHasNoImplementor { capability_count: Int } + | NativeEffectDeclarationAbsent + | NativeEffectDeclarationAmbiguous { candidates: Int } + +type NativeEffectRealizationAdmission + = NativeEffectHandlerBound { effect_contract: NativeEffectContract, handler_identity: NativeEffectHandler } + | NativeEffectOutsideProductionDenominator { effect_contract: NativeEffectContract, denominator: NativeEffectDenominator } + | NativeEffectRealizationUnavailable { effect_contract: NativeEffectContract, cause: NativeEffectUnavailableCause } + +// The demanded effect's resolved declaration, or why there is none. A name that resolves to no +// declaration in the closure, or to more than one, is refused rather than guessed. +type NativeEffectDeclSite { + item: Node + module_name: String +} + +type NativeEffectResolution + = NativeEffectResolved { site: NativeEffectDeclSite } + | NativeEffectUnresolved { cause: NativeEffectUnavailableCause } + +fn native_effect_resolution(sites: List) -> NativeEffectResolution { + match sites |> first { + Absent => NativeEffectUnresolved { cause: NativeEffectDeclarationAbsent } + Present { value: site } => + if (sites |> count) == 1 { NativeEffectResolved { site: site } } + else { NativeEffectUnresolved { cause: NativeEffectDeclarationAmbiguous { candidates: sites |> count } } } + } +} + +fn native_effect_class_matches(class: NativeEffectClass, declaration: DeclarationRef, transports: List) -> Bool { + match class { + NativeEffectDeclaration { declaration: d } => + (d.module_path as String) == (declaration.module_path as String) && (d.decl_name as String) == (declaration.decl_name as String) + NativeOutgoingHttp => transports |> any(t => t == RestTransport) + } +} + +fn native_effect_outside_denominator(denominator: NativeEffectDenominator, declaration: DeclarationRef, transports: List) -> Bool { + match denominator.scope { + NativeAdmitAllExcept { excluded: xs } => xs |> any(x => native_effect_class_matches(class: x, declaration: declaration, transports: transports)) + NativeAdmitOnly { admitted: xs } => (xs |> any(x => native_effect_class_matches(class: x, declaration: declaration, transports: transports))) == false + } +} + +fn bound_operation_transport_kind(bound: BoundOperation) -> TransportKind? { + match bound { + RestBound { transport: _ } => Present { value: RestTransport } + ShellBound { transport: _, result_fields: _ } => Present { value: ShellTransport } + FileBound { verb: _, path_template: _, result_fields: _ } => Present { value: FileTransport } + LocalBound => Present { value: LocalTransport } + BindingRefused { cause: _ } => none + } +} + +fn native_transport_kinds_insert(kinds: List, kind: TransportKind) -> List { + if kinds |> any(k => k == kind) { kinds } else { concat(kinds, [kind]) } +} + +type BoundOperationAt { + operation: String + bound: BoundOperation +} + +fn native_service_bound_operations(svc: Node, source_indices: Map) -> List { + let fallback = service_fallback_transport(item: svc) + svc.children |> map(op => BoundOperationAt { + operation: authored_name_at(source_indices: source_indices, node: op), + bound: bind_operation_transport(t: effective_operation_transport(op_node: op, fallback: fallback), op_node: op, source_indices: source_indices) + }) +} + +// A service is admitted when every operation binds through bind_operation_transport, and the handler +// it names is that binding. Whether the route admits it at all is asked first, of the resolved +// declaration and the transports it binds. +fn admit_native_service_effect(service: String, resolution: NativeEffectResolution, denominator: NativeEffectDenominator, source_indices: Map) -> NativeEffectRealizationAdmission { + match resolution { + NativeEffectUnresolved { cause: cz } => + NativeEffectRealizationUnavailable { effect_contract: NativeServiceEffect { declaration: decl_ref(module_path: "", decl_name: service) }, cause: cz } + NativeEffectResolved { site: site } => { + let declaration = decl_ref(module_path: site.module_name, decl_name: service) + let contract = NativeServiceEffect { declaration: declaration } + let bounds = native_service_bound_operations(svc: site.item, source_indices: source_indices) + let transports = bounds |> fold(init: [], f: (acc, b) => + match bound_operation_transport_kind(bound: b.bound) { + Present { value: k } => native_transport_kinds_insert(kinds: acc, kind: k) + Absent => acc + }) + if native_effect_outside_denominator(denominator: denominator, declaration: declaration, transports: transports) { + NativeEffectOutsideProductionDenominator { effect_contract: contract, denominator: denominator } + } else { + match bounds |> filter(b => bound_operation_transport_kind(bound: b.bound) == none) |> first { + Present { value: r } => match r.bound { + BindingRefused { cause: cz } => NativeEffectRealizationUnavailable { effect_contract: contract, cause: NativeOperationBindingRefused { operation: r.operation, refusal: cz } } + _ => NativeEffectRealizationUnavailable { effect_contract: contract, cause: NativeEffectDeclarationAbsent } + } + Absent => NativeEffectHandlerBound { effect_contract: contract, handler_identity: NativeTransportHandler { transports: transports } } + } + } + } + } +} + +fn admit_native_resource_effect(binding: String, resource: String, resolution: NativeEffectResolution, denominator: NativeEffectDenominator) -> NativeEffectRealizationAdmission { + match resolution { + NativeEffectUnresolved { cause: cz } => + NativeEffectRealizationUnavailable { effect_contract: NativeResourceEffect { binding: binding, declaration: decl_ref(module_path: "", decl_name: resource) }, cause: cz } + NativeEffectResolved { site: site } => { + let declaration = decl_ref(module_path: site.module_name, decl_name: resource) + let contract = NativeResourceEffect { binding: binding, declaration: declaration } + let capability_count = site.item.children |> count + if native_effect_outside_denominator(denominator: denominator, declaration: declaration, transports: []) { + NativeEffectOutsideProductionDenominator { effect_contract: contract, denominator: denominator } + } else if capability_count == 0 { + NativeEffectHandlerBound { effect_contract: contract, handler_identity: NativeCapabilityToken } + } else { + NativeEffectRealizationUnavailable { effect_contract: contract, cause: NativeResourceTraitHasNoImplementor { capability_count: capability_count } } + } + } + } +} + +fn native_declaration_label(d: DeclarationRef) -> String { + concat(d.module_path as String, concat(" ", d.decl_name as String)) +} + +fn native_effect_contract_label(c: NativeEffectContract) -> String { + match c { + NativeServiceEffect { declaration: d } => concat("service ", native_declaration_label(d: d)) + NativeResourceEffect { binding: b, declaration: d } => concat("resource ", concat(native_declaration_label(d: d), concat(" (uses ", concat(b, ")")))) + } +} + +fn native_route_label(route: CompilerEntryDriver) -> String { + match route { + NativeCliDriver => "NativeCliDriver" + NativeClaimDriver => "NativeClaimDriver" + NativeServeDriver => "NativeServeDriver" + SourceRootEvalDriver => "SourceRootEvalDriver" + DirectIngestDriver => "DirectIngestDriver" + RetainedHostCliKernel => "RetainedHostCliKernel" + } +} + +// The refusal text for an admission that is not bound; none for a bound one. +fn native_effect_admission_refusal(a: NativeEffectRealizationAdmission) -> String? { + match a { + NativeEffectHandlerBound { effect_contract: _, handler_identity: _ } => none + NativeEffectOutsideProductionDenominator { effect_contract: c, denominator: d } => + Present { value: concat(native_effect_contract_label(c: c), concat(" is outside the ", concat(native_route_label(route: d.route), concat(" denominator (ruling: ", concat(native_declaration_label(d: d.ruling), ")"))))) } + NativeEffectRealizationUnavailable { effect_contract: c, cause: cz } => + Present { value: concat(native_effect_contract_label(c: c), concat(" has no native realization: ", native_effect_unavailable_fact(cause: cz))) } + } +} + +fn native_effect_unavailable_fact(cause: NativeEffectUnavailableCause) -> String { + match cause { + NativeOperationBindingRefused { operation: op, refusal: r } => concat("operation '", concat(op, concat("' binds no transport: ", transport_binding_refusal_fact(cause: r)))) + NativeResourceTraitHasNoImplementor { capability_count: n } => concat("it is a resource trait with ", concat(to_string(value: n), " capabilities and no native implementor")) + NativeEffectDeclarationAbsent => "its declaration is not in the emitted closure" + NativeEffectDeclarationAmbiguous { candidates: n } => concat(to_string(value: n), " declarations in the closure carry its name, so which one it demands is not decided") + } +} + // Target selection, and ONLY target selection -- the Go/Python twin of the Rust // `emit_transport_call`. Both consume a BoundOperation decided once; neither // classifies a transport, and neither asks what the operation means. @@ -2577,6 +2834,31 @@ fn file_operation_has_content_input(op_node: Node, source_indices: Map DeclarationRef { + decl_ref(module_path: "std.shell_stream_capture", decl_name: "WitnessStderrCapturePolicy") +} + +fn param_type_is_stderr_capture_policy_declaration(p: Node, env: TypeEnv) -> Bool { + match type_reference_declaration_reading(n: param_node_type_expr(n: p), source_indices: env.source_indices, env: env) { + TypeReferenceNamesDeclaration { declaration: d } => + declaration_ref_eq(a: d, b: required_stderr_capture_policy_declaration()) + _ => false + } +} + +fn param_is_required_stderr_capture_policy(p: Node, env: TypeEnv) -> Bool { + let ty = param_node_type_expr(n: p) + param_node_name_at(n: p, source_indices: env.source_indices) == "stderr_capture" + && p.return_cardinality == Required + && ty.return_cardinality == Required + && !node_is_collection(n: ty, source_indices: env.source_indices) + && param_type_is_stderr_capture_policy_declaration(p: p, env: env) +} + +fn operation_declares_required_stderr_capture_policy(op_node: Node, env: TypeEnv) -> Bool { + op_node.params |> any(p => param_is_required_stderr_capture_policy(p: p, env: env)) +} + // The channels the interpreter's map_file_outputs answers, read back as the emitter's obligation. // Emission and interpretation are one decision procedure run in two directions (DESIGN §4), so // this list is the emitted side of that map and diverging from it is a behavioral fork. @@ -2816,7 +3098,24 @@ fn unmodeled_shell_transport_operation_diagnostics(tm: TypedModule, item: Node, }, module_name: module_name)] } Present { value: c } => - if shell_channel_realized_by_target(c: c, target: target) { [] } + if shell_channel_is_capture_accounting(c: c) + && target_is_rust(target: target) + && !operation_declares_required_stderr_capture_policy(op_node: op_node, env: env) + { + [make_error_node( + diagnostic: TransportEmissionNotModeled { + transport_kind: "shell", + service: service_name, + operation: authored_name(env: env, node: op_node), + declaring_module: module_name, + target: render_target_name(target: target), + missing_realization_fact: shell_emission_refusal_fact(refusal: ShellCapturePolicyInputAbsent { + key: shell_result_channel_key(c: c) + }), + span: ch.span + }, + module_name: module_name)] + } else if shell_channel_realized_by_target(c: c, target: target) { [] } else { [make_error_node( diagnostic: TransportEmissionNotModeled { transport_kind: "shell", diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b5477ac289f..a9c17912220 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -55,7 +55,7 @@ import v1.std.core { Unmarked, PlainMethodSemantics, AlgebraMethodSemantics, ServiceMethodSemantics, MatchPattern, field_binding_pattern, TextFile, ErrorNode, make_error_node, InternalError, is_error_diagnostic, - UnlistedImportUse, ReferenceDerivedImportProviderUnknown, ReferenceDerivedImportExportUnproven, AmbiguousAnonymousRecordLiteral, EffectfulSelfRecursionUnrealized, AmbiguousReference, SourceSpan, resource_use_resource, UnaryOpKind, Neg, unaryop_operand, StringPart, Text, + UnlistedImportUse, ReferenceDerivedImportProviderUnknown, ReferenceDerivedImportExportUnproven, NativeEffectRealizationRefused, AmbiguousAnonymousRecordLiteral, EffectfulSelfRecursionUnrealized, AmbiguousReference, SourceSpan, resource_use_resource, UnaryOpKind, Neg, unaryop_operand, StringPart, Text, Interpolation, transport_base_url, transport_has_auth, transport_auth_token, transport_auth_header_name, transport_headers, transport_env, transport_method, transport_path_template, transport_query, transport_request_body, transport_stdin, @@ -76,6 +76,7 @@ import v1.std.core { Unmarked, LeafOwner, SingleOwner, LeafAmbiguous } +import std.compiler_entry { NativeEffectDenominator, native_compiler_route_denominator, native_service_route_denominator, native_claim_route_denominator } import v1.compiler.artifact { RenderTarget, Rust, RustModuleRenderSelection, RenderEveryModule, RenderSelectedMirrors } import extdeps.languages.rust.emit { rust_trait_derive_attr_from_traits, @@ -184,7 +185,7 @@ import std.types { is_container_type, is_kernel_type, container_template_algebra import std.serialization { CoproductWireContract, VariantEncoding, VariantNaming } import v1.compiler.infer_env { lookup_unit_variant_phantom_type, UnitVariantPhantomPresent, UnitVariantPhantomAbsent, UnitVariantPhantomEvidenceUnavailable, - TypeEnv, TypeBinding, authored_name, lookup_type_by_name, lookup_binding_by_name, lookup_type_for, GlobalBareLookupState, GlobalBareUniqueBinding, GlobalBareAmbiguousBinding, empty_symbol_index, binding_declares_span, type_reference_declaration_ref } + TypeEnv, TypeBinding, authored_name, lookup_type_by_name, lookup_binding_by_name, lookup_type_for, GlobalBareLookupState, GlobalBareUniqueBinding, GlobalBareAmbiguousBinding, empty_symbol_index, empty_type_env, declaration_ref_of_declaration_node, bare_occurrence_binding, BareOccurrenceBinding, BareOccurrenceDeclaredInItsFile, BareOccurrenceImportedByName, BareOccurrenceIsKernelName, BareOccurrenceUndecided, binding_declares_span, type_reference_declaration_ref } import std.operator_realization { OperatorRealization, HostOperator, StructuralEquality, StructuralComparison, OperatorRealizationRefused, OperandRealization, HostNumericOperand, StructuralOperand, OperandIdentityUnavailable, @@ -196,7 +197,7 @@ import std.operator_realization { } import gunbc.structural_realization_bindings { structural_ordering_rows, kernel_grounding_rows, kernel_mint_declaration_rows, kernel_optional_mint_name } import gunbc.rust_source_type_bindings { rust_host_option_carrier_declarations } -import std.literal_elaboration { KernelIntLiteral, kernel_grounding_for, KernelGroundingFound, KernelGroundingAbsent, KernelGroundingAmbiguous, kernel_mint_ownership, DeclarationOwnsTheMint, DeclarationDoesNotOwnTheMint, KernelMintOwnershipAmbiguous } +import std.literal_elaboration { KernelIntLiteral, kernel_grounding_for, KernelGroundingFound, KernelGroundingAbsent, KernelGroundingAmbiguous, kernel_mint_ownership, DeclarationOwnsTheMint, DeclarationDoesNotOwnTheMint, KernelMintOwnershipAmbiguous, kernel_mint_declaration_for, KernelMintDeclarationFound, KernelMintDeclarationAbsent, KernelMintDeclarationAmbiguous } import v1.compiler.infer_types { resolved_type, normalize_access_type_node, for_each_element_type_node, child_type_node, emit_map_has, @@ -231,9 +232,9 @@ import v1.compiler.infer { caller_resource_requirements, established_resource_binding } import v1.compiler.infer_emit_info { - EmitGraphInfo, TypeSummary, StructRepr, EnumRepr, + EmitGraphInfo, TypeSummary, StructRepr, EnumRepr, TypeSummaryIndex, TypeSummaryLookup, TypeSummaryFound, TypeSummaryLeafAmbiguous, TypeSummaryNotDeclared, TypeSummaryQuestion, QuestionDecided, QuestionNameAmbiguous, QuestionNotDeclared, type_summary_lookup, type_summary_values, type_summary_of_reference, type_summary_answer, type_summary_decided, summary_is_enum_with_variant, type_summary_keys_of_leaf, lookup_emit_type_summary, empty_emit_graph_info, - TypeDeclIndex, type_decl_identity, + TypeDeclIndex, type_decl_identity, type_decl_identities_of_leaf, TypeDeclResolved, TypeDeclLeafAmbiguous, TypeDeclKernelSpellingUndecided, TypeDeclNotDeclared, resolve_type_decl_reference, emit_info_with_fn_type_context, emit_info_with_fn_return, emit_info_with_expected_type, is_enum_in_summaries, find_variant_parent, is_known_variant, @@ -256,7 +257,7 @@ import v1.compiler.emit { emit_lambda_params, emit_null_coalesce, emit_list_lit_expr, emit_shared_expr, emit_typed_cast_shared, emit_string_literal, emit_simple_expr, escape_rust_interp_text, module_emit_scope, scope_after_expr, lookup_item_by_identity, has_nested_records_node, - emit_data_value_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items, + emit_data_value_json, emit_declared_optional_row_json, EmitterOutcome, Emitted, Refused, is_null_coalesce, has_service_items, order_typed_call_args_from_semantics, is_tco_eligible, is_self_recursive, emit_shared_tco_expr, tco_reassign_core, shared_tco_reassign, tco_loop_slot_name, tco_loop_iteration_lets, service_fallback_transport, effective_operation_transport, ServiceFieldSet, compute_service_fields, service_field_decls, @@ -267,8 +268,12 @@ import v1.compiler.emit { ShellResultField, ShellResultChannel, ShellChanStdout, ShellChanStderr, ShellChanExitSuccess, ShellChanExitCode, ShellChanStdoutLines, ShellChanStderrTruncated, ShellChanStderrTotalBytes, ShellChanStderrRetainedBytes, - ShellChannelNotRealizedByTarget, shell_emission_refusal_fact, shell_result_channel_key, + ShellChannelNotRealizedByTarget, ShellCapturePolicyInputAbsent, shell_emission_refusal_fact, shell_result_channel_key, + shell_channel_is_capture_accounting, operation_declares_required_stderr_capture_policy, bind_operation_transport, transport_binding_refusal_fact, + NativeEffectRealizationAdmission, NativeEffectHandlerBound, NativeEffectOutsideProductionDenominator, NativeEffectRealizationUnavailable, + NativeServiceEffect, NativeResourceEffect, NativeTransportHandler, NativeCapabilityToken, NativeEffectDeclSite, native_effect_resolution, + admit_native_service_effect, admit_native_resource_effect, native_effect_admission_refusal, FileVerb, FileRead, FileWrite, FileWriteOwnerOnly, FileWriteCreateNew, FileWriteCreateNewWithMode, FileLinkCreateNew, FileDelete, FileList, FileResultField, FileResultChannel, FileChanSuccess, FileChanByteCount, FileChanPath, FileChanError, FileChanErrorKind, FileChanContent @@ -300,7 +305,7 @@ fn render_rust_applied_via_emit_info(n: Node, shared_types: Set, source_ fn render_rust_type(n: Node, shared_types: Set, source_indices: Map, emit_info: EmitGraphInfo) -> String { if is_host_text_carrier_type(n: n, source_indices: source_indices) { return rust_carrier_optional_wrap(n: n, rendered: "String") } - if is_host_optional_carrier_type(n: n, source_indices: source_indices) { + if is_host_optional_carrier_type(n: n, source_indices: source_indices, env: emit_info.fn_type_env) { return render_rust_optional_carrier_applied(n: n, generic_param_names: emit_info.fn_generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: emit_info.variant_to_enum, env: emit_info.fn_type_env) } if is_host_diagnostics_carrier_type(n: n, source_indices: source_indices) { @@ -308,7 +313,7 @@ fn render_rust_type(n: Node, shared_types: Set, source_indices: Map - if type_var_in_fn_generic_scope(id: tv, generic_param_names: emit_info.fn_generic_param_names) { to_pascal(name: tv) } else { "_" } + if type_var_in_fn_generic_scope(id: tv, generic_param_names: emit_info.fn_generic_param_names) { rust_carrier_optional_wrap(n: n, rendered: to_pascal(name: tv)) } else { "_" } _ => match find_property(props: n.properties, prop_name: "__applied_type_args", source_indices: source_indices) { Present { value: applied } => @@ -395,7 +400,7 @@ fn render_rust_arrow_type_in_scope(n: Node, generic_param_names: List, s // documented unresolved-reference shape), so every identity-keyed arm ahead of this one answers // about the wrong declaration. Rendering the bare name can only bind where the emitted module // declares or imports that name, and for the String spelling neither holds: kernel names are never -// overridden by imports (#9813, v1.compiler.infer overlay_skips_kernel_name), so the use-line that +// overridden by imports (#9813, v1.compiler.infer_env overlay_skips_kernel_name), so the use-line that // would bind the alias is dropped by import_name_resolves_to_host_realized_kernel_scalar and the // bare token binds the prelude String -- while every value position renders the same carrier // Rc>. That is the v2_compiler_tokenize.rs E0308 family on the XL-N board (41 of 72), @@ -430,7 +435,7 @@ fn rust_overlayless_alias_leaf_requires_peel(env: TypeEnv, n: Node) -> Bool { fn render_rust_type_without_applied_binding(n: Node, shared_types: Set, source_indices: Map, emit_info: EmitGraphInfo) -> String { if is_host_text_carrier_type(n: n, source_indices: source_indices) { return rust_carrier_optional_wrap(n: n, rendered: "String") } - if is_host_optional_carrier_type(n: n, source_indices: source_indices) { + if is_host_optional_carrier_type(n: n, source_indices: source_indices, env: emit_info.fn_type_env) { return render_rust_optional_carrier_applied(n: n, generic_param_names: emit_info.fn_generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: emit_info.variant_to_enum, env: emit_info.fn_type_env) } if is_host_diagnostics_carrier_type(n: n, source_indices: source_indices) { @@ -544,10 +549,6 @@ fn is_host_freemonoid_vec_alias(name: String) -> Bool { name == "FreeMonoid" } -fn is_host_optional_carrier_alias(name: String) -> Bool { - name == kernel_optional_mint_name -} - // THE NAME PROJECTION OF ONE ROW, not a second table. Which declarations the Rust target realizes // as its host option is gunbc.rust_source_type_bindings rust_host_option_carrier_declarations; the // sites that still hold only a type's NAME (type rendering, and pattern arms inference did not @@ -558,12 +559,39 @@ fn is_host_diagnostics_carrier_alias(name: String) -> Bool { rust_host_option_carrier_declarations |> any(d => (d.decl_name as String) == name) } -fn is_grounded_coproduct_native_alias(name: String) -> Bool { - is_host_freemonoid_vec_alias(name: name) - || is_host_optional_carrier_alias(name: name) - || is_host_diagnostics_carrier_alias(name: name) +// WHETHER A DECLARATION IS REALIZED AS A HOST CARRIER (Vec for FreeMonoid, Option for the kernel +// optional and the diagnostics carrier) is a fact about the declaration: whether this declaration is realized as a host carrier is a fact about it, so a coproduct +// a module declares as `Optional` is not the host option (declaration_owns_host_option). +fn declaration_is_grounded_coproduct_native_alias(module_path: String, decl_name: String) -> Bool { + if contains(decl_name, ".") { + let segments = decl_name |> split(delimiter: ".") + declaration_is_grounded_coproduct_native_alias(module_path: segments |> take((segments |> count) - 1) |> join(separator: "."), decl_name: qualified_last_segment(name: decl_name)) + } else { + is_host_freemonoid_vec_alias(name: decl_name) + || declaration_owns_host_option(module_path: module_path, decl_name: decl_name) + || declaration_ref_in_list(target: decl_ref(module_path: module_path, decl_name: decl_name), refs: rust_host_option_carrier_declarations) + } +} + +// A parent named only by its leaf, reached through a module that may re-export rather than declare +// it: it is a host carrier when every declaration of that leaf in the closure is one. Nothing is +// chosen, so nothing is guessed; where declarations disagree the reader keeps its own declaring-module +// check. +fn every_declarer_is_grounded_coproduct_native_alias(decls: TypeDeclIndex, name: String) -> Bool { + let identities = type_decl_identities_of_leaf(index: decls, leaf: qualified_last_segment(name: name)) + (identities |> count) > 0 && identities |> all(identity => + declaration_is_grounded_coproduct_native_alias(module_path: "", decl_name: identity) + ) +} + +// A TypeSummary key is `.`: the identity type_decl_identity minted for it. +fn summary_declares_grounded_coproduct_native_alias(summary: TypeSummary) -> Bool { + let segments = summary.key |> split(delimiter: ".") + let module_path = segments |> take((segments |> count) - 1) |> join(separator: ".") + declaration_is_grounded_coproduct_native_alias(module_path: module_path, decl_name: summary.name) } + // A COPRODUCT THE CORPUS BINDS NATIVELY (gunbc.rust_source_type_bindings, keyed on the declaration) // REALIZES ONLY AS ITS HOST SPELLING: no enum is emitted for it, so there is no enum to glob-import // and no bridge to convert from. Its arms reach the target as the carrier's values @@ -579,8 +607,69 @@ fn rust_declaration_realizes_as_native_alias(module_path: String, decl_name: Str } } -fn is_host_optional_carrier_type(n: Node, source_indices: Map) -> Bool { - authored_name_at(source_indices: source_indices, node: n) == "Optional" && n.children |> count > 0 +// WHICH DECLARATION IS THE HOST OPTION is a fact about the declaration, read from the one row that +// binds it: the declaration the kernel optional mint stands for +// (gunbc.structural_realization_bindings kernel_mint_declaration_rows), the same reading +// host_option_arm_reading makes for a pattern arm. A declaration that is not that one is not the host +// option, whatever it is named: a coproduct a module declares as `Optional` emits as its own enum. +// Two rows binding the kernel optional decide no declaration, and none is answered as the host +// option here (the pattern side refuses that state, HostOptionArmBindingAmbiguous). +fn declaration_owns_host_option(module_path: String, decl_name: String) -> Bool { + match kernel_mint_ownership(rows: kernel_mint_declaration_rows, minted_name: kernel_optional_mint_name, declaration: decl_ref(module_path: module_path, decl_name: decl_name)) { + DeclarationOwnsTheMint => true + DeclarationDoesNotOwnTheMint => false + KernelMintOwnershipAmbiguous { row_count: _ } => false + } +} + +// A type reference that carries the declaration it names is decided by that declaration. A reference +// resolved to a declaration's STRUCTURE (lookup_type_for hands the emitter the declaration node itself, +// unstamped) is decided by WHICH declaration it is, read through the join every emitter reader of a +// declaration node uses (v1.compiler.infer_env declaration_ref_of_declaration_node: the declaring span +// against the corpus symbol index), so a coproduct another module declares as `Optional` is never read +// as the host option. A reference that is neither is decided by unstamped_reference_is_host_option. +// AN UNSTAMPED REFERENCE is decided by the one occurrence decision the type-declaration router reads +// (v1.compiler.infer_env bare_occurrence_binding: own file, then named import, then kernel -- the +// typechecker's lookup_binding order): the declaration that arm names, or on the kernel arm the +// declaration the kernel mint names. No leaf comparison stands in for any of them. +fn unstamped_reference_is_host_option(n: Node, source_indices: Map, env: TypeEnv) -> Bool { + let leaf = authored_name_at(source_indices: source_indices, node: n) + match bare_occurrence_binding(index: env.symbol_index, name: leaf, reference: n) { + BareOccurrenceDeclaredInItsFile { declaration: decl } => declaration_node_owns_host_option(decl: decl, source_indices: source_indices, env: env) + BareOccurrenceImportedByName { module_path: from_module, declaration: decl } => + declaration_owns_host_option(module_path: from_module, decl_name: leaf) || declaration_node_owns_host_option(decl: decl, source_indices: source_indices, env: env) + BareOccurrenceIsKernelName => + match kernel_mint_declaration_for(rows: kernel_mint_declaration_rows, minted_name: leaf) { + KernelMintDeclarationFound { declaration: d } => declaration_owns_host_option(module_path: d.module_path as String, decl_name: d.decl_name as String) + KernelMintDeclarationAbsent => false + KernelMintDeclarationAmbiguous { row_count: _ } => false + } + BareOccurrenceUndecided => false + } +} + +fn declaration_node_owns_host_option(decl: Node, source_indices: Map, env: TypeEnv) -> Bool { + match declaration_ref_of_declaration_node(decl: decl, source_indices: source_indices, env: env) { + Present { value: d } => declaration_owns_host_option(module_path: d.module_path as String, decl_name: d.decl_name as String) + Absent => false + } +} + +fn is_host_optional_carrier_type(n: Node, source_indices: Map, env: TypeEnv) -> Bool { + match n.declaration { + Present { value: d } => + match d.field { + WholeDeclaration => declaration_owns_host_option(module_path: d.module_path as String, decl_name: d.decl_name as String) + _ => false + } + Absent => + let is_declaration_structure = (n.module_item_kind == ModuleItemTypeDeclaration || n.connective == Disj) && n.ident_span != none + let declared = if is_declaration_structure { declaration_ref_of_declaration_node(decl: n, source_indices: source_indices, env: env) } else { none } + match declared { + Present { value: d } => declaration_owns_host_option(module_path: d.module_path as String, decl_name: d.decl_name as String) + Absent => unstamped_reference_is_host_option(n: n, source_indices: source_indices, env: env) + } + } } fn is_host_diagnostics_carrier_type(n: Node, source_indices: Map) -> Bool { @@ -1441,7 +1530,7 @@ fn render_rust_applied_type_arg(n: Node, generic_param_names: List, shar } else { match n.inferred { Present { value: TypeVariable { id: tv } } => - if type_var_in_fn_generic_scope(id: tv, generic_param_names: generic_param_names) { to_pascal(name: tv) } else { "_" } + if type_var_in_fn_generic_scope(id: tv, generic_param_names: generic_param_names) { rust_carrier_optional_wrap(n: n, rendered: to_pascal(name: tv)) } else { "_" } _ => match rust_render_checkpoint_scalar_bare(n: n, source_indices: source_indices, shared_types: shared_types, env: env) { Present { value: scalar } => scalar @@ -1453,7 +1542,7 @@ fn render_rust_applied_type_arg(n: Node, generic_param_names: List, shar fn render_rust_applied_type(n: Node, generic_param_names: List, shared_types: Set, source_indices: Map, variant_to_enum: Map, env: TypeEnv) -> String { if is_host_text_carrier_type(n: n, source_indices: source_indices) { return rust_carrier_optional_wrap(n: n, rendered: "String") } - if is_host_optional_carrier_type(n: n, source_indices: source_indices) { + if is_host_optional_carrier_type(n: n, source_indices: source_indices, env: env) { return render_rust_optional_carrier_applied(n: n, generic_param_names: generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: variant_to_enum, env: env) } if is_host_diagnostics_carrier_type(n: n, source_indices: source_indices) { @@ -1644,7 +1733,7 @@ fn render_rust_decl_type(n: Node, generic_param_names: List, shared_type return render_rust_arrow_type_in_scope(n: n, generic_param_names: generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: variant_to_enum, env: env) } if is_host_text_carrier_type(n: n, source_indices: source_indices) { return rust_carrier_optional_wrap(n: n, rendered: "String") } - if is_host_optional_carrier_type(n: n, source_indices: source_indices) { + if is_host_optional_carrier_type(n: n, source_indices: source_indices, env: env) { return render_rust_optional_carrier_applied(n: n, generic_param_names: generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: variant_to_enum, env: env) } if is_host_diagnostics_carrier_type(n: n, source_indices: source_indices) { @@ -1683,7 +1772,7 @@ fn render_rust_decl_type(n: Node, generic_param_names: List, shared_type Present { value: exact } => rust_carrier_optional_wrap(n: n, rendered: exact) Absent => if n.connective == NoConnective && n.children |> count == 0 && (generic_param_names |> any(g => g == name)) { - to_pascal(name: name) + rust_carrier_optional_wrap(n: n, rendered: to_pascal(name: name)) } else if bare_native_alias != none { rust_carrier_optional_wrap(n: n, rendered: bare_native_alias.value) } else if n.connective == NoConnective && n.children |> count == 0 && applied_prop == none && is_host_text_carrier_type(n: n, source_indices: source_indices) { @@ -1755,6 +1844,12 @@ fn rust_fn_sig_preserves_authored_alias_leaf(name: String, decision: TypeRealiza } } +// A TYPE PARAMETER CARRIES ITS OWN CARDINALITY, AND EVERY ARM THAT SPELLS ONE WRAPS IT HERE. `K?` on a +// declared parameter is a leaf whose return_cardinality is CardOptional. The four arms that spell a +// type parameter -- the declared-generic leaf in render_rust_decl_type and in +// render_rust_alias_rhs_type, and the TypeVariable arms of render_rust_type and render_rust_applied_type_arg +// -- returned the bare name, so `get: K?` in a generic record emitted `K`: an absent value had no +// representation (gunbc.recurring_failure_mode optional_type_parameter_emitted_without_its_cardinality). fn rust_carrier_optional_wrap(n: Node, rendered: String) -> String { let is_optional = n.return_cardinality == CardOptional if is_optional { @@ -1766,7 +1861,7 @@ fn rust_carrier_optional_wrap(n: Node, rendered: String) -> String { fn render_rust_fn_sig_type(n: Node, generic_param_names: List, shared_types: Set, source_indices: Map, variant_to_enum: Map, env: TypeEnv) -> String { if is_host_text_carrier_type(n: n, source_indices: source_indices) { return rust_carrier_optional_wrap(n: n, rendered: "String") } - if is_host_optional_carrier_type(n: n, source_indices: source_indices) { + if is_host_optional_carrier_type(n: n, source_indices: source_indices, env: env) { return render_rust_optional_carrier_applied(n: n, generic_param_names: generic_param_names, shared_types: shared_types, source_indices: source_indices, variant_to_enum: variant_to_enum, env: env) } if is_host_diagnostics_carrier_type(n: n, source_indices: source_indices) { @@ -1928,7 +2023,7 @@ fn render_rust_alias_rhs_type(n: Node, generic_param_names: List, shared } let name = authored_name_at(source_indices: source_indices, node: n) if n.connective == NoConnective && n.children |> count == 0 && (generic_param_names |> any(g => g == name)) { - to_pascal(name: name) + rust_carrier_optional_wrap(n: n, rendered: to_pascal(name: name)) } else if n.connective == NoConnective && n.children |> count == 0 { match rust_exact_reference_spelling(n: n, source_indices: source_indices, env: scope.type_env) { Present { value: exact } => exact @@ -3138,7 +3233,7 @@ fn emit_rust_init_block_stmts(remaining: List, text: List, fn rust_qualify_type_leaf_name(name: String, variant_to_enum: Map) -> String { match map_get(variant_to_enum, name) { Present { value: parent } => - if parent != "" { rust_variant_path(parent: parent, rust_name: name) } else { name } + if parent != "" { rust_variant_path(parent: qualified_last_segment(name: parent), rust_name: name) } else { name } Absent => name } } @@ -3246,7 +3341,7 @@ fn maybe_mark_shared_type(acc: Set, summary: TypeSummary, recursive_type StructRepr => true EnumRepr { unit_only: unit_only } => unit_only == false } - if is_grounded_coproduct_native_alias(name: summary.name) { + if summary_declares_grounded_coproduct_native_alias(summary: summary) { acc } else if needs_sharing && !is_type_constant(summary: summary, recursive_type_set: recursive_type_set) { set_insert(acc, summary.name) @@ -3255,11 +3350,30 @@ fn maybe_mark_shared_type(acc: Set, summary: TypeSummary, recursive_type } } -fn build_shared_types(type_summaries: Map, recursive_type_set: Set, target: RenderTarget) -> Set { +// THE SHARED SET IS KEYED BY SPELLING, and the declaration and every use of a type read it by that +// spelling, so what keeps an emission consistent is that one spelling has one answer. A spelling two +// declarations own is shared only when EVERY declaration of it would be: a coproduct a module +// declares as `Optional` beside the host option (which is never shared) is then unshared at its +// declaration and at every use alike, and the host Option is not wrapped because a same-leaf type +// elsewhere needs sharing. The set is one of the indexes +// gunbc.recurring_failure_mode emitter_index_keyed_on_a_type_leaf_takes_the_last_declarer still +// names as leaf-keyed; this rule is what makes its leaf answer independent of fold order. +fn shared_by_every_declarer(type_summaries: TypeSummaryIndex, name: String, recursive_type_set: Set, target_needs_sharing: Bool) -> Bool { + type_summary_keys_of_leaf(index: type_summaries, leaf: name) |> all(key => + match type_summary_decided(index: type_summaries, key: key) { + Present { value: summary } => set_contains(maybe_mark_shared_type(acc: empty_set(), summary: summary, recursive_type_set: recursive_type_set, target_needs_sharing: target_needs_sharing), name) + Absent => true + } + ) +} + +fn build_shared_types(type_summaries: TypeSummaryIndex, recursive_type_set: Set, target: RenderTarget) -> Set { let sharing = sharing_for_target(target: target) - let user_shared = map_values(type_summaries) |> fold(init: empty_set(), f: (acc, summary) => - maybe_mark_shared_type(acc: acc, summary: summary, recursive_type_set: recursive_type_set, target_needs_sharing: sharing.needs_sharing) + let user_shared = type_summary_values(index: type_summaries) |> fold(init: empty_set(), f: (acc, summary) => + if shared_by_every_declarer(type_summaries: type_summaries, name: summary.name, recursive_type_set: recursive_type_set, target_needs_sharing: sharing.needs_sharing) { + maybe_mark_shared_type(acc: acc, summary: summary, recursive_type_set: recursive_type_set, target_needs_sharing: sharing.needs_sharing) + } else { acc } ) let collection_keys = sorted_map_keys(rust_container_templates) @@ -3636,7 +3750,12 @@ fn emit_rust_selected(typed: ResolvedGraph, selection: RustModuleRenderSelection renders_clap_cli: has_pipeline, renders_async_services: has_services || closure_renders_async_trait_resource(typed: typed) }) - let main_file = emit_main_rs(modules: typed.modules, has_services: has_services, crate_name: crate_name, registry: ctx.registry) + let native_effects = native_entry_effects(modules: typed.modules, registry: registry) + let native_effect_refusals = native_entry_effect_diagnostics(effects: native_effects, modules: typed.modules) + if native_effect_refusals |> count > 0 { + return EmitResult { files: [], diagnostics: native_effect_refusals, emitted_edges: [] } + } + let main_file = emit_main_rs(modules: typed.modules, has_services: has_services, crate_name: crate_name, effects: native_effects) let cli_dispatch_file = if has_pipeline { [emit_gunbc_cli_dispatch_generated(crate_name: crate_name)] } else { [] } let rt_file = emit_v2_rt_module() let compiler_tests_file = if has_pipeline { [emit_compiler_tests_module()] } else { [] } @@ -4159,7 +4278,7 @@ fn emit_inferred_type_leaf_name(n: Node, source_indices: Map, type_summaries: Map) -> String { +fn anonymous_record_lit_surface_name(n: Node, source_indices: Map, type_summaries: TypeSummaryIndex) -> String { let lit_field_names = n.children |> map(f => field_init_node_name_at(n: f, source_indices: source_indices)) let field_type_hints = fold(n.children, init: empty_map(), f: (acc, f) => let fname = field_init_node_name_at(n: f, source_indices: source_indices) @@ -4178,12 +4297,12 @@ fn anonymous_record_lit_surface_name(n: Node, source_indices: Map let rt = resolved_type(n: n) let rt_name = authored_name_at(source_indices: source_indices, node: rt) - if rt.ident_span != none && map_contains_key(type_summaries, rt_name) { rt_name } else { "" } + if rt.ident_span != none && (type_summary_decided(index: type_summaries, key: rt_name) != none) { rt_name } else { "" } } } } -fn record_lit_variant_payload_struct_surfaces(n: Node, source_indices: Map, type_summaries: Map, variant_to_enum: Map) -> List { +fn record_lit_variant_payload_struct_surfaces(n: Node, source_indices: Map, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> List { let variant_name = match record_lit_type_name_at(texpr: n, source_indices: source_indices) { Present { value: v } => v Absent => authored_name_at(source_indices: source_indices, node: n) @@ -4194,10 +4313,10 @@ fn record_lit_variant_payload_struct_surfaces(n: Node, source_indices: Map "" } if enum_name == "" { [] } else { - match map_get(type_summaries, variant_summary_key(enum_name: enum_name, variant_name: variant_name)) { + match type_summary_decided(index: type_summaries, key: variant_summary_key(enum_name: enum_name, variant_name: variant_name)) { Present { value: summary } => map_values(summary.field_type_map) |> filter(ft => - ft != "" && match map_get(type_summaries, ft) { + ft != "" && match type_summary_decided(index: type_summaries, key: ft) { Present { value: fts } => match fts.repr { StructRepr => true @@ -4224,7 +4343,7 @@ fn record_lit_field_type_hints(n: Node, source_indices: Map, source_indices: Map) -> List { +fn record_lit_resolved_ctor_import_names(type_name: String, n: Node, type_summaries: TypeSummaryIndex, source_indices: Map) -> List { let from_template = match container_template_algebra(name: type_name) { Present { value: algebra } => if algebra != "" && algebra != type_name { [algebra] } else { [] } Absent => [] @@ -4244,7 +4363,7 @@ fn record_lit_resolved_ctor_import_names(type_name: String, n: Node, type_summar unique_strings(items: concat(from_template, from_fields)) } -fn record_lit_ref_names(n: Node, source_indices: Map, type_summaries: Map, variant_to_enum: Map) -> List { +fn record_lit_ref_names(n: Node, source_indices: Map, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> List { match n.expr_data { ExprRecordLit { parent_enum: pe } => let tn_list = match record_lit_type_name_at(texpr: n, source_indices: source_indices) { @@ -4286,14 +4405,14 @@ fn record_lit_ref_names(n: Node, source_indices: Map, type // the text gate declares (P2a candidate-producer / BoundReferenceProvider deriving use-lines from // bound-reference structure). -fn collect_anonymous_record_lit_heads(n: Node, source_indices: Map, type_summaries: Map) -> List { +fn collect_anonymous_record_lit_heads(n: Node, source_indices: Map, type_summaries: TypeSummaryIndex) -> List { let self_head = match n.expr_data { ExprRecordLit { parent_enum: _ } => match record_lit_type_name_at(texpr: n, source_indices: source_indices) { Present { value: _ } => [] Absent => let inferred = emit_inferred_type_leaf_name(n: n, source_indices: source_indices) - if inferred != "" && map_contains_key(type_summaries, inferred) { [inferred] } else { + if inferred != "" && (type_summary_decided(index: type_summaries, key: inferred) != none) { [inferred] } else { let anon = anonymous_record_lit_surface_name(n: n, source_indices: source_indices, type_summaries: type_summaries) if anon != "" { [anon] } else { [] } } @@ -4335,13 +4454,13 @@ fn collect_items_field_import_surface_names(surface_names: List, emit_in let variant_to_enum = emit_info.variant_to_enum unique_strings(items: surface_names |> flat_map(type_name => - let summary_lookup = match map_get(type_summaries, type_name) { + let summary_lookup = match type_summary_decided(index: type_summaries, key: type_name) { Present { value: summary } => Present { value: summary } Absent => match map_get(variant_to_enum, type_name) { Present { value: enum_name } => if enum_name == "" { none } else { - map_get(type_summaries, variant_summary_key(enum_name: enum_name, variant_name: type_name)) + type_summary_decided(index: type_summaries, key: variant_summary_key(enum_name: enum_name, variant_name: type_name)) } Absent => none } @@ -4349,7 +4468,7 @@ fn collect_items_field_import_surface_names(surface_names: List, emit_in match summary_lookup { Present { value: summary } => summary.field_import_surface_names |> filter(field_type => - field_type != "" && match map_get(type_summaries, field_type) { + field_type != "" && match type_summary_decided(index: type_summaries, key: field_type) { Present { value: ft } => match ft.repr { StructRepr => true @@ -4370,7 +4489,7 @@ fn collect_items_field_import_surface_names(surface_names: List, emit_in // scrutinee type available at collection time the parent is resolved only when // unique_variant_parent finds exactly one owner, so a homonym variant proposes nothing rather // than guessing an enum (the candidate set may under-propose; it must never fabricate). -fn collect_pattern_ref_names(pattern: MatchPattern, type_summaries: Map) -> List { +fn collect_pattern_ref_names(pattern: MatchPattern, type_summaries: TypeSummaryIndex) -> List { match pattern { VariantPattern { name: n, parent_enum: parent_enum, field_bindings: fbs, parent_identity: _ } => let own = match pattern_parent_enum(name: qualified_last_segment(name: n), parent_enum: parent_enum, scrut_type: "", type_summaries: type_summaries) { @@ -4382,7 +4501,7 @@ fn collect_pattern_ref_names(pattern: MatchPattern, type_summaries: Map, type_summaries: Map, variant_to_enum: Map) -> List { +fn collect_value_ref_names(n: Node, source_indices: Map, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> List { let self_name = match n.expr_data { ExprVar { binding_kind: bk } => match bk { @@ -4459,7 +4578,7 @@ fn emit_scrutinee_type_name(scrutinee: Node, source_indices: Map) -> List { +fn collect_pattern_rc_prelude_parent_enums(pattern: MatchPattern, scrut_type: String, type_summaries: TypeSummaryIndex) -> List { match pattern { VariantPattern { name: n, parent_enum: parent_enum, field_bindings: _, parent_identity: _ } => let bare_n = qualified_last_segment(name: n) @@ -4544,7 +4663,7 @@ fn variant_record_lit_summary_key(n: Node, source_indices: Map, type_summaries: Map, variant_to_enum: Map) -> List { +fn collect_record_lit_field_struct_surfaces(parent: Node, field_init: Node, source_indices: Map, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> List { let field_name = field_init_node_name_at(n: field_init, source_indices: source_indices) if field_name == "" { [] } else { let field_value = field_init_node_value(n: field_init) @@ -4552,11 +4671,11 @@ fn collect_record_lit_field_struct_surfaces(parent: Node, field_init: Node, sour ExprRecordLit { parent_enum: _ } => let summary_key = variant_record_lit_summary_key(n: parent, source_indices: source_indices, variant_to_enum: variant_to_enum) if summary_key == "" { [] } else { - match map_get(type_summaries, summary_key) { + match type_summary_decided(index: type_summaries, key: summary_key) { Present { value: summary } => match map_get(summary.field_type_map, field_name) { Present { value: ft } => - if ft != "" && match map_get(type_summaries, ft) { + if ft != "" && match type_summary_decided(index: type_summaries, key: ft) { Present { value: fts } => match fts.repr { StructRepr => true @@ -4591,7 +4710,7 @@ fn collect_value_emit_type_surface_names(n: Node, source_indices: Map if parent != "" { [parent] } else { [] } + Present { value: parent } => if parent != "" { [qualified_last_segment(name: parent)] } else { [] } Absent => [] } unique_strings(items: concat(from_inferred_list, from_variant)) @@ -4652,15 +4771,15 @@ fn imported_names_in_use_line(line: String) -> List { data reference_derived_use_lines_note: String = "emit_import_closure_root (§5). emit_imports wires a per-module use-line only for names in an authored import list. Namespace-only resolution (post-PR 6848) references cross-module names WITHOUT importing them, so the ref is KNOWN but the use-line is declined (advisory UnlistedImportUse, is_error_diagnostic=false) — a §5 fail-open (⊤-as-ignorance) that emits invalid Rust (E0422/E0433/E0425 downstream). This pass derives the missing use-lines from the SAME resolver signal, split by reference kind onto its precise authority (§2 Realization: one closure, two consumers): (1) TYPE refs come from the resolver's UnlistedImportUse diagnostics (04_resolve.dag resolve_node, masked && not-in-SVN at type positions) threaded through ResolvedGraph.diagnostics — zero-drift by construction, the resolver already applied its SVN mask AT RESOLVE TIME; (2) VALUE-position refs come from collect_value_ref_names, a NARROW walk that structurally excludes the type over-collection classes (container heads, field labels, deep-inferred type names): fn/data refs (FunctionValueBinding ExprVar + ExprCall callee names) AND record-literal type constructions (ExprRecordLit type name + its parent_enum) — the latter matter because a GENERIC user type constructed as `T{..}` (e.g. RealizedStep) is grounded by resolve_node (masked flips false into the defining-module descent) so it NEVER fires UnlistedImportUse, yet its bare `T` still needs a use-line; (3) TYPE-surface refs on item signatures come from collect_item_emit_surface_names (collect_item_type_surface_names + collect_value_emit_type_surface_names over data bodies) over each item's type_annotation, param types, inferred Resolved return/signature, AND value-position record-literal / variant-parent-enum surfaces the emitter will qualify (e.g. UriScheme::Https in a data anchor) — covering masked-at-resolve TYPE positions (e.g. partial-import return type PilotWidget) that never enter UnlistedImportUse and are not ExprVar/ExprCall harvests. Registry cross-module resolve + is_known_variant fallback keep variant constructors routed through their parent's import. CONSTRUCTION WALL (ROOT 3, §5): a candidate that registry-resolves to a provider module MUST also pass provider_proven_exports_symbol (name_in_transitive_export_surface) before any use-line is synthesized — never a plausible guess from the bare-name registry alone (the v1_rt::member fabrication class: registry homonym without export proof). NOTE the SVN authority is resolve-time-only: env.source_visible_names is built in 04_infer's unresolved_env and consumed by resolve_node, but is NOT persisted onto TypedModule.type_env (emit reads empty_map), so emit MUST NOT re-apply an SVN filter — it would be a no-op that (worse, when non-empty) diverges from the resolve-time mask. The union is instead already-imported filtered (a name already carried by an authored import / prelude / carrier use-line is skipped — this is what keeps a fully-imported SEED module zero-drift: its refs are all in an import line) and kernel filtered (no E0252 against the runtime prelude), then LOCAL-DECL filtered (any name the module itself declares — local_decl_names from authored_name_at over items, plus local_type_names aliases/phantoms — is never an import candidate: the containment tree binds a bare ref to the module's own declaration before any cross-module lookup, and the bare-name registry is last-write-wins, so a dual-tree homonym — dag/std List/Map/GroupCompletion vs their src/v2/std twins — steals the registry row and would otherwise synthesize a SELF-COLLIDING pub-use, the E0255 std_dup class measured on the 2026-07-22 curated 4-module baseline), then cross-module registry-resolved (a ref the registry maps to this same module is skipped), then reuses emit_specific_import_block for variant/reexport correctness with a §5 direct-emit fallback (arm (c): the name resolved via registry to provider AND export-proven). A candidate that registry-resolves to nothing, or resolves but fails export proof, is left unsynthesized, and it never fabricates a use-line. STEP 2 HAS LANDED: the reason is no longer silent. Each such candidate carries a ReferenceDerivedCandidateDisposition -- CandidateRegistryAbsent or CandidateExportProofFailed, distinct because the remedies are opposite -- and CandidateExportProofFailed emits the typed, located, counted ReferenceDerivedImportExportUnproven. THE EMISSION IS NOT REFUSED, and the distinction matters because a reader of the phrase typed refusal will otherwise expect one: the diagnostic is ADVISORY, so emission completes and produces its files beside it. Measured on the required build lane at this branch head, entry:src/v2/compiler/00_compile.dag: emitted=175 blocking=0 -- so what step 2 closed is the SILENCE, not the emission. Whether the line stops is the severity question, and v1.compiler.core reference_derived_import_refusal_severity_note owns it, including why the blocking flip was executed and then falsified by its own first run over a second closure. SCOPE (emit_module_full): import-free modules run the full union (TYPE unlisted + VALUE refs) — the namespace-resolution case the post-PR-6848 regression is about. SUPERSEDED-MECHANISM, THIS CUT (eager-deer-389): the two sentences that follow describe the corpus_repr_is_faithful / HostNative gate, which NO LONGER EXISTS -- RustCorpusRepr and both its variants are deleted and the gate is gone, so import-bearing modules are no longer split by corpus mode. They are retained as the history of WHY the walk was gated at all (the spurious-use-line and seed zero-drift hazards below are real and still constrain any future gating), not as a live description; reference_use_lines_representation_invariant_note in this module carries the post-cut statement. Read them as quarry. AS WRITTEN BEFORE THE CUT: Import-bearing modules run reference_derived_use_lines ONLY when corpus_repr_is_faithful (FaithfulFreeMonoid / v2 namespace corpus): a partial-import namespace module (e.g. v2.std.node_query importing Outcome but calling outcome_with_diagnostics, or importing Outcome but annotating NamedEdgeTargetLookup) must synthesize BOTH the missing fn-value use-line AND the missing type use-line without duplicating names emit_imports already owns. HostNative import-bearing modules (v1 seed) get [] — running the walk there adds spurious/wrong use-lines (registry homonyms like kernel_span/is_type_variable) and breaks zero-drift seed regen." -fn expand_variant_payload_struct_imports(names: List, type_summaries: Map, variant_to_enum: Map) -> List { +fn expand_variant_payload_struct_imports(names: List, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> List { names |> flat_map(n => match map_get(variant_to_enum, n) { Present { value: enum_name } => if enum_name == "" { [] } else { - match map_get(type_summaries, variant_summary_key(enum_name: enum_name, variant_name: n)) { + match type_summary_decided(index: type_summaries, key: variant_summary_key(enum_name: enum_name, variant_name: n)) { Present { value: summary } => map_values(summary.field_type_map) |> filter(ft => - ft != "" && match map_get(type_summaries, ft) { + ft != "" && match type_summary_decided(index: type_summaries, key: ft) { Present { value: fts } => match fts.repr { StructRepr => true @@ -4716,14 +4835,14 @@ fn reference_is_host_realized_builtin(name: String) -> Bool { // fabricate. This arm carries no dissolution condition of its own — it dissolves with the text gate // it corrects, on the trigger reference_derived_authored_source_gate_note already declares. -fn reference_derived_variant_induced_parent_spelled(module_source: String, enum_name: String, type_summaries: Map, variant_to_enum: Map) -> Bool { - match map_get(type_summaries, enum_name) { +fn reference_derived_variant_induced_parent_spelled(module_source: String, enum_name: String, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> Bool { + match type_summary_decided(index: type_summaries, key: enum_name) { Present { value: summary } => match summary.repr { EnumRepr { unit_only: _ } => sorted_map_keys(summary.variant_name_set) |> any(vn => match map_get(variant_to_enum, vn) { - Present { value: parent } => parent == enum_name && reference_derived_candidate_spelled_in_module(module_source: module_source, name: vn) + Present { value: parent } => parent != "" && qualified_last_segment(name: parent) == qualified_last_segment(name: enum_name) && reference_derived_candidate_spelled_in_module(module_source: module_source, name: vn) Absent => false } ) @@ -4733,7 +4852,7 @@ fn reference_derived_variant_induced_parent_spelled(module_source: String, enum_ } } -fn reference_derived_candidate_authored(module_source: String, name: String, type_summaries: Map, variant_to_enum: Map) -> Bool { +fn reference_derived_candidate_authored(module_source: String, name: String, type_summaries: TypeSummaryIndex, variant_to_enum: Map) -> Bool { reference_derived_candidate_spelled_in_module(module_source: module_source, name: name) || reference_derived_variant_induced_parent_spelled(module_source: module_source, enum_name: name, type_summaries: type_summaries, variant_to_enum: variant_to_enum) } @@ -5010,12 +5129,12 @@ fn local_coproduct_variant_names(items: List, source_indices: Map, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex, type_summaries: Map, variant_to_enum: Map, in_type_position: Bool) -> ReferenceDerivedCandidateDisposition { +fn reference_derived_candidate_disposition(name: String, this_module_name: String, emit_info: EmitGraphInfo, registry: Map, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex, type_summaries: TypeSummaryIndex, variant_to_enum: Map, in_type_position: Bool) -> ReferenceDerivedCandidateDisposition { if is_known_variant(type_summaries: type_summaries, name: name) && !in_type_position { match map_get(variant_to_enum, name) { Present { value: parent } => if parent == "" { CandidateVariantParentUnresolved } - else { CandidateVariantDelegatedToParent { parent_enum: parent } } + else { CandidateVariantDelegatedToParent { parent_enum: qualified_last_segment(name: parent) } } Absent => CandidateVariantParentUnresolved } } else { @@ -5059,7 +5178,7 @@ fn reference_derived_candidate_disposition(name: String, this_module_name: Strin // vocabulary, so Vec and BTreeSet -- the Rust spellings of List and Set -- // slip a filter built to remove exactly this. // (b) OUT-OF-CLOSURE empty_map 33, Optional 25, and Optional's VARIANTS Present 47 and Absent 13 -// -- REAL .dag declarations in v2.std.collection and v2.std.optional whose +// -- REAL .dag declarations in v2.std.collection and std.optional whose // providers were not selected into the measured closure. Movable, and // precisely the closure-conditioned class this change exists to surface. // @@ -5328,7 +5447,7 @@ fn canonical_string_set(items: List) -> List { fn reference_derived_use_line_plan(items: List, unlisted_type_names: List, this_module_name: String, emit_info: EmitGraphInfo, registry: Map, local_type_names: List, already_imported_names: List, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex, shared_types: Set, emitted_source: String) -> ReferenceDerivedUseLinePlan { { - let module_source = match typed_modules |> filter(tm => authored_name_at(source_indices: source_indices, node: tm.module) == this_module_name) |> first { + let module_source = match typed_module_by_name(module_name: this_module_name, typed_modules: typed_modules, source_indices: source_indices, module_index: module_index) { Present { value: tm } => module_authored_source_text(module: tm.module, source_indices: source_indices) Absent => "" } @@ -5666,7 +5785,7 @@ fn emit_module_full(typed_module: TypedModule, registry: Map, else { concat("\n", uncovered_svc_imports |> join(separator: "\n")) } let local_enum_uses = typed_module.items |> filter(item => is_type_def_item(item: item) && is_coproduct_type(n: item) - && !is_grounded_coproduct_native_alias(name: authored_name(env: scope.type_env, node: item)) + && !declaration_is_grounded_coproduct_native_alias(module_path: scope.type_env.module_path, decl_name: authored_name(env: scope.type_env, node: item)) && !rust_declaration_realizes_as_native_alias(module_path: authored_name(env: scope.type_env, node: m), decl_name: authored_name(env: scope.type_env, node: item)) && rust_kernel_grounded_type_decl(module_name: scope.module_name, item_text: authored_name(env: scope.type_env, node: item)) == none) |> map(item => concat("use self::", authored_name(env: scope.type_env, node: item), "::*;")) @@ -5733,7 +5852,7 @@ fn graph_type_import_module_path(name: String, import_module: String, mod_name: } } -fn is_import_graph_type_name(name: String, import_module: String, typed_modules: List, registry: Map, export_sets: Map>, type_summaries: Map, source_indices: Map, module_index: ModuleIndex) -> Bool { +fn is_import_graph_type_name(name: String, import_module: String, typed_modules: List, registry: Map, export_sets: Map>, type_summaries: TypeSummaryIndex, source_indices: Map, module_index: ModuleIndex) -> Bool { if has_physical_type_def_in_module_filename(rhs_name: name, mod_filename: module_to_filename(name: import_module), typed_modules: typed_modules, source_indices: source_indices, module_index: module_index) { true } else if match reexport_source_module_name(name: name, import_module: import_module, typed_modules: typed_modules, export_sets: export_sets, source_indices: source_indices, module_index: module_index) { @@ -5741,7 +5860,11 @@ fn is_import_graph_type_name(name: String, import_module: String, typed_modules: Absent => false } { true - } else if is_enum_in_summaries(type_summaries: type_summaries, type_name: name) { + } else if match is_enum_in_summaries(type_summaries: type_summaries, type_name: name) { + QuestionDecided { value: v } => v + QuestionNameAmbiguous { leaf: _ } => false + QuestionNotDeclared => false + } { false } else if is_known_variant(type_summaries: type_summaries, name: name) { false @@ -6033,13 +6156,17 @@ fn reexport_source_module_name_with_visited(name: String, import_module: String, } } -fn import_module_enum_scope(import_module: String, registry: Map, type_summaries: Map, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex) -> List { +fn import_module_enum_scope(import_module: String, registry: Map, type_summaries: TypeSummaryIndex, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex) -> List { let physical = enum_names_in_module(module_name: import_module, typed_modules: typed_modules, source_indices: source_indices, module_index: module_index) let reexport_parents = match map_get(export_sets, import_module) { Present { value: exported } => sorted_map_keys(exported) |> filter(n => is_known_variant(type_summaries: type_summaries, name: n) - && is_enum_in_summaries(type_summaries: type_summaries, type_name: n) == false + && match is_enum_in_summaries(type_summaries: type_summaries, type_name: n) { + QuestionDecided { value: v } => v == false + QuestionNotDeclared => true + QuestionNameAmbiguous { leaf: _ } => false + } ) |> map(n => match reexport_variant_parent_in_import_module( variant_name: n, @@ -6224,7 +6351,7 @@ fn explicit_import_source_module_for_name(name: String, import_module: String, t } } -fn reexport_variant_parent_in_import_module(variant_name: String, import_module: String, registry: Map, type_summaries: Map, typed_modules: List, export_sets: Map>, source_indices: Map, module_index: ModuleIndex) -> String? { +fn reexport_variant_parent_in_import_module(variant_name: String, import_module: String, registry: Map, type_summaries: TypeSummaryIndex, typed_modules: List, export_sets: Map>, source_indices: Map, module_index: ModuleIndex) -> String? { match reexport_source_module_name(name: variant_name, import_module: import_module, typed_modules: typed_modules, export_sets: export_sets, source_indices: source_indices, module_index: module_index) { Absent => Absent Present { value: src_mod } => @@ -6574,7 +6701,7 @@ fn is_emittable_parametric_type_alias_item(item: Node, item_text: String, source } } -fn import_variant_parent_for_name(n: String, import_module: String, registry: Map, type_summaries: Map, typed_modules: List, export_sets: Map>, source_indices: Map, module_index: ModuleIndex, imported_enums: List, import_module_enums: List) -> String { +fn import_variant_parent_for_name(n: String, import_module: String, registry: Map, type_summaries: TypeSummaryIndex, typed_modules: List, export_sets: Map>, source_indices: Map, module_index: ModuleIndex, imported_enums: List, import_module_enums: List) -> String { if is_import_graph_type_name(name: n, import_module: import_module, typed_modules: typed_modules, registry: registry, export_sets: export_sets, type_summaries: type_summaries, source_indices: source_indices, module_index: module_index) { "" } else if import_module_enums |> any(e => e == n) { @@ -6884,7 +7011,7 @@ fn emit_specific_import_use_lines(import_module: String, mod_name: String, filte ) let main_lines = concat(direct_lines, parent_main_lines) let variant_lines = parent_list |> flat_map(parent => - if is_grounded_coproduct_native_alias(name: parent) || rust_declaration_realizes_as_native_alias(module_path: import_module, decl_name: parent) { [] } else { + if declaration_is_grounded_coproduct_native_alias(module_path: import_module, decl_name: parent) || every_declarer_is_grounded_coproduct_native_alias(decls: emit_info.type_decl_items, name: parent) || rust_declaration_realizes_as_native_alias(module_path: import_module, decl_name: parent) { [] } else { let variants = deduped_names |> filter(n => if n == "None" || n == "Some" { false @@ -6918,7 +7045,7 @@ fn emit_specific_import_use_lines(import_module: String, mod_name: String, filte let final_imported_enums = top_level |> filter(n => import_module_enums |> any(e => e == n)) let wildcard_enum_lines = final_imported_enums |> filter(en => parent_list |> any(p => p == en) == false - && !is_grounded_coproduct_native_alias(name: en) + && !declaration_is_grounded_coproduct_native_alias(module_path: import_module, decl_name: en) && !rust_declaration_realizes_as_native_alias(module_path: import_module, decl_name: en) && rust_kernel_grounded_type_decl(module_name: import_module, item_text: en) == none) |> map(en => RustUseLine { target_module: mod_name, text: concat("use crate::", mod_name, "::", en, "::*;") }) @@ -7281,7 +7408,7 @@ The predicate was `has_physical_type_def_in_module_filename` — field type PHYS INCREMENT 2 (generic-argument axis, #6986): build_field_type_map also walks the resolved field-type NODE TREE via collect_type_node_import_surface_names (04_emit_info.dag), populating TypeSummary.field_import_surface_names; this synth consumes that list so List admits CacheEvidence (field_type_map still maps field→HEAD for lookup). Witness: field_import_surface_generic_arg_test. Re-probe 2026-07-21: materialization_carriers CacheEvidence E0422 cleared; first error advanced to E0425 cannot find value extdeps (namespace/dotted-path class — distinct blocker)." -fn module_data_field_struct_import_names(items: List, type_summaries: Map, import_module: String, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex) -> List { +fn module_data_field_struct_import_names(items: List, type_summaries: TypeSummaryIndex, import_module: String, export_sets: Map>, typed_modules: List, source_indices: Map, module_index: ModuleIndex) -> List { unique_strings(items: items |> filter(item => item.module_item_kind == ModuleItemDataValue) |> flat_map(item => { @@ -7292,12 +7419,12 @@ fn module_data_field_struct_import_names(items: List, type_summaries: Map< if type_name == "" { [] } else { - match map_get(type_summaries, type_name) { + match type_summary_decided(index: type_summaries, key: type_name) { Present { value: summary } => summary.field_import_surface_names |> filter(field_type => field_type != "" && name_in_transitive_export_surface(name: field_type, module_name: import_module, visited: [], export_sets: export_sets, typed_modules: typed_modules, source_indices: source_indices, module_index: module_index) - && match map_get(type_summaries, field_type) { + && match type_summary_decided(index: type_summaries, key: field_type) { Present { value: ft } => match ft.repr { StructRepr => true @@ -7853,7 +7980,7 @@ fn emit_type_def_from_connective(item: Node, recursive_types: Set, share let clone_bound_refusal = emit_item_clone_bound_refusal(item: item, item_name: item_text, emit_info: emit_info, source_indices: env.source_indices) let struct_text = emit_struct_from_children(name: item_text, type_params: type_params, generic_param_names: generic_param_names, header_clone_param_names: header_clone_param_names, children: item.children, recursive_types: recursive_types, shared_types: shared_types, sealed: sealed, deserialize_forbidden: deserialize_forbidden, env: env, emit_info: emit_info) if clone_bound_refusal == "" { struct_text } else { concat(clone_bound_refusal, "\n", struct_text) } - } else if is_host_optional_carrier_alias(name: item_text) { + } else if declaration_owns_host_option(module_path: env.module_path, decl_name: item_text) { let type_params = emit_type_params(params: item.params, source_indices: env.source_indices) let elem_csv = item.params |> map(p => to_pascal(name: generic_param_name_at(n: p, source_indices: env.source_indices))) |> join(separator: ", ") concat(rust_visibility_prefix(), "type ", item_text, type_params, " = Option<", elem_csv, ">;") @@ -7941,7 +8068,7 @@ fn rust_phantom_marker_inner(unused: List) -> String { fn rust_phantom_field_name() -> String { "_phantom" } fn type_has_fn_fields(name: String, emit_info: EmitGraphInfo) -> Bool { - match map_get(emit_info.type_summaries, name) { + match type_summary_decided(index: emit_info.type_summaries, key: name) { Present { value: ts } => ts.has_fn_fields Absent => false } @@ -8576,33 +8703,47 @@ fn v1_fn_body_derived_clone_param_names(params: List, inferred: Node, body type_decl_items: emit_info.type_decl_items, source_indices: si ) - let rc_match_clone_param_names = match body.expr_data { + let rc_match_clone_param_names = v1_fn_body_rc_match_clone_bound_param_names(body: body, generic_param_names: generic_param_names, shared_types: shared_types, emit_info: emit_info, source_indices: si) + v1_union_bound_param_names( + base: derived_clone_param_names, + extra: rc_match_clone_param_names + ) +} + +// EVERY MATCH WHOSE Rc SCRUTINEE IS DEREFERENCED AND CLONED OWES Clone ON THE GENERICS IT CARRIES, +// wherever the match sits. This walk is the next-rung trigger BoundedToDirectTopLevelMatchBody +// named, firing on its specimen: gunbc.product.data_class admit_data_effect clones the +// accumulator `acc: DataEffectAdmission` it matches on INSIDE a fold step, so the match is a +// lambda's body, not the fn's, and the top-level-only reading emitted a bare `` that rustc +// refused (E0599, `clone` exists for DataEffectAdmission but its trait bounds were not +// satisfied). The traversal is generous and the predicate exact, as in the equality walker below: +// analyze_rc_match still decides whether this match derefs, and the pure core still keeps only the +// scrutinee's type arguments that are this fn's own generics. +fn v1_fn_body_rc_match_clone_bound_param_names(body: Node, generic_param_names: List, shared_types: Set, emit_info: EmitGraphInfo, source_indices: Map) -> List { + let here = match body.expr_data { ExprMatch => let scrutinee = match_scrutinee(texpr: body) match scrutinee.inferred { Present { value: Resolved { node: rt } } => - let scrut_type = authored_name_at(source_indices: si, node: rt) - let arms = match_arm_nodes(texpr: body) let rc_match = analyze_rc_match( scrutinee: scrutinee, - arms: arms, - scrut_type: scrut_type, + arms: match_arm_nodes(texpr: body), + scrut_type: authored_name_at(source_indices: source_indices, node: rt), shared_types: shared_types, emit_info: emit_info, - source_indices: si + source_indices: source_indices ) v1_rc_match_scrutinee_clone_bound_param_names( generic_param_names: generic_param_names, - scrutinee_type_arg_names: rt.children |> map(c => authored_name_at(source_indices: si, node: c)), + scrutinee_type_arg_names: rt.children |> map(c => authored_name_at(source_indices: source_indices, node: c)), rc_match_needs_deref: rc_match.needs_deref ) _ => [] } _ => [] } - v1_union_bound_param_names( - base: derived_clone_param_names, - extra: rc_match_clone_param_names + body.children |> fold(init: here, f: (acc, child) => + v1_union_bound_param_names(base: acc, extra: v1_fn_body_rc_match_clone_bound_param_names(body: child, generic_param_names: generic_param_names, shared_types: shared_types, emit_info: emit_info, source_indices: source_indices)) ) } @@ -8675,8 +8816,8 @@ fn v1_fn_signature_map_key_param_names(value_params: List, ret: Node, gene // lives there, the Node walk lives at the call site, and reversing that would close an // emit_rust -> trait_bound_witness -> emit_rust cycle. // -// This is the RECURSIVE BODY WALKER that BoundedToDirectTopLevelMatchBody and -// BoundedToDirectSingleCallLambdaBody both name as their next-rung trigger. Unlike those two it +// This is the RECURSIVE BODY WALKER that the retired BoundedToDirectTopLevelMatchBody and +// BoundedToDirectSingleCallLambdaBody scopes both named as their next-rung trigger. Unlike those two it // declares no depth boundary: the specimen that motivated it puts the earning comparison inside a // lambda, inside any(), inside a pipeline, inside the && of another lambda inside another any(). // @@ -10177,25 +10318,32 @@ fn emit_pattern(pattern: MatchPattern, path_prefix: List, shared_types: } } -fn pattern_parent_enum(name: String, parent_enum: String?, scrut_type: String, type_summaries: Map) -> String? { - let scrut_is_known_enum = scrut_type != "" && is_enum_type_name(type_name: scrut_type, type_summaries: type_summaries) +fn rust_ambiguous_type_name_refusal(leaf: String) -> String { + concat("compile_error!(\"ambiguous type name '", leaf, "': declarations disagree\")") +} + +fn pattern_parent_enum(name: String, parent_enum: String?, scrut_type: String, type_summaries: TypeSummaryIndex) -> String? { if parent_enum != none { parent_enum - } else if scrut_is_known_enum { - Present { value: scrut_type } - } else { + } else if scrut_type == "" { unique_variant_parent(type_summaries: type_summaries, variant_name: name) + } else { + match is_enum_type_name(type_name: scrut_type, type_summaries: type_summaries) { + QuestionDecided { value: true } => Present { value: scrut_type } + QuestionNameAmbiguous { leaf: leaf } => Present { value: rust_ambiguous_type_name_refusal(leaf: leaf) } + _ => unique_variant_parent(type_summaries: type_summaries, variant_name: name) + } } } -fn unique_variant_parent(type_summaries: Map, variant_name: String) -> String? { - let parent_matches = sorted_map_keys(type_summaries) - |> filter(type_name => - is_enum_type_name(type_name: type_name, type_summaries: type_summaries) && - variant_belongs_to_enum(type_summaries: type_summaries, variant_name: variant_name, enum_name: type_name) - ) +fn unique_variant_parent(type_summaries: TypeSummaryIndex, variant_name: String) -> String? { + let parent_matches = type_summary_values(index: type_summaries) + |> filter(summary => summary_is_enum_with_variant(summary: summary, variant_name: variant_name)) if parent_matches |> count == 1 { - parent_matches |> first + match parent_matches |> first { + Present { value: summary } => Present { value: summary.name } + Absent => none + } } else { none } @@ -10215,8 +10363,19 @@ fn is_optional_variant_name(name: String) -> Bool { name == "Present" || name == "Absent" || name == "Some" || name == "None" } +// A parent arrives as a bare name, or -- from variant_to_enum, whose values are owners' identities -- +// as `.`. An identity is decided by the declaration it names (the kernel optional's +// mint row and rust_host_option_carrier_declarations); only a bare name keeps the spelling rule. fn is_optional_like_parent_name(name: String) -> Bool { - name == kernel_optional_mint_name || is_host_diagnostics_carrier_alias(name: name) + if contains(name, ".") { + let segments = name |> split(delimiter: ".") + let module_path = segments |> take((segments |> count) - 1) |> join(separator: ".") + let decl_name = qualified_last_segment(name: name) + declaration_owns_host_option(module_path: module_path, decl_name: decl_name) + || declaration_ref_in_list(target: decl_ref(module_path: module_path, decl_name: decl_name), refs: rust_host_option_carrier_declarations) + } else { + name == kernel_optional_mint_name || is_host_diagnostics_carrier_alias(name: name) + } } fn is_some_like_variant_name(name: String) -> Bool { @@ -10565,7 +10724,7 @@ fn analyze_rc_pattern(pattern: MatchPattern, scrut_type: String, shared_types: S let matches_rc_variant = match resolved_parent { Present { value: enum_name } => set_contains(shared_types, enum_name) Absent => - match map_get(emit_info.type_summaries, bare_n) { + match type_summary_decided(index: emit_info.type_summaries, key: bare_n) { Present { value: _ } => set_contains(shared_types, bare_n) Absent => false } @@ -10931,7 +11090,7 @@ fn rust_as_ref_let_is_irrefutable(pattern: MatchPattern, emit_info: EmitGraphInf let resolved = pattern_parent_enum(name: qualified_last_segment(name: n), parent_enum: parent, scrut_type: "", type_summaries: emit_info.type_summaries) match resolved { Present { value: enum_name } => - match map_get(emit_info.type_summaries, enum_name) { + match type_summary_decided(index: emit_info.type_summaries, key: enum_name) { Present { value: summary } => (sorted_map_keys(summary.variant_name_set) |> count) <= 1 Absent => false } @@ -11096,7 +11255,7 @@ fn freemonoid_empty_from_variant_parent(leaf_name: String, enum_name: String) -> fn freemonoid_empty_from_emit_info(leaf_name: String, emit_info: EmitGraphInfo) -> Bool { leaf_name == "Empty" && match map_get(emit_info.variant_to_enum, "Empty") { - Present { value: p } => p == "FreeMonoid" + Present { value: p } => qualified_last_segment(name: p) == "FreeMonoid" Absent => false } } @@ -11111,7 +11270,10 @@ fn emit_freemonoid_empty_variant_body() -> String { fn effective_variant_parent_from_enum_lookup(leaf_name: String, rt: Node, binding_kind: VarBindingKind?, emit_info: EmitGraphInfo, source_indices: Map) -> String? { let rt_name = authored_name_at(source_indices: source_indices, node: rt) - if rt.ident_span != none && rt_name != leaf_name && variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: leaf_name, enum_name: rt_name) { + if rt.ident_span != none && rt_name != leaf_name && match variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: leaf_name, enum_name: rt_name) { + QuestionDecided { value: v } => v + _ => false + } { Present { value: rt_name } } else { variant_parent_from_binding_kind(binding_kind: binding_kind) @@ -11129,7 +11291,7 @@ fn effective_variant_parent_from_resolved(leaf_name: String, rt: Node, binding_k fn effective_variant_parent(name: String, binding_kind: VarBindingKind?, resolved_type: InferredNode?, emit_info: EmitGraphInfo, source_indices: Map) -> String? { let leaf_name = value_ref_qualified_leaf(name: name) let cached = match map_get(emit_info.variant_to_enum, leaf_name) { - Present { value: p } => if p != "" { Present { value: p } } else { none } + Present { value: p } => if p != "" { Present { value: qualified_last_segment(name: p) } } else { none } Absent => none } match cached { @@ -12055,11 +12217,15 @@ fn emit_rust_expr_record_lit(expr: Node, registry: Map, scope: let variant_name = match tn { Present { value: n } => n Absent => "" } let concrete_peel_would_erase_optional_ctor = is_optional_variant_name(name: variant_name) - && !variant_belongs_to_enum( + && match variant_belongs_to_enum( type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: authored_name_at(source_indices: si, node: expanded_rt) - ) + ) { + QuestionDecided { value: v } => v == false + QuestionNotDeclared => true + QuestionNameAmbiguous { leaf: _ } => false + } let peeled_type_name = if parent_enum == none && expanded_rt.connective == Conj && expanded_rt.ident_span != none && !concrete_peel_would_erase_optional_ctor { let concrete = authored_name_at(source_indices: si, node: expanded_rt) if concrete != "" && concrete != variant_name { Present { value: concrete } } else { tn } @@ -12273,13 +12439,16 @@ fn emit_cloned_arg(texpr: Node, registry: Map, scope: InferSco emit_typed_expr(texpr: texpr, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024) } -fn is_enum_type_name(type_name: String, type_summaries: Map) -> Bool { +fn is_enum_type_name(type_name: String, type_summaries: TypeSummaryIndex) -> TypeSummaryQuestion { is_enum_in_summaries(type_summaries: type_summaries, type_name: type_name) } fn contextual_variant_parent_from_type_name(variant_name: String, resolved_type: Node, emit_info: EmitGraphInfo, source_indices: Map) -> String? { let rt_name = authored_name_at(source_indices: source_indices, node: resolved_type) - if resolved_type.ident_span != none && rt_name != variant_name && variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: rt_name) { + if resolved_type.ident_span != none && rt_name != variant_name && match variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: rt_name) { + QuestionDecided { value: v } => v + _ => false + } { Present { value: rt_name } } else { none @@ -12297,7 +12466,10 @@ fn contextual_variant_parent_absent(variant_name: String, resolved_type: Node, e fn contextual_variant_parent(variant_name: String, parent_enum: String?, resolved_type: Node, emit_info: EmitGraphInfo, source_indices: Map) -> String? { match parent_enum { Present { value: explicit_parent } => - if variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: explicit_parent) { + if match variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: explicit_parent) { + QuestionDecided { value: v } => v + _ => false + } { Present { value: explicit_parent } } else { none } Absent => contextual_variant_parent_absent(variant_name: variant_name, resolved_type: resolved_type, emit_info: emit_info, source_indices: source_indices) @@ -12471,7 +12643,7 @@ fn rust_call_arg_fail_closed_unwrap(arg_str: String, arg: Node, callee: ItemInfo match function_value_params(params: info.params) |> skip(idx) |> first { Present { value: param } => let param_type = param_node_type_expr(n: param) - let param_required = param_type.return_cardinality != CardOptional && !is_host_optional_carrier_type(n: param_type, source_indices: source_indices) && !rust_param_type_is_type_variable(param_type: param_type, source_indices: source_indices) + let param_required = param_type.return_cardinality != CardOptional && !is_host_optional_carrier_type(n: param_type, source_indices: source_indices, env: empty_type_env()) && !rust_param_type_is_type_variable(param_type: param_type, source_indices: source_indices) let arg_optional = resolved_type(n: arg).return_cardinality == CardOptional && !rust_call_arg_is_function_value(arg: arg) if param_required && arg_optional { concat(arg_str, ".expect(\"fail-closed: an optional value flowed into non-optional parameter ", to_string(value: idx), " of ", func, " (empty Optional at runtime)\")") @@ -12629,6 +12801,52 @@ fn lambda_argument_scope(arg: Node, scope: InferScope) -> InferScope { // emit_func_def names the declaration it renders on the body scope (caller_decl_name) so that read // has a subject. +// ONE HOST-TEXT DISCRIMINATOR FOR BOTH SPELLINGS. AlgebraMethodSemantics remaps `.length` on a +// host-string receiver to `string_length`; the function form `v2.std.algebra` `length` over +// `FreeMonoid` is that operation spelled as a call. Kernel `String` inhabits the monoid, so +// `length(xs: s)` is legal source. Emitting it as the generic FreeMonoid body (`Rc>`) is +// E0308 against the host `String` at `request_field`. `v2.std.text.String` is the other carrier: +// the structural alias `FreeMonoid` (`Rc>` here). Rewriting THAT call (or +// `.length` on it) to `v1_rt::string_length` is E0308 the other way (self-host `v2_std_text.rs` +// `string_split_step`). Both arms ask `is_host_text_typed_expr` — leaf name `String` is not enough. +// THE CALLEE IS THE DECLARATION, NOT THE LEAF. A call spelled `length` that resolves to another +// module's function (or a local) must emit that function; rewriting by the authored name would +// replace a byte-count or a constant with a scalar count and answer wrongly in silence. +fn rust_call_target_is_v2_algebra_length(target: CallTargetIdentity) -> Bool { + match rust_call_target_declared_length(target: target) { + Absent => false + Present { value: id } => + id.decl_name == "length" && id.owner_module_path == "v2.std.algebra" + } +} + +fn rust_call_target_declared_length(target: CallTargetIdentity) -> DeclaredCallableIdentity? { + match target { + SourceDeclarationCall { owner_module_path: owner, decl_name: decl } => + Present { value: DeclaredCallableIdentity { owner_module_path: owner, decl_name: decl } } + RuntimePrimitiveCall { primitive_name: _, projected_from: projected } => projected + LocallyBoundCall { name: _ } => none + CallableTargetUndetermined => none + } +} + +fn rust_emit_length_on_host_string(call_target: CallTargetIdentity, func: String, args: List, call_semantics: CallSemantics?, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String? { + if rust_call_target_is_v2_algebra_length(target: call_target) == false { none } else { + let length_args = order_typed_call_args_from_semantics(args: args, func: func, call_semantics: call_semantics, scope: scope) + match length_args |> first { + Absent => none + Present { value: xs_arg } => + if is_host_text_typed_expr(e: arg_value(n: xs_arg), source_indices: scope.type_env.source_indices) { + Present { value: concat( + "v1_rt::string_length(&", + emit_typed_expr_base(texpr: arg_value(n: xs_arg), registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info), + ")" + ) } + } else { none } + } + } +} + fn emit_typed_call(func: String, args: List, inferred: InferredNode?, call_semantics: CallSemantics?, callee_is_function_value: Bool, call_target: CallTargetIdentity, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { if callee_is_function_value { return emit_typed_function_value_call(func: func, args: args, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info) @@ -12709,6 +12927,10 @@ fn emit_typed_call(func: String, args: List, inferred: InferredNode?, call } return disc_result } + match rust_emit_length_on_host_string(call_target: call_target, func: func, args: args, call_semantics: call_semantics, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info) { + Present { value: lowered } => return lowered + Absent => {} + } let ordered_args = order_typed_call_args_from_semantics(args: args, func: func, call_semantics: call_semantics, scope: scope) let application_plan = call_semantics_supplied_application_plan(cs: call_semantics) let callee = match call_target { @@ -13565,7 +13787,7 @@ fn emit_rust_first_method_call(receiver: Node, registry: Map, let skip_args = method_arg_nodes(texpr: receiver) let recv_str = emit_typed_expr(texpr: skip_recv, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024) let n_str = emit_typed_first_arg(args: skip_args, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info) - concat(recv_str, ".iter().cloned().skip(", n_str, " as usize).next()") + concat(recv_str, ".get((", n_str, ") as usize).cloned()") } else { let recv_str = emit_typed_expr(texpr: receiver, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024) concat(recv_str, ".first().cloned()") @@ -13695,7 +13917,7 @@ fn emit_typed_method_call(method_call_node: Node, receiver: Node, method: String let method_name_raw = authored_name_at(source_indices: scope.type_env.source_indices, node: method_def) let method_name = if method_name_raw == "length" { - if is_string_typed_expr(e: receiver, source_indices: scope.type_env.source_indices) { "string_length" } else { "count" } + if is_host_text_typed_expr(e: receiver, source_indices: scope.type_env.source_indices) { "string_length" } else { "count" } } else { method_name_raw } let algebra_field_call = rust_callable_field_method_call(method_name: method_name, receiver: receiver, args: args, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info) if algebra_field_call != none { @@ -13787,7 +14009,7 @@ fn freemonoid_match_arm_for(arms: List, variant: String) -> Node? { }) |> first } -fn arm_resolved_parent_enum(arm: Node, scrut_type: String, type_summaries: Map) -> String? { +fn arm_resolved_parent_enum(arm: Node, scrut_type: String, type_summaries: TypeSummaryIndex) -> String? { match arm_pattern(n: arm) { VariantPattern { name: n, parent_enum: pe, field_bindings: _, parent_identity: _ } => pattern_parent_enum(name: qualified_last_segment(name: n), parent_enum: pe, scrut_type: scrut_type, type_summaries: type_summaries) @@ -13814,7 +14036,7 @@ fn freemonoid_catchall_arm(arms: List) -> Node? { }) |> first } -fn arms_are_freemonoid_coproduct(arms: List, scrut_type: String, type_summaries: Map) -> Bool { +fn arms_are_freemonoid_coproduct(arms: List, scrut_type: String, type_summaries: TypeSummaryIndex) -> Bool { let has_empty = match freemonoid_match_arm_for(arms: arms, variant: "Empty") { Present { value: _ } => true Absent => false } let has_cons = match freemonoid_match_arm_for(arms: arms, variant: "Cons") { Present { value: _ } => true Absent => false } let empty_is_fm = match freemonoid_match_arm_for(arms: arms, variant: "Empty") { @@ -14925,7 +15147,10 @@ fn emit_field_value_with_context(field_value: Node, struct_node: Node, outer_typ } let corrected_parent = match expected_type { Present { value: et } => - if variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: et) { + if match variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_name, enum_name: et) { + QuestionDecided { value: v } => v + _ => false + } { Present { value: et } } else { pe } Absent => pe @@ -14969,10 +15194,10 @@ fn record_field_expected_type(struct_node: Node, outer_type_name: String?, field // struct per anonymous record literal. The count needs no order, so it reads the keys unsorted. The // literal's names are unique (02_parse refuses a repeated field init), so presence plus equal count // is exactly set equality, as before. -fn struct_candidates_by_field_names(field_names: List, type_summaries: Map) -> List { +fn struct_candidates_by_field_names(field_names: List, type_summaries: TypeSummaryIndex) -> List { let n_fields = field_names |> count if n_fields == 0 { return [] } - map_values(type_summaries) |> filter(summary => + type_summary_values(index: type_summaries) |> filter(summary => match summary.repr { StructRepr => field_names |> all(fn_name => map_contains_key(summary.field_type_map, fn_name)) @@ -15026,7 +15251,7 @@ fn effectful_self_recursion_diagnostics(tm: TypedModule, registry: Map, field_type_hints: Map, type_summaries: Map) -> List { +fn anonymous_record_struct_candidates(field_names: List, field_type_hints: Map, type_summaries: TypeSummaryIndex) -> List { let candidates = struct_candidates_by_field_names(field_names: field_names, type_summaries: type_summaries) if candidates |> count <= 1 { candidates } else { @@ -15046,7 +15271,7 @@ fn anonymous_record_struct_candidates(field_names: List, field_type_hint } } -fn ambiguous_anonymous_record_literal_diagnostics(n: Node, type_summaries: Map, source_indices: Map, module_name: String) -> List { +fn ambiguous_anonymous_record_literal_diagnostics(n: Node, type_summaries: TypeSummaryIndex, source_indices: Map, module_name: String) -> List { let current = match n.expr_data { ExprRecordLit { parent_enum: _ } => match record_lit_type_name_at(texpr: n, source_indices: source_indices) { @@ -15077,7 +15302,7 @@ fn ambiguous_anonymous_record_literal_diagnostics(n: Node, type_summaries: Map, field_type_hints: Map, type_summaries: Map) -> String? { +fn find_struct_name_by_fields(field_names: List, field_type_hints: Map, type_summaries: TypeSummaryIndex) -> String? { let candidates = anonymous_record_struct_candidates(field_names: field_names, field_type_hints: field_type_hints, type_summaries: type_summaries) if candidates |> count == 1 { match candidates |> first { @@ -15087,7 +15312,7 @@ fn find_struct_name_by_fields(field_names: List, field_type_hints: Map, type_summaries: Map) -> String? { +fn find_unique_struct_name_by_fields(field_names: List, type_summaries: TypeSummaryIndex) -> String? { let candidates = struct_candidates_by_field_names(field_names: field_names, type_summaries: type_summaries) if candidates |> count == 1 { match candidates |> first { @@ -15108,7 +15333,7 @@ fn emit_typed_record_lit(type_name: String?, fields: List, parent_enum: St Present { value: sn } => Present { value: sn } Absent => let rt_name = authored_name_at(source_indices: scope.type_env.source_indices, node: resolved_type) - if resolved_type.ident_span != none && map_contains_key(emit_info.type_summaries, rt_name) { + if resolved_type.ident_span != none && (type_summary_decided(index: emit_info.type_summaries, key: rt_name) != none) { Present { value: rt_name } } else { none } } @@ -15193,7 +15418,7 @@ fn emit_typed_record_lit(type_name: String?, fields: List, parent_enum: St if keyed_container_has_target_inhabitant(container_name: tn, target: Rust) { return "panic!(\"record-shaped carrier has no realization in the selected Rust target inhabitant\")" } - let tn_is_known_struct = map_contains_key(emit_info.type_summaries, tn) + let tn_is_known_struct = (type_summary_decided(index: emit_info.type_summaries, key: tn) != none) let ctor_name = if tn_is_known_struct { tn } else { let resolved_struct = resolve_node(n: resolved_type, env: scope.type_env, module_name: scope.module_name).resolved @@ -15214,7 +15439,10 @@ fn emit_typed_record_lit(type_name: String?, fields: List, parent_enum: St let rt_name = qualified_last_segment(name: authored_name_at(source_indices: si, node: resolved_type)) if resolved_type.ident_span != none && rt_name != variant_surface_name && !rt_is_type_var && rt_name != "Error" - && variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_surface_name, enum_name: rt_name) { + && match variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: variant_surface_name, enum_name: rt_name) { + QuestionDecided { value: v } => v + _ => false + } { Present { value: rt_name } } else { bare_parent_enum @@ -15774,6 +16002,16 @@ fn is_string_typed_expr(e: Node, source_indices: Map) -> B } } +fn is_host_text_typed_expr(e: Node, source_indices: Map) -> Bool { + match e.inferred { + Present { value: Resolved { node: rt } } => + let is_optional = rt.return_cardinality == CardOptional + let inner = if is_optional { with_required_cardinality(n: rt) } else { rt } + is_host_text_carrier_type(n: inner, source_indices: source_indices) + _ => false + } +} + fn emit_typed_string_interp(parts: List, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { let has_interpolations = parts |> any(p => match p { Interpolation { expr: _ } => true _ => false }) let fmt_parts = parts |> map(p => typed_interp_format_part(part: p, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, has_interpolations: has_interpolations)) @@ -16341,7 +16579,7 @@ fn emit_dry_run_branch_from_props(op_name: String, inferred: Node, mock_props: L fn emit_transport_call(bound: BoundOperation, op_name: String, registry: Map, depth: Int, service_item: Node, op_node: Node, source_indices: Map, shared_types: Set, env: TypeEnv) -> String { match bound { RestBound { transport: t } => emit_rest_call(op_name: op_name, transport: t, registry: registry, depth: depth, service_item: service_item, op_node: op_node, source_indices: source_indices, shared_types: shared_types, env: env) - ShellBound { transport: t, result_fields: rsf } => emit_shell_call(op_name: op_name, transport: t, registry: registry, depth: depth, result_fields: rsf, op_node: op_node, source_indices: source_indices) + ShellBound { transport: t, result_fields: rsf } => emit_shell_call(op_name: op_name, transport: t, registry: registry, depth: depth, result_fields: rsf, op_node: op_node, source_indices: source_indices, env: env) FileBound { verb: v, path_template: pt, result_fields: fs } => emit_file_call(verb: v, path_template: pt, result_fields: fs, source_indices: source_indices) LocalBound => emit_local_call(op_name: op_name) @@ -16871,7 +17109,7 @@ fn emit_exit_code_handling(op_node: Node, result_fields: List, let has_nonzero = exit_props |> any(p => field_init_node_name_at(n: p, source_indices: source_indices) == "exit_nonzero") let exit_arms = exit_props |> map(p => emit_exit_arm(prop: p, result_fields: result_fields, source_indices: source_indices)) let default_arm = if has_nonzero { "" } - else { concat("\n _ => { ", shell_stderr_binding_line, " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } + else { concat("\n _ => { ", shell_error_stderr_binding(result_fields: result_fields), " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } concat( "let exit_code = output.status.code().unwrap_or(-1);\n", "match exit_code {\n", @@ -16890,7 +17128,7 @@ fn emit_exit_arm(prop: Node, result_fields: List, source_indic if is_success { concat(" ", pattern, " => { ", emit_shell_return(result_fields: result_fields), " },") } else { - concat(" ", pattern, " => { ", shell_stderr_binding_line, " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") + concat(" ", pattern, " => { ", shell_error_stderr_binding(result_fields: result_fields), " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } } @@ -16900,7 +17138,10 @@ fn emit_exit_arm(prop: Node, result_fields: List, source_indic // replace one loud stop with another whose red this route cannot adjudicate (an emitted panic // COMPILES, so the fixture pairs beside this change would report it green). Only the LATER elements // admit both shapes; see shell_argv_element_is_word_list. -fn emit_shell_call(op_name: String, transport: Node, registry: Map, depth: Int, result_fields: List, op_node: Node, source_indices: Map) -> String { +// Capture order: bind/validate the authored policy BEFORE spawn; start both drains +// immediately; write stdin concurrently; join; then project BoundedTail or refuse +// Complete overflow. +fn emit_shell_call(op_name: String, transport: Node, registry: Map, depth: Int, result_fields: List, op_node: Node, source_indices: Map, env: TypeEnv) -> String { let argv = transport.children let optional_params = op_node.params |> filter(p => param_node_type_expr(n: p).return_cardinality == CardOptional) @@ -16912,7 +17153,8 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map true Absent => false } - let let_kw = if has_stdin { "let mut output" } else { "let output" } + let needs_capture = shell_needs_capture_accounting(result_fields: result_fields) + let let_kw = if has_stdin || needs_capture { "let mut output" } else { "let output" } let cmd_line = if argv |> count > 0 { match argv |> first { Present { value: first_arg } => concat(let_kw, " = std::process::Command::new(", @@ -16938,7 +17180,24 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map emit_ident(name: expr_var_name_at(texpr: stdin_expr, source_indices: source_indices), target: Rust) + _ => emit_simple_expr(expr: stdin_expr, target: Rust, source_indices: source_indices) + } + concat("let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(", stdin_var, ".as_bytes())?;\n }\n Ok(())\n});\n") + } else { + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n" + } + let capture_lines = concat(shell_capture_drain_start, stdin_thread, shell_capture_join_project) + concat([policy_bind, cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, capture_lines, return_line]) |> join(separator: "\n") + } else if has_stdin { let stdin_expr = transport_stdin(t: transport, source_indices: source_indices).value let stdin_var = match stdin_expr.expr_data { ExprVar { binding_kind: _ } => emit_ident(name: expr_var_name_at(texpr: stdin_expr, source_indices: source_indices), target: Rust) @@ -16949,15 +17208,11 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map join(separator: "\n") + concat([cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, write_block, wait_line, check_line, return_line]) |> join(separator: "\n") } else { let output_line = " .output()?;" let check_line = "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" - let return_line = emit_exit_code_handling(op_node: op_node, result_fields: result_fields, source_indices: source_indices) - let all_lines = concat([cmd_line], arg_lines, env_lines, [wd_line, output_line, check_line, return_line]) - all_lines |> join(separator: "\n") + concat([cmd_line], arg_lines, env_lines, [wd_line, output_line, check_line, return_line]) |> join(separator: "\n") } } @@ -17065,6 +17320,48 @@ fn emit_shell_argv_element(arg: Node, optional_params: Map, source // error. data shell_stderr_binding_line: String = "let stderr = String::from_utf8_lossy(&output.stderr).to_string();" +fn shell_needs_capture_accounting(result_fields: List) -> Bool { + result_fields |> any(f => shell_channel_is_capture_accounting(c: f.channel)) +} + +fn shell_error_stderr_binding(result_fields: List) -> String { + if shell_needs_capture_accounting(result_fields: result_fields) { "" } else { concat(shell_stderr_binding_line, " ") } +} + +fn emit_shell_stderr_policy_binding(op_node: Node, env: TypeEnv) -> String { + if operation_declares_required_stderr_capture_policy(op_node: op_node, env: env) { + concat( + "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", + " match v1_rt::read_host_budget_bytes() {\n", + " (Some(n), source) => (Some(n as usize), source),\n", + " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n", + " }\n", + " }\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n", + " let n = crate::std_measure::byte_size_count(bytes.clone());\n", + " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n", + " (None, String::new())\n", + " }\n", + "};\n", + "let __stderr_tail_bytes: usize = match &*stderr_capture {\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n", + "};\n" + ) + } else { + concat( + "return Err(\"", + shell_emission_refusal_fact(refusal: ShellCapturePolicyInputAbsent { key: "stderr_truncated" }), + "\".into());\n" + ) + } +} + +data shell_capture_drain_start: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n" + +data shell_capture_join_project: String = "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" + // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. // @@ -17103,11 +17400,14 @@ fn shell_projection_value(result_fields: List) -> String { // `stderr` is captured only when a declared field asks for it, so an operation // that does not is not handed an unused binding. fn shell_stderr_prelude(result_fields: List) -> String { - let needs_stderr = result_fields |> any(f => match f.channel { - ShellChanStderr => true - _ => false - }) - if needs_stderr { concat(shell_stderr_binding_line, "\n") } else { "" } + if shell_needs_capture_accounting(result_fields: result_fields) { "" } + else { + let needs_stderr = result_fields |> any(f => match f.channel { + ShellChanStderr => true + _ => false + }) + if needs_stderr { concat(shell_stderr_binding_line, "\n") } else { "" } + } } // TOTAL BY CONSTRUCTION over the channels the binding resolves. The `stdout` @@ -17125,9 +17425,9 @@ fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: Bool) -> St ShellChanExitCode => "(output.status.code().unwrap_or(-1) as i64)" ShellChanStdoutLines => rust_shared_wrap_ctor(inner_expr: "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()") - ShellChanStderrTruncated => emit_unrealizable_shell_channel(channel: channel) - ShellChanStderrTotalBytes => emit_unrealizable_shell_channel(channel: channel) - ShellChanStderrRetainedBytes => emit_unrealizable_shell_channel(channel: channel) + ShellChanStderrTruncated => "stderr_truncated" + ShellChanStderrTotalBytes => "stderr_total_bytes" + ShellChanStderrRetainedBytes => "stderr_retained_bytes" } if is_optional { concat("Some(", base, ")") } else { base } } @@ -17432,6 +17732,13 @@ fn data_def_annotation_is_named_refinement(annotation: Node, source_indices: Map // declared type decided by the carrier and its wrap decided by the spelling, and needs_rc is // threaded into emit_data_def_body, so the value took the same answer as the signature. Routed to // the carrier authority; the spelling key is deleted rather than kept beside it. +// A NAMED REFINEMENT ROW KEEPS ITS DECLARED `?`. A where-refinement such as std.types NonEmptyStr +// resolves to a Conj carrying its base as one child, so it fails preserves_declared_brand's +// childless test and is rendered by the named-refinement arm -- the one arm that rendered through +// coerce_primitive_type without rust_carrier_optional_wrap. gunbc.fleet_tailnet +// fleet_tailnet_domain (`NonEmptyStr? = Present { .. }`) was emitted `-> NonEmptyStr` and its +// consumer refused E0308. The control is gunbc.instruments.native_emission_controls +// optional_data_row_cases. fn emit_data_def(name: String, type_node: Node, value: Node, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { let ambiguous_annotation_head = v1_type_exprs_ambiguous_declared_head(type_exprs: [type_node], type_decl_items: emit_info.type_decl_items, source_indices: scope.type_env.source_indices) if ambiguous_annotation_head != "" { @@ -17453,7 +17760,7 @@ fn emit_data_def(name: String, type_node: Node, value: Node, registry: Map Bool { + type_node.return_cardinality == CardOptional +} + fn emit_data_def_body(type_node: Node, value: Node, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo, needs_rc: Bool) -> String { let raw_ty_str = render_rust_type(n: type_node, shared_types: shared_types, source_indices: scope.type_env.source_indices, emit_info: emit_info) let type_name = authored_name_at(source_indices: scope.type_env.source_indices, node: type_node) @@ -17579,7 +17891,12 @@ fn emit_data_def_body(type_node: Node, value: Node, registry: Map true _ => false } { concat(" ", emit_typed_expr(texpr: value, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info, fuel: 1024)) } else if has_nested_records_node(n: type_node, source_indices: scope.type_env.source_indices) && data_value_is_json_literal_tree(value: value) && !data_row_type_forbids_deserialize(type_node: type_node, emit_info: emit_info, source_indices: scope.type_env.source_indices) { - match emit_data_value_json(value: value, source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) { + let row_json = if data_row_declares_optional(type_node: type_node) { + emit_declared_optional_row_json(value: value, inner_type_name: authored_name_at(source_indices: scope.type_env.source_indices, node: type_node), source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) + } else { + emit_data_value_json(value: value, source_indices: scope.type_env.source_indices, variant_wire: emit_info.data_variant_wire_spellings) + } + match row_json { Refused { reason: r } => concat(" compile_error!(\"", escape_string_literal_body(s: r), "\")") Emitted { json: json_str } => concat(" serde_json::from_str(\"", escape_string_literal_body(s: json_str), "\")\n", " .expect(\"valid data definition\")") @@ -18179,7 +18496,152 @@ fn emit_gunbc_cli_dispatch_generated(crate_name: String) -> TextFile { // a library: its main is the typed entry-point-absent refusal. The derived "workflow" CLI that used // to answer below the pipeline arms -- one clap subcommand per function carrying `uses` or reaching // a service -- is deleted: it renamed the effect axis an entry role, and no built closure reached it. -fn emit_main_rs(modules: List, has_services: Bool, crate_name: String, registry: Map) -> TextFile { +// ── THE NATIVE ENTRY'S EFFECT HANDLES, FROM ONE VERDICT ──────────────────────────────────── +// v1.compiler.emit NativeEffectRealizationAdmission is computed here once per native entry, in the +// order emit_func_params renders the entry function's effect parameters (its `uses` resources, then +// the services it reaches), and BOTH consumers read that one list: the emission refusal below, and +// the rendered main, which binds exactly the bound handlers. No main writes a handle by hand. +type NativeEntryEffects { + entry_module: String + entry_function: String + admissions: List +} + +fn native_entry_function_name(modules: List) -> String? { + if compiler_pipeline_entry_is_native_serve(modules: modules) { Present { value: native_serve_entry_function } } + else if compiler_pipeline_entry_is_native_cli(modules: modules) { Present { value: native_cli_entry_function } } + else if compiler_pipeline_entry_is_native_claim(modules: modules) { Present { value: native_claim_entry_function } } + else { none } +} + +data native_serve_entry_function: String = "native_serve_handle" +data native_cli_entry_function: String = "v2_cli_main" +data native_claim_entry_function: String = "native_claim_report" + +fn native_effect_decl_sites(modules: List, kind: ParsedModuleItemKind, name: String) -> List { + modules |> flat_map(tm => { + let si = tm.type_env.source_indices + let module_name = authored_name_at(source_indices: si, node: tm.module) + tm.items + |> filter(item => item.module_item_kind == kind && authored_name_at(source_indices: si, node: item) == name) + |> map(item => NativeEffectDeclSite { item: item, module_name: module_name }) + }) +} + +fn native_entry_denominator(modules: List) -> NativeEffectDenominator { + if compiler_pipeline_entry_is_native_serve(modules: modules) { native_service_route_denominator } + else if compiler_pipeline_entry_is_native_claim(modules: modules) { native_claim_route_denominator } + else { native_compiler_route_denominator } +} + +fn native_entry_effects(modules: List, registry: Map) -> NativeEntryEffects? { + match native_entry_function_name(modules: modules) { + Absent => none + Present { value: entry_fn } => { + let entry_module = compiler_pipeline_entry_module_name(modules: modules) + let denominator = native_entry_denominator(modules: modules) + let entry_si = modules + |> filter(tm => authored_name_at(source_indices: tm.type_env.source_indices, node: tm.module) == entry_module) + |> map(tm => tm.type_env.source_indices) + |> first + let si = match entry_si { Present { value: x } => x Absent => empty_map() } + let info = lookup_item_by_identity(registry: registry, id: DeclaredCallableIdentity { owner_module_path: entry_module, decl_name: entry_fn }) + let resources = match info { Present { value: i } => i.resource_requirements Absent => [] } + let services = match info { Present { value: i } => i.service_names Absent => [] } + let resource_admissions = resources |> map(r => { + let rname = type_leaf_name_for_collapse(n: r.resource, source_indices: si) + admit_native_resource_effect(binding: r.binding_name, resource: rname, + resolution: native_effect_resolution(sites: native_effect_decl_sites(modules: modules, kind: ModuleItemResource, name: rname)), + denominator: denominator) + }) + let service_admissions = services |> map(sn => + admit_native_service_effect(service: sn, + resolution: native_effect_resolution(sites: native_effect_decl_sites(modules: modules, kind: ModuleItemService, name: sn)), + denominator: denominator, source_indices: si) + ) + Present { value: NativeEntryEffects { + entry_module: entry_module, + entry_function: entry_fn, + admissions: concat(resource_admissions, service_admissions) + } } + } + } +} + +fn native_entry_effect_diagnostics(effects: NativeEntryEffects?, modules: List) -> List { + match effects { + Absent => [] + Present { value: e } => { + let refusals = e.admissions |> flat_map(a => match native_effect_admission_refusal(a: a) { + Present { value: r } => [r] + Absent => [] + }) + if (refusals |> count) == 0 { [] } else { + let span = modules + |> filter(tm => authored_name_at(source_indices: tm.type_env.source_indices, node: tm.module) == e.entry_module) + |> map(tm => tm.module.span) + |> first + match span { + Present { value: sp } => [make_error_node( + diagnostic: NativeEffectRealizationRefused { entry: concat(e.entry_module, concat(".", e.entry_function)), refusals: refusals, span: sp }, + module_name: e.entry_module)] + Absent => [make_error_node( + diagnostic: NativeEffectRealizationRefused { entry: concat(e.entry_module, concat(".", e.entry_function)), refusals: concat(refusals, ["the entry module is not in the closure"]), span: no_span() }, + module_name: e.entry_module)] + } + } + } + } +} + +// The rendered binding of one admitted handler: the `let` that constructs it and the argument that +// lends it to the entry function. Only a bound admission renders; the refusal above has already +// stopped emission for any other arm, so nothing here guesses. +type NativeEffectBindingText { + var: String + type_path: String + binding_line: String + argument: String +} + +fn native_effect_binding_of(var: String, type_path: String, constructor: String) -> NativeEffectBindingText { + NativeEffectBindingText { + var: var, + type_path: type_path, + binding_line: concat(" let ", var, " = ", constructor, ";\n"), + argument: concat("&", var) + } +} + +fn native_effect_binding_text(a: NativeEffectRealizationAdmission, crate_name: String) -> NativeEffectBindingText? { + match a { + NativeEffectHandlerBound { effect_contract: c, handler_identity: _ } => match c { + NativeServiceEffect { declaration: d } => { + let sn = d.decl_name as String + let path = concat(crate_name, "::", module_to_filename(name: d.module_path as String), "::", sanitize_service_name(name: sn)) + Present { value: native_effect_binding_of(var: service_var_name(service_name: sn), type_path: path, constructor: concat(path, "::new(Default::default())")) } + } + NativeResourceEffect { binding: b, declaration: d } => { + let path = concat(crate_name, "::", module_to_filename(name: d.module_path as String), "::", d.decl_name as String) + Present { value: native_effect_binding_of(var: emit_ident(name: b, target: Rust), type_path: path, constructor: path) } + } + } + _ => none + } +} + +fn native_effect_bindings(effects: NativeEntryEffects?, crate_name: String) -> List { + match effects { + Absent => [] + Present { value: e } => e.admissions |> flat_map(a => + match native_effect_binding_text(a: a, crate_name: crate_name) { + Present { value: t } => [t] + Absent => [] + }) + } +} + +fn emit_main_rs(modules: List, has_services: Bool, crate_name: String, effects: NativeEntryEffects?) -> TextFile { let retained_host_pipeline = compiler_pipeline_entry_is_retained_host(modules: modules) let pipeline_module = compiler_pipeline_entry_module_name(modules: modules) if compiler_pipeline_entry_is_ambiguous(modules: modules) { @@ -18198,17 +18660,13 @@ fn emit_main_rs(modules: List, has_services: Bool, crate_name: Stri return emit_source_root_eval_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) } if compiler_pipeline_entry_is_native_cli(modules: modules) { - return emit_native_cli_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) + return emit_native_cli_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module, bindings: native_effect_bindings(effects: effects, crate_name: crate_name)) } if compiler_pipeline_entry_is_native_claim(modules: modules) { - return emit_native_claim_driver_main_rs( - crate_name: crate_name, - pipeline_module: pipeline_module, - demand: native_claim_report_effect_demand(registry: registry, pipeline_module: pipeline_module) - ) + return emit_native_claim_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module, bindings: native_effect_bindings(effects: effects, crate_name: crate_name)) } if compiler_pipeline_entry_is_native_serve(modules: modules) { - return emit_native_serve_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) + return emit_native_serve_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module, bindings: native_effect_bindings(effects: effects, crate_name: crate_name)) } if compiler_pipeline_entry_is_direct_ingest(modules: modules) { return emit_direct_ingest_driver_main_rs(crate_name: crate_name, pipeline_module: pipeline_module) @@ -18335,8 +18793,12 @@ fn emit_host_source_root_read_rs(crate_name: String) -> String { " let observed = runtime.block_on(", crate_name, "::gunbc_source_root_read::source_root_read(Rc::new(source_roots.iter().cloned().collect()), &filesystem));\n", " match observed {\n", " Ok(files) => match &*files {\n", - " ", crate_name, "::gunbc_source_root_read::SourceRootFiles::SourceRootFilesRead { files } =>\n", - " ", crate_name, "::v2_compiler_source_authority::source_root_ingest_from_files(files.clone()).iter().cloned().collect(),\n", + " ", crate_name, "::gunbc_source_root_read::SourceRootFiles::SourceRootFilesRead { files } => match &*", crate_name, "::v2_compiler_source_authority::source_root_ingest_from_files(files.clone()) {\n", + " ", crate_name, "::v2_compiler_source_authority::SourceRootIngestFromFiles::SourceRootIngestFromFilesBuilt { ingest } => ingest.iter().cloned().collect(),\n", + " ", crate_name, "::v2_compiler_source_authority::SourceRootIngestFromFiles::SourceRootIngestFromFilesUnrecognizedRoot { argument, path } => {\n", + " eprintln!(\"REFUSED: source root is not a dag or src/v2 tree: {} (file {})\", argument, path); std::process::exit(2);\n", + " }\n", + " },\n", " ", crate_name, "::gunbc_source_root_read::SourceRootFiles::SourceRootFilesRefused { path, kind, cause } => {\n", " eprintln!(\"REFUSED: could not read source root at {} [{}]: {}\", path, kind, cause); std::process::exit(2);\n", " }\n", @@ -18353,37 +18815,18 @@ fn emit_host_source_root_read_rs(crate_name: String) -> String { // gunbc test reads (0 held, 1 not held, 2 no observation). No code is taken from the program, so no // program can reach a fourth status or a failure spelled as 0. The reason goes to stderr so the case // rows on stdout stay the whole observation a reader joins by identity. -// WHAT THE REPORT DEMANDS OF ITS HOST, read from the one effect authority (item_is_effectful_callee -// over the registry row the report's declaration produces), never from the entry module's imports. -// The declared resource set of a NativeClaimDriver is {Filesystem}: a pure report gets the sync main -// below unchanged, a report whose only demand is the Filesystem service gets that service bound to -// the existing filesystem_io handler -- the binding NativeCliDriver's main already performs -- and -// anything else is a typed refusal in place of a main, because binding an effect this driver does -// not declare would be a transport no row admitted. -type NativeClaimEffectDemand - = NativeClaimPure - | NativeClaimFilesystem - | NativeClaimUndeclaredEffect { detail: String } - -fn native_claim_report_effect_demand(registry: Map, pipeline_module: String) -> NativeClaimEffectDemand { - match lookup_item_by_identity(registry: registry, id: DeclaredCallableIdentity { owner_module_path: pipeline_module, decl_name: "native_claim_report" }) { - Present { value: info } => - if !item_is_effectful_callee(info: info) { NativeClaimPure } - else if (item_resource_names(info: info) |> count) == 0 && (info.service_names |> count) == 1 && (info.service_names |> all(sn => service_var_name(service_name: sn) == "filesystem")) { NativeClaimFilesystem } - else { NativeClaimUndeclaredEffect { detail: concat("services [", concat(info.service_names |> join(separator: ", "), concat("] resources [", concat(item_resource_names(info: info) |> join(separator: ", "), "]")))) } } - Absent => NativeClaimUndeclaredEffect { detail: "native_claim_report has no registry row" } - } -} - -fn emit_native_claim_driver_main_rs(crate_name: String, pipeline_module: String, demand: NativeClaimEffectDemand) -> TextFile { +// WHAT THE REPORT DEMANDS OF ITS HOST is v1.compiler.emit NativeEffectRealizationAdmission, the one +// verdict every native main reads (native_entry_effects, under native_claim_route_denominator: the +// declared resource set {Filesystem}). A pure report gets the sync main below unchanged; a report that +// demands effects gets the handlers the verdict admitted -- today only the existing filesystem_io +// handler, the binding NativeCliDriver's main performs -- and any other demand has already refused +// emission with NativeEffectRealizationAdmission's typed diagnostic, so no main is rendered for it. +fn emit_native_claim_driver_main_rs(crate_name: String, pipeline_module: String, bindings: List) -> TextFile { let pipeline_mod = module_to_filename(name: pipeline_module) - match demand { - NativeClaimPure => emit_native_claim_driver_pure_main_rs(crate_name: crate_name, pipeline_mod: pipeline_mod) - NativeClaimFilesystem => emit_native_claim_driver_filesystem_main_rs(crate_name: crate_name, pipeline_mod: pipeline_mod) - NativeClaimUndeclaredEffect { detail } => TextFile { - path: concat(rust_source_root(), "main", rust_source_ext()), - content: concat("// Generated by v1 compiler -- do not edit.\n\ncompile_error!(\"REFUSED: a NativeClaimDriver report may demand only the Filesystem service; ", concat(escape_string_literal_body(s: detail), "\");\n")) - } + if (bindings |> count) == 0 { + emit_native_claim_driver_pure_main_rs(crate_name: crate_name, pipeline_mod: pipeline_mod) + } else { + emit_native_claim_driver_effect_main_rs(crate_name: crate_name, pipeline_mod: pipeline_mod, bindings: bindings) } } @@ -18404,9 +18847,11 @@ fn native_claim_terminal_exit_lines() -> String { " \}", "\n") } -// A transport error out of the bound handler is no observation (2), never a held or not-held case: +// A transport error out of a bound handler is no observation (2), never a held or not-held case: // the report did not finish deciding. -fn emit_native_claim_driver_filesystem_main_rs(crate_name: String, pipeline_mod: String) -> TextFile { +fn emit_native_claim_driver_effect_main_rs(crate_name: String, pipeline_mod: String, bindings: List) -> TextFile { + let binding_lines = bindings |> map(b => b.binding_line) |> join(separator: "") + let effect_args = bindings |> map(b => b.argument) |> join(separator: ", ") TextFile { path: concat(rust_source_root(), "main", rust_source_ext()), content: concat( @@ -18419,10 +18864,10 @@ fn emit_native_claim_driver_filesystem_main_rs(crate_name: String, pipeline_mod: "\n", "#[tokio::main(flavor = \"current_thread\")]", "\n", "async fn main() \{", "\n", - " let filesystem = ", crate_name, "::extdeps_filesystem_filesystem_io::Filesystem::new(Default::default());", "\n", - " let report = match native_claim_report(&filesystem).await \{", "\n", + binding_lines, + " let report = match native_claim_report(", effect_args, ").await \{", "\n", " Ok(report) => report,", "\n", - " Err(cause) => \{ eprintln!(\"NO OBSERVATION: native filesystem transport: \{\}\", cause); std::process::exit(2); \}", "\n", + " Err(cause) => \{ eprintln!(\"NO OBSERVATION: native effect transport: \{\}\", cause); std::process::exit(2); \}", "\n", " \};", "\n", native_claim_terminal_exit_lines(), "\}", "\n") @@ -18471,8 +18916,41 @@ fn emit_native_claim_driver_pure_main_rs(crate_name: String, pipeline_mod: Strin // also runs during unwinding, so an abandoned worker uncounts itself exactly once however it ends. Every value -- deadline, // ceiling, both bodies, the exit code -- is gunbc.native_serve's; the worker sends back only owned // plain parts because the handler's values are reference-counted and stay on its thread. -fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) -> TextFile { +// +// THE EFFECT WORKER, BY GENERATION. An entry with effects is an async function returning Result +// (emit_func_def); one without is a pure function returning the response. The admitted bindings decide +// which, so the call shape and the handles come from the same verdict. Every handler call runs on ONE +// worker generation: a thread that realizes the current-thread executor and every admitted handle +// once, reports ready or not before the service announces, and is reused after each answered call. A +// generation that misses the deadline (counted stuck, exactly as above) or panics is abandoned -- it +// keeps only its own executor and handles -- and a replacement is realized. A generation that cannot +// be realized is a process-level refusal, never native_serve_effect_failed (which is a handler's +// transport error): at startup the main refuses before announcing, so /livez is never answered by a +// process that cannot serve; a failed replacement ends the process with +// native_serve_worker_replacement_exit_code once the request in hand is answered. +fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String, bindings: List) -> TextFile { let pipeline_mod = module_to_filename(name: pipeline_module) + let effectful = (bindings |> count) > 0 + let binding_lines = bindings |> map(b => b.binding_line) |> join(separator: "") + let handler_effect_args = bindings |> map(b => concat(", ", b.argument)) |> join(separator: "") + let effect_failed_import = if effectful { " native_serve_effect_failed," } else { "" } + let worker_realize = if effectful { + concat( + " let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() \{", "\n", + " Ok(runtime) => runtime,", "\n", + " Err(cause) => \{ let _ = ready.send(Err(format!(\"native effect executor: \{\}\", cause))); return; \}", "\n", + " \};", "\n", + binding_lines) + } else { "" } + let worker_call = if effectful { + concat( + " let response = match runtime.block_on(native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision", handler_effect_args, ")) \{", "\n", + " Ok(response) => response,", "\n", + " Err(cause) => native_serve_effect_failed(cause.to_string()),", "\n", + " \};", "\n") + } else { + " let response = native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision);\n" + } TextFile { path: concat(rust_source_root(), "main", rust_source_ext()), content: concat( @@ -18486,8 +18964,9 @@ fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) "use ", crate_name, "::extdeps_http_server::ServeWireResponse;", "\n", "use ", crate_name, "::std_measure::\{byte_size_count, millisecond_count\};", "\n", "use ", crate_name, "::gunbc_native_serve::\{", "\n", - " native_serve_announcement, native_serve_frame, native_serve_handler_name, native_serve_read_refused,", "\n", + " native_serve_announcement,", effect_failed_import, " native_serve_frame, native_serve_handler_name, native_serve_read_refused,", "\n", " native_serve_admission, native_serve_deadline_refused, native_serve_handler_failed, native_serve_outgoing, native_serve_route,", "\n", + " native_serve_worker_replacement_exit_code, native_serve_worker_unrealized,", "\n", " native_serve_start, NativeServeAdmission, NativeServeContract, NativeServeFrame, NativeServeOutgoing,", "\n", " NativeServeRoute, NativeServeStart,", "\n", "\};", "\n", @@ -18564,6 +19043,39 @@ fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) " release_revision: String,", "\n", "\}", "\n", "\n", + "// One worker generation: the thread that owns the effect executor and every admitted handle and runs", "\n", + "// handler calls one at a time. Its state is the WorkerGuard protocol above; an abandoned generation", "\n", + "// finishes the call in hand (if it ever does) and ends, uncounting itself.", "\n", + "struct Generation \{", "\n", + " calls: std::sync::mpsc::Sender<(HandlerCall, std::sync::mpsc::Sender)>,", "\n", + " state: std::sync::Arc>,", "\n", + "\}", "\n", + "", "\n", + "fn realize_generation(stuck: &std::sync::Arc>, ready_within: std::time::Duration) -> Result \{", "\n", + " let (calls, inbox) = std::sync::mpsc::channel::<(HandlerCall, std::sync::mpsc::Sender)>();", "\n", + " let (ready, readiness) = std::sync::mpsc::channel::>();", "\n", + " let state = std::sync::Arc::new(std::sync::Mutex::new(WORKER_WAITING));", "\n", + " let guard = WorkerGuard \{ state: state.clone(), stuck: stuck.clone() \};", "\n", + " let worker_state = state.clone();", "\n", + " std::thread::spawn(move || \{", "\n", + " let _guard = guard;", "\n", + worker_realize, + " let _ = ready.send(Ok(()));", "\n", + " while let Ok((call, reply)) = inbox.recv() \{", "\n", + worker_call, + " let _ = reply.send(wire_parts(&native_serve_outgoing(response)));", "\n", + " if *lock_state(&worker_state) == WORKER_ABANDONED \{", "\n", + " return;", "\n", + " \}", "\n", + " \}", "\n", + " \});", "\n", + " match readiness.recv_timeout(ready_within) \{", "\n", + " Ok(Ok(())) => Ok(Generation \{ calls, state \}),", "\n", + " Ok(Err(cause)) => Err(cause),", "\n", + " Err(_) => Err(\"the worker ended or did not report ready within the request deadline\".to_string()),", "\n", + " \}", "\n", + "\}", "\n", + "", "\n", "fn read_line(reader: &mut impl BufRead, budget: &mut usize) -> Result, String> \{", "\n", " let mut bytes = Vec::new();", "\n", " match reader.read_until(b'\\n', &mut bytes) \{", "\n", @@ -18647,10 +19159,17 @@ fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) " \};", "\n", " let bound_host = bound.ip().to_string();", "\n", " let bound_port = bound.port() as i64;", "\n", - " eprintln!(\"\{\}\", native_serve_announcement(contract.clone(), bound_host.clone(), bound_port));", "\n", - " let timeout = std::time::Duration::from_millis(millisecond_count(contract.read_timeout.clone()) as u64);", "\n", " let deadline = std::time::Duration::from_millis(millisecond_count(contract.request_deadline.clone()) as u64);", "\n", " let stuck = std::sync::Arc::new(std::sync::Mutex::new(0usize));", "\n", + " let mut generation = match realize_generation(&stuck, deadline) \{", "\n", + " Ok(generation) => generation,", "\n", + " Err(cause) => \{", "\n", + " eprintln!(\"REFUSED: \{\}\", native_serve_worker_unrealized(cause));", "\n", + " std::process::exit(2);", "\n", + " \}", "\n", + " \};", "\n", + " eprintln!(\"\{\}\", native_serve_announcement(contract.clone(), bound_host.clone(), bound_port));", "\n", + " let timeout = std::time::Duration::from_millis(millisecond_count(contract.read_timeout.clone()) as u64);", "\n", " for accepted in listener.incoming() \{", "\n", " let mut stream = match accepted \{", "\n", " Ok(stream) => stream,", "\n", @@ -18671,29 +19190,41 @@ fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) " std::process::exit(*exit_code as i32);", "\n", " \}", "\n", " NativeServeAdmission::NativeServeAdmitWorker => \{", "\n", - " let (sender, receiver) = std::sync::mpsc::channel::();", "\n", - " let state = std::sync::Arc::new(std::sync::Mutex::new(WORKER_WAITING));", "\n", - " let guard = WorkerGuard \{ state: state.clone(), stuck: stuck.clone() \};", "\n", - " std::thread::spawn(move || \{", "\n", - " let _guard = guard;", "\n", - " let response = native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision);", "\n", - " let _ = sender.send(wire_parts(&native_serve_outgoing(response)));", "\n", - " \});", "\n", - " match receiver.recv_timeout(deadline) \{", "\n", - " Ok(parts) => write_parts(&mut stream, &parts),", "\n", - " Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => \{", "\n", - " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));", "\n", - " \}", "\n", - " Err(std::sync::mpsc::RecvTimeoutError::Timeout) => \{", "\n", - " \{", "\n", - " let mut s = lock_state(&state);", "\n", - " if *s != WORKER_DONE \{", "\n", - " *s = WORKER_ABANDONED;", "\n", - " *lock_state(&stuck) += 1;", "\n", + " let (reply, answer) = std::sync::mpsc::channel::();", "\n", + " let replace = if generation.calls.send((call, reply)).is_err() \{", "\n", + " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));", "\n", + " true", "\n", + " \} else \{", "\n", + " match answer.recv_timeout(deadline) \{", "\n", + " Ok(parts) => \{", "\n", + " write_parts(&mut stream, &parts);", "\n", + " false", "\n", + " \}", "\n", + " Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => \{", "\n", + " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));", "\n", + " true", "\n", + " \}", "\n", + " Err(std::sync::mpsc::RecvTimeoutError::Timeout) => \{", "\n", + " \{", "\n", + " let mut s = lock_state(&generation.state);", "\n", + " if *s != WORKER_DONE \{", "\n", + " *s = WORKER_ABANDONED;", "\n", + " *lock_state(&stuck) += 1;", "\n", + " \}", "\n", " \}", "\n", + " write_response(&mut stream, &native_serve_outgoing(native_serve_deadline_refused(contract.clone())));", "\n", + " true", "\n", " \}", "\n", - " write_response(&mut stream, &native_serve_outgoing(native_serve_deadline_refused(contract.clone())));", "\n", " \}", "\n", + " \};", "\n", + " if replace \{", "\n", + " generation = match realize_generation(&stuck, deadline) \{", "\n", + " Ok(generation) => generation,", "\n", + " Err(cause) => \{", "\n", + " eprintln!(\"REFUSED: \{\}\", native_serve_worker_unrealized(cause));", "\n", + " std::process::exit(native_serve_worker_replacement_exit_code() as i32);", "\n", + " \}", "\n", + " \};", "\n", " \}", "\n", " \}", "\n", " \}", "\n", @@ -18705,10 +19236,14 @@ fn emit_native_serve_driver_main_rs(crate_name: String, pipeline_module: String) } } -// Bind the existing file transport once. v2_cli_main owns parse, acquisition and compilation; -// the host supplies argv and realizes output and ProcessExit, with no source-root walk. -fn emit_native_cli_driver_main_rs(crate_name: String, pipeline_module: String) -> TextFile { +// Bind the handlers NativeEffectRealizationAdmission admitted for v2_cli_main (today the existing +// file transport, extdeps.filesystem.filesystem_io Filesystem) and nothing else. v2_cli_main owns +// parse, acquisition and compilation; the host supplies argv and realizes output and ProcessExit, +// with no source-root walk. +fn emit_native_cli_driver_main_rs(crate_name: String, pipeline_module: String, bindings: List) -> TextFile { let pipeline_mod = module_to_filename(name: pipeline_module) + let binding_lines = bindings |> map(b => b.binding_line) |> join(separator: "") + let effect_args = bindings |> map(b => concat(", ", b.argument)) |> join(separator: "") TextFile { path: concat(rust_source_root(), "main", rust_source_ext()), content: concat( @@ -18726,10 +19261,10 @@ fn emit_native_cli_driver_main_rs(crate_name: String, pipeline_module: String) - "#[tokio::main(flavor = \"current_thread\")]", "\n", "async fn main() \{", "\n", " let argv: Vec = std::env::args().skip(1).collect();", "\n", - " let filesystem = ", crate_name, "::extdeps_filesystem_filesystem_io::Filesystem::new(Default::default());", "\n", - " let outcome = match v2_cli_main(Rc::new(argv.into()), &filesystem).await {", "\n", + binding_lines, + " let outcome = match v2_cli_main(Rc::new(argv.into())", effect_args, ").await {", "\n", " Ok(outcome) => outcome,", "\n", - " Err(cause) => { eprintln!(\"REFUSED: native filesystem transport: {}\", cause); std::process::exit(2); }", "\n", + " Err(cause) => { eprintln!(\"REFUSED: native effect transport: {}\", cause); std::process::exit(2); }", "\n", " };", "\n", " let text = v2_cli_outcome_text(outcome.clone());", "\n", " print!(\"\{text\}\");", "\n", @@ -18861,6 +19396,8 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str "use ", crate_name, "::", pipeline_mod, "::\{", "\n", " native_census_module_completeness, native_census_module_resolution, native_census_modules,", "\n", " native_census_module_inference, native_census_infer_type_contribution,", "\n", + " native_census_infer_completeness, native_census_cause_groups_add_infer_module, native_census_module_paths,", "\n", + " native_census_subject_closure, native_census_root_subject_membership, native_census_subject_label,", "\n", " native_census_residual_rows,", "\n", " native_census_cause_groups_add_module, native_census_cause_groups_from_file_refusals,", "\n", " native_census_cause_group_rows, native_census_closure_index, native_census_roots_ranked,", "\n", @@ -18873,7 +19410,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " native_test_front_end_prepare,", "\n", " native_driver_parse, native_driver_plan_exit, native_driver_plan_source_roots,", "\n", " NativeDriverPlan, NativeDriverVerb,", "\n", - " NativeCensusModuleOutcome,", "\n", + " NativeCensusModuleOutcome, NativeInferCensusOutcome,", "\n", " NativeLaneHostFacts,", "\n", " NativeRouteMemberRow,", "\n", " native_demand_eval_nanos, native_demand_eval_samples,", "\n", @@ -18888,12 +19425,10 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " native_demand_tested_tree_input,", "\n", "\};", "\n", "use ", crate_name, "::v2_compiler_program_assembly::\{", "\n", - " program_assembly_measured_outcome, program_assembly_memo_hits,", "\n", - " program_assembly_memo_lookup_calls, program_assembly_memo_misses,", "\n", - " program_assembly_allocator_next, program_assembly_measured_alloc,", "\n", + " program_assembly_allocator_next, program_assembly_parsed_alloc,", "\n", " program_assembly_phase_normalize_seeded, program_assembly_normalized_outcome,", "\n", " program_assembly_normalized_alloc, program_assembly_normalized_spans, program_assembly_ingest_scope_outcome,", "\n", - " program_assembly_phase_parse_measured_seeded,", "\n", + " program_assembly_phase_parse_seeded,", "\n", " program_assembly_phase_token_count, program_assembly_phase_tokenize,", "\n", "\};", "\n", "use ", crate_name, "::gunbc_witness_v2_native_route::\{", "\n", @@ -19125,7 +19660,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " // fan-out -- and take each module as it is resolved: a cascade member once under its root,", "\n", " // any other refusal per chain (v2.compiler.compile native_census_cause_groups_add_module).", "\n", " let closure = native_census_closure_index(context.clone(), refusal_index.clone(), modules.clone());", "\n", - " let mut groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure, true);", "\n", + " let mut groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure.clone(), true);", "\n", " let mut resolve_total_nanos: u128 = 0;", "\n", " let mut resolve_samples: Vec = Vec::new();", "\n", " let mut resolved_ok: u64 = 0;", "\n", @@ -19145,7 +19680,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " context = step.context.clone();", "\n", " let outcome = step.outcome.clone();", "\n", " let module_resolve_nanos = span_nanos(resolve_started);", "\n", - " groups = native_census_cause_groups_add_module(groups.clone(), entry.clone(), step.clone());", "\n", + " groups = native_census_cause_groups_add_module(groups.clone(), closure.clone(), entry.clone(), step.clone());", "\n", " let outcome_label = match &*outcome \{", "\n", " NativeCensusModuleOutcome::NativeCensusModuleFileRefused => \{", "\n", " file_refused += 1;", "\n", @@ -19256,6 +19791,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " std::process::exit(2);", "\n", " \}", "\n", " let reads = read_ingest(source_roots);", "\n", + " let corpus_reads = reads.len();", "\n", " let ingest = Rc::new(im::vector::Vector::from(reads));", "\n", " let mut context = match &*native_test_context_from_ingest(ingest.clone()) \{", "\n", " Outcome::Accepted \{ value, .. \} => value.clone(),", "\n", @@ -19264,26 +19800,100 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " std::process::exit(2);", "\n", " \}", "\n", " \};", "\n", + " // THE FRONT-END POPULATION IS PRINTED IN THE SHAPE census-resolve PRINTS IT: the same context fact.", "\n", + " for refusal in context.file_refusals.iter() \{", "\n", + " println!(\"{}\", serde_json::json!(\{ \"file_refusal\": refusal \}));", "\n", + " \}", "\n", + " for advised in context.accepted_file_advisories.iter() \{", "\n", + " println!(\"{}\", serde_json::json!(\{ \"accepted_file_advisories\": advised \}));", "\n", + " \}", "\n", + " let file_refusals = context.file_refusals.len();", "\n", + " let advised_files = context.accepted_file_advisories.len();", "\n", " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());", "\n", " let modules = native_census_modules(ingest.clone());", "\n", + " // THE INFER CENSUS'S GROUPS START FROM THE FRONT-END ROOTS, as census-resolve's do, and take each", "\n", + " // module's step through ONE fold (v2.compiler.compile native_census_cause_groups_add_infer_module):", "\n", + " // a resolve refusal filed as census-resolve files it, an infer refusal as one Infer-stage row.", "\n", + " let closure = native_census_closure_index(context.clone(), refusal_index.clone(), modules.clone());", "\n", + " let module_paths = native_census_module_paths(modules.clone());", "\n", + " let mut groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure.clone(), true);", "\n", " let mut run = type_census_run_empty();", "\n", + " let mut file_refused: u64 = 0;", "\n", + " let mut resolve_refused: u64 = 0;", "\n", + " let mut residual_rows: u64 = 0;", "\n", + " let mut inferred: u64 = 0;", "\n", + " let mut infer_refused: u64 = 0;", "\n", " for entry in modules.iter() \{", "\n", " let started = Instant::now();", "\n", " let step = native_census_module_inference(context.clone(), refusal_index.clone(), entry.clone());", "\n", " context = step.context.clone();", "\n", + " groups = native_census_cause_groups_add_infer_module(groups.clone(), closure.clone(), module_paths.clone(), entry.clone(), step.clone());", "\n", + " let outcome_label = match &*step.outcome \{", "\n", + " NativeInferCensusOutcome::NativeInferCensusFileRefused => \{ file_refused += 1; \"file_refused\" \}", "\n", + " NativeInferCensusOutcome::NativeInferCensusResolveRefused \{ first, rest, .. \} => \{", "\n", + " resolve_refused += 1;", "\n", + " for row in native_census_residual_rows(entry.clone(), first.clone(), rest.clone()).iter() \{", "\n", + " residual_rows += 1;", "\n", + " println!(\"{}\", serde_json::json!(\{ \"census_residual\": row \}));", "\n", + " \}", "\n", + " \"resolve_refused\"", "\n", + " \}", "\n", + " NativeInferCensusOutcome::NativeInferCensusInferred \{ .. \} => \{ inferred += 1; \"inferred\" \}", "\n", + " NativeInferCensusOutcome::NativeInferCensusInferRefused \{ .. \} => \{ infer_refused += 1; \"infer_refused\" \}", "\n", + " \};", "\n", " let contribution = native_census_infer_type_contribution(context.clone(), entry.clone(), step.outcome.clone());", "\n", " run = type_census_run_add(run, contribution);", "\n", - " eprintln!(\"[native-census-infer] \{\}\", serde_json::json!(\{ \"module\": entry.module.clone(), \"nanos\": span_nanos(started) \}));", "\n", + " eprintln!(\"[native-census-infer] \{\}\", serde_json::json!(\{", "\n", + " \"module\": entry.module.clone(),", "\n", + " \"path\": entry.path.clone(),", "\n", + " \"nanos\": span_nanos(started),", "\n", + " \"outcome\": outcome_label,", "\n", + " \"occurrence_completeness\": native_census_infer_completeness(step.outcome.clone()),", "\n", + " \}));", "\n", + " \}", "\n", + " let cause_groups = native_census_cause_group_rows(groups.clone());", "\n", + " for group in cause_groups.iter() \{", "\n", + " println!(\"{}\", serde_json::json!(\{ \"cause_group\": group \}));", "\n", " \}", "\n", + " let roots = native_census_roots_ranked(cause_groups.clone());", "\n", + " // EACH ROOT IS MARKED AGAINST THE NATIVE TEST TARGET'S CLOSURE (v2.compiler.compile", "\n", + " // native_census_subject_closure): derived from the label, never from a list.", "\n", + " let subject = native_census_subject_closure(closure.clone(), modules.clone(), native_census_subject_label());", "\n", + " println!(\"{}\", serde_json::json!(\{ \"census_subject\": subject.clone() \}));", "\n", + " for root in roots.iter() \{", "\n", + " println!(\"{}\", serde_json::json!(\{ \"census_root\": root, \"subject_closure\": native_census_root_subject_membership(subject.clone(), root.clone()) \}));", "\n", + " \}", "\n", + " // THE TYPE-DECLARATION CENSUS'S LINES SHARE THIS STDOUT AND KEEP THEIR OWN LINE KIND (design 2a).", "\n", " let receipt = type_census_receipt(run);", "\n", " for line in receipt.lines.iter() \{", "\n", " println!(\"\{\}\", line);", "\n", " \}", "\n", + " // THE WALK COMPLETED, SO THE TERMINAL IS PRINTED WHATEVER THE TYPE CENSUS DECIDES: the refusal", "\n", + " // census and the type census are two facts, and the type census's verdict is the exit status alone.", "\n", + " let type_census = match &*receipt.terminal \{", "\n", + " NativeClaimTerminal::NativeClaimHeld => \"held\",", "\n", + " NativeClaimTerminal::NativeClaimNotHeld \{ .. \} => \"not_held\",", "\n", + " NativeClaimTerminal::NativeClaimNoObservation \{ .. \} => \"no_observation\",", "\n", + " \};", "\n", + " println!(\"\{\}\", serde_json::json!(\{", "\n", + " \"_terminal\": \"complete\",", "\n", + " \"mode\": \"census-infer\",", "\n", + " \"corpus_reads\": corpus_reads,", "\n", + " \"modules\": modules.len(),", "\n", + " \"file_refusals\": file_refusals,", "\n", + " \"advised_files\": advised_files,", "\n", + " \"file_refused\": file_refused,", "\n", + " \"resolve_refused\": resolve_refused,", "\n", + " \"residual_rows\": residual_rows,", "\n", + " \"inferred\": inferred,", "\n", + " \"infer_refused\": infer_refused,", "\n", + " \"cause_groups\": cause_groups.len(),", "\n", + " \"roots\": roots.len(),", "\n", + " \"type_census\": type_census,", "\n", + " \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024),", "\n", + " \}));", "\n", " match &*receipt.terminal \{", "\n", - " NativeClaimTerminal::NativeClaimHeld => \{", "\n", - " println!(\"\{\}\", serde_json::json!(\{ \"_terminal\": \"complete\", \"mode\": \"census-infer\", \"modules\": modules.len(), \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024) \}));", "\n", - " std::process::exit(0);", "\n", - " \}", "\n", + " NativeClaimTerminal::NativeClaimHeld => std::process::exit(0),", "\n", " NativeClaimTerminal::NativeClaimNotHeld \{ reason \} => \{", "\n", " eprintln!(\"NOT HELD: \{reason\}\");", "\n", " std::process::exit(1);", "\n", @@ -19507,7 +20117,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " // CANNOT go red -- the composition is defined as the phase chain -- so the honest answer is", "\n", " // the row and its trigger (a realization seam that yields per-phase spans FROM a modeled", "\n", " // fold), not a green check. Reordering these calls or dropping the merge that", "\n", - " // program_assembly_phase_parse_measured_seeded performs would change the context with", "\n", + " // program_assembly_phase_parse_seeded performs would change the context with", "\n", " // nothing red.", "\n", " let context_started = cpu_mark();", "\n", " // THE ONCE-PER-FOLD INVARIANT IS PAID ONCE, HERE, AND CARRIED INTO EVERY FILE. A second", "\n", @@ -19538,9 +20148,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " let mut absorb_nanos: u128 = 0;", "\n", " let mut phase_calls: u64 = 0;", "\n", " let mut token_total: u64 = 0;", "\n", - " let mut memo_hits_total: u64 = 0;", "\n", - " let mut memo_misses_total: u64 = 0;", "\n", - " let mut memo_lookups_total: u64 = 0;", "\n", + " let mut byte_total: u64 = 0;", "\n", " let mut context_file_samples: Vec = Vec::new();", "\n", " for read in closure_ingest.iter() \{", "\n", @@ -19559,29 +20167,18 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " // respelling has to thread it explicitly or it keeps the reset the fold no longer has.", "\n", " let file_seed = context_state.alloc.clone();", "\n", " let file_seed_reported = file_seed.clone();", "\n", - " let measured = program_assembly_phase_parse_measured_seeded(", "\n", + " let parsed = program_assembly_phase_parse_seeded(", "\n", " tokens.clone(),", "\n", " front_end.prepared.clone(),", "\n", - " // The residue field is an Optional whose inner type rides a recursion cycle, so the", "\n", - " // emitted record BOXES it while the phase takes the option by value. Deref the box", "\n", - " // rather than clone it; the sibling fields above are Rc and clone correctly.", "\n", - " (*front_end.residue).clone(),", "\n", " file_seed.clone(),", "\n", " );", "\n", " let file_parse_nanos = cpu_span_nanos(parse_started);", "\n", - " // The memo accounting rides BESIDE the outcome, so it reports on a refusal too --", "\n", - " // which is the expensive case this instrument exists to read. Counted outside every", "\n", - " // phase span so it prices none of them.", "\n", - " let file_memo_hits = program_assembly_memo_hits(measured.clone());", "\n", - " let file_memo_misses = program_assembly_memo_misses(measured.clone());", "\n", - " let file_memo_lookups = program_assembly_memo_lookup_calls(measured.clone());", "\n", - " // Read BEFORE program_assembly_measured_outcome consumes `measured`. On a refused parse", "\n", + " // On a refused parse", "\n", " // this returns the seed unchanged. Not because nothing was minted -- a refused parse", "\n", " // HAS minted ids -- but because ParseExprRejected carries no provenance state, so the", "\n", " // mark the attempt reached is not recoverable. See the precondition on", "\n", " // v2.compiler.compile native_test_context_absorb.", "\n", - " let file_alloc = program_assembly_measured_alloc(measured.clone(), file_seed);", "\n", - " let parsed = program_assembly_measured_outcome(measured);", "\n", + " let file_alloc = program_assembly_parsed_alloc(parsed.clone(), file_seed);", "\n", " let normalize_started = cpu_mark();", "\n", " // NORMALIZE ADVANCES THE ALLOCATOR: lowering allocates every image member an id, so the", "\n", " // next file seeds from the mark normalize reached, not the one parse reached.", "\n", @@ -19604,9 +20201,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " absorb_nanos += file_absorb_nanos;", "\n", " phase_calls += 1;", "\n", " token_total += file_tokens.max(0) as u64;", "\n", - " memo_hits_total += file_memo_hits.max(0) as u64;", "\n", - " memo_misses_total += file_memo_misses.max(0) as u64;", "\n", - " memo_lookups_total += file_memo_lookups.max(0) as u64;", "\n", + " let file_bytes = read.source.carried.len() as u64;", "\n", " byte_total += file_bytes;", "\n", " context_file_samples.push(file_nanos);", "\n", @@ -19622,9 +20217,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " \"normalize_nanos\": file_normalize_nanos,", "\n", " \"absorb_nanos\": file_absorb_nanos,", "\n", " \"file_nanos\": file_nanos,", "\n", - " \"memo_hits\": file_memo_hits,", "\n", - " \"memo_misses\": file_memo_misses,", "\n", - " \"memo_lookup_calls\": file_memo_lookups,", "\n", + " // THE THREADING, MADE OBSERVABLE. occurrence_seed is the mark this file was", "\n", " // handed and occurrence_next the mark it reached, so a multi-file run shows", "\n", " // one sequence continuing across files instead of each file restarting at 0.", "\n", @@ -19662,9 +20255,7 @@ fn emit_source_root_eval_driver_main_rs(crate_name: String, pipeline_module: Str " \"absorb_calls\": phase_calls,", "\n", " \"front_end_prepare_calls\": 1,", "\n", " \"tokens\": token_total,", "\n", - " \"memo_hits\": memo_hits_total,", "\n", - " \"memo_misses\": memo_misses_total,", "\n", - " \"memo_lookup_calls\": memo_lookups_total,", "\n", + " \"bytes\": byte_total,", "\n", " \"file_refusals\": context.file_refusals.len(),", "\n", " \"file_p50_nanos\": native_cost_quantile_nanos(&context_file_samples, 1, 2),", "\n", @@ -20245,9 +20836,7 @@ fn emit_main_pipeline_fns(crate_name: String, pipeline_module: String) -> String concat( emit_collect_dag_files_fn(), emit_extract_module_path_fn(), - emit_extract_import_paths_fn(), emit_dep_pool_type_and_fns(), - emit_resolve_transitively_fn(crate_name: crate_name, pipeline_mod: pipeline_mod), emit_compile_target_helpers(crate_name: crate_name, pipeline_mod: pipeline_mod, artifact_mod: artifact_mod)) } @@ -20369,25 +20958,6 @@ fn emit_extract_module_path_fn() -> String { "}\n\n") } -fn emit_extract_import_paths_fn() -> String { - concat( - "/// Extract import module paths from a .dag file's import declarations.\n", - "fn extract_import_paths(content: &str) -> Vec {\n", - " let mut imports = Vec::new();\n", - " for line in content.lines() {\n", - " let trimmed = line.trim();\n", - " if trimmed.starts_with(\"import \") {\n", - " let rest = trimmed[\"import \".len()..].trim();\n", - " let module_path = rest.split('{').next().unwrap_or(rest).trim();\n", - " if !module_path.is_empty() {\n", - " imports.push(module_path.to_string());\n", - " }\n", - " }\n", - " }\n", - " imports\n", - "}\n\n") -} - fn emit_dep_pool_type_and_fns() -> String { concat( "#[derive(Clone, Copy, PartialEq, Eq)]\nenum DependencyPoolIndex {\n Strict,\n PrimaryPrecedence,\n}\n\n", @@ -20460,46 +21030,6 @@ fn emit_dep_pool_type_and_fns() -> String { "}\n\n") } -fn emit_resolve_transitively_fn(crate_name: String, pipeline_mod: String) -> String { - concat( - "/// Resolve imports transitively from entry modules.\n", - "/// Fail-closed: panics on unreadable imported files.\n", - "/// Returns sources sorted by path for deterministic fixed-point convergence.\n", - "fn resolve_transitively_with_seen(\n", - " entry_sources: Vec<(String, String)>,\n", - " index: &HashMap,\n", - " mut seen: HashMap>,\n", - ") -> Vec> {\n", - " let mut queue: Vec<(String, String)> = entry_sources;\n", - "\n", - " while let Some((_path, content)) = queue.pop() {\n", - " let imports = extract_import_paths(&content);\n", - " for module_path in imports {\n", - " if seen.contains_key(&module_path) {\n", - " continue;\n", - " }\n", - " if let Some(file_path) = index.get(&module_path) {\n", - " let file_content = std::fs::read_to_string(file_path)\n", - " .unwrap_or_else(|e| panic!(\"failed to read imported module '{}' at {:?}: {}\",\n", - " module_path, file_path, e));\n", - " let rel_path = file_path.to_string_lossy().to_string();\n", - " let source = Rc::new(", crate_name, "::", pipeline_mod, "::SourceFile {\n", - " path: rel_path.clone(),\n", - " content: file_content.clone(),\n", - " });\n", - " seen.insert(module_path, source);\n", - " queue.push((rel_path, file_content));\n", - " }\n", - " // If not found in index, the compiler's resolve stage will report the error.\n", - " }\n", - " }\n", - "\n", - " let mut result: Vec<_> = seen.into_iter().map(|(_, v)| v).collect();\n", - " result.sort_by(|a, b| a.path.cmp(&b.path));\n", - " result\n", - "}\n\n") -} - fn emit_main_diagnostic_fns(crate_name: String) -> String { let core_mod = module_to_filename(name: "v1.std.core") concat( diff --git a/src/v1/compiler_tests_rust.dag b/src/v1/compiler_tests_rust.dag index 340dda11f6b..bd6eccfe6a2 100644 --- a/src/v1/compiler_tests_rust.dag +++ b/src/v1/compiler_tests_rust.dag @@ -70,26 +70,35 @@ fn ct_file_discovery_helpers() -> String { " root: &std::path::Path,\n", " out: &mut Vec<(String, String)>,\n", " ) {\n", - " crate::cli_run::derived_row_roster::ensure_if_row_dir_or_panic(dir);\n", " if let Ok(entries) = std::fs::read_dir(dir) {\n", + " let mut here = Vec::new();\n", " for entry in entries.flatten() {\n", " let path = entry.path();\n", " if path.is_dir() {\n", " collect_dag_recursive(&path, root, out);\n", " } else if path.extension().map_or(false, |e| e == \"dag\") {\n", - " let rel = path\n", - " .strip_prefix(root)\n", - " .unwrap()\n", - " .to_string_lossy()\n", - " .to_string();\n", - " // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules.\n", - " if rel.contains(\"/tests/\") {\n", - " continue;\n", - " }\n", - " let content = std::fs::read_to_string(&path)\n", - " .unwrap_or_else(|e| panic!(\"failed to read {}: {}\", path.display(), e));\n", - " out.push((rel, content));\n", + " here.push(path);\n", + " }\n", + " }\n", + " // Admitted per directory, so a ledger row directory contributes its derived roster\n", + " // in memory (`derived_row_roster::acquire_dir_files`) rather than as a written file.\n", + " let acquired = crate::cli_run::derived_row_roster::acquire_dir_files(dir, here)\n", + " .unwrap_or_else(|e| panic!(\"{}\", e));\n", + " for source in acquired {\n", + " let path = source.path();\n", + " let rel = path\n", + " .strip_prefix(root)\n", + " .unwrap()\n", + " .to_string_lossy()\n", + " .to_string();\n", + " // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules.\n", + " if rel.contains(\"/tests/\") {\n", + " continue;\n", " }\n", + " let content = source\n", + " .read()\n", + " .unwrap_or_else(|e| panic!(\"failed to read {}: {}\", path.display(), e));\n", + " out.push((rel, content));\n", " }\n", " }\n", " }\n\n") @@ -1706,6 +1715,101 @@ fn ct_unbound_reference_to_a_twice_declared_leaf_refuses_test() -> String { ) } +// A same-leaf declaration in one module does not change what another module emits. +fn ct_a_coproduct_named_optional_emits_as_its_own_enum_test() -> String { + [ + " // A COPRODUCT NAMED Optional EMITS AS ITS OWN ENUM. still-raven-321's fixture,\n", + " // fixtures/native_emission_controls_optional_collision.dag, is a non-kernel declaration that shares\n", + " // the kernel mint's leaf. Host-Option is decided by declaration (v1.compiler.emit_rust\n", + " // declaration_owns_host_option over gunbc.structural_realization_bindings kernel_mint_declaration_rows),\n", + " // so its positions name its own enum. THE RED is the leaf rule this replaced: every position rendered\n", + " // as Option<..> and the variants were never emitted (E0425 cannot find CollisionWrapped). The fixture\n", + " // lives outside the source roots, because a second Optional in the accepted corpus is a leaf-name fork,\n", + " // so it is compiled here, from its file, beside a kernel std.optional. Its one import line names a\n", + " // kernel type and is dropped so the pair compiles standalone.\n", + " #[test]\n", + " fn a_coproduct_named_optional_emits_as_its_own_enum() {\n", + " use crate::v1_compiler_compile::SourceFile;\n", + " let collision = read_dag(\"fixtures/native_emission_controls_optional_collision.dag\").replace(\"import std.types \{ Int \}\\n\", \"\");\n", + " let sources = vec![\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/optional.dag\".to_string(), content: \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\".to_string() }),\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/native_emission_controls_optional_collision.dag\".to_string(), content: collision }),\n", + " ];\n", + " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n", + " let emitted = result.files.iter().find(|f| f.path.contains(\"native_emission_controls_optional_collision\")).map(|f| f.content.clone()).unwrap_or_default();\n", + " assert!(emitted.contains(\"pub enum Optional\") && emitted.contains(\"CollisionWrapped\"), \"the collision coproduct must emit as its own enum:\\n\{emitted}\");\n", + " assert!(!emitted.contains(\"Option<\") && !emitted.contains(\"compile_error!\"), \"no position of the collision coproduct may realize as the host Option:\\n\{emitted}\");\n", + " }\n", + "\n", + ] |> join(separator: "") +} + +fn ct_unstamped_bare_optional_follows_the_lookup_binding_order_test() -> String { + [ + " // AN UNSTAMPED BARE `Optional` FOLLOWS THE TYPECHECKER'S lookup_binding ORDER: own file, then\n", + " // the file's named import (followed through re-exports), then the kernel\n", + " // (v1.compiler.infer_env bare_occurrence_binding). The member references of a coproduct body reach\n", + " // the emitter from the type environment's copy with no Node.declaration, so the census route is\n", + " // the emitter's own decision. THE RED is the importer: with the kernel ahead of the named import\n", + " // (the order direct_import_export_precedence_note states) its payload routes kernel_mint to\n", + " // std.optional, measured on gunbc#13454 at 57551c4b. The control keeps a module that names\n", + " // Optional without importing it on the kernel arm.\n", + " #[test]\n", + " fn unstamped_bare_optional_follows_the_lookup_binding_order() {\n", + " use crate::v1_compiler_compile::SourceFile;\n", + " let sources = vec![\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/optional.dag\".to_string(), content: \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\".to_string() }),\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/collision.dag\".to_string(), content: \"module hom.collision\\n\\ntype Optional\\n = CollisionWrapped { value: Int }\\n | CollisionEmpty\\n\".to_string() }),\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/importer.dag\".to_string(), content: \"module hom.importer\\n\\nimport hom.collision { Optional }\\n\\ntype HeldChoice\\n = HeldOptional { held: Optional }\\n | HeldNothing\\n\".to_string() }),\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/plain.dag\".to_string(), content: \"module hom.plain\\n\\ntype PlainChoice\\n = PlainHeld { held: Optional }\\n | PlainNothing\\n\".to_string() }),\n", + " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/user.dag\".to_string(), content: \"module hom.user\\n\\nimport hom.importer { HeldChoice }\\nimport hom.plain { PlainChoice }\\n\\nfn pick(h: HeldChoice, p: PlainChoice) -> Int {\\n 1\\n}\\n\".to_string() }),\n", + " ];\n", + " let receipt = crate::v1_tests_claim_carrier_realization_census::typed_census_from_sources(std::rc::Rc::new(sources.into()));\n", + " assert!(!receipt.starts_with(\"REFUSED\"), \"\{receipt}\");\n", + " let routes = |enclosing: &str| -> Vec {\n", + " receipt.lines().skip(1).map(|l| l.split('\\t').collect::>())\n", + " .filter(|c| c[1] == enclosing && c[2] == \"declaration_field\" && c[3] == \"Optional\")\n", + " .map(|c| c[13].to_string()).collect()\n", + " };\n", + " let held = routes(\"HeldChoice\");\n", + " assert!(!held.is_empty() && held.iter().all(|r| r == \"referencing_module_import:Resolved:hom.collision.Optional\"), \"the imported Optional must route to the import, never the kernel mint:\\n\{receipt}\");\n", + " let plain = routes(\"PlainChoice\");\n", + " assert!(!plain.is_empty() && plain.iter().all(|r| r == \"kernel_mint:Resolved:std.optional.Optional\"), \"an unimported Optional is the kernel's:\\n\{receipt}\");\n", + " assert!(!receipt.contains(\"LeafAmbiguous:Optional\"), \"\{receipt}\");\n", + " }\n", + "\n", + ] |> join(separator: "") +} + +fn ct_same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged_test() -> String { + concat( + " // A SAME-LEAF TYPE ANYWHERE MUST NOT CHANGE ANOTHER MODULE'S EMISSION. hom.user returns the kernel\n", + " // optional's Absent; hom.edge declares an unrelated enum that also has an Absent variant. With the\n", + " // leaf-keyed type summaries, adding hom.coll (a coproduct it declares as Optional) replaced the\n", + " // kernel Optional's summary, so Absent stopped being an Optional variant and hom.user emitted\n", + " // Rc::new(Lookup::Absent) where it had emitted None. Keyed by declaration, both summaries stand:\n", + " // hom.user's file is byte-identical with and without hom.coll, and its Absent is the host None.\n", + " #[test]\n", + " fn same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged() {\n", + " use crate::v1_compiler_compile::SourceFile;\n", + " let src = |path: &str, content: &str| std::rc::Rc::new(SourceFile { path: path.to_string(), content: content.to_string() });\n", + " let optional = || src(\"fixtures/same_leaf_optional/optional.dag\", \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\");\n", + " let edge = || src(\"fixtures/same_leaf_optional/edge.dag\", \"module hom.edge\\n\\ntype Lookup\\n = Found { at: Int }\\n | Absent\\n\");\n", + " let user = || src(\"fixtures/same_leaf_optional/user.dag\", \"module hom.user\\nimport std.optional { Optional, Present, Absent }\\n\\nfn nothing_here(x: Int) -> Optional {\\n Absent\\n}\\n\");\n", + " let coll = || src(\"fixtures/same_leaf_optional/coll.dag\", \"module hom.coll\\n\\ntype Optional\\n = CollisionWrapped { value: Int }\\n | CollisionEmpty\\n\");\n", + " let emitted_user = |sources: Vec>| -> String {\n", + " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n", + " result.files.iter().find(|f| f.path.contains(\"hom_user\")).map(|f| f.content.clone()).unwrap_or_default()\n", + " };\n", + " let without = emitted_user(vec![optional(), edge(), user()]);\n", + " let with = emitted_user(vec![optional(), edge(), user(), coll()]);\n", + " assert!(without.contains(\"None\") && !without.contains(\"Lookup::Absent\"), \"the control: hom.user's Absent is the host None:\\n\{without}\");\n", + " assert_eq!(with, without, \"adding a same-leaf Optional elsewhere changed hom.user's emission\");\n", + " }\n", + "\n" + ) +} + // The referencing-module arm admits only a bare spelling: a qualified reference to another module's // declaration, arriving unstamped, is not decided by the referencing module's own same-leaf one. fn ct_qualified_reference_is_not_decided_by_the_local_declarer_test() -> String { @@ -1713,7 +1817,7 @@ fn ct_qualified_reference_is_not_decided_by_the_local_declarer_test() -> String " // A QUALIFIED REFERENCE IS NOT DECIDED BY THE LOCAL DECLARER. hom.local declares its own Slot\n", " // and ALSO names no.such.module.Slot, which resolve cannot bind, so the reference reaches the\n", " // emitter unstamped while two modules declare Slot. The referencing-module arm\n", - " // (v1.compiler.infer_emit_info type_decl_referencing_module_declarer) would pick the LOCAL Slot:\n", + " // (v1.compiler.infer_emit_info type_decl_occurrence_binding) would pick the LOCAL Slot:\n", " // the wrong-declarer class itself. It is admitted only for a bare spelling, so this refuses.\n", " // THE CONTROL is the same module without the second declarer: the spelling names a sole\n", " // declarer, and nothing refuses.\n", @@ -1889,6 +1993,9 @@ fn ct_coercion_tests() -> String { ct_same_leaf_type_declarations_emit_by_identity_test(), ct_unbound_reference_to_a_twice_declared_leaf_refuses_test(), ct_qualified_reference_is_not_decided_by_the_local_declarer_test(), + ct_same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged_test(), + ct_unstamped_bare_optional_follows_the_lookup_binding_order_test(), + ct_a_coproduct_named_optional_emits_as_its_own_enum_test(), ct_duplicate_type_parameter_refuses_test(), ct_resource_item_is_not_a_type_item_test()) } @@ -2317,7 +2424,11 @@ fn ct_diagnostics_carrier_grounding_test() -> String { " fn diagnostics_carrier_grounds_to_native_option() {\n", " assert!(crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Diagnostics\".to_string()));\n", " assert!(!crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Optional\".to_string()));\n", - " assert!(crate::v1_compiler_emit_rust::is_grounded_coproduct_native_alias(\"Diagnostics\".to_string()));\n", + " assert!(crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"v2.std.diagnostic\".to_string(), \"Diagnostics\".to_string()));\n", + " // The host option is a DECLARATION, not a name: the kernel optional's declaration is it, and a\n", + " // coproduct another module declares as Optional is not.\n", + " assert!(crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"std.optional\".to_string(), \"Optional\".to_string()));\n", + " assert!(!crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"gunbc.instruments.native_emission_controls_optional_collision\".to_string(), \"Optional\".to_string()));\n", " let empty_shared = std::rc::Rc::new(im::OrdSet::new());\n", " assert_eq!(\n", " crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied(empty_shared.clone()),\n", @@ -4404,7 +4515,7 @@ fn ct_method_existence_wall_witness_test() -> String { " // row, and the call is STILL refused, because the receiver does not\n", " // fail to resolve in the way the row was measured on. A (module, method)\n", " // key passed this input; that is the fail-open the third component closes.\n", - " let listed_wrong_shape = compile_one(\"listed.dag\", frontier_src(\"extdeps.dns.domain_name\", \"list_push\"));\n", + " let listed_wrong_shape = compile_one(\"listed.dag\", frontier_src(\"v1.compiler.trace\", \"map\"));\n", " assert!(\n", " listed_wrong_shape.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. })),\n", " \"a declared row must NOT admit a new call in the same module on a receiver whose shape it was never measured on, got: {:?}\",\n", diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 5129153804f..ced1aeadd94 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -255,6 +255,118 @@ fn rt_collection_ops() -> String { "pub fn map_values(m: &HashMap) -> Vec { m.values().cloned().collect() }\n\n", "pub fn list_concat(mut a: Vec, b: Vec) -> Vec { a.append(b); a }\n\n", "pub fn list_push(mut list: Vec, item: T) -> Vec { list.push_back(item); list }\n\n", + "/// std.primitives skip_contract on the persistent carrier: the suffix after `n` members,\n", + "/// sharing the receiver's tree in O(log n) instead of copying the remainder, so a walk that\n", + "/// slices by offset is O(n log n) rather than quadratic. A negative `n` skips every member, as\n", + "/// the copying form's saturating `n as usize` did.\n", + "pub fn list_skip(items: &Vec, n: i64) -> Vec \{\n", + " let k = if n < 0 \{\n", + " items.len()\n", + " \} else \{\n", + " (n as usize).min(items.len())\n", + " \};\n", + " items.skip(k)\n", + "\}\n", + "\n", + "/// std.primitives take_contract on the persistent carrier: the first `n` members, sharing\n", + "/// the receiver's tree. A negative `n` keeps every member, as the copying form did.\n", + "pub fn list_take(items: &Vec, n: i64) -> Vec \{\n", + " let k = if n < 0 \{\n", + " items.len()\n", + " \} else \{\n", + " (n as usize).min(items.len())\n", + " \};\n", + " items.take(k)\n", + "\}\n", + "\n", + "#[cfg(test)]\n", + "mod list_slice_tests \{\n", + " use super::*;\n", + "\n", + " thread_local! \{\n", + " static CLONES: Cell = const \{ Cell::new(0) \};\n", + " \}\n", + "\n", + " /// A code point whose every copy is counted, so a slice's cost is read off the carrier's\n", + " /// own work rather than a clock.\n", + " #[derive(Debug, PartialEq)]\n", + " struct CountedCodePoint(u32);\n", + "\n", + " impl Clone for CountedCodePoint \{\n", + " fn clone(&self) -> Self \{\n", + " CLONES.with(|c| c.set(c.get() + 1));\n", + " CountedCodePoint(self.0)\n", + " \}\n", + " \}\n", + "\n", + " fn clones_during(f: impl FnOnce() -> R) -> (R, u64) \{\n", + " let before = CLONES.with(|c| c.get());\n", + " let r = f();\n", + " (r, CLONES.with(|c| c.get()) - before)\n", + " \}\n", + "\n", + " // Non-ASCII on purpose: emitted char_at / substring over a bare &str are O(offset) here,\n", + " // which is the walk the code-point slice replaces.\n", + " fn non_ascii_code_points(n: usize) -> Vec \{\n", + " [0xe9u32, 0x4e2d, 0x1f600, 0x61]\n", + " .iter()\n", + " .cycle()\n", + " .take(n)\n", + " .map(|cp| CountedCodePoint(*cp))\n", + " .collect()\n", + " \}\n", + "\n", + " const N: usize = 200_000;\n", + " // im's RRB split copies at most one boundary chunk per tree level; this is that bound with\n", + " // headroom, and three orders of magnitude below N.\n", + " const SLICE_COPY_BUDGET: u64 = 2_048;\n", + "\n", + " #[test]\n", + " fn a_slice_from_the_middle_copies_only_boundary_chunks() \{\n", + " let cps = Rc::new(non_ascii_code_points(N));\n", + " let (slice, copied) = clones_during(|| list_take(&list_skip(&cps, (N / 2) as i64), 7));\n", + " assert!(\n", + " copied <= SLICE_COPY_BUDGET,\n", + " \"skip+take copied \{\} members of \{\}\",\n", + " copied,\n", + " N\n", + " );\n", + " let expected: std::vec::Vec = cps.iter().skip(N / 2).take(7).map(|c| c.0).collect();\n", + " assert_eq!(\n", + " slice.iter().map(|c| c.0).collect::>(),\n", + " expected\n", + " );\n", + " \}\n", + "\n", + " // THE RED CONTROL: the extdeps.languages.rust.emit skip / take templates this runtime pair\n", + " // replaced, spelled as they emitted, on the same input and bound. It must exceed the budget,\n", + " // or the budget does not discriminate the quadratic.\n", + " #[test]\n", + " fn the_copying_template_form_exceeds_the_budget() \{\n", + " let cps = Rc::new(non_ascii_code_points(N));\n", + " let (_, copied) = clones_during(|| \{\n", + " let rest = cps.iter().cloned().skip(N / 2).collect::>();\n", + " rest.iter().cloned().take(7).collect::>()\n", + " \});\n", + " assert!(\n", + " copied > SLICE_COPY_BUDGET,\n", + " \"copying form copied only \{\}\",\n", + " copied\n", + " );\n", + " \}\n", + "\n", + " #[test]\n", + " fn negative_and_overlong_counts_keep_the_copying_forms_reading() \{\n", + " let xs: Vec = (0..10).collect();\n", + " for n in [-3i64, 0, 4, 10, 11, i64::MAX] \{\n", + " let copying_skip = xs.iter().cloned().skip(n as usize).collect::>();\n", + " let copying_take = xs.iter().cloned().take(n as usize).collect::>();\n", + " assert_eq!(list_skip(&xs, n), copying_skip, \"skip \{\}\", n);\n", + " assert_eq!(list_take(&xs, n), copying_take, \"take \{\}\", n);\n", + " \}\n", + " \}\n", + "\}\n", + "\n", "pub fn append(list: Rc>, item: T) -> Vec {\n", " let mut v = (*list).clone();\n", " v.push_back(item);\n", @@ -1303,7 +1415,201 @@ fn rust_runtime_source() -> String { rt_filesystem(), rt_checked_int_ops(), rt_realization_measurement(), - rt_accelerator_demo_kernel()) + rt_accelerator_demo_kernel(), + rt_host_budget()) +} + +fn rt_host_budget() -> String { + concat( + "/// The one host-budget precedence. `read_host_budget_bytes` and\n", + "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n", + "#[derive(Clone, Debug, PartialEq, Eq)]\n", + "pub enum HostBudgetJoinSource \{\n", + " CgroupMemoryHigh \{ cgroup_dir: String \},\n", + " CgroupMemoryMax \{ cgroup_dir: String \},\n", + " CgroupV1HierarchicalMemoryLimit \{ cgroup_dir: String \},\n", + " DarwinPhysicalMemory,\n", + "\}\n\n", + "impl HostBudgetJoinSource \{\n", + " pub fn label(&self) -> String \{\n", + " match self \{\n", + " HostBudgetJoinSource::CgroupMemoryHigh \{ cgroup_dir \} => format!(\"cgroup memory.high (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::CgroupMemoryMax \{ cgroup_dir \} => format!(\"cgroup memory.max (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit \{ cgroup_dir \} => format!(\"cgroup v1 memory.stat hierarchical_memory_limit (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n", + " \}\n", + " \}\n", + "\}\n\n", + "#[derive(Clone, Debug, PartialEq, Eq)]\n", + "pub enum HostBudgetJoin \{\n", + " Resolved \{ effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 \},\n", + " DeclaredUnverified \{ requested_bytes: u64, reason: String \},\n", + " Unreadable \{ reason: String \},\n", + "\}\n\n", + "impl HostBudgetJoin \{\n", + " pub fn bytes(&self) -> Option \{\n", + " match self \{\n", + " HostBudgetJoin::Resolved \{ effective_bytes, .. \} => Some(*effective_bytes),\n", + " HostBudgetJoin::DeclaredUnverified \{ requested_bytes, .. \} => Some(*requested_bytes),\n", + " HostBudgetJoin::Unreadable \{ .. \} => None,\n", + " \}\n", + " \}\n", + " pub fn label(&self) -> String \{\n", + " match self \{\n", + " HostBudgetJoin::Resolved \{ effective_bytes, requested_bytes, source, observed_bytes \} => match requested_bytes \{\n", + " Some(requested) => format!(\"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n", + " None => source.label(),\n", + " \},\n", + " HostBudgetJoin::Unreadable \{ reason \} => format!(\"unreadable: \{\}\", reason),\n", + " HostBudgetJoin::DeclaredUnverified \{ requested_bytes, reason \} => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; \{\}\", requested_bytes, reason),\n", + " \}\n", + " \}\n", + "\}\n\n", + "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin \{\n", + " let cgroup_v1_limit = match cgroup_v1_limit \{\n", + " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => \{\n", + " return HostBudgetJoin::Unreadable \{ reason: format!(\"cgroup v1 memory hierarchy at \{\} holds this process but its hierarchical_memory_limit is unreadable (\{\}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) \};\n", + " \}\n", + " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n", + " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n", + " \};\n", + " let observation = [\n", + " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh \{ cgroup_dir \}, b)),\n", + " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax \{ cgroup_dir \}, b)),\n", + " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit \{ cgroup_dir \}, b)),\n", + " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best \{ Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) \});\n", + " if let Some((source, observed_bytes)) = observation \{\n", + " return HostBudgetJoin::Resolved \{ effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes \};\n", + " \}\n", + " if let Some(bytes) = darwin_physical \{\n", + " return HostBudgetJoin::Resolved \{ effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes \};\n", + " \}\n", + " if let Some(requested_bytes) = env_override \{\n", + " return HostBudgetJoin::DeclaredUnverified \{ requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() \};\n", + " \}\n", + " HostBudgetJoin::Unreadable \{ reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os=\{\}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) \}\n", + "\}\n\n", + "pub fn read_host_budget_bytes() -> (Option, String) \{\n", + " let join = resolve_host_budget_join(\n", + " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n", + " host_budget_tightest_cgroup(\"memory.high\"),\n", + " host_budget_tightest_cgroup(\"memory.max\"),\n", + " host_budget_cgroup_v1(),\n", + " host_budget_darwin_physical(),\n", + " );\n", + " (join.bytes(), join.label())\n", + "\}\n\n", + "#[derive(Clone)]\n", + "pub enum HostBudgetCgroupV1 \{\n", + " Limited(u64),\n", + " Unlimited,\n", + " Unparseable(String),\n", + "\}\n\n", + "pub fn host_budget_tightest_cgroup_under(self_cg: &str, root: &std::path::Path, limit_file: &str) -> Option<(String, u64)> \{\n", + " let rel = self_cg\n", + " .lines()\n", + " .find_map(|l| l.strip_prefix(\"0::\"))\n", + " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n", + " let mut dir = root.join(&rel);\n", + " let mut best: Option<(u64, std::path::PathBuf)> = None;\n", + " loop \{\n", + " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) \{\n", + " let s = s.trim();\n", + " if s != \"max\" \{\n", + " if let Ok(v) = s.parse::() \{\n", + " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n", + " if take \{\n", + " best = Some((v, dir.clone()));\n", + " \}\n", + " \}\n", + " \}\n", + " \}\n", + " if dir == root || !dir.pop() \{\n", + " break;\n", + " \}\n", + " \}\n", + " best.map(|(v, d)| (d.display().to_string(), v))\n", + "\}\n\n", + "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", + " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + " host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new(\"/sys/fs/cgroup\"), limit_file)\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 \{\n", + " (i64::MAX as u64 / page_size) * page_size\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> \{\n", + " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n", + " let page_size = unsafe \{ libc::sysconf(libc::_SC_PAGESIZE) \};\n", + " if page_size <= 0 \{\n", + " return Some((\n", + " \"/proc/self/mountinfo\".to_string(),\n", + " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n", + " ));\n", + " \}\n", + " host_budget_cgroup_v1_under(std::path::Path::new(\"/\"), &self_cg, &mountinfo, page_size as u64)\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_under(fs_root: &std::path::Path, self_cg: &str, mountinfo: &str, page_size: u64) -> Option<(String, HostBudgetCgroupV1)> \{\n", + " let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?;\n", + " let dir_path = std::path::Path::new(&dir);\n", + " let joined = fs_root.join(dir_path.strip_prefix(\"/\").unwrap_or(dir_path));\n", + " let value = match std::fs::read_to_string(joined.join(\"memory.stat\")) \{\n", + " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n", + " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: \{\}\", e)),\n", + " \};\n", + " Some((joined.display().to_string(), value))\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option \{\n", + " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| \{\n", + " let fields: Vec<&str> = line.split(' ').collect();\n", + " let dash = fields.iter().position(|f| *f == \"-\")?;\n", + " let fstype = fields.get(dash + 1)?;\n", + " let super_opts = fields.get(dash + 3)?;\n", + " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") \{\n", + " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n", + " \} else \{\n", + " None\n", + " \}\n", + " \})?;\n", + " let path = self_cg.lines().find_map(|l| \{\n", + " let mut parts = l.splitn(3, ':');\n", + " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n", + " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n", + " \})?;\n", + " let rel = if mount_root == \"/\" \{\n", + " path.as_str()\n", + " \} else \{\n", + " let rest = path.strip_prefix(mount_root.as_str())?;\n", + " if !(rest.is_empty() || rest.starts_with('/')) \{ return None; \}\n", + " rest\n", + " \};\n", + " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 \{\n", + " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n", + " let [body] = hits.as_slice() else \{ return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); \};\n", + " let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size);\n", + " match body.trim().parse::() \{\n", + " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", + " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n", + " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n", + " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", + " \}\n", + "\}\n\n", + "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n", + "pub fn host_budget_darwin_physical() -> Option \{\n", + " #[cfg(target_os = \"macos\")]\n", + " \{\n", + " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n", + " let mut value: u64 = 0;\n", + " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n", + " let rc = unsafe \{ libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) \};\n", + " if rc == 0 && value > 0 \{ Some(value) \} else \{ None \}\n", + " \}\n", + " #[cfg(not(target_os = \"macos\"))]\n", + " \{ None \}\n", + "\}\n" + ) } fn rt_accelerator_demo_kernel() -> String { diff --git a/src/v1/stage0/src/bin/auth_declared_but_unwired_witness.rs b/src/v1/stage0/src/bin/auth_declared_but_unwired_witness.rs index f1c55bc9610..399cc0426dd 100644 --- a/src/v1/stage0/src/bin/auth_declared_but_unwired_witness.rs +++ b/src/v1/stage0/src/bin/auth_declared_but_unwired_witness.rs @@ -1,6 +1,5 @@ #![allow(clippy::disallowed_macros)] -use im::HashMap; use std::process::ExitCode; use std::rc::Rc; @@ -8,8 +7,7 @@ use v1_compiler::cli_run::workspace_root; use v1_compiler::v1_compiler_compile::{compile_to_resolved, ResolvedPipelineResult, SourceFile}; use v1_compiler::v1_interpreter::{self, AuthResolution, ExecutionMode, InterpError}; -type ModuleIndex = HashMap; -type WitnessCase = (&'static str, fn(&ModuleIndex)); +type WitnessCase = (&'static str, fn()); const SERVICE_AUTH_BEARER_NO_SOURCE: &str = r#"module auth_unwired_t1 @@ -259,108 +257,23 @@ fn source_roots() -> [std::path::PathBuf; 2] { [ws.join("src/v1"), ws.join("dag")] } -fn extract_module_declaration(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with("//") { - continue; - } - return trimmed - .strip_prefix("module ") - .and_then(|rest| rest.split_whitespace().next()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - } - None -} - -fn scan_dag_files(dir: &std::path::Path, index: &mut ModuleIndex) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - scan_dag_files(&path, index); - } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - if let Some(module_path) = extract_module_declaration(&path) { - index.insert(module_path, path); - } - } - } -} - -fn build_module_index() -> ModuleIndex { - let mut index = HashMap::new(); - for root in source_roots() { - if root.exists() { - scan_dag_files(&root, &mut index); - } - } - index -} - -fn extract_imports(source: &str) -> Vec { - let tokens = v1_compiler::v1_compiler_tokenize::tokenize( - source.to_string(), - "test.dag".to_string(), - v1_compiler::extdeps_languages_dag_syntax::dag_parse_environment(), - ); - let source_index = - v1_compiler::v1_std_core::build_newline_index("test.dag".to_string(), source.to_string()); - let mut source_indices = HashMap::new(); - source_indices.insert("test.dag".to_string(), source_index); - let result = v1_compiler::v1_compiler_parse::parse(tokens, Rc::new(source_indices)); - match &result.module { - Some(module) => v1_compiler::v1_std_core::module_imports(module.clone()) - .iter() - .map(|imp| imp.name.clone()) - .collect(), - None => vec![], - } +fn pool_root_strings() -> Vec { + source_roots() + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect() } -fn resolve_imports_transitively( - entry_path: &str, - entry_content: &str, - module_index: &ModuleIndex, -) -> Vec> { - let ws = workspace_root(); - let mut seen: HashMap> = HashMap::new(); - let mut queue = vec![(entry_path.to_string(), entry_content.to_string())]; - - while let Some((_path, content)) = queue.pop() { - for module_path in extract_imports(&content) { - if seen.contains_key(&module_path) { - continue; - } - if let Some(file_path) = module_index.get(&module_path) { - if let Ok(file_content) = std::fs::read_to_string(file_path) { - let rel_path = file_path - .strip_prefix(&ws) - .unwrap_or(file_path) - .to_string_lossy() - .to_string(); - seen.insert( - module_path.clone(), - Rc::new(SourceFile { - path: rel_path.clone(), - content: file_content.clone(), - }), - ); - queue.push((rel_path, file_content)); - } - } - } - } - - let mut sources: Vec> = seen.into_iter().map(|(_, v)| v).collect(); - sources.push(Rc::new(SourceFile { - path: entry_path.to_string(), - content: entry_content.to_string(), - })); - sources +/// Compile-subject closure of a witness entry. Not an import-line BFS. +fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Vec> { + v1_compiler::cli_run::resolve_seeded_compile_closure( + vec![Rc::new(SourceFile { + path: entry_path.to_string(), + content: entry_content.to_string(), + })], + &pool_root_strings(), + ) + .unwrap_or_else(|e| panic!("witness compile-subject closure: {e}")) } // A hard error is what the diagnostic disposition authority says blocks the interpreter; @@ -381,15 +294,15 @@ fn assert_resolved_no_hard_errors(result: &ResolvedPipelineResult) { ); } -fn resolve(module_index: &ModuleIndex, src: &str) -> Rc { - let sources = resolve_imports_transitively("test.dag", src, module_index); +fn resolve(src: &str) -> Rc { + let sources = resolve_imports_transitively("test.dag", src); let resolved = compile_to_resolved(Rc::new(sources.into())); assert_resolved_no_hard_errors(&resolved); resolved } -fn auth_declared_no_source_fails_closed_pre_send(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_AUTH_BEARER_NO_SOURCE); +fn auth_declared_no_source_fails_closed_pre_send() { + let resolved = resolve(SERVICE_AUTH_BEARER_NO_SOURCE); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -410,8 +323,8 @@ fn auth_declared_no_source_fails_closed_pre_send(module_index: &ModuleIndex) { } } -fn endpoint_by_reference_resolves_to_its_value(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_ENDPOINT_BY_REFERENCE); +fn endpoint_by_reference_resolves_to_its_value() { + let resolved = resolve(SERVICE_ENDPOINT_BY_REFERENCE); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -435,8 +348,8 @@ fn endpoint_by_reference_resolves_to_its_value(module_index: &ModuleIndex) { } } -fn endpoint_resolving_empty_refuses(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_ENDPOINT_RESOLVES_EMPTY); +fn endpoint_resolving_empty_refuses() { + let resolved = resolve(SERVICE_ENDPOINT_RESOLVES_EMPTY); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -461,8 +374,8 @@ fn endpoint_resolving_empty_refuses(module_index: &ModuleIndex) { } } -fn endpoint_resolving_non_string_refuses(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_ENDPOINT_RESOLVES_NON_STRING); +fn endpoint_resolving_non_string_refuses() { + let resolved = resolve(SERVICE_ENDPOINT_RESOLVES_NON_STRING); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -488,8 +401,8 @@ fn endpoint_resolving_non_string_refuses(module_index: &ModuleIndex) { } } -fn endpoint_absent_refuses(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_ENDPOINT_ABSENT); +fn endpoint_absent_refuses() { + let resolved = resolve(SERVICE_ENDPOINT_ABSENT); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -514,8 +427,8 @@ fn endpoint_absent_refuses(module_index: &ModuleIndex) { } } -fn auth_input_empty_fails_closed_pre_send(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_AUTH_INPUT_NOT_PROVIDED); +fn auth_input_empty_fails_closed_pre_send() { + let resolved = resolve(SERVICE_AUTH_INPUT_NOT_PROVIDED); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -528,8 +441,8 @@ fn auth_input_empty_fails_closed_pre_send(module_index: &ModuleIndex) { } } -fn no_auth_declared_does_not_fire_guard(module_index: &ModuleIndex) { - let resolved = resolve(module_index, SERVICE_NO_AUTH); +fn no_auth_declared_does_not_fire_guard() { + let resolved = resolve(SERVICE_NO_AUTH); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -546,14 +459,14 @@ fn no_auth_declared_does_not_fire_guard(module_index: &ModuleIndex) { } } -fn resolve_auth_three_way_split_matches_dispatch_behavior(module_index: &ModuleIndex) { +fn resolve_auth_three_way_split_matches_dispatch_behavior() { let cases: &[(&str, &str, bool)] = &[ ("Bearer + no source", SERVICE_AUTH_BEARER_NO_SOURCE, true), ("auth_input empty", SERVICE_AUTH_INPUT_NOT_PROVIDED, true), ("no auth declared", SERVICE_NO_AUTH, false), ]; for (label, src, expect_unwired) in cases { - let resolved = resolve(module_index, src); + let resolved = resolve(src); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -571,10 +484,10 @@ fn resolve_auth_three_way_split_matches_dispatch_behavior(module_index: &ModuleI // Regression guard: dual-declare (auth_input + auth_source), api_key empty but env var present → // must fall through to auth_source and NOT raise AuthDeclaredButUnwired. -fn dual_declare_env_var_fallback_resolves_when_input_empty(module_index: &ModuleIndex) { +fn dual_declare_env_var_fallback_resolves_when_input_empty() { // Set a synthetic env var the service fixture reads. std::env::set_var("TEST_AUTH_GUARD_DUAL_FALLBACK_VAR", "test-token-sentinel"); - let resolved = resolve(module_index, SERVICE_DUAL_DECLARE); + let resolved = resolve(SERVICE_DUAL_DECLARE); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -594,9 +507,9 @@ fn dual_declare_env_var_fallback_resolves_when_input_empty(module_index: &Module } // Dual-declare, api_key empty AND env var absent → guard must still fire (fail-closed). -fn dual_declare_both_empty_fails_closed(module_index: &ModuleIndex) { +fn dual_declare_both_empty_fails_closed() { std::env::remove_var("TEST_AUTH_GUARD_DUAL_FALLBACK_VAR"); - let resolved = resolve(module_index, SERVICE_DUAL_DECLARE); + let resolved = resolve(SERVICE_DUAL_DECLARE); let graph = resolved.graph.as_ref().expect("graph"); let ctx = v1_interpreter::InterpContext::new( graph, @@ -614,7 +527,7 @@ fn dual_declare_both_empty_fails_closed(module_index: &ModuleIndex) { // Pub-API smoke: confirms the 3 variants are reachable from outside v1_compiler. // Execution discrimination lives in the wet-dispatch tests above. -fn auth_resolution_enum_is_pub_and_discriminable(_module_index: &ModuleIndex) { +fn auth_resolution_enum_is_pub_and_discriminable() { let _ = AuthResolution::NoAuthDeclared; let _ = AuthResolution::Resolved { header: "Authorization".to_string(), @@ -626,8 +539,6 @@ fn auth_resolution_enum_is_pub_and_discriminable(_module_index: &ModuleIndex) { } fn main() -> ExitCode { - let module_index = build_module_index(); - let tests: Vec = vec![ ( "auth_declared_no_source_fails_closed_pre_send", @@ -673,8 +584,7 @@ fn main() -> ExitCode { ]; for (name, test) in tests { - let index = module_index.clone(); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| test(&index))); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)); if result.is_err() { return fail(format!("{name} panicked")); } diff --git a/src/v1/stage0/src/bin/bootstrap_witness.rs b/src/v1/stage0/src/bin/bootstrap_witness.rs index a67982f4aec..b8d69b613bc 100644 --- a/src/v1/stage0/src/bin/bootstrap_witness.rs +++ b/src/v1/stage0/src/bin/bootstrap_witness.rs @@ -1,6 +1,5 @@ #![allow(clippy::disallowed_macros)] -use im::HashMap; use std::process::ExitCode; use std::rc::Rc; @@ -53,127 +52,23 @@ where cold.output().expect("failed to spawn cargo cold retry") } -fn tokenize_for_parse(source: &str) -> Rc>> { - v1_compiler::v1_compiler_tokenize::tokenize( - source.to_string(), - "test.dag".to_string(), - v1_compiler::extdeps_languages_dag_syntax::dag_parse_environment(), - ) -} - -fn parse_source(source: &str) -> Rc { - let tokens = tokenize_for_parse(source); - let source_index = - v1_compiler::v1_std_core::build_newline_index("test.dag".to_string(), source.to_string()); - let mut source_indices = HashMap::new(); - source_indices.insert("test.dag".to_string(), source_index); - v1_compiler::v1_compiler_parse::parse(tokens, Rc::new(source_indices)) -} - -fn build_module_index_for_roots( - roots: &[std::path::PathBuf], -) -> HashMap { - let mut index = HashMap::new(); - for root in roots { - if root.exists() { - scan_dag_files(root, &mut index); - } - } - index -} - -fn build_module_index() -> HashMap { - build_module_index_for_roots(&source_roots()) -} - -fn scan_dag_files(dir: &std::path::Path, index: &mut HashMap) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - scan_dag_files(&path, index); - } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - if let Some(module_path) = extract_module_declaration(&path) { - index.insert(module_path, path); - } - } - } -} - -fn extract_module_declaration(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with("//") { - continue; - } - return trimmed - .strip_prefix("module ") - .and_then(|rest| rest.split_whitespace().next()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - } - None -} - -fn extract_imports(source: &str) -> Vec { - let result = parse_source(source); - match &result.module { - Some(module) => v1_compiler::v1_std_core::module_imports(module.clone()) - .iter() - .map(|imp| imp.name.clone()) - .collect(), - None => vec![], - } -} - -fn resolve_imports_transitively_with_index( - entry_path: &str, - entry_content: &str, - module_index: &HashMap, -) -> Vec> { - let ws = workspace_root(); - let mut seen: HashMap> = HashMap::new(); - let mut queue: Vec<(String, String)> = Vec::new(); - - queue.push((entry_path.to_string(), entry_content.to_string())); - - while let Some((_path, content)) = queue.pop() { - let imports = extract_imports(&content); - for module_path in imports { - if seen.contains_key(&module_path) { - continue; - } - if let Some(file_path) = module_index.get(&module_path) { - if let Ok(file_content) = std::fs::read_to_string(file_path) { - let rel_path = file_path - .strip_prefix(&ws) - .unwrap_or(file_path) - .to_string_lossy() - .to_string(); - let source = Rc::new(SourceFile { - path: rel_path.clone(), - content: file_content.clone(), - }); - seen.insert(module_path.clone(), source); - queue.push((rel_path, file_content)); - } - } - } - } - - let mut sources: Vec> = seen.into_iter().map(|(_, v)| v).collect(); - sources.push(Rc::new(SourceFile { - path: entry_path.to_string(), - content: entry_content.to_string(), - })); - sources +fn pool_root_strings() -> Vec { + source_roots() + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect() } +/// Compile-subject closure of a witness entry. Not an import-line BFS. fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Vec> { - resolve_imports_transitively_with_index(entry_path, entry_content, &build_module_index()) + v1_compiler::cli_run::resolve_seeded_compile_closure( + vec![Rc::new(SourceFile { + path: entry_path.to_string(), + content: entry_content.to_string(), + })], + &pool_root_strings(), + ) + .unwrap_or_else(|e| panic!("witness compile-subject closure: {e}")) } fn compile_dag_named(filename: &str, source: &str, target: RenderTarget) -> Rc { diff --git a/src/v1/stage0/src/bin/diagnostics_witness.rs b/src/v1/stage0/src/bin/diagnostics_witness.rs index 4ea5d568a58..2a538b679c1 100644 --- a/src/v1/stage0/src/bin/diagnostics_witness.rs +++ b/src/v1/stage0/src/bin/diagnostics_witness.rs @@ -18,8 +18,7 @@ use v1_compiler::v1_std_core::{ CompilerDiagnostic, ErrorNode, }; -type ModuleIndex = HashMap; -type WitnessCase = (&'static str, fn(&ModuleIndex)); +type WitnessCase = (&'static str, fn()); const SUITE_IMPORT_RESOLUTION: &str = "import_resolution"; const SUITE_REEXPORT_SURFACE: &str = "reexport_surface"; @@ -39,114 +38,37 @@ fn source_roots() -> [std::path::PathBuf; 2] { [ws.join("src/v1"), ws.join("dag")] } -fn extract_module_declaration(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with("//") { - continue; - } - return trimmed - .strip_prefix("module ") - .and_then(|rest| rest.split_whitespace().next()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - } - None -} - -fn scan_dag_files(dir: &std::path::Path, index: &mut ModuleIndex) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - scan_dag_files(&path, index); - } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - if let Some(module_path) = extract_module_declaration(&path) { - index.insert(module_path, path); - } - } - } -} - -fn build_module_index() -> ModuleIndex { - let mut index = HashMap::new(); - for root in source_roots() { - if root.exists() { - scan_dag_files(&root, &mut index); - } - } - index -} - -fn extract_imports(source: &str) -> Vec { - let tokens = v1_compiler::v1_compiler_tokenize::tokenize( - source.to_string(), - "test.dag".to_string(), - v1_compiler::extdeps_languages_dag_syntax::dag_parse_environment(), - ); - let source_index = - v1_compiler::v1_std_core::build_newline_index("test.dag".to_string(), source.to_string()); - let mut source_indices = HashMap::new(); - source_indices.insert("test.dag".to_string(), source_index); - let result = v1_compiler::v1_compiler_parse::parse(tokens, Rc::new(source_indices)); - match &result.module { - Some(module) => v1_compiler::v1_std_core::module_imports(module.clone()) - .iter() - .map(|imp| imp.name.clone()) - .collect(), - None => vec![], - } +fn pool_root_strings() -> Vec { + source_roots() + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect() } -fn resolve_imports_transitively( +/// Compile-subject closure of a witness entry. Not an import-line BFS. +fn resolve_over_roots( entry_path: &str, entry_content: &str, - module_index: &ModuleIndex, + pool_roots: &[String], ) -> Vec> { - let ws = workspace_root(); - let mut seen: HashMap> = HashMap::new(); - let mut queue = vec![(entry_path.to_string(), entry_content.to_string())]; - - while let Some((_path, content)) = queue.pop() { - for module_path in extract_imports(&content) { - if seen.contains_key(&module_path) { - continue; - } - if let Some(file_path) = module_index.get(&module_path) { - if let Ok(file_content) = std::fs::read_to_string(file_path) { - let rel_path = file_path - .strip_prefix(&ws) - .unwrap_or(file_path) - .to_string_lossy() - .to_string(); - seen.insert( - module_path.clone(), - Rc::new(SourceFile { - path: rel_path.clone(), - content: file_content.clone(), - }), - ); - queue.push((rel_path, file_content)); - } - } - } - } + v1_compiler::cli_run::resolve_seeded_compile_closure( + vec![Rc::new(SourceFile { + path: entry_path.to_string(), + content: entry_content.to_string(), + })], + pool_roots, + ) + .unwrap_or_else(|e| panic!("witness compile-subject closure: {e}")) +} - let mut sources: Vec> = seen.into_iter().map(|(_, v)| v).collect(); - sources.push(Rc::new(SourceFile { - path: entry_path.to_string(), - content: entry_content.to_string(), - })); - sources +fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Vec> { + resolve_over_roots(entry_path, entry_content, &pool_root_strings()) } -fn compile_multi(module_index: &ModuleIndex, files: &[(&str, &str)]) -> Rc { +fn compile_multi(files: &[(&str, &str)]) -> Rc { let mut all_sources: HashMap> = HashMap::new(); for (path, content) in files { - let resolved = resolve_imports_transitively(path, content, module_index); + let resolved = resolve_imports_transitively(path, content); for src in resolved { all_sources.entry(src.path.clone()).or_insert(src); } @@ -170,7 +92,7 @@ fn type_mismatch_count(result: &PipelineResult) -> usize { .count() } -fn establishment_report(module_index: &ModuleIndex) { +fn establishment_report() { let probes: &[(&str, &str, &str)] = &[ ( "distinct_record_at_arg", @@ -229,7 +151,7 @@ fn establishment_report(module_index: &ModuleIndex) { ), ]; for (label, path, source) in probes { - let result = compile_multi(module_index, &[(path, source)]); + let result = compile_multi(&[(path, source)]); eprintln!( "establishment {label}: type_mismatch={} total_diags={}", type_mismatch_count(&result), @@ -265,13 +187,11 @@ fn first_arity_mismatch_message(result: &PipelineResult) -> Option { /// Golden: MissingExport names export/module/importer and span points at the missing name. /// Also covers UnresolvedImport variant + grounded message. -fn import_resolution_variants_and_span(module_index: &ModuleIndex) { +fn import_resolution_variants_and_span() { let provider = "module provider\ntype User { name: String }\n"; let missing_export = "module consumer\nimport provider { NonExistent }\n"; - let missing_result = compile_multi( - module_index, - &[("provider.dag", provider), ("consumer.dag", missing_export)], - ); + let missing_result = + compile_multi(&[("provider.dag", provider), ("consumer.dag", missing_export)]); assert_eq!(missing_result.diagnostics.len(), 1); let missing = &missing_result.diagnostics[0]; assert!( @@ -303,7 +223,7 @@ fn import_resolution_variants_and_span(module_index: &ModuleIndex) { ); let unresolved = "module consumer\nimport nonexistent { Thing }\n"; - let unresolved_result = compile_multi(module_index, &[("consumer.dag", unresolved)]); + let unresolved_result = compile_multi(&[("consumer.dag", unresolved)]); assert!( !unresolved_result.diagnostics.is_empty(), "expected UnresolvedImport diagnostic" @@ -323,7 +243,7 @@ fn import_resolution_variants_and_span(module_index: &ModuleIndex) { } /// Golden: variant not re-exported through a type-only import surface. -fn reexport_surface_missing_variant(module_index: &ModuleIndex) { +fn reexport_surface_missing_variant() { let files = &[ ("def.dag", "module self_gen8_def\ntype E = A | B\n"), ( @@ -335,7 +255,7 @@ fn reexport_surface_missing_variant(module_index: &ModuleIndex) { "module self_gen8_use\nimport self_gen8_proxy { B }\n", ), ]; - let result = compile_multi(module_index, files); + let result = compile_multi(files); assert_eq!(result.diagnostics.len(), 1); let d = &result.diagnostics[0]; assert!( @@ -350,9 +270,9 @@ fn reexport_surface_missing_variant(module_index: &ModuleIndex) { /// Golden: UnresolvedType names the unknown type; ArityMismatch fires on bare containers /// but not on parameterized std containers or user-defined types. -fn type_and_arity_discrimination(module_index: &ModuleIndex) { +fn type_and_arity_discrimination() { let unresolved_source = "module types\ntype Wrapper { inner: Bogus }\n"; - let unresolved_result = compile_multi(module_index, &[("types.dag", unresolved_source)]); + let unresolved_result = compile_multi(&[("types.dag", unresolved_source)]); let unresolved: Vec<_> = unresolved_result .diagnostics .iter() @@ -367,7 +287,7 @@ fn type_and_arity_discrimination(module_index: &ModuleIndex) { assert!(unresolved_msg.contains("Bogus"), "{unresolved_msg}"); let bare = "module bare\nimport std.types { List }\ntype Foo { items: List }\n"; - let bare_result = compile_multi(module_index, &[("bare.dag", bare)]); + let bare_result = compile_multi(&[("bare.dag", bare)]); assert!( has_arity_mismatch(&bare_result), "bare List should trigger ArityMismatch, got: {:?}", @@ -377,7 +297,7 @@ fn type_and_arity_discrimination(module_index: &ModuleIndex) { assert!(bare_msg.contains("List"), "ArityMismatch should name List"); let parameterized = "module param\nimport std.types { List }\ntype Foo { items: List }\n"; - let parameterized_result = compile_multi(module_index, &[("param.dag", parameterized)]); + let parameterized_result = compile_multi(&[("param.dag", parameterized)]); assert!( !has_arity_mismatch(¶meterized_result), "parameterized List should not trigger ArityMismatch, got: {:?}", @@ -385,7 +305,7 @@ fn type_and_arity_discrimination(module_index: &ModuleIndex) { ); let user_defined = "module custom\ntype Widget { label: String }\ntype Bag { item: Widget }\n"; - let user_defined_result = compile_multi(module_index, &[("custom.dag", user_defined)]); + let user_defined_result = compile_multi(&[("custom.dag", user_defined)]); assert!( !has_arity_mismatch(&user_defined_result), "user-defined type should not trigger ArityMismatch, got: {:?}", @@ -394,9 +314,9 @@ fn type_and_arity_discrimination(module_index: &ModuleIndex) { } /// Golden: empty list literal fails without collection context; succeeds with List context. -fn empty_list_literal_context(module_index: &ModuleIndex) { +fn empty_list_literal_context() { let wrong = "module elist\nfn make_stuff() -> String {\n []\n}\n"; - let wrong_result = compile_multi(module_index, &[("elist.dag", wrong)]); + let wrong_result = compile_multi(&[("elist.dag", wrong)]); let wrong_diags: Vec<_> = wrong_result .diagnostics .iter() @@ -418,7 +338,7 @@ fn empty_list_literal_context(module_index: &ModuleIndex) { let ok = "module elist_ok\nimport std.types { List }\nfn make_list() -> List {\n []\n}\n"; - let ok_result = compile_multi(module_index, &[("elist_ok.dag", ok)]); + let ok_result = compile_multi(&[("elist_ok.dag", ok)]); let ok_diags: Vec<_> = ok_result .diagnostics .iter() @@ -444,13 +364,13 @@ fn empty_list_literal_context(module_index: &ModuleIndex) { /// deleted expected-type pick) must now raise VariantCollision. RED control: /// an unbound constructor literal must raise UnresolvedType. GREEN control: /// a sole-owner arm constructs clean. -fn constructor_owner_ruling_walls(module_index: &ModuleIndex) { +fn constructor_owner_ruling_walls() { let collided = "module test.claim.variant_owner_expected_type\n\ import std.logic { Bool }\n\ type AEarlyOwner = SharedVariant | AOnlyVariant\n\ type ZLaterOwner = SharedVariant | ZOnlyVariant\n\ fn make_z_variant() -> ZLaterOwner { SharedVariant }\n"; - let collided_result = compile_multi(module_index, &[("variant_owner.dag", collided)]); + let collided_result = compile_multi(&[("variant_owner.dag", collided)]); assert!( collided_result.diagnostics.iter().any(|d| matches!( &*d.diagnostic, @@ -462,7 +382,7 @@ fn constructor_owner_ruling_walls(module_index: &ModuleIndex) { let unbound = "module test.claim.unbound_ctor\n\ fn mk() -> Int { let g = GhostArm { x: 1 } 2 }\n"; - let unbound_result = compile_multi(module_index, &[("unbound_ctor.dag", unbound)]); + let unbound_result = compile_multi(&[("unbound_ctor.dag", unbound)]); assert!( unbound_result.diagnostics.iter().any(|d| matches!( &*d.diagnostic, @@ -475,7 +395,7 @@ fn constructor_owner_ruling_walls(module_index: &ModuleIndex) { let sole = "module test.claim.sole_owner\n\ type OnlyOwner = SoleArm | OtherArm\n\ fn mk() -> OnlyOwner { SoleArm }\n"; - let sole_result = compile_multi(module_index, &[("sole_owner.dag", sole)]); + let sole_result = compile_multi(&[("sole_owner.dag", sole)]); let sole_hard: Vec = diagnostic_messages(&sole_result) .into_iter() .filter(|m| !m.starts_with("complexity: ")) @@ -526,13 +446,13 @@ fn suite_cases(suite: &str) -> Result, String> { /// value cannot fill a differently-named declared formal (TypeMismatch) — in record /// fields and direct call args alike. Greens pin the sanctioned skips: optional /// fields and dag_can_cast pairs (Int->Float). -fn record_literal_field_walls(module_index: &ModuleIndex) { +fn record_literal_field_walls() { // RED: omitted required field on a record literal let missing = "module fieldwall_missing\n\ type Ev = EvA | EvB { n: Int, m: Int }\n\ type Prov { id: String, ev: Ev }\n\ data p: Prov = Prov { id: \"x\" }\n"; - let missing_result = compile_multi(module_index, &[("fieldwall_missing.dag", missing)]); + let missing_result = compile_multi(&[("fieldwall_missing.dag", missing)]); let missing_diags: Vec<_> = missing_result .diagnostics .iter() @@ -551,7 +471,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { let variant_missing = "module fieldwall_variant\n\ type Ev = EvA | EvB { n: Int, m: Int }\n\ data v: Ev = EvB { n: 1 }\n"; - let variant_result = compile_multi(module_index, &[("fieldwall_variant.dag", variant_missing)]); + let variant_result = compile_multi(&[("fieldwall_variant.dag", variant_missing)]); let variant_diags: Vec<_> = variant_result .diagnostics .iter() @@ -568,7 +488,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Ev = EvA | EvB { n: Int, m: Int }\n\ type Prov { id: String, ev: Ev }\n\ data q: Prov = Prov { id: \"x\", ev: 42 }\n"; - let wrong_result = compile_multi(module_index, &[("fieldwall_wrongtype.dag", wrong_typed)]); + let wrong_result = compile_multi(&[("fieldwall_wrongtype.dag", wrong_typed)]); let wrong_diags: Vec<_> = wrong_result .diagnostics .iter() @@ -584,7 +504,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { let kernel_kernel = "module fieldwall_kernel\n\ type Prov2 { id: String }\n\ data k: Prov2 = Prov2 { id: 7 }\n"; - let kernel_result = compile_multi(module_index, &[("fieldwall_kernel.dag", kernel_kernel)]); + let kernel_result = compile_multi(&[("fieldwall_kernel.dag", kernel_kernel)]); assert!( kernel_result .diagnostics @@ -600,8 +520,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Box { label: String }\n\ fn takes_node(n: Node) -> Bool { true }\n\ fn bad_node_arg() -> Bool { takes_node(n: Box { label: \"x\" }) }\n"; - let node_arg_result = - compile_multi(module_index, &[("fieldwall_node_arg.dag", record_at_node)]); + let node_arg_result = compile_multi(&[("fieldwall_node_arg.dag", record_at_node)]); assert!( node_arg_result .diagnostics @@ -617,10 +536,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Box { label: String }\n\ type Prov { id: String, ev: Ev }\n\ data r: Prov = Prov { id: \"x\", ev: Box { label: \"y\" } }\n"; - let record_union_result = compile_multi( - module_index, - &[("fieldwall_record_at_union.dag", record_at_union)], - ); + let record_union_result = compile_multi(&[("fieldwall_record_at_union.dag", record_at_union)]); assert!( record_union_result .diagnostics @@ -636,10 +552,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Other = OtherA | OtherB { k: Int }\n\ type Prov { id: String, ev: Ev }\n\ data w: Prov = Prov { id: \"x\", ev: OtherA { k: 1 } }\n"; - let wrong_variant_result = compile_multi( - module_index, - &[("fieldwall_wrong_variant.dag", wrong_variant)], - ); + let wrong_variant_result = compile_multi(&[("fieldwall_wrong_variant.dag", wrong_variant)]); assert!( wrong_variant_result .diagnostics @@ -660,13 +573,10 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { dissolves_to: Terminal { reason: \"phantom-mechanism\" },\n\ bind: \"anchor\",\n\ }\n"; - let missing_variant_mechanism_result = compile_multi( - module_index, - &[( - "fieldwall_missing_variant_mechanism.dag", - missing_variant_mechanism, - )], - ); + let missing_variant_mechanism_result = compile_multi(&[( + "fieldwall_missing_variant_mechanism.dag", + missing_variant_mechanism, + )]); assert!( missing_variant_mechanism_result .diagnostics @@ -686,13 +596,10 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { dissolves_to: SingleAuthority,\n\ bind: \"anchor\",\n\ }\n"; - let missing_variant_mechanism_green_result = compile_multi( - module_index, - &[( - "fieldwall_missing_variant_mechanism_green.dag", - missing_variant_mechanism_green, - )], - ); + let missing_variant_mechanism_green_result = compile_multi(&[( + "fieldwall_missing_variant_mechanism_green.dag", + missing_variant_mechanism_green, + )]); assert!( !missing_variant_mechanism_green_result .diagnostics @@ -707,7 +614,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Ev = EvA | EvB { n: Int, m: Int }\n\ fn wants(e: Ev) -> Int { 7 }\n\ fn call_bad() -> Int { wants(e: 9) }\n"; - let call_result = compile_multi(module_index, &[("fieldwall_callarg.dag", call_arg)]); + let call_result = compile_multi(&[("fieldwall_callarg.dag", call_arg)]); assert!( call_result .diagnostics @@ -722,7 +629,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Ev = EvA | EvB { n: Int, m: Int }\n\ type Prov { id: String, ev: Ev }\n\ data g: Prov = Prov { id: \"x\", ev: EvA }\n"; - let complete_result = compile_multi(module_index, &[("fieldwall_green.dag", complete)]); + let complete_result = compile_multi(&[("fieldwall_green.dag", complete)]); assert!( !complete_result.diagnostics.iter().any(|d| matches!( &*d.diagnostic, @@ -737,13 +644,10 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { type Payload { n: Int }\n\ type Holder { value: Payload? }\n\ data h: Holder = Holder { value: Present { value: Payload { n: 1 } } }\n"; - let optional_wrapper_green_result = compile_multi( - module_index, - &[( - "fieldwall_optional_wrapper_green.dag", - optional_wrapper_green, - )], - ); + let optional_wrapper_green_result = compile_multi(&[( + "fieldwall_optional_wrapper_green.dag", + optional_wrapper_green, + )]); assert!( !optional_wrapper_green_result .diagnostics @@ -764,10 +668,8 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { }\n\ }\n\ }\n"; - let optional_wrapper_red_result = compile_multi( - module_index, - &[("fieldwall_optional_wrapper_red.dag", optional_wrapper_red)], - ); + let optional_wrapper_red_result = + compile_multi(&[("fieldwall_optional_wrapper_red.dag", optional_wrapper_red)]); assert!( optional_wrapper_red_result .diagnostics @@ -781,7 +683,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { let optional = "module fieldwall_opt\n\ type Opt { a: Int, b: Int? }\n\ data o: Opt = Opt { a: 1 }\n"; - let optional_result = compile_multi(module_index, &[("fieldwall_opt.dag", optional)]); + let optional_result = compile_multi(&[("fieldwall_opt.dag", optional)]); assert!( !optional_result .diagnostics @@ -795,7 +697,7 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { let cast_ok = "module fieldwall_cast\n\ type Fl { x: Float }\n\ data f: Fl = Fl { x: 3 }\n"; - let cast_result = compile_multi(module_index, &[("fieldwall_cast.dag", cast_ok)]); + let cast_result = compile_multi(&[("fieldwall_cast.dag", cast_ok)]); assert!( !cast_result .diagnostics @@ -806,10 +708,9 @@ fn record_literal_field_walls(module_index: &ModuleIndex) { ); } -fn run_suite(module_index: &ModuleIndex, suite: &str) -> Result<(), String> { +fn run_suite(suite: &str) -> Result<(), String> { for (name, test) in suite_cases(suite)? { - let index = module_index.clone(); - if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| test(&index))).is_err() { + if std::panic::catch_unwind(std::panic::AssertUnwindSafe(test)).is_err() { return Err(format!("{name} panicked")); } } @@ -823,8 +724,7 @@ fn main() -> ExitCode { return fail("usage: diagnostics_witness "); } - let module_index = build_module_index(); - match run_suite(&module_index, suite) { + match run_suite(suite) { Ok(()) => ExitCode::SUCCESS, Err(msg) => fail(msg), } diff --git a/src/v1/stage0/src/bin/infer_semantics_witness.rs b/src/v1/stage0/src/bin/infer_semantics_witness.rs index e0e79713b8a..30cf3619e0a 100644 --- a/src/v1/stage0/src/bin/infer_semantics_witness.rs +++ b/src/v1/stage0/src/bin/infer_semantics_witness.rs @@ -4,7 +4,6 @@ use im::HashMap; use im::{vector as vec, Vector as Vec}; use std::process::ExitCode; use std::rc::Rc; -use v1_compiler::v1_rt::VecCompat; use v1_compiler::cli_run::workspace_root; use v1_compiler::std_induction::SubValueRelation; @@ -45,119 +44,33 @@ fn source_roots() -> [std::path::PathBuf; 2] { [ws.join("src/v1"), ws.join("dag")] } -fn extract_module_declaration(path: &std::path::Path) -> Option { - let content = std::fs::read_to_string(path).ok()?; - for line in content.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with("//") { - continue; - } - return trimmed - .strip_prefix("module ") - .and_then(|rest| rest.split_whitespace().next()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()); - } - None -} - -fn scan_dag_files(dir: &std::path::Path, index: &mut HashMap) { - let Ok(entries) = std::fs::read_dir(dir) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - scan_dag_files(&path, index); - } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - if let Some(module_path) = extract_module_declaration(&path) { - index.insert(module_path, path); - } - } - } -} - -fn build_module_index() -> HashMap { - let mut index = HashMap::new(); - for root in source_roots() { - if root.exists() { - scan_dag_files(&root, &mut index); - } - } - index -} - -fn extract_imports(source: &str) -> Vec { - let tokens = v1_compiler::v1_compiler_tokenize::tokenize( - source.to_string(), - "test.dag".to_string(), - v1_compiler::extdeps_languages_dag_syntax::dag_parse_environment(), - ); - let source_index = - v1_compiler::v1_std_core::build_newline_index("test.dag".to_string(), source.to_string()); - let mut source_indices = HashMap::new(); - source_indices.insert("test.dag".to_string(), source_index); - let result = v1_compiler::v1_compiler_parse::parse(tokens, Rc::new(source_indices)); - match &result.module { - Some(module) => v1_compiler::v1_std_core::module_imports(module.clone()) - .iter() - .map(|imp| imp.name.clone()) - .collect(), - None => vec![], - } +fn pool_root_strings() -> std::vec::Vec { + source_roots() + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect() } -fn resolve_imports_transitively( - entry_path: &str, - entry_content: &str, - module_index: &HashMap, -) -> Vec> { - let ws = workspace_root(); - let mut seen: HashMap> = HashMap::new(); - let mut queue = vec![(entry_path.to_string(), entry_content.to_string())]; - - while let Some((_path, content)) = queue.pop_back() { - for module_path in extract_imports(&content) { - if seen.contains_key(&module_path) { - continue; - } - if let Some(file_path) = module_index.get(&module_path) { - if let Ok(file_content) = std::fs::read_to_string(file_path) { - let rel_path = file_path - .strip_prefix(&ws) - .unwrap_or(file_path) - .to_string_lossy() - .to_string(); - seen.insert( - module_path.clone(), - Rc::new(SourceFile { - path: rel_path.clone(), - content: file_content.clone(), - }), - ); - queue.push((rel_path, file_content)); - } - } - } - } - - let mut sources: Vec> = seen.into_iter().map(|(_, v)| v).collect(); - sources.push(Rc::new(SourceFile { - path: entry_path.to_string(), - content: entry_content.to_string(), - })); - sources +/// Compile-subject closure of a witness entry. Not an import-line BFS. +fn resolve_imports_transitively(entry_path: &str, entry_content: &str) -> Vec> { + v1_compiler::cli_run::resolve_seeded_compile_closure( + std::vec![Rc::new(SourceFile { + path: entry_path.to_string(), + content: entry_content.to_string(), + })], + &pool_root_strings(), + ) + .unwrap_or_else(|e| panic!("witness compile-subject closure: {e}")) + .into() } fn compile_dag(source: &str) -> Rc { - let module_index = build_module_index(); - let sources = resolve_imports_transitively("test.dag", source, &module_index); + let sources = resolve_imports_transitively("test.dag", source); compile_sources(Rc::new(sources), RenderTarget::Rust) } fn compile_dag_resolved(source: &str) -> Rc { - let module_index = build_module_index(); - let sources = resolve_imports_transitively("test.dag", source, &module_index); + let sources = resolve_imports_transitively("test.dag", source); compile_to_resolved(Rc::new(sources)) } diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index e8fd87d1520..af43c89b1d7 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -50,7 +50,7 @@ use crate::module_path_index::{ }; use crate::std_node::compiler_recursive_types; use crate::std_syntax::LiteralValue; -use crate::std_types::{kernel_type_set, SourceSpan}; +use crate::std_types::{is_kernel_type, kernel_type_set, SourceSpan}; use crate::v1_compiler_compile; use crate::v1_compiler_infer; use crate::v1_compiler_infer_env::{ @@ -77,9 +77,9 @@ use crate::v1_std_core::{ has_child_named, inferred_to_node, intern, is_error_diagnostic, is_interpreter_blocking_diagnostic, let_binding_name_at, let_value, make_error_node, match_arm_nodes, match_scrutinee, method_arg_nodes, method_receiver, module_items, no_span, - param_node_name_at, param_node_type_expr, Cardinality, CompilerDiagnostic, Connective, - ErrorNode, ExprData, ExprErrorKind, InferredNode, InternTable, LeafOwner, MatchPattern, - NewlineIndex, Node, + param_node_name_at, param_node_type_expr, type_reference_identity, Cardinality, + CompilerDiagnostic, Connective, ErrorNode, ExprData, ExprErrorKind, InferredNode, InternTable, + LeafOwner, MatchPattern, NewlineIndex, Node, TypeDeclarationProvenance, TypeReferenceIdentity, }; use serde::Serialize; @@ -329,19 +329,16 @@ pub(crate) fn is_cargo_target_output_dir( pub(crate) fn collect_dag_files_result( dir: &std::path::Path, - files: &mut Vec, + files: &mut Vec, ) -> Result<(), String> { - derived_row_roster::ensure_if_row_dir(dir).map_err(|e| { - format!( - "failed to derive recurring_failure_mode roster in {:?}: {}", - dir, e - ) - })?; let mut entries: Vec<_> = std::fs::read_dir(dir) .map_err(|e| format!("failed to read dir {:?}: {}", dir, e))? .map(|e| e.map_err(|e| format!("failed to read dir entry in {:?}: {}", dir, e))) .collect::, String>>()?; entries.sort_by_key(|e| e.file_name()); + // This directory's own `.dag` files are admitted as one listing, so a ledger row directory + // contributes its derived roster from the same bytes (`derived_row_roster::acquire_dir_files`). + let mut here = Vec::new(); for entry in entries { let path = entry.path(); if path.is_dir() { @@ -350,13 +347,14 @@ pub(crate) fn collect_dag_files_result( } collect_dag_files_result(&path, files)?; } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - files.push(path); + here.push(path); } } + files.extend(derived_row_roster::acquire_dir_files(dir, here)?); Ok(()) } -fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) { +fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) { collect_dag_files_result(dir, files).unwrap_or_else(|e| panic!("{e}")); } @@ -405,8 +403,9 @@ fn moduleless_dag_entry_paths_under_root(root_prefix: &str) -> Result = Vec::new(); - for path in paths { - let content = std::fs::read_to_string(&path).map_err(|error| { + for source in paths { + let path = source.path(); + let content = source.read().map_err(|error| { format!( "cannot read {} while taking the module-less population under {root_prefix}: \ {error} (the population exists to report which files were dropped from the \ @@ -3239,16 +3238,52 @@ mod process_workspace_root_tests { /// fixture spellings. Witness: `dag/test/claim/cli_run_repo_grant_hand_rust_equivalence_witness_test.dag`. #[cfg(test)] mod cli_run_arg_channel_tests { - use super::parse_run_args; - use crate::v1_interpreter::Value; + use super::{ + bind_run_arg_specs_against_admissions, bind_run_args_for_entry, parse_run_arg_specs, + CliArgAdmission, + }; + use crate::v1_compiler_compile::{compile_to_resolved, SourceFile}; + use crate::v1_interpreter::{run_in_context_with_args, ExecutionMode, InterpContext, Value}; + use std::rc::Rc; fn spec(items: &[&str]) -> Vec { items.iter().map(|s| s.to_string()).collect() } + fn admission(label: &str) -> CliArgAdmission { + match label { + "String" => CliArgAdmission::String, + "Int" => CliArgAdmission::Int, + "Bool" => CliArgAdmission::Bool, + "NonEmptyStr" => CliArgAdmission::RefinedString { + display: "NonEmptyStr".to_string(), + predicates: vec!["string_non_empty".to_string()], + }, + other => CliArgAdmission::Unsupported { + display: other.to_string(), + }, + } + } + + fn declared(pairs: &[(&str, &str)]) -> Vec<(String, CliArgAdmission)> { + pairs + .iter() + .map(|(n, t)| (n.to_string(), admission(t))) + .collect() + } + + fn bind( + function: &str, + declared: &[(String, CliArgAdmission)], + specs: &[(String, String)], + ) -> Result, Value)>, String> { + bind_run_arg_specs_against_admissions(function, declared, specs) + } + #[test] fn named_arg_parses_to_a_named_string_value() { - let got = parse_run_args(&spec(&["node_id=roadmap-7"])).expect("well-formed --arg"); + let specs = parse_run_arg_specs(&spec(&["node_id=roadmap-7"])).expect("well-formed --arg"); + let got = bind("entry", &declared(&[("node_id", "String")]), &specs).expect("String bind"); assert_eq!(got.len(), 1); assert_eq!(got[0].0.as_deref(), Some("node_id")); assert!(matches!(&got[0].1, Value::Str(s) if s.as_ref() == "roadmap-7")); @@ -3256,13 +3291,16 @@ mod cli_run_arg_channel_tests { #[test] fn value_may_contain_further_equals_signs() { - let got = parse_run_args(&spec(&["diff=a=b=c"])).expect("split on the first `=` only"); + let specs = + parse_run_arg_specs(&spec(&["diff=a=b=c"])).expect("split on the first `=` only"); + let got = bind("entry", &declared(&[("diff", "String")]), &specs).expect("String bind"); assert!(matches!(&got[0].1, Value::Str(s) if s.as_ref() == "a=b=c")); } #[test] fn empty_value_is_admitted_and_distinct_from_absent() { - let got = parse_run_args(&spec(&["flag="])).expect("empty value is a value"); + let specs = parse_run_arg_specs(&spec(&["flag="])).expect("empty value is a value"); + let got = bind("entry", &declared(&[("flag", "String")]), &specs).expect("String bind"); assert!(matches!(&got[0].1, Value::Str(s) if s.is_empty())); } @@ -3271,7 +3309,7 @@ mod cli_run_arg_channel_tests { // into one. #[test] fn bare_token_without_equals_refuses() { - let err = parse_run_args(&spec(&["node_id"])).expect_err("no `=` must refuse"); + let err = parse_run_arg_specs(&spec(&["node_id"])).expect_err("no `=` must refuse"); assert!( err.contains("name=value"), "diagnostic names the form: {err}" @@ -3280,7 +3318,7 @@ mod cli_run_arg_channel_tests { #[test] fn empty_parameter_name_refuses() { - let err = parse_run_args(&spec(&["=orphan"])).expect_err("empty name must refuse"); + let err = parse_run_arg_specs(&spec(&["=orphan"])).expect_err("empty name must refuse"); assert!( err.contains("empty parameter name"), "diagnostic locates the fault: {err}" @@ -3290,10 +3328,275 @@ mod cli_run_arg_channel_tests { #[test] fn one_malformed_spec_refuses_the_whole_list() { assert!( - parse_run_args(&spec(&["ok=1", "broken", "also_ok=2"])).is_err(), + parse_run_arg_specs(&spec(&["ok=1", "broken", "also_ok=2"])).is_err(), "a partial parse would silently drop a caller's argument" ); } + + #[test] + fn int_parameter_given_non_numeric_refuses() { + let err = bind( + "belt_scm_corrective_integrate_cli", + &declared(&[("supersedes", "Int")]), + &[("supersedes".into(), "stale".into())], + ) + .expect_err("non-numeric Int must refuse"); + assert!( + err.contains("supersedes") && err.contains("Int") && err.contains("stale"), + "diagnostic names parameter, type, and value: {err}" + ); + } + + #[test] + fn int_parameter_given_decimal_two_is_int_two() { + let got = bind( + "belt_scm_corrective_integrate_cli", + &declared(&[("supersedes", "Int")]), + &[("supersedes".into(), "2".into())], + ) + .expect("decimal Int"); + assert!( + matches!(got[0].1, Value::Int(2)), + "specimen: `--arg supersedes=2` must inhabit Int 2, not Str(\"2\")" + ); + } + + #[test] + fn unsupported_declared_type_refuses() { + let err = bind( + "entry", + &declared(&[("source", "List")]), + &[("source".into(), "a".into())], + ) + .expect_err("unsupported type must refuse"); + assert!( + err.contains("List") && err.contains("cannot inhabit"), + "diagnostic names the unsupported type: {err}" + ); + } + + #[test] + fn qualified_or_branded_int_label_refuses_rather_than_matching_the_leaf() { + for label in ["foo.Int", "std.integer.Int"] { + let err = bind( + "entry", + &declared(&[("source", label)]), + &[("source".into(), "2".into())], + ) + .expect_err("leaf-name agreement must not inhabit a different type"); + assert!( + err.contains(label) && err.contains("cannot inhabit"), + "diagnostic names the authored type {label}: {err}" + ); + } + } + + #[test] + fn nonemptystr_non_empty_binds_as_string() { + let got = bind( + "fci1_assert_checkpoint_token", + &declared(&[("token", "NonEmptyStr")]), + &[("token".into(), "none".into())], + ) + .expect("non-empty NonEmptyStr inhabits"); + assert!(matches!(&got[0].1, Value::Str(s) if s.as_ref() == "none")); + } + + #[test] + fn nonemptystr_empty_refuses_naming_the_parameter() { + let err = bind( + "fci1_assert_checkpoint_token", + &declared(&[("token", "NonEmptyStr")]), + &[("token".into(), "".into())], + ) + .expect_err("empty NonEmptyStr must refuse"); + assert!( + err.contains("token") + && err.contains("NonEmptyStr") + && err.contains("string_non_empty"), + "diagnostic names parameter, type, and admission: {err}" + ); + } + + #[test] + fn bool_parameter_accepts_true_false_only() { + let got = bind( + "entry", + &declared(&[("flag", "Bool")]), + &[("flag".into(), "true".into())], + ) + .expect("Bool true"); + assert!(matches!(got[0].1, Value::Bool(true))); + let err = bind( + "entry", + &declared(&[("flag", "Bool")]), + &[("flag".into(), "yes".into())], + ) + .expect_err("non-canonical Bool must refuse"); + assert!(err.contains("true") && err.contains("false"), "{err}"); + } + + fn compile_ctx(files: Vec>) -> InterpContext { + let result = compile_to_resolved(Rc::new(files.into())); + let graph = result + .graph + .as_ref() + .unwrap_or_else(|| panic!("fixture must resolve: {:?}", result.diagnostics)); + InterpContext::new( + graph, + result.source_indices.clone(), + ExecutionMode::Hermetic, + ) + } + + fn source(path: &str, content: &str) -> Rc { + Rc::new(SourceFile { + path: path.to_string(), + content: content.to_string(), + }) + } + + fn kernel_int_entry_ctx() -> InterpContext { + compile_ctx(vec![source( + "probe_cli_arg_int.dag", + "module probe.cli_arg_int\nfn bind_probe(n: Int) -> Int { n }\n", + )]) + } + + fn std_types_nonempty_entry_ctx() -> InterpContext { + compile_ctx(vec![ + source( + "std_types.dag", + "module std.types\n\ + type NonEmptyStr = String where string_non_empty\n\ + fn string_non_empty(value: String) -> Bool { value.length() > 0 }\n", + ), + source( + "probe_cli_arg_nes.dag", + "module probe.cli_arg_nes\n\ + import std.types { NonEmptyStr }\n\ + fn bind_token(token: NonEmptyStr) -> String { token }\n", + ), + ]) + } + + #[test] + fn bind_run_args_for_entry_int_executes_as_int_two() { + let ctx = kernel_int_entry_ctx(); + let bound = + bind_run_args_for_entry(&ctx, "bind_probe", &[("n".to_string(), "2".to_string())]) + .expect("kernel Int --arg n=2 must bind"); + let got = run_in_context_with_args(&ctx, "bind_probe", &bound, false) + .expect("bound Int 2 must execute"); + assert!( + matches!(got, Value::Int(2)), + "specimen: execute bind_probe after --arg n=2 must yield Int 2, got {got:?}" + ); + } + + #[test] + fn bind_run_args_for_entry_std_types_nonemptystr_inhabits_and_empty_refuses() { + let ctx = std_types_nonempty_entry_ctx(); + let got = bind_run_args_for_entry( + &ctx, + "bind_token", + &[("token".to_string(), "none".to_string())], + ) + .expect("std.types NonEmptyStr must inhabit through its declaration"); + assert!(matches!(&got[0].1, Value::Str(s) if s.as_ref() == "none")); + let err = + bind_run_args_for_entry(&ctx, "bind_token", &[("token".to_string(), "".to_string())]) + .expect_err("empty std.types NonEmptyStr must refuse"); + assert!( + err.contains("token") + && err.contains("NonEmptyStr") + && err.contains("string_non_empty"), + "diagnostic names parameter, std.types alias, and declared predicate: {err}" + ); + } + + #[test] + fn isolated_std_types_nonemptystr_whose_string_is_int_refuses() { + let ctx = compile_ctx(vec![source( + "std_types.dag", + "module std.types\n\ + type String = Int\n\ + fn string_non_empty(value: String) -> Bool { true }\n\ + type NonEmptyStr = String where string_non_empty\n\ + fn bind_probe(n: NonEmptyStr) -> Int { n }\n", + )]); + let err = bind_run_args_for_entry( + &ctx, + "bind_probe", + &[("n".to_string(), "2".to_string())], + ) + .expect_err( + "std.types NonEmptyStr whose ground is not kernel String must refuse, not Str(\"2\")", + ); + assert!( + err.contains("cannot inhabit"), + "isolated std.types with type String = Int must refuse inhabitance: {err}" + ); + } + + #[test] + fn same_spelling_user_int_or_nonemptystr_refuses() { + let int_ctx = compile_ctx(vec![source( + "probe_shadow_int.dag", + "module probe.shadow_int\n\ + type Int = String\n\ + fn shadow_int(n: Int) -> String { n }\n", + )]); + let int_err = bind_run_args_for_entry( + &int_ctx, + "shadow_int", + &[("n".to_string(), "2".to_string())], + ) + .expect_err("a user type spelled Int must not bind as kernel Int"); + assert!( + int_err.contains("cannot inhabit"), + "user Int must refuse inhabitance: {int_err}" + ); + + let nes_ctx = compile_ctx(vec![source( + "probe_shadow_nes.dag", + "module probe.shadow_nes\n\ + type NonEmptyStr = String where string_non_empty\n\ + fn shadow_nes(token: NonEmptyStr) -> String { token }\n", + )]); + let nes_err = bind_run_args_for_entry( + &nes_ctx, + "shadow_nes", + &[("token".to_string(), "none".to_string())], + ) + .expect_err("a user type spelled NonEmptyStr must not bind as std.types"); + assert!( + nes_err.contains("cannot inhabit"), + "user NonEmptyStr must refuse inhabitance: {nes_err}" + ); + } + + #[test] + fn bind_run_args_for_entry_duplicate_or_unknown_name_refuses() { + let ctx = kernel_int_entry_ctx(); + let dup = bind_run_args_for_entry( + &ctx, + "bind_probe", + &[ + ("n".to_string(), "1".to_string()), + ("n".to_string(), "2".to_string()), + ], + ) + .expect_err("duplicate --arg name must refuse on the production loop"); + assert!(dup.contains("more than once"), "{dup}"); + let unknown = + bind_run_args_for_entry(&ctx, "bind_probe", &[("nope".to_string(), "1".to_string())]) + .expect_err("unknown --arg name must refuse on the production loop"); + assert!( + unknown.contains("nope") && unknown.contains("declared:"), + "{unknown}" + ); + } } #[cfg(test)] @@ -3511,14 +3814,15 @@ fn for_each_parsed_module_binding( let root_path = Path::new(&anchored_root); let mut dag_files = Vec::new(); collect_dag_files(root_path, &mut dag_files); - for path in dag_files { - let content = std::fs::read_to_string(&path).unwrap_or_else(|e| { + for source in dag_files { + let path = source.path(); + let content = source.read().unwrap_or_else(|e| { panic!( "for_each_parsed_module_binding: failed to read {:?}: {}", path, e ) }); - let binding = match parse_module_binding(&path, &content) { + let binding = match parse_module_binding(path, &content) { Ok(ModuleBindingOutcome::Bound(binding)) => binding, Ok(ModuleBindingOutcome::ModuleBindingUnclassified) => continue, // Fail-closed, but the line stops with a TYPED, LOCATED refusal @@ -3531,7 +3835,7 @@ fn for_each_parsed_module_binding( continue; } }; - visit(root_idx, &path, binding); + visit(root_idx, path, binding); } } refuse_unparseable_module_sources(&refusals); @@ -3607,29 +3911,29 @@ fn collect_module_binding_manifest_rows(source_roots: &[String]) -> Vec Result { + let layers = witness_layer_roots(); + let shared = entry_resolve::try_process_shared_index(&layers)?; + let scratch = entry_resolve::new_multi_entry_index_scratch_over( + shared.source_files.clone(), + &shared.source_roots, + ); + *scratch.scratch_underlay.borrow_mut() = Some(shared); + Ok(scratch) +} + +/// Authored-import seeds of an in-memory fixture: the corpus modules its `import` lines name, +/// read through `module_index`. The both-closure fixpoint is applied by the caller on a chosen +/// index, so GREEN (scratch) and RED (process-shared) differ only by that index. +fn fixture_imported_corpus_sources( entry_content: &str, module_index: &HashMap, -) -> Result>, String> { +) -> Vec> { let ws = process_workspace_root(); let mut seen: HashMap> = HashMap::new(); let mut queue: Vec = vec![entry_content.to_string()]; @@ -3653,28 +3957,80 @@ pub(crate) fn resolve_virtual_source_with_imports( } } } - let imported: Vec> = - seen.into_iter().map(|(_, v)| v).collect(); + seen.into_iter().map(|(_, v)| v).collect() +} + +fn close_fixture_imported_corpus( + imported: Vec>, + index: &MultiEntryIndex, +) -> Result>, String> { + if imported.is_empty() { + return Ok(imported); + } + Ok(extend_sources_to_both_closure_fixpoint(imported, index)? + .into_iter() + // One spelling per file: the index may carry a pulled module under its absolute + // path, and a recorder keyed by path must not see one file as two members. + .map(|source| { + let rel = workspace_relative_repo_path(&source.path); + if rel == source.path { + source + } else { + Rc::new(v1_compiler_compile::SourceFile { + path: rel, + content: source.content.clone(), + }) + } + }) + .collect()) +} + +/// THE PRE-FIX LOADER, test-only: the same import seeds and closure authority as +/// `resolve_virtual_source_with_imports`, on `try_index_for_run_or_owned_pool` over the layer +/// roots. That is the slot the claim fold reads. The only difference from production is the +/// index. RFM `fixture_compile_retained_on_the_process_shared_index`. +#[cfg(test)] +pub(crate) fn extend_fixture_imports_on_process_shared_index( + entry_content: &str, + module_index: &HashMap, +) -> Result>, String> { + let imported = fixture_imported_corpus_sources(entry_content, module_index); + let layers = witness_layer_roots(); + let index = entry_resolve::try_index_for_run_or_owned_pool(&layers)?; + close_fixture_imported_corpus(imported, &index) +} + +/// The closure an in-memory (not-on-disk) fixture entry compiles against: the corpus modules its +/// authored `import` lines name, read from the workspace through `module_index`, CLOSED BY THE +/// ONE CLOSURE AUTHORITY (`extend_sources_to_both_closure_fixpoint`), then the entry itself. +/// +/// WHY NOT THE IMPORT EDGES ALONE (DESIGN §3, §6b). This walker used to stop at `import` lines, +/// a fourth closure rule beside the one the gate, the witness loader and regen share. An `import` +/// line, a qualified reference and a bare reference are the same dependency edge, so the +/// import-only walk was not a narrower closure but a blind one: `std.syllogism` reaches +/// `std.graph` by the bare name `GraphEdge` and `v2.std.artifact` reaches `v2.std.refinement` by +/// qualified reference, so a fixture whose imports reached either compiled it WITHOUT its +/// provider and reported `unresolved type` / `undefined variable 'v2'` against a module that +/// resolves in every closure the corpus authority builds. #13195's union render made that +/// fork refuse the floor (46 such diagnostics on #13420's run, none in its own modules). +/// +/// The extension is seeded from the corpus modules only. The entry is the fixture's subject, +/// authored with an explicit import manifest a witness may be probing (an unlisted use, a +/// refused import), so its own spelling stays exactly what it declares; every module it reaches +/// is closed as the corpus closes it. An extension failure is returned, never widened past. +/// The fixpoint mutates a scratch index (dropped with the loader), never the process-shared +/// slot the claim fold reads. +pub(crate) fn resolve_virtual_source_with_imports( + entry_path: &str, + entry_content: &str, + module_index: &HashMap, +) -> Result>, String> { + let imported = fixture_imported_corpus_sources(entry_content, module_index); let mut sources = if imported.is_empty() { imported } else { - let index = entry_resolve::try_index_for_run_or_owned_pool(&witness_layer_roots())?; - extend_sources_to_both_closure_fixpoint(imported, &index)? - .into_iter() - // One spelling per file: the index may carry a pulled module under its absolute - // path, and a recorder keyed by path must not see one file as two members. - .map(|source| { - let rel = workspace_relative_repo_path(&source.path); - if rel == source.path { - source - } else { - Rc::new(v1_compiler_compile::SourceFile { - path: rel, - content: source.content.clone(), - }) - } - }) - .collect() + let index = scratch_index_for_fixture_closure_extension()?; + close_fixture_imported_corpus(imported, &index)? }; sources.sort_by(|a, b| a.path.cmp(&b.path)); sources.dedup_by(|a, b| a.path == b.path); @@ -4521,9 +4877,11 @@ fn regen_input_sources_over_roots( // Seed: every `.dag` under the entry root that declares a module path. let mut seeds: Vec> = Vec::new(); - for path in &entry_paths { - let content = - std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; + for source in &entry_paths { + let path = source.path(); + let content = source + .read() + .map_err(|e| format!("read {}: {e}", path.display()))?; let rel = workspace_relative_repo_path(&path.to_string_lossy()); if extract_module_path(&content).is_some() { seeds.push(Rc::new(v1_compiler_compile::SourceFile { @@ -4624,12 +4982,13 @@ pub(crate) fn is_test_dag(path: &str) -> bool { pub(crate) fn corpus_dag_files() -> Vec<(String, String)> { let mut paths = Vec::new(); for root in witness_layer_roots() { - collect_dag_files_tolerant(&Path::new(&anchor_source_root(&root)), &mut paths); + collect_dag_files_tolerant(&Path::new(&anchor_source_root(&root)), &mut paths) + .unwrap_or_else(|cause| panic!("corpus walk over {root}: {cause}")); } let mut out = Vec::new(); for p in paths { - let rel = repo_rel(&p); - if let Ok(content) = std::fs::read_to_string(&p) { + let rel = repo_rel(p.path()); + if let Ok(content) = p.read() { out.push((rel, content)); } } @@ -4701,11 +5060,13 @@ fn try_build_module_index(source_roots: &[String]) -> Result = HashMap::new(); - for path in dag_files { - let content = std::fs::read_to_string(&path) + for source in dag_files { + let path = source.path(); + let content = source + .read() .map_err(|e| format!("failed to read {}: {e}", path.display()))?; if let Some(module_path) = extract_module_path(&content) { - let rel_path = module_index_path_key(&path); + let rel_path = module_index_path_key(path); if pool_fill_only { if index.contains_key(&module_path) { continue; @@ -4868,14 +5231,16 @@ fn load_compile_clean_entry_sources( if first_root.is_dir() { let mut dag_paths = Vec::new(); collect_dag_files(first_root, &mut dag_paths); - for path in dag_paths { + for source in dag_paths { + let path = source.path(); let rel = workspace_relative_repo_path(&path.to_string_lossy()); if let Some(filter) = entry_path_filter { if !filter.contains(&rel) { continue; } } - let content = std::fs::read_to_string(&path) + let content = source + .read() .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); entry_files.push((path.to_string_lossy().to_string(), content)); } @@ -5356,8 +5721,8 @@ pub const RUN_CLASS_B_GATE_INPUT_CLOSURE_FAILED_LABEL: &str = /// deleted; the index stays, because which file declares a module is what any import walk needs. fn dag_module_index( roots: &[PathBuf], -) -> Result>, String> { - let mut index: std::collections::HashMap> = +) -> Result>, String> { + let mut index: std::collections::HashMap> = std::collections::HashMap::new(); for root in roots { if !root.exists() { @@ -5365,11 +5730,12 @@ fn dag_module_index( } let mut dag_paths = Vec::new(); collect_dag_files(root, &mut dag_paths); - for path in dag_paths { - let content = std::fs::read_to_string(&path) - .map_err(|e| format!("read {}: {e}", path.display()))?; + for source in dag_paths { + let content = source + .read() + .map_err(|e| format!("read {}: {e}", source.path().display()))?; if let Some(module_path) = extract_module_path(&content) { - index.entry(module_path).or_default().push(path); + index.entry(module_path).or_default().push(source); } } } @@ -8348,6 +8714,11 @@ fn parsed_file_references_of( if let Some(hit) = index.parsed_references.borrow().get(&file) { return hit.clone(); } + if let Some(base) = index.scratch_underlay.borrow().as_ref() { + if let Some(hit) = base.parsed_references.borrow().get(&file) { + return hit.clone(); + } + } let module_names = pool_module_names(index); let self_module = extract_module_path(&sf.content).unwrap_or_default(); index @@ -8717,6 +9088,11 @@ fn admit_bare_references_of_file( if let Some(verdict) = index.bare_reference_admission.borrow().get(&file) { return verdict.clone(); } + if let Some(base) = index.scratch_underlay.borrow().as_ref() { + if let Some(verdict) = base.bare_reference_admission.borrow().get(&file) { + return verdict.clone(); + } + } let verdict = if source_declares_import_lines(&source.content) { Ok(()) } else { @@ -9244,6 +9620,13 @@ fn build_both_closure_edge_index( return Ok(hit.clone()); } } + if let Some(base) = index.scratch_underlay.borrow().as_ref() { + if let Some(hit) = base.both_closure_edges.borrow().as_ref() { + if hit.ref_out.contains_key(&file) { + return Ok(hit.clone()); + } + } + } let ref_started = std::time::Instant::now(); let ref_paths = reference_pull_paths_for_source(source, index)?; resolve_stage_slot_add(|st| st.edge_index_ref_half += ref_started.elapsed().as_nanos()); @@ -9838,6 +10221,128 @@ mod closure_edge_demand_tests { ); } + /// Inhabitance: `load_sources_for_entry_with_pool` already binds by the ancestor chain. + /// The sibling plant is nearer by prefix and must not enter the compile closure. + #[test] + fn load_sources_binds_the_on_chain_ancestor_not_the_sibling_plant() { + let fixture = Fixture::new(&[ + ( + "parent.dag", + "module frontier\nfn duplicated() -> Int { 1 }\n", + ), + ( + "plant.dag", + "module frontier.child.plant\nfn duplicated() -> Int { 99 }\n", + ), + ( + "consumer.dag", + "module frontier.child.consumer\nfn read() -> Int { duplicated() }\n", + ), + ]); + let index = fixture.index(); + let sources = load_sources_for_entry_with_pool( + &index, + &fixture.0.join("consumer.dag").to_string_lossy(), + ) + .unwrap(); + let modules: BTreeSet = sources + .iter() + .map(|s| extract_module_path(&s.content).unwrap()) + .collect(); + assert!(modules.contains("frontier"), "{modules:?}"); + assert!(!modules.contains("frontier.child.plant"), "{modules:?}"); + assert!(modules.contains("frontier.child.consumer"), "{modules:?}"); + } + + /// Unique off-chain declarer: UniqueBinding still accepts, so UniqueBare must emit + /// (dropping it is an undercount) and the real resolve path must succeed. + #[test] + fn unique_off_chain_bare_name_compiles_and_keeps_its_uniquebare_edge() { + let fixture = Fixture::new(&[ + ( + "decl.dag", + "module test.decl\nfn shared_fn() -> Int { 1 }\n", + ), + ( + "user.dag", + "module test.user\nfn use_it() -> Int { shared_fn() }\n", + ), + ]); + let index = fixture.index(); + let sources = + load_sources_for_entry_with_pool(&index, &fixture.0.join("user.dag").to_string_lossy()) + .expect("UniqueBinding pulls the census-unique off-chain provider"); + let compiled = v1_compiler_compile::compile_sources( + Rc::new(sources.iter().cloned().collect()), + crate::v1_compiler_artifact::RenderTarget::Dag, + ); + assert!( + !compiled + .diagnostics + .iter() + .any(|d| { is_interpreter_blocking_diagnostic(d.diagnostic.clone()) }), + "resolver-accepted UniqueBinding must compile: {:?}", + compiled.diagnostics + ); + let roots = vec![fixture.0.to_string_lossy().into_owned()]; + let facts = dependency_resolution_facts(&roots, &roots, &[]); + assert!( + facts + .iter() + .any(|f| f.path.contains("user.dag") && f.import_module == "test.decl"), + "resolver-accepted UniqueBinding must remain a UniqueBare edge: {facts:?}" + ); + } + + /// Two off-chain homonyms, none on the consumer's chain: proximity UniqueBare is gone, + /// and the real resolve path refuses rather than silently picking a neighbor. + #[test] + fn off_chain_homonym_with_no_lexical_binder_is_refused_on_the_resolve_path() { + let fixture = Fixture::new(&[ + ("a.dag", "module aa.one\nfn shared() -> Int { 1 }\n"), + ("b.dag", "module bb.two\nfn shared() -> Int { 2 }\n"), + ( + "user.dag", + "module cc.user\nfn use_it() -> Int { shared() }\n", + ), + ]); + let index = fixture.index(); + let sources = + load_sources_for_entry_with_pool(&index, &fixture.0.join("user.dag").to_string_lossy()) + .expect("admission does not fabricate a proximity provider"); + let compiled = v1_compiler_compile::compile_sources( + Rc::new(sources.iter().cloned().collect()), + crate::v1_compiler_artifact::RenderTarget::Dag, + ); + let msgs: Vec = compiled + .diagnostics + .iter() + .filter(|d| is_interpreter_blocking_diagnostic(d.diagnostic.clone())) + .map(|d| format!("{:?}", d.diagnostic)) + .collect(); + let joined = msgs.join("\n"); + assert!( + !msgs.is_empty() + && joined.contains("shared") + && (joined.contains("undefined") + || joined.contains("not found in scope") + || joined.contains("AMBIGUOUS") + || joined.contains("unresolved")), + "refusal must name the bare identifier: {joined:?}" + ); + let roots = vec![fixture.0.to_string_lossy().into_owned()]; + let facts = dependency_resolution_facts(&roots, &roots, &[]); + let user_targets: Vec<&str> = facts + .iter() + .filter(|f| f.path.contains("user.dag")) + .map(|f| f.import_module.as_str()) + .collect(); + assert!( + user_targets.is_empty(), + "no UniqueBare to a proximity winner: {user_targets:?}" + ); + } + /// ONE PARSE PER FILE ON THE ENTRY ROUTE. A module reached only by a dotted reference is /// pulled by the fixpoint's reference half, which reads the index's shared parse; the /// per-entry module-path scan (`extend_with_reference_closure`, timed as @@ -10587,6 +11092,18 @@ fn entry_source_from_index_or_disk( entry_path: &str, ) -> Result, String> { let path = std::path::Path::new(entry_path); + // A DERIVED LEDGER ROSTER HAS NO FILE: it is contributed to the index in memory by source + // acquisition (`derived_row_roster::acquire_dir_files`), so an entry naming it is answered from + // the index the loader built, never from a file that would have to be written first. + if !path.exists() && derived_row_roster::is_derived_roster_path(entry_path) { + let wanted = workspace_relative_repo_path(entry_path); + if let Some(indexed) = index + .values() + .find(|s| workspace_relative_repo_path(&s.path) == wanted) + { + return Ok(indexed.clone()); + } + } if !path.is_file() { return Err(format!( "entry file does not exist or is not a file: {}", @@ -12568,6 +13085,8 @@ pub struct MultiEntryIndex { live_read_manifest: RefCell, String>>>, /// Only produced rows; consumers needing every row call whole_pool_closure_edge_index. both_closure_edges: RefCell>>, + /// Fixture-scratch read-through: the process-shared index. Writes stay on this index. + scratch_underlay: RefCell>>, /// Admission and edge selection need declaration heads, not resolved signatures. /// None identifies the whole-pool name census; Some(root) the existing tree/import view. closure_name_censuses: RefCell, Rc>>, @@ -12933,11 +13452,20 @@ fn next_index_generation() -> u64 { /// One `MultiEntryIndex` construction: the module-name set it indexes (as a digest over the sorted /// module paths, with its size) and the first caller outside the `#[track_caller]` chain of index /// builders that demanded it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum MultiEntryIndexBuildKind { + /// One demand for a module-name set. Two of these with one digest refuse. + NameSetIndex, + /// Isolated caches over an already-indexed name set. Countable, not a second index. + ScratchCachesOverExistingSet, +} + #[derive(Clone, Debug)] pub(crate) struct MultiEntryIndexBuild { pub(crate) name_set_digest: u64, pub(crate) modules: usize, pub(crate) site: String, + pub(crate) kind: MultiEntryIndexBuildKind, } static MULTI_ENTRY_INDEX_BUILDS: std::sync::Mutex> = @@ -12946,6 +13474,7 @@ static MULTI_ENTRY_INDEX_BUILDS: std::sync::Mutex> = pub(crate) fn record_multi_entry_index_site( site: &std::panic::Location<'_>, source_files: &ModuleSourceIndex, + kind: MultiEntryIndexBuildKind, ) { use std::hash::{Hash, Hasher}; // The POOL, not only its names: two scratch pools declaring one module path at different @@ -12962,6 +13491,7 @@ pub(crate) fn record_multi_entry_index_site( name_set_digest: hasher.finish(), modules: pool.len(), site: format!("{}:{}", site.file(), site.line()), + kind, }); } } @@ -12973,15 +13503,18 @@ pub(crate) fn multi_entry_index_builds() -> Vec { .unwrap_or_default() } -/// ONE INDEX PER MODULE-NAME SET. An index is a pure function of the name set it covers, so two -/// constructions over one set are one demand built twice, and every file each serves is parsed -/// again per index. Refuses with the sites of every set built more than once; distinct sets are -/// distinct demands and are not limited. +/// ONE NAME-SET INDEX PER MODULE-NAME SET. Two `NameSetIndex` constructions over one set are one +/// demand built twice. `ScratchCachesOverExistingSet` is recorded (countable) and is not a second +/// index of that set. Refuses with the sites of every set indexed more than once; distinct sets +/// are distinct demands and are not limited. pub(crate) fn multi_entry_index_sharing_control( builds: &[MultiEntryIndexBuild], ) -> Result { let mut by_set: BTreeMap> = BTreeMap::new(); for build in builds { + if build.kind != MultiEntryIndexBuildKind::NameSetIndex { + continue; + } by_set.entry(build.name_set_digest).or_default().push(build); } let repeated: Vec = by_set @@ -17456,7 +17989,7 @@ pub fn run_dag_parse_sweep(workspace: &Path, roots: &[&str]) -> Result = Vec::new(); + let mut dag_paths: Vec = Vec::new(); // PER-ROOT, so the empty-walk refusal below is per root: one root going missing must not // be absorbed by another root's files (the empty-observation narrow, DESIGN §5). for root in roots { @@ -17464,14 +17997,11 @@ pub fn run_dag_parse_sweep(workspace: &Path, roots: &[&str]) -> Result = vec![root_dir.clone()]; while let Some(dir) = stack.pop() { - // Derive gitignored `gunbc.recurring_failure_mode.roster` before this directory's - // listing, so the required-CI index contains the module the parse join reads. - derived_row_roster::ensure_if_row_dir(&dir).map_err(|e| { - vec![format!( - "failed to derive recurring_failure_mode roster in {}: {e}", - dir.display() - )] - })?; + // This directory's `.dag` files are admitted as one listing after the walk of its + // entries, so a ledger row directory contributes its derived roster IN MEMORY and the + // required-CI index contains the module the parse join reads -- with nothing written + // under the root. + let mut here: Vec = Vec::new(); let read_dir = match std::fs::read_dir(&dir) { Ok(d) => d, Err(e) => return Err(vec![format!("read_dir {}: {e}", dir.display())]), @@ -17511,9 +18041,11 @@ pub fn run_dag_parse_sweep(workspace: &Path, roots: &[&str]) -> Result Result>> = Arc::new(Mutex::new(Vec::new())); let count = Arc::new(AtomicUsize::new(0)); @@ -17537,13 +18069,13 @@ pub fn run_dag_parse_sweep(workspace: &Path, roots: &[&str]) -> Result` — no shared parse state — so parsing is // embarrassingly parallel. Thread panics propagate via scope (fail-closed). std::thread::scope(|scope| { - for path in &dag_paths { + for source in &dag_paths { let errors = Arc::clone(&errors); let count = Arc::clone(&count); let records = Arc::clone(&records); - let path = path.clone(); + let path = source.path().to_path_buf(); scope.spawn(move || { - let content = match std::fs::read_to_string(&path) { + let content = match source.read() { Ok(c) => c, Err(e) => { errors @@ -18326,10 +18858,14 @@ fn module_emit_repr_fingerprint( ) -> Result { use crate::v1_compiler_infer_emit_info::TypeSummary; + // Summaries are keyed by declaration identity (`.`), so a module's own types are + // read under its own path and never under a same-leaf declaration elsewhere in the closure. + let module_path = authored_name_at(source_indices.clone(), module.module.clone()); let type_names = module_defined_type_names(module, source_indices); let mut type_summaries = BTreeMap::::new(); for name in type_names { - if let Some(summary) = emit_info.type_summaries.get(&name) { + let key = format!("{module_path}.{name}"); + if let Some(summary) = emit_info.type_summaries.by_key.get(&key) { type_summaries.insert(name, summary.as_ref().clone()); } } @@ -19735,23 +20271,18 @@ pub fn handle_pre_push() -> std::process::ExitCode { /// after a `dag run` entry returns -- the periodic dump alone races the /// process's natural completion and under-reports on fast runs. -/// Parse repeated `--arg name=value` into the interpreter's named-argument -/// channel (`run_in_context_with_args`, v1_interpreter.rs:1564 — already the -/// channel `claim_executor` uses internally; `Commands::Run` simply never grew -/// the flag, which is why every parameter-shaped value reached `.dag` entries -/// through the process environment instead). +/// Parse repeated `--arg name=value` into named text pairs. /// /// Named-only by construction: a `.dag` entry's parameters are named, so /// positional order across the CLI boundary would be an unchecked coincidence. -/// A missing `=` refuses (§5) rather than guessing a position. Values enter as -/// `Value::Str` — no coercion is fabricated here; a parameter wanting another -/// type is the typed-argument follow-on, not a silent conversion. -fn parse_run_args(raw: &[String]) -> Result, v1_interpreter::Value)>, String> { +/// A missing `=` refuses (§5) rather than guessing a position. The pairs stay +/// text until [`bind_run_args_for_entry`], which is the typed binder: argv is +/// always text (`CliTextValue` on the run verb); inhabiting the declared +/// parameter type is a later, located decision — never a silent `String`. +pub fn parse_run_arg_specs(raw: &[String]) -> Result, String> { raw.iter() .map(|spec| match spec.split_once('=') { - Some((name, value)) if !name.is_empty() => { - Ok((Some(name.to_string()), str_value(value.to_string()))) - } + Some((name, value)) if !name.is_empty() => Ok((name.to_string(), value.to_string())), Some(_) => Err(format!("--arg `{spec}`: empty parameter name before `=`")), None => Err(format!( "--arg `{spec}`: expected `name=value` (named arguments only)" @@ -19760,6 +20291,303 @@ fn parse_run_args(raw: &[String]) -> Result, v1_interpreter: .collect() } +fn parse_cli_decimal_int(text: &str) -> Option { + if text.is_empty() { + return None; + } + let digits = match text.as_bytes() { + [b'-', rest @ ..] | [b'+', rest @ ..] => rest, + rest => rest, + }; + if digits.is_empty() || !digits.iter().all(|b| b.is_ascii_digit()) { + return None; + } + text.parse().ok() +} + +/// Admission derived from a parameter's resolved type (or supplied in tests). +#[derive(Clone, Debug)] +pub enum CliArgAdmission { + String, + Int, + Bool, + RefinedString { + display: String, + predicates: Vec, + }, + Unsupported { + display: String, + }, +} + +fn cli_arg_no_inhabitance_route(function: &str, param: &str, type_label: &str) -> String { + format!( + "--arg `{param}`: function `{function}` declares `{param}: {type_label}`, \ + which `gunbc run --arg` cannot inhabit; only kernel String, Int, Bool, \ + and std.types NonEmptyStr are admitted" + ) +} + +/// Bind one `--arg` text value against a resolved admission. +pub fn bind_cli_arg_text( + function: &str, + param: &str, + admission: &CliArgAdmission, + text: &str, +) -> Result { + match admission { + CliArgAdmission::String => Ok(str_value(text)), + CliArgAdmission::Int => bind_cli_int(function, param, "Int", text), + CliArgAdmission::Bool => match text { + "true" => Ok(Value::Bool(true)), + "false" => Ok(Value::Bool(false)), + _ => Err(format!( + "--arg `{param}`: function `{function}` declares `{param}: Bool`, \ + but `{text}` is not `true` or `false`" + )), + }, + CliArgAdmission::RefinedString { + display, + predicates, + } => { + for pred in predicates { + match v1_compiler_infer::decidable_where_string_predicate_holds( + pred.clone(), + text.to_string(), + ) { + Some(true) => {} + Some(false) => { + return Err(format!( + "--arg `{param}`: function `{function}` declares `{param}: {display}`, \ + but the value fails `{pred}`" + )); + } + None => { + return Err(cli_arg_no_inhabitance_route(function, param, display)); + } + } + } + Ok(str_value(text)) + } + CliArgAdmission::Unsupported { display } => { + Err(cli_arg_no_inhabitance_route(function, param, display)) + } + } +} + +fn bind_cli_int( + function: &str, + param: &str, + type_label: &str, + text: &str, +) -> Result { + match parse_cli_decimal_int(text) { + Some(n) => Ok(Value::Int(n)), + None => Err(format!( + "--arg `{param}`: function `{function}` declares `{param}: {type_label}`, \ + but `{text}` is not a decimal integer" + )), + } +} + +fn cli_arg_resolved_type_node(type_expr: Rc) -> Rc { + match type_expr.inferred.as_deref() { + Some(InferredNode::Resolved { node }) => node.clone(), + _ => type_expr, + } +} + +fn type_nodes_name_the_same_declaration(a: &Rc, b: &Rc) -> bool { + Rc::ptr_eq(a, b) + || match (&a.ident_span, &b.ident_span) { + (Some(left), Some(right)) => left.file == right.file && left.start == right.start, + _ => false, + } +} + +fn std_types_nonempty_str_item(ctx: &v1_interpreter::InterpContext) -> Option> { + let tm = ctx.typed_module_for_authored_path("std.types")?; + tm.items + .iter() + .find(|item| authored_name_at(ctx.source_indices.clone(), (*item).clone()) == "NonEmptyStr") + .cloned() +} + +fn nonempty_str_predicates_from_decl( + ctx: &v1_interpreter::InterpContext, + decl: &Rc, +) -> Option> { + let mut ty = v1_interpreter::type_declaration_rhs(decl)?; + let mut preds = Vec::new(); + for _ in 0..8 { + if !v1_compiler_infer::is_where_refinement_type(ty.clone()) { + break; + } + for pred in v1_compiler_infer::type_expr_where_refinement_predicates(ty.clone()) + .iter() + .cloned() + { + let pname = v1_compiler_infer::where_predicate_name_at( + pred.clone(), + ctx.source_indices.clone(), + ); + if pname.is_empty() + || v1_compiler_infer::where_refinement_is_deferred_predicate(pname.clone()) + || !v1_compiler_infer::where_refinement_is_string_literal_predicate(pname.clone()) + { + return None; + } + preds.push(pname); + } + ty = ty.children.iter().next().cloned()?; + } + let kernel_string = match type_reference_identity(ty.clone()).as_ref() { + TypeReferenceIdentity::ReferenceResolvedToDeclaration { provenance } + | TypeReferenceIdentity::ReferenceIsTheDeclaration { provenance } => { + matches!( + provenance.as_ref(), + TypeDeclarationProvenance::KernelMinted { minted_name } if minted_name == "String" + ) + } + TypeReferenceIdentity::ReferenceIsTypeVariableBinder { .. } + | TypeReferenceIdentity::ReferenceIdentityUnavailable { .. } => false, + }; + if kernel_string && preds == ["string_non_empty".to_string()] { + Some(preds) + } else { + None + } +} + +/// Admission from the parameter's resolved type identity (DESIGN §4): kernel +/// String/Int/Bool, or the exact `std.types` `NonEmptyStr` declaration and its +/// `string_non_empty` refinement. Leaf names do not authorize conversion. +fn cli_arg_admission_from_type_expr( + ctx: &v1_interpreter::InterpContext, + type_expr: Rc, +) -> CliArgAdmission { + let display = v1_compiler_infer::type_node_label(type_expr.clone(), ctx.source_indices.clone()); + match type_reference_identity(type_expr.clone()).as_ref() { + TypeReferenceIdentity::ReferenceResolvedToDeclaration { provenance } + | TypeReferenceIdentity::ReferenceIsTheDeclaration { provenance } => { + match provenance.as_ref() { + TypeDeclarationProvenance::KernelMinted { minted_name } => { + return cli_arg_admission_for_kernel(minted_name, display); + } + TypeDeclarationProvenance::CorpusDeclared { .. } + | TypeDeclarationProvenance::DeclarationIdentityAbsent => {} + } + } + TypeReferenceIdentity::ReferenceIsTypeVariableBinder { .. } + | TypeReferenceIdentity::ReferenceIdentityUnavailable { .. } => {} + } + std_types_nonempty_str_admission(ctx, type_expr, display) +} + +fn refers_to_std_types_nonempty_str( + ctx: &v1_interpreter::InterpContext, + type_expr: Rc, + decl: &Rc, +) -> bool { + let resolved = cli_arg_resolved_type_node(type_expr.clone()); + if type_nodes_name_the_same_declaration(&resolved, decl) { + return true; + } + let decl_id = + v1_compiler_infer::node_declaration_identity(decl.clone(), ctx.source_indices.clone()); + let ref_id = v1_compiler_infer::type_reference_identity(type_expr, ctx.source_indices.clone()); + !decl_id.is_empty() && ref_id == decl_id +} + +fn std_types_nonempty_str_admission( + ctx: &v1_interpreter::InterpContext, + type_expr: Rc, + display: String, +) -> CliArgAdmission { + let Some(decl) = std_types_nonempty_str_item(ctx) else { + return CliArgAdmission::Unsupported { display }; + }; + if !refers_to_std_types_nonempty_str(ctx, type_expr, &decl) { + return CliArgAdmission::Unsupported { display }; + } + match nonempty_str_predicates_from_decl(ctx, &decl) { + Some(predicates) => CliArgAdmission::RefinedString { + display, + predicates, + }, + None => CliArgAdmission::Unsupported { display }, + } +} + +fn cli_arg_admission_for_kernel(ground: &str, display: String) -> CliArgAdmission { + match ground { + "String" => CliArgAdmission::String, + "Int" => CliArgAdmission::Int, + "Bool" => CliArgAdmission::Bool, + _ => CliArgAdmission::Unsupported { display }, + } +} + +/// Bind `--arg` specs against caller-supplied admissions (unit tests). +pub fn bind_run_arg_specs_against_admissions( + function: &str, + declared: &[(String, CliArgAdmission)], + specs: &[(String, String)], +) -> Result, v1_interpreter::Value)>, String> { + let mut seen = HashSet::new(); + let mut bound = Vec::with_capacity(specs.len()); + for (name, text) in specs { + if !seen.insert(name.clone()) { + return Err(format!( + "--arg `{name}`: supplied more than once for function `{function}`" + )); + } + let admission = match declared.iter().find(|(n, _)| n == name) { + Some((_, a)) => a, + None => { + let known: Vec<&str> = declared.iter().map(|(n, _)| n.as_str()).collect(); + return Err(format!( + "--arg `{name}`: function `{function}` has no parameter `{name}` \ + (declared: {})", + if known.is_empty() { + "(none)".to_string() + } else { + known.join(", ") + } + )); + } + }; + bound.push(( + Some(name.clone()), + bind_cli_arg_text(function, name, admission, text)?, + )); + } + Ok(bound) +} + +/// Bind `--arg` text against `entry_fn`'s resolved parameter types. +/// +/// No `.dag` binder exists: `gunbc.cli_dispatch_surface` owns the option as +/// `CliTextValue` (argv is text). This seed function derives admissions from +/// the resolved where-chain, then the one bind loop in +/// [`bind_run_arg_specs_against_admissions`]. +pub fn bind_run_args_for_entry( + ctx: &v1_interpreter::InterpContext, + entry_fn: &str, + specs: &[(String, String)], +) -> Result, v1_interpreter::Value)>, String> { + if specs.is_empty() { + return Ok(Vec::new()); + } + let declared = v1_interpreter::declared_parameter_type_exprs(ctx, entry_fn) + .ok_or_else(|| format!("--arg: function `{entry_fn}` is not in the loaded entry"))?; + let admissions: Vec<(String, CliArgAdmission)> = declared + .into_iter() + .map(|(name, ty)| (name, cli_arg_admission_from_type_expr(ctx, ty))) + .collect(); + bind_run_arg_specs_against_admissions(entry_fn, &admissions, specs) +} + struct ScopedRunObservation { ctx: v1_interpreter::InterpContext, entry_file: String, @@ -22226,27 +23054,396 @@ fn extract_bool_witness_transport( (entry, function) } -fn defining_module_for_resolved_type( - graph: &ResolvedGraph, - source_indices: &HashMap>, +fn module_from_qualified_identity(identity: &str) -> Option { + let (module, name) = identity.rsplit_once('.')?; + if module.is_empty() || name.is_empty() { + None + } else { + Some(module.to_string()) + } +} + +fn defining_module_for_variant( variant_to_enum: &im::HashMap, - type_name: &str, + variant_name: &str, ) -> Option { - let si = Rc::new(source_indices.clone()); - for tm in graph.modules.iter() { - let mod_name = authored_name_at(si.clone(), tm.module.clone()); - if lookup_type_by_name(tm.type_env.clone(), type_name.to_string()).is_some() { - return Some(mod_name); + // variant_to_enum is keyed by VARIANT leaf. Values are owner identities `.`. + // No row, the collision sentinel "", or an unqualified identity has no module part: + // return None so the caller refuses. Do not scan type environments for a leaf match. + let parent_enum = variant_to_enum.get(variant_name)?; + module_from_qualified_identity(parent_enum) +} + +fn defining_module_for_type_identity( + type_summaries: &Rc, + identity: &str, +) -> Option { + // Exact declaration key only. type_summary_lookup / type_summary_by_leaf are leaf scans. + if !identity.contains('.') { + return None; + } + match (*crate::v1_compiler_infer_emit_info::type_summary_at_key( + type_summaries.clone(), + identity.to_string(), + )) + .clone() + { + crate::v1_compiler_infer_emit_info::TypeSummaryLookup::TypeSummaryFound { summary } => { + module_from_qualified_identity(&summary.key) } + _ => None, } - let parent_enum = variant_to_enum.get(type_name).cloned()?; - for tm in graph.modules.iter() { - let mod_name = authored_name_at(si.clone(), tm.module.clone()); - if lookup_type_by_name(tm.type_env.clone(), parent_enum.clone()).is_some() { - return Some(mod_name); +} + +fn defining_module_for_type_expr( + type_summaries: &Rc, + type_decls: &Rc, + source_indices: &HashMap>, + type_expr: &Rc, +) -> Option { + match (*crate::v1_compiler_infer_emit_info::type_summary_of_reference( + type_summaries.clone(), + type_decls.clone(), + type_expr.clone(), + Rc::new(source_indices.clone()), + )) + .clone() + { + crate::v1_compiler_infer_emit_info::TypeSummaryLookup::TypeSummaryFound { summary } => { + module_from_qualified_identity(&summary.key) } + _ => None, + } +} + +#[cfg(test)] +mod defining_module_lookup_tests { + use super::*; + + #[test] + fn bool_witness_claim_variant_resolves_to_verification_module() { + let mut variant_to_enum = im::HashMap::new(); + variant_to_enum.insert( + "BoolWitnessClaim".to_string(), + "v2.std.verification.UnifiedTestClaim".to_string(), + ); + variant_to_enum.insert( + "NodeCorpus".to_string(), + "v2.std.verification.UnifiedTestClaim".to_string(), + ); + assert_eq!( + defining_module_for_variant(&variant_to_enum, "BoolWitnessClaim"), + Some("v2.std.verification".to_string()), + "the owned-data BoolWitnessClaim path keys variant_to_enum by the variant leaf" + ); + assert_eq!( + defining_module_for_variant(&variant_to_enum, "UnifiedTestClaim"), + None, + "the coproduct leaf is not a variant key" + ); + } + + #[test] + fn unqualified_or_sentinel_variant_identity_refuses() { + let mut variant_to_enum = im::HashMap::new(); + variant_to_enum.insert("NoDot".to_string(), "UnifiedTestClaim".to_string()); + variant_to_enum.insert("Sentinel".to_string(), "".to_string()); + assert_eq!(defining_module_for_variant(&variant_to_enum, "NoDot"), None); + assert_eq!( + defining_module_for_variant(&variant_to_enum, "Sentinel"), + None + ); + assert_eq!( + defining_module_for_variant(&variant_to_enum, "Missing"), + None + ); + } + + #[test] + fn type_identity_uses_exact_summary_key_not_a_leaf() { + use crate::v1_compiler_infer_emit_info::{ + empty_type_summary_index, type_summary_index_insert, TypeRepr, TypeSummary, + }; + let summary = Rc::new(TypeSummary { + name: "UnifiedTestClaim".to_string(), + key: "v2.std.verification.UnifiedTestClaim".to_string(), + repr: Rc::new(TypeRepr::EnumRepr { unit_only: false }), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let index = type_summary_index_insert( + empty_type_summary_index(), + "v2.std.verification.UnifiedTestClaim".to_string(), + "UnifiedTestClaim".to_string(), + summary, + ); + assert_eq!( + defining_module_for_type_identity(&index, "v2.std.verification.UnifiedTestClaim"), + Some("v2.std.verification".to_string()) + ); + assert_eq!( + defining_module_for_type_identity(&index, "UnifiedTestClaim"), + None, + "a bare type leaf must not scan keys_by_leaf" + ); + } + + #[test] + fn user_type_named_like_a_known_variant_keeps_its_module() { + use crate::v1_compiler_infer_emit_info::{ + empty_type_decl_index, empty_type_summary_index, type_summary_index_insert, TypeRepr, + TypeSummary, + }; + let mut variant_to_enum = im::HashMap::new(); + variant_to_enum.insert( + "BoolWitnessClaim".to_string(), + "v2.std.verification.UnifiedTestClaim".to_string(), + ); + let summary = Rc::new(TypeSummary { + name: "BoolWitnessClaim".to_string(), + key: "owner.types.BoolWitnessClaim".to_string(), + repr: Rc::new(TypeRepr::StructRepr), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let index = type_summary_index_insert( + empty_type_summary_index(), + "owner.types.BoolWitnessClaim".to_string(), + "BoolWitnessClaim".to_string(), + summary, + ); + let source_indices = HashMap::new(); + let decls = empty_type_decl_index(); + assert_eq!( + defining_module_for_inferred_result( + &variant_to_enum, + &index, + &decls, + &source_indices, + None, + "owner.types.BoolWitnessClaim", + ), + Some("owner.types".to_string()), + "a user type identity named like a variant leaf is not routed to v2.std.verification" + ); + assert_eq!( + defining_module_for_variant(&variant_to_enum, "BoolWitnessClaim"), + Some("v2.std.verification".to_string()), + "the variant map still answers the record-lit arm" + ); + } + + fn synthetic_named_node(name: &str) -> Rc { + let span = no_span(); + Rc::new(Node { + occurrence_identity: Rc::new( + crate::std_occurrence_identity::NodeOccurrenceIdentity::OccurrenceSynthetic, + ), + name: name.to_string(), + ident: None, + span: span.clone(), + ident_span: Some(span), + children: Rc::new(im::Vector::new()), + connective: Connective::NoConnective, + params: Rc::new(im::Vector::new()), + inferred: None, + return_cardinality: Cardinality::Required, + uses: Rc::new(im::Vector::new()), + body: None, + transport: None, + properties: Rc::new(im::Vector::new()), + type_annotation: None, + is_self_recursive: false, + has_non_tail_self_call: false, + match_pattern: None, + module_item_kind: crate::v1_std_core::ParsedModuleItemKind::NotAModuleItem, + declaration_marker: crate::v1_std_core::DeclarationMarker::Unmarked, + declaration: None, + expr_data: Rc::new(ExprData::NoExprData), + }) + } + + #[test] + fn inferred_user_bool_witness_claim_keeps_its_module_at_initializer_ref() { + use crate::std_decl_ref::decl_ref; + use crate::v1_compiler_infer_emit_info::{ + empty_emit_graph_info, empty_type_decl_index, empty_type_summary_index, + type_decl_index_insert, type_summary_index_insert, TypeRepr, TypeSummary, + }; + + let mut variant_to_enum = im::HashMap::new(); + variant_to_enum.insert( + "BoolWitnessClaim".to_string(), + "v2.std.verification.UnifiedTestClaim".to_string(), + ); + + let mut type_node = (*synthetic_named_node("BoolWitnessClaim")).clone(); + type_node.declaration = Some(decl_ref( + "owner.types".to_string(), + "BoolWitnessClaim".to_string(), + )); + let type_node = Rc::new(type_node); + + let decl_item = type_node.clone(); + let decls = type_decl_index_insert( + empty_type_decl_index(), + "owner.types".to_string(), + "BoolWitnessClaim".to_string(), + decl_item, + ); + let summary = Rc::new(TypeSummary { + name: "BoolWitnessClaim".to_string(), + key: "owner.types.BoolWitnessClaim".to_string(), + repr: Rc::new(TypeRepr::StructRepr), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let summaries = type_summary_index_insert( + empty_type_summary_index(), + "owner.types.BoolWitnessClaim".to_string(), + "BoolWitnessClaim".to_string(), + summary, + ); + + let mut emit_info = (*empty_emit_graph_info()).clone(); + emit_info.variant_to_enum = Rc::new(variant_to_enum); + emit_info.type_summaries = summaries; + emit_info.type_decl_items = decls; + + let mut body = (*synthetic_named_node("")).clone(); + body.inferred = Some(Rc::new(InferredNode::Resolved { node: type_node })); + let body = Rc::new(body); + + let graph = ResolvedGraph { + modules: Rc::new(im::Vector::new()), + item_registry: Rc::new(im::HashMap::new()), + item_leaf_owner_modules: Rc::new(im::HashMap::new()), + diagnostics: Rc::new(im::Vector::new()), + }; + let source_indices = HashMap::new(); + let resolved = + resolved_initializer_decl_ref(&graph, &source_indices, &emit_info, &body, None) + .expect("initializer ref must resolve the carried user declaration"); + assert_eq!( + resolved.module, "owner.types", + "carried BoolWitnessClaim declaration must beat variant_to_enum UnifiedTestClaim" + ); + assert_eq!(resolved.name, "BoolWitnessClaim"); + } + + #[test] + fn ambiguous_enum_name_is_not_answered_false() { + use crate::v1_compiler_infer_emit_info::{ + empty_type_summary_index, is_enum_in_summaries, type_summary_index_insert, TypeRepr, + TypeSummary, TypeSummaryQuestion, + }; + let enum_summary = Rc::new(TypeSummary { + name: "SharedName".to_string(), + key: "owner.enums.SharedName".to_string(), + repr: Rc::new(TypeRepr::EnumRepr { unit_only: false }), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let struct_summary = Rc::new(TypeSummary { + name: "SharedName".to_string(), + key: "owner.types.SharedName".to_string(), + repr: Rc::new(TypeRepr::StructRepr), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let index = type_summary_index_insert( + type_summary_index_insert( + empty_type_summary_index(), + "owner.enums.SharedName".to_string(), + "SharedName".to_string(), + enum_summary, + ), + "owner.types.SharedName".to_string(), + "SharedName".to_string(), + struct_summary, + ); + assert!( + matches!( + (*is_enum_in_summaries(index, "SharedName".to_string())).clone(), + TypeSummaryQuestion::QuestionNameAmbiguous { leaf } if leaf == "SharedName" + ), + "disagreement must not collapse to a decided non-enum" + ); + } + + #[test] + fn ambiguous_enum_name_refuses_instead_of_non_enum_emission() { + use crate::v1_compiler_infer_emit_info::{ + empty_type_summary_index, type_summary_index_insert, TypeRepr, TypeSummary, + }; + let enum_summary = Rc::new(TypeSummary { + name: "SharedName".to_string(), + key: "owner.enums.SharedName".to_string(), + repr: Rc::new(TypeRepr::EnumRepr { unit_only: false }), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let struct_summary = Rc::new(TypeSummary { + name: "SharedName".to_string(), + key: "owner.types.SharedName".to_string(), + repr: Rc::new(TypeRepr::StructRepr), + field_summaries: crate::v1_rt::rc_empty_map(), + field_type_map: crate::v1_rt::rc_empty_map(), + field_import_surface_names: Rc::new(im::Vector::new()), + variant_name_set: crate::v1_rt::rc_empty_map(), + generic_param_names: Rc::new(im::Vector::new()), + has_fn_fields: false, + }); + let index = type_summary_index_insert( + type_summary_index_insert( + empty_type_summary_index(), + "owner.enums.SharedName".to_string(), + "SharedName".to_string(), + enum_summary, + ), + "owner.types.SharedName".to_string(), + "SharedName".to_string(), + struct_summary, + ); + let parent = crate::v1_compiler_emit_rust::pattern_parent_enum( + "Arm".to_string(), + None, + "SharedName".to_string(), + index, + ); + let refusal = crate::v1_compiler_emit_rust::rust_ambiguous_type_name_refusal( + "SharedName".to_string(), + ); + assert_eq!( + parent, + Some(refusal), + "an ambiguous enum name must emit the typed refusal, not a non-enum parent" + ); } - None } fn lookup_resolved_type_node(graph: &ResolvedGraph, type_name: &str) -> Option> { @@ -22271,27 +23468,53 @@ fn declared_type_name_from_annotation( } } -fn resolved_decl_ref_from_type_name( - graph: &ResolvedGraph, +fn defining_module_for_inferred_result( + variant_to_enum: &im::HashMap, + type_summaries: &Rc, + type_decls: &Rc, source_indices: &HashMap>, + inferred_node: Option<&Rc>, + name: &str, +) -> Option { + // Type declaration identity first. A user type whose leaf matches a known variant + // (BoolWitnessClaim) must not be stolen by variant_to_enum spelling. + inferred_node + .and_then(|node| { + defining_module_for_type_expr(type_summaries, type_decls, source_indices, node) + }) + .or_else(|| defining_module_for_type_identity(type_summaries, name)) + .or_else(|| defining_module_for_variant(variant_to_enum, name)) +} + +fn resolved_decl_ref_from_type_name( variant_to_enum: &im::HashMap, + type_summaries: &Rc, name: &str, ) -> Result { - let module = defining_module_for_resolved_type(graph, source_indices, variant_to_enum, name) + let module = defining_module_for_type_identity(type_summaries, name) + .or_else(|| defining_module_for_variant(variant_to_enum, name)) .ok_or_else(|| format!("no defining module for resolved type '{}'", name))?; + let stored_name = crate::v1_std_core::qualified_last_segment(name.to_string()); Ok(ResolvedDeclRef { module, - name: name.to_string(), + name: if stored_name.is_empty() { + name.to_string() + } else { + stored_name + }, }) } fn resolved_initializer_decl_ref( graph: &ResolvedGraph, source_indices: &HashMap>, - variant_to_enum: &im::HashMap, + emit_info: &crate::v1_compiler_infer_emit_info::EmitGraphInfo, body: &Rc, type_annotation: Option<&Rc>, ) -> Result { + let variant_to_enum = emit_info.variant_to_enum.as_ref(); + let type_summaries = &emit_info.type_summaries; + let type_decls = &emit_info.type_decl_items; let si = Rc::new(source_indices.clone()); if let ExprData::ExprRecordLit { parent_enum } = &*body.expr_data { if let Some(parent_name) = parent_enum.as_deref() { @@ -22321,18 +23544,13 @@ fn resolved_initializer_decl_ref( variant_name, parent_name )); } - let module = defining_module_for_resolved_type( - graph, - source_indices, - variant_to_enum, - parent_name, - ) - .ok_or_else(|| { - format!( - "no defining module for resolved coproduct '{}'", - parent_name - ) - })?; + let module = + defining_module_for_variant(variant_to_enum, &variant_name).ok_or_else(|| { + format!( + "no defining module for resolved coproduct variant '{}'", + variant_name + ) + })?; return Ok(ResolvedDeclRef { module, name: variant_name, @@ -22361,11 +23579,38 @@ fn resolved_initializer_decl_ref( None => None, }; if let Some(name) = inferred_name { - return resolved_decl_ref_from_type_name(graph, source_indices, variant_to_enum, &name); + let inferred_node = match body.inferred.as_deref() { + Some(InferredNode::Resolved { node }) => Some(node), + _ => None, + }; + if let Some(module) = defining_module_for_inferred_result( + variant_to_enum, + type_summaries, + type_decls, + source_indices, + inferred_node, + &name, + ) { + return Ok(ResolvedDeclRef { + module, + name: crate::v1_std_core::qualified_last_segment(name), + }); + } + return Err(format!("no defining module for resolved type '{}'", name)); } if let Some(ann) = type_annotation { + if let Some(module) = + defining_module_for_type_expr(type_summaries, type_decls, source_indices, ann) + { + let name = declared_type_name_from_annotation(source_indices, ann) + .unwrap_or_else(|| "type".to_string()); + return Ok(ResolvedDeclRef { + module, + name: crate::v1_std_core::qualified_last_segment(name), + }); + } if let Some(name) = declared_type_name_from_annotation(source_indices, ann) { - return resolved_decl_ref_from_type_name(graph, source_indices, variant_to_enum, &name); + return resolved_decl_ref_from_type_name(variant_to_enum, type_summaries, &name); } } Err( @@ -22502,7 +23747,7 @@ pub fn discover_owned_data_decls( let mut files = Vec::new(); collect_dag_files(scan_path, &mut files); - files.retain(|p| !path_excluded(p, exclude_subpaths)); + files.retain(|p| !path_excluded(p.path(), exclude_subpaths)); // The process-shared index, so every entry's reference closure reads the one parse per file // the floor's other closure walks already hold. @@ -22512,9 +23757,11 @@ pub fn discover_owned_data_decls( let mut groups: Vec = Vec::new(); let mut group_split_collisions: Vec = Vec::new(); let mut entry_count = 0usize; - for path in files { + for source in files { + let path = source.path(); let entry = path.to_string_lossy().to_string(); - let content = std::fs::read_to_string(&path) + let content = source + .read() .map_err(|e| format!("failed to read {:?}: {}", path, e))?; if !entry_likely_has_unified_claim_owned_data(&content) { continue; @@ -22585,15 +23832,13 @@ pub fn discover_owned_data_decls( // THE ONE READER OF variant_to_enum BUILDS IT, once per group graph, through the builder // emission uses: the resolve no longer carries EmitGraphInfo (v1.compiler.infer_items // ResolvedGraph), and owned-data discovery is the consumer that demands this projection. - let variant_to_enum = v1_compiler_infer::build_emit_graph_info( + let emit_info = v1_compiler_infer::build_emit_graph_info( graph.modules.clone(), graph.item_registry.clone(), - ) - .variant_to_enum - .clone(); + ); for (entry, entry_module, marker_count) in group.entries { let records = - owned_data_decls_for_entry(&graph, &si, &variant_to_enum, &entry, &entry_module)?; + owned_data_decls_for_entry(&graph, &si, &emit_info, &entry, &entry_module)?; if records.len() != marker_count { return Err(format!( "{}: merged-resolve discovery found {} owned unified_claim record(s) but the entry declares {} top-level `data unified_claim_` marker(s)", @@ -24297,25 +25542,31 @@ pub(crate) fn dag_tree_holds_any_file(dir: &Path) -> bool { false } -pub(crate) fn collect_dag_files_tolerant(dir: &Path, out: &mut Vec) { - // This walk swallows unreadable directories. Write failure here must not abort it; - // `run_dag_parse_sweep` is the loud required-CI writer. - let _ = derived_row_roster::ensure_if_row_dir(dir); +/// Tolerant of unreadable DIRECTORIES (they are skipped), never of a ledger row directory whose +/// roster cannot be derived: a legacy physical roster or an unreadable row file is returned as a +/// located refusal, because swallowing it would hand the caller a short or stale roster. +pub(crate) fn collect_dag_files_tolerant( + dir: &Path, + out: &mut Vec, +) -> Result<(), String> { let entries = match std::fs::read_dir(dir) { Ok(e) => e, - Err(_) => return, + Err(_) => return Ok(()), }; + let mut here = Vec::new(); for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { if is_cargo_target_output_dir(dir, &path) { continue; } - collect_dag_files_tolerant(&path, out); + collect_dag_files_tolerant(&path, out)?; } else if path.extension().and_then(|e| e.to_str()) == Some("dag") { - out.push(path); + here.push(path); } } + out.extend(derived_row_roster::acquire_dir_files(dir, here)?); + Ok(()) } fn scan_test_decl_names(content: &str) -> Vec { @@ -25168,11 +26419,13 @@ pub fn construction_authority_graph_unresolved( std::collections::HashMap::new(); let mut authorities: Vec<(String, String, String)> = Vec::new(); for root in source_roots { - let mut dag_files: Vec = Vec::new(); - collect_dag_files_tolerant(Path::new(root), &mut dag_files); + let mut dag_files = Vec::new(); + collect_dag_files_tolerant(Path::new(root), &mut dag_files)?; dag_files.sort(); - for path in dag_files { - let content = std::fs::read_to_string(&path) + for source in dag_files { + let path = source.path(); + let content = source + .read() .map_err(|e| format!("read {}: {e}", path.display()))?; let file = path.to_string_lossy().into_owned(); for (module_path, decl_name) in wall_now_authority_refs(&content) { @@ -29151,12 +30404,14 @@ pub fn discover_source_root_reads( let mut dag_files = Vec::new(); collect_dag_files(scan_path, &mut dag_files); - for path in dag_files { + for source in dag_files { + let path = source.path(); let rel_forward = path.to_string_lossy().replace('\\', "/"); if path_matches_any_subpath(&rel_forward, exclude_subpaths) { continue; } - let content = std::fs::read_to_string(&path) + let content = source + .read() .map_err(|e| format!("failed to read {:?}: {}", path, e))?; let module_path = extract_module_path(&content).ok_or_else(|| { format!( @@ -29971,10 +31226,11 @@ fn collect_layer_import_scoped_paths(roots: &[String]) -> HashSet { if !root_path.is_dir() { continue; } - let mut dag_files: Vec = Vec::new(); - collect_dag_files_tolerant(root_path, &mut dag_files); + let mut dag_files = Vec::new(); + collect_dag_files_tolerant(root_path, &mut dag_files) + .unwrap_or_else(|cause| panic!("layer-import scope over {root}: {cause}")); for file in dag_files { - scoped.insert(rel_path_for_layer_import(&file)); + scoped.insert(rel_path_for_layer_import(file.path())); } } scoped @@ -29992,10 +31248,11 @@ fn importer_roots_have_importless_dag_files(roots: &[String]) -> bool { if !root_path.is_dir() { continue; } - let mut dag_files: Vec = Vec::new(); - collect_dag_files_tolerant(root_path, &mut dag_files); + let mut dag_files = Vec::new(); + collect_dag_files_tolerant(root_path, &mut dag_files) + .unwrap_or_else(|cause| panic!("importless preflight over {root}: {cause}")); for file in dag_files { - let content = match std::fs::read_to_string(&file) { + let content = match file.read() { Ok(c) => c, Err(_) => continue, }; @@ -30023,18 +31280,19 @@ fn project_layer_import_syntax_facts( if !root_path.is_dir() { return; } - let mut dag_files: Vec = Vec::new(); - collect_dag_files_tolerant(root_path, &mut dag_files); + let mut dag_files = Vec::new(); + collect_dag_files_tolerant(root_path, &mut dag_files) + .unwrap_or_else(|cause| panic!("[layer-import] declared root {root}: {cause}")); dag_files.sort(); for file in dag_files { - let content = match std::fs::read_to_string(&file) { + let content = match file.read() { Ok(c) => c, Err(_) => continue, }; if extract_import_paths(&content).is_empty() { continue; } - let rel = rel_path_for_layer_import(&file); + let rel = rel_path_for_layer_import(file.path()); let importer = extract_module_path(&content).unwrap_or_default(); let layer = layer_prefix_from_dotted_module(&importer); for import_module in extract_import_paths(&content) { @@ -31247,12 +32505,45 @@ fn collect_node_refs_inner( bound.truncate(restore_to); } -/// Count of shared leading dot-separated segments between two module paths (containment proximity). -fn module_prefix_shared_len(a: &str, b: &str) -> usize { - a.split('.') - .zip(b.split('.')) - .take_while(|(x, y)| x == y) - .count() +/// Declarers on the referencing module's ancestor chain, sorted for a stable AmbiguousBare dump. +/// Containment is `type_ref_module_path_is_containment_prefix` (segment LCP), the same +/// predicate `global_bare_chain_candidates` applies to each candidate — not a second rule. +fn on_chain_declarers<'a>( + referencing_module: &str, + declarers: impl IntoIterator, +) -> Vec<&'a String> { + let mut out: Vec<&'a String> = declarers + .into_iter() + .filter(|m| { + crate::v1_compiler_infer_env::type_ref_module_path_is_containment_prefix( + (*m).clone(), + referencing_module.to_string(), + ) + }) + .collect(); + out.sort(); + out +} + +/// Import-less UniqueBare pick: on-chain unique, else the census-unique declarer the +/// resolver UniqueBinding-accepts, else nothing (never nearest-prefix among homonyms). +enum ImportlessBarePick<'a> { + Unique(&'a String), + Ambiguous(Vec<&'a String>), + None, +} + +fn pick_importless_bare<'a>( + referencing_module: &str, + mods: &'a std::collections::BTreeSet, +) -> ImportlessBarePick<'a> { + let on_chain = on_chain_declarers(referencing_module, mods.iter()); + match on_chain.len() { + 1 => ImportlessBarePick::Unique(on_chain[0]), + n if n > 1 => ImportlessBarePick::Ambiguous(on_chain), + _ if mods.len() == 1 => ImportlessBarePick::Unique(mods.iter().next().unwrap()), + _ => ImportlessBarePick::None, + } } /// Longest module-path prefix of a qualified chain that names a declared module. @@ -31328,7 +32619,7 @@ pub struct BareRefReachability { #[cfg(test)] mod reference_edge_producer_tests { - use super::reference_resolution_facts; + use super::*; fn fixture_root(tag: &str) -> std::path::PathBuf { // Under the workspace `target/` (gitignored): `rel_path_for_layer_import` fail-closes on @@ -31387,8 +32678,8 @@ mod reference_edge_producer_tests { let emits_any = |from_sub: &str| edges.iter().any(|e| e.path.contains(from_sub)); assert!( - has_edge("refless.dag", "test.decl"), - "import-less file referencing shared_fn must yield an edge to its declaring module" + !has_edge("refless.dag", "test.decl"), + "test.decl and test.reflocal both declare shared_fn; neither is on test.refless's chain, so UniqueBare/AmbiguousBare must not pick a neighbor" ); assert!( !emits_any("reflocal.dag"), @@ -31401,6 +32692,82 @@ mod reference_edge_producer_tests { let _ = std::fs::remove_dir_all(&root); } + /// THE IMPORT-LESS WRONG-EDGE RED on the real `dependency_resolution_facts` union + /// (`reference_edges_for_file` inside it — not a mutant of the producer). + /// + /// `frontier.child.consumer` calls `duplicated`. The lexical binder is the ancestor + /// `frontier`. A sibling `frontier.child.plant` also declares the spelling and shares a + /// longer module-path prefix, so proximity UniqueBare-binds the plant. After the climb the + /// edge is the ancestor, never the sibling. + #[test] + fn proximity_must_not_bind_an_importless_bare_name_to_a_sibling_homonym() { + let root = fixture_root("proximity-wrong-edge"); + let _ = std::fs::remove_dir_all(&root); + write( + &root, + "parent.dag", + "module frontier\nfn duplicated() -> Int { 1 }\n", + ); + write( + &root, + "plant.dag", + "module frontier.child.plant\nfn duplicated() -> Int { 99 }\n", + ); + write( + &root, + "consumer.dag", + "module frontier.child.consumer\nfn read() -> Int { duplicated() }\n", + ); + let roots = vec![root.to_string_lossy().into_owned()]; + let facts = super::dependency_resolution_facts(&roots, &roots, &[]); + let consumer_targets: Vec<&str> = facts + .iter() + .filter(|f| f.path.contains("consumer.dag")) + .map(|f| f.import_module.as_str()) + .collect(); + assert!( + !consumer_targets.contains(&"frontier.child.plant"), + "proximity UniqueBare bound the sibling plant: {consumer_targets:?}" + ); + assert!( + consumer_targets.contains(&"frontier"), + "the on-chain ancestor must remain the UniqueBare target: {consumer_targets:?}" + ); + let _ = std::fs::remove_dir_all(&root); + } + + /// Positive control: an import-less bare name whose only declarer is an ancestor still + /// produces a UniqueBare edge (lexical binding, not pool uniqueness). + #[test] + fn an_importless_bare_name_on_the_ancestor_chain_still_resolves() { + let root = fixture_root("lexical-on-chain"); + let _ = std::fs::remove_dir_all(&root); + write( + &root, + "parent.dag", + "module frontier\nfn duplicated() -> Int { 1 }\n", + ); + write( + &root, + "consumer.dag", + "module frontier.child.consumer\nfn read() -> Int { duplicated() }\n", + ); + write( + &root, + "unrelated.dag", + "module other.real\nfn unused() -> Int { 0 }\n", + ); + let roots = vec![root.to_string_lossy().into_owned()]; + let facts = super::dependency_resolution_facts(&roots, &roots, &[]); + let consumer_targets: Vec<&str> = facts + .iter() + .filter(|f| f.path.contains("consumer.dag")) + .map(|f| f.import_module.as_str()) + .collect(); + assert_eq!(consumer_targets, vec!["frontier"]); + let _ = std::fs::remove_dir_all(&root); + } + fn str_list_value(items: &[String]) -> crate::v1_interpreter::Value { super::list_value_from_vec( items @@ -36417,9 +37784,10 @@ mod import_closure_equivalence_tests { let mut entries = BTreeSet::new(); for root in default_source_roots() { let mut dag_files = Vec::new(); - super::collect_dag_files_tolerant(Path::new(&root), &mut dag_files); + super::collect_dag_files_tolerant(Path::new(&root), &mut dag_files) + .expect("acquire witness layer root"); for path in dag_files { - let rel = workspace_relative_repo_path(&path.to_string_lossy()); + let rel = workspace_relative_repo_path(&path.path().to_string_lossy()); if !rel.ends_with("_test.dag") || floor_discovery_path_excluded(&rel) { continue; } @@ -36542,12 +37910,18 @@ mod peel_alias_fixpoint_termination { }, ), ); + // No files in this probe: the peel tree is synthetic, module_path is empty. + // file_modules / module_named_imports therefore come from empty_symbol_index, + // the same constructor every other empty SymbolIndex uses. + let empty_index = crate::v1_compiler_infer_env::empty_symbol_index(); let symbol_index = std::rc::Rc::new(crate::v1_compiler_infer_env::SymbolIndex { - entries: crate::v1_rt::rc_empty_map(), + entries: empty_index.entries.clone(), global_bare, - services: crate::v1_rt::rc_empty_map(), - transparent_alias_rep: crate::v1_rt::rc_empty_map(), - type_head_exposures: crate::v1_rt::rc_empty_map(), + services: empty_index.services.clone(), + transparent_alias_rep: empty_index.transparent_alias_rep.clone(), + type_head_exposures: empty_index.type_head_exposures.clone(), + file_modules: empty_index.file_modules.clone(), + module_named_imports: empty_index.module_named_imports.clone(), }); let env = std::rc::Rc::new(crate::v1_compiler_infer_env::TypeEnv { module_path: "".to_string(), @@ -37638,13 +39012,14 @@ mod import_bearing_reference_edges { assert!(!t.contains(&"test.fixture.planted".to_string()), "{t:?}"); } - /// Control: the same bare name in an IMPORT-LESS file still resolves by proximity (unchanged), - /// and a qualified reference in an import-bearing file is still an edge (the other test). + /// After the proximity climb: neither homonym is on `test.claim.a`'s ancestor chain, so + /// the import-less file must not UniqueBare-bind the nearer planted fixture. #[test] - fn the_same_bare_name_without_the_import_still_resolves() { + fn the_same_bare_name_without_the_import_does_not_proximity_bind() { let src = "module test.claim.a\nfn g() -> Int { Present }\n"; let t = homonym_edges(src); - assert!(t.contains(&"test.fixture.planted".to_string()), "{t:?}"); + assert!(!t.contains(&"test.fixture.planted".to_string()), "{t:?}"); + assert!(!t.contains(&"std.optional".to_string()), "{t:?}"); } } @@ -38407,6 +39782,7 @@ pub fn prepare_repository_from_corpus( /// /// So the repository holds a projection with the cache emptied. The resolve is a fresh compile /// with no process-level memo, so the original modules drop here and their caches with them. +/// /// Every other field is the same `Rc`, so no evaluated value changes. fn prepared_graph_without_typecheck_caches( graph: &Rc, @@ -39089,20 +40465,16 @@ pub fn reference_targets_of(index: &ReferenceClosureIndex, module: &str) -> Vec< if mods.contains(module) { continue; } - let mut best_len = 0usize; - let mut winners: Vec<&String> = Vec::new(); - for m in mods.iter() { - let shared = module_prefix_shared_len(module, m); - if winners.is_empty() || shared > best_len { - best_len = shared; - winners.clear(); - winners.push(m); - } else if shared == best_len { - winners.push(m); + match pick_importless_bare(module, mods) { + ImportlessBarePick::None => {} + ImportlessBarePick::Unique(w) => { + out.insert(w.clone()); + } + ImportlessBarePick::Ambiguous(winners) => { + for w in winners { + out.insert(w.clone()); + } } - } - for w in winners { - out.insert(w.clone()); } } out.into_iter().collect() @@ -42619,13 +43991,25 @@ mod reference_closure_single_parse_differential { #[cfg(test)] mod multi_entry_index_sharing_control_tests { - use super::{multi_entry_index_sharing_control, MultiEntryIndexBuild}; + use super::{ + multi_entry_index_sharing_control, MultiEntryIndexBuild, MultiEntryIndexBuildKind, + }; fn build(digest: u64, site: &str) -> MultiEntryIndexBuild { MultiEntryIndexBuild { name_set_digest: digest, modules: 7, site: site.to_string(), + kind: MultiEntryIndexBuildKind::NameSetIndex, + } + } + + fn scratch(digest: u64, site: &str) -> MultiEntryIndexBuild { + MultiEntryIndexBuild { + name_set_digest: digest, + modules: 7, + site: site.to_string(), + kind: MultiEntryIndexBuildKind::ScratchCachesOverExistingSet, } } @@ -42653,6 +44037,18 @@ mod multi_entry_index_sharing_control_tests { "{err}" ); } + + #[test] + fn scratch_kind_does_not_count_as_a_second_name_set_index() { + assert_eq!( + multi_entry_index_sharing_control(&[ + build(1, "a:1"), + scratch(1, "s:1"), + scratch(1, "s:2"), + ]), + Ok(1) + ); + } } #[cfg(test)] diff --git a/src/v1/stage0/src/cli_run/compile_clean.rs b/src/v1/stage0/src/cli_run/compile_clean.rs index 53b67d4e8d4..5c3d456c6f3 100644 --- a/src/v1/stage0/src/cli_run/compile_clean.rs +++ b/src/v1/stage0/src/cli_run/compile_clean.rs @@ -1107,6 +1107,7 @@ pub fn compile_clean_diagnostic_class_specimen() -> Vec { EffectfulSelfRecursionUnrealized { name: s(), span: no_span() }, ModuleFilenameCollision { filename: s(), modules: istrings(), span: no_span() }, EmittedSymbolCollision { symbol: s(), identities: istrings(), span: no_span() }, + NativeEffectRealizationRefused { entry: s(), refusals: istrings(), span: no_span() }, EffectSummaryIncompleteAtFunctionValue { caller: s(), span: no_span() }, EffectSummaryIncompleteAtLocalBinding { caller: s(), name: s(), span: no_span() }, CallArgumentNameUnknown { callee: s(), argument: s(), declared: istrings(), span: no_span() }, @@ -1599,6 +1600,9 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str } CompilerDiagnostic::ModuleFilenameCollision { .. } => "ModuleFilenameCollision", CompilerDiagnostic::EmittedSymbolCollision { .. } => "EmittedSymbolCollision", + CompilerDiagnostic::NativeEffectRealizationRefused { .. } => { + "NativeEffectRealizationRefused" + } CompilerDiagnostic::EffectSummaryIncompleteAtFunctionValue { .. } => { "EffectSummaryIncompleteAtFunctionValue" } @@ -1717,6 +1721,12 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str CompilerDiagnostic::EffectfulSelfRecursionUnrealized { name, .. } => name.clone(), CompilerDiagnostic::ModuleFilenameCollision { filename, .. } => filename.clone(), CompilerDiagnostic::EmittedSymbolCollision { symbol, .. } => symbol.clone(), + CompilerDiagnostic::NativeEffectRealizationRefused { + entry, refusals, .. + } => format!( + "{entry}: {}", + refusals.iter().cloned().collect::>().join("; ") + ), CompilerDiagnostic::EffectSummaryIncompleteAtFunctionValue { caller, .. } => caller.clone(), CompilerDiagnostic::EffectSummaryIncompleteAtLocalBinding { caller, .. } => caller.clone(), CompilerDiagnostic::CallArgumentNameUnknown { argument, .. } => argument.clone(), diff --git a/src/v1/stage0/src/cli_run/derived_row_roster.rs b/src/v1/stage0/src/cli_run/derived_row_roster.rs index c46bbb189c5..abfeafa9eff 100644 --- a/src/v1/stage0/src/cli_run/derived_row_roster.rs +++ b/src/v1/stage0/src/cli_run/derived_row_roster.rs @@ -13,31 +13,36 @@ //! `rostered_row_join` reads the compiled declaration population, and a `RowType` row the selector //! missed refuses there as `DeclaredNotRostered`. The two disagree only loudly. //! -//! THIS IS NOT CALLED FROM EVERY DIRECTORY WALK. It is invoked from the collect sites -//! (`collect_dag_files_result`, `collect_dag_files_tolerant`, `main.rs` `collect_dag_files`, -//! `compiler_tests` `collect_dag_recursive`) and from `run_dag_parse_sweep`, which is the -//! required-CI parse phase. That last site is load-bearing: modules import each roster -//! (`gunbc.design_ledgers`, `gunbc.ledger_row_coherence`, and for rung drops -//! `gunbc.rung_drop.standing` and its readers), and the parse join reads that module from the -//! sweep index. A checkout with no committed roster must derive it before the -//! sweep lists `.dag` files, or the join reports `RosterModuleAbsent` on the intended steady -//! state. The collect sites are convenience for other compilers; they are not the merge-path -//! writer. +//! THE ROSTER IS CONTRIBUTED TO THE COMPILER'S INPUT IN MEMORY; NOTHING IS WRITTEN. Every walk +//! that acquires a source population (`collect_dag_files_result`, `collect_dag_files_tolerant`, +//! `main.rs` `collect_dag_files`, `compiler_tests` `collect_dag_recursive`, and +//! `run_dag_parse_sweep`, the required-CI parse phase) hands each directory's listing to +//! `acquire_dir_files`, which returns `AcquiredDag` sources: the listed files, plus -- for a row +//! directory -- the roster rendered from the sibling contents that same call read. So +//! `compiler_sources = acquired_sources + derive_rosters(acquired_sources)`, from one read of the +//! rows, and the parse sweep, the module indexes and entry lookup see one population. The parse +//! sweep is load-bearing: modules import each roster (`gunbc.design_ledgers`, +//! `gunbc.ledger_row_coherence`, and for rung drops `gunbc.rung_drop.standing` and its readers), +//! and the parse join reads that module from the sweep index, or reports `RosterModuleAbsent`. +//! +//! An earlier realization wrote a gitignored `roster.dag` into the row directory on every walk. +//! That made every invocation dirty its own checkout, so deploy candidate admission refused the +//! checkout the deploy was about to admit. A physical file at the reserved identity now REFUSES, +//! located; it is never consumed, overridden or deleted by the loader. +//! //! An empty list is a different state — a present `= []` — and only arises if the directory -//! contains no sibling row files. A `read_dir` or dirent error refuses the collect; a `.dag` -//! file whose stem is not utf-8 refuses rather than being dropped from the list. If a listed -//! row file is compiled as `RecurringFailureMode` and the derived list omits it, the parse -//! join reports `DeclaredNotRostered`. Neither arm is a silent zero. +//! contains no sibling row files. A `read_dir` or dirent error refuses the collect; an unreadable +//! row file or a `.dag` file whose stem is not utf-8 refuses rather than being dropped from the +//! list. If a listed row file is compiled as the row type and the derived list omits it, the +//! parse join reports `DeclaredNotRostered`. Neither arm is a silent zero. //! -//! The compiler writes this gitignored file as a realization of a capability the substrate -//! lacks (directory enumeration sufficient to bind each sibling as a typed value). That -//! write is a read-path side effect; a failed write refuses the collect rather than proceeding -//! with a missing module. Dissolution: declaration-value binding over the declared population, -//! same grounding as `gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall`. +//! This host fold realizes a capability the substrate lacks (directory enumeration sufficient to +//! bind each sibling as a typed value). Dissolution: declaration-value binding over the declared +//! population, same grounding as `gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall`. use std::fs; use std::io; -use std::path::Path; +use std::path::{Path, PathBuf}; pub const ROSTER_BASENAME: &str = "roster.dag"; @@ -52,7 +57,7 @@ pub const ROSTER_BASENAME: &str = "roster.dag"; /// ONE SPELLING, BECAUSE `rostered_row_join` LOOKS THE ROSTER MODULE UP BY NAME. /// `ENROLLED_ROW_TYPES` carries each roster's module identity from this table and /// `run_rostered_row_join` resolves it with `index_get(index, enrolled.roster_module)` against the -/// module header `render_roster` WRITES. A divergence fails closed as the typed, located +/// module header `render_roster` RENDERS. A divergence fails closed as the typed, located /// `RosterModuleAbsent` finding. /// /// THOSE ASSERTIONS ARE NOT A WALL, AND THE RUNG IS 1 -- MITIGATION. They are `#[cfg(test)]` under @@ -96,7 +101,7 @@ pub const ROW_DIR_REL: &str = RECURRING_FAILURE_MODE.row_dir_rel; /// Which derived roster, if any, `rel` IS -- under any sweep root. /// /// THE ROSTER HAS NO BASE SIDE IN A DIFF, WHICH IS THE WHOLE REASON THIS PREDICATE EXISTS. -/// The file is gitignored and written on the read path, so it never appears in +/// The module has no file -- it is contributed in memory on the read path -- so it never appears in /// `git diff --name-status` — and a baseline reconstructed by dropping only the paths the diff /// TOUCHED therefore inherits the HEAD's roster bytes as the BASE's. That inheritance made a /// row append report a binding the base already spelled (`base {} -> head {row}` on a key @@ -122,7 +127,7 @@ pub fn roster_root_prefix(rel: &str) -> Option<&str> { } /// The roster that `roster_rel` would derive from an ARBITRARY path listing rather than from the -/// filesystem — the base tree's answer, rendered by the SAME function the writer uses, so there is +/// filesystem — the base tree's answer, rendered by the SAME function the head acquisition uses, so there is /// one authority for the roster's bytes and not a second reconstruction beside it. /// /// `paths` is a repo-relative listing (`git ls-tree -r --name-only `) and `read` returns a @@ -179,79 +184,114 @@ fn row_dir_roster(dir: &Path) -> Option<&'static DerivedRowRoster> { .find(|roster| dir.ends_with(Path::new(roster.row_dir_rel))) } -pub fn ensure_if_row_dir(dir: &Path) -> io::Result<()> { - match row_dir_roster(dir) { - Some(roster) => ensure_derived_roster(roster, dir), - None => Ok(()), - } +/// One `.dag` source admitted into a compiler input population. +/// +/// A walk over a source root yields these rather than bare paths, because one member of the +/// population -- each derived ledger roster -- has NO FILE: it is contributed in memory from the +/// sibling row contents the same walk acquired. A bare `PathBuf` for it would be a phantom that a +/// consumer's `read_to_string` turns into a missing-file error (or, worse, into whatever stale file +/// an older binary left there). So there is deliberately no `Deref`: every consumer +/// reads through `read`, and the compiler refuses to build a consumer that reaches for the disk. +/// +/// `content` is `Some` for every file this walk already had to read (the row-directory siblings +/// whose declared type selects membership) and for the derived roster, so the roster and the +/// compiler consume the SAME bytes; it is `None` for an ordinary file, read on demand. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct AcquiredDag { + path: PathBuf, + content: Option, + derived: bool, } -pub fn ensure_if_row_dir_or_panic(dir: &Path) { - if let Err(e) = ensure_if_row_dir(dir) { - panic!("failed to derive row roster in {:?}: {}", dir, e); +impl AcquiredDag { + pub fn on_disk(path: PathBuf) -> Self { + AcquiredDag { + path, + content: None, + derived: false, + } } -} -pub fn ensure_derived_roster(roster: &DerivedRowRoster, dir: &Path) -> io::Result<()> { - let names = row_stems(roster, dir)?; - let body = render_roster(roster, &names); - let path = dir.join(ROSTER_BASENAME); - match fs::read(&path) { - Ok(existing) if existing == body.as_bytes() => Ok(()), - _ => write_atomically(&path, body.as_bytes()), + /// The module's location in the source tree. For a derived roster this is the reserved + /// identity `/roster.dag`, which no file occupies (`acquire_dir_files` refuses one). + pub fn path(&self) -> &Path { + &self.path } -} -fn write_atomically(path: &Path, body: &[u8]) -> io::Result<()> { - let nanos = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - let tmp = path.with_file_name(format!( - "roster.dag.{}.{}.deriving", - std::process::id(), - nanos - )); - fs::write(&tmp, body)?; - match fs::rename(&tmp, path) { - Ok(()) => Ok(()), - Err(e) => { - let _ = fs::remove_file(&tmp); - Err(e) + pub fn is_derived(&self) -> bool { + self.derived + } + + pub fn read(&self) -> io::Result { + match &self.content { + Some(content) => Ok(content.clone()), + None => fs::read_to_string(&self.path), } } } -fn row_stems(roster: &DerivedRowRoster, dir: &Path) -> io::Result> { +/// Admit ONE directory's `.dag` files -- the walker's own listing of `dir`, any order -- into the +/// compiler input population, contributing the derived roster when `dir` is a ledger row +/// directory. +/// +/// THIS NEVER WRITES. The roster is `render_roster` over the sibling rows' declared types, read +/// from the bytes this call acquired and handed to the compiler in the same population. Source +/// roots may be read-only; a second invocation sees the same tree the first one did. +/// +/// A PHYSICAL FILE AT THE RESERVED IDENTITY REFUSES. Older binaries wrote `roster.dag` into the +/// row directory. Such a file is never consumed (it may be stale against the rows), never silently +/// overridden by the derived module (two sources for one module identity), and never deleted here +/// (source loading performs no cleanup) -- the refusal names it and its retirement. +/// An unreadable sibling or a non-utf8 stem also refuses, located, rather than shortening the list. +pub fn acquire_dir_files(dir: &Path, files: Vec) -> Result, String> { + let Some(roster) = row_dir_roster(dir) else { + return Ok(files.into_iter().map(AcquiredDag::on_disk).collect()); + }; + let mut out = Vec::with_capacity(files.len() + 1); let mut names = Vec::new(); - let entries = fs::read_dir(dir)?; - for entry in entries { - let entry = entry?; - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("dag") { - continue; - } - let stem = match path.file_stem().and_then(|n| n.to_str()) { - Some(s) => s, - None => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "{} row file {:?} has a non-utf8 stem; refusing to derive a shortened roster", - roster.row_module, path - ), - )); - } - }; - if stem == "roster" { - continue; + for path in files { + let stem = path.file_stem().and_then(|n| n.to_str()).ok_or_else(|| { + format!( + "{}: {} row file has a non-utf8 stem; refusing to derive a shortened roster", + path.display(), + roster.row_module + ) + })?; + if path.file_name().and_then(|n| n.to_str()) == Some(ROSTER_BASENAME) { + return Err(format!( + "{}: a physical file occupies the reserved derived-roster module identity {} -- \ + the roster is derived in memory from the sibling {} rows, so a file there is \ + never consumed and never overridden. It is a legacy artifact of an older gunbc \ + that wrote rosters into the checkout; retire it (remove the file) and re-run", + path.display(), + roster.roster_module, + roster.row_type + )); } - if declares_row(&fs::read_to_string(&path)?, stem, roster.row_type) { + let content = fs::read_to_string(&path).map_err(|e| { + format!( + "{}: cannot read {} row file ({e}); refusing to derive a shortened {}", + path.display(), + roster.row_module, + roster.roster_module + ) + })?; + if declares_row(&content, stem, roster.row_type) { names.push(stem.to_string()); } + out.push(AcquiredDag { + path, + content: Some(content), + derived: false, + }); } names.sort(); - Ok(names) + out.push(AcquiredDag { + path: dir.join(ROSTER_BASENAME), + content: Some(render_roster(roster, &names)), + derived: true, + }); + Ok(out) } pub fn render_roster(roster: &DerivedRowRoster, names: &[String]) -> String { @@ -454,4 +494,200 @@ mod tests { ); } } + + // ---- In-memory acquisition (`acquire_dir_files`) and its real consumer, the parse sweep. ---- + + fn scratch_workspace(tag: &str) -> std::path::PathBuf { + let dir = std::env::temp_dir().join(format!( + "gunbc-derived-roster-{tag}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0) + )); + std::fs::create_dir_all(dir.join("dag/gunbc/rung_drop")).unwrap(); + dir + } + + fn author(ws: &std::path::Path, rel: &str, body: &str) { + let path = ws.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, body).unwrap(); + } + + fn drop_row(stem: &str) -> String { + format!("module gunbc.rung_drop.{stem}\n\ndata {stem}: RungDrop = RungDrop {{ name: \"{stem}\" }}\n") + } + + /// Every file under `root`, so "nothing was written" is a statement about the tree, not about + /// one expected path. + fn tree(root: &std::path::Path) -> Vec { + let mut out = Vec::new(); + let mut stack = vec![root.to_path_buf()]; + while let Some(dir) = stack.pop() { + for entry in std::fs::read_dir(&dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + stack.push(path.clone()); + } + out.push(path); + } + } + out.sort(); + out + } + + fn rung_drop_fixture(tag: &str) -> std::path::PathBuf { + let ws = scratch_workspace(tag); + author(&ws, "dag/gunbc/rung_drop/beta.dag", &drop_row("beta")); + author(&ws, "dag/gunbc/rung_drop/alpha.dag", &drop_row("alpha")); + author( + &ws, + "dag/gunbc/rung_drop/standing.dag", + "module gunbc.rung_drop.standing\n\ndata standing_note: Int = 1\n", + ); + ws + } + + fn acquire_rung_drop_dir(ws: &std::path::Path) -> Result, String> { + let mut files = Vec::new(); + crate::cli_run::collect_dag_files_result(&ws.join("dag"), &mut files)?; + Ok(files) + } + + fn derived_roster_body(files: &[super::AcquiredDag]) -> String { + let derived: Vec<_> = files.iter().filter(|f| f.is_derived()).collect(); + assert_eq!( + derived.len(), + 1, + "exactly one derived module per row directory" + ); + derived[0].read().expect("derived content is in memory") + } + + #[test] + fn acquisition_contributes_the_roster_in_memory_and_writes_nothing() { + let ws = rung_drop_fixture("contributes"); + let before = tree(&ws); + let files = acquire_rung_drop_dir(&ws).expect("acquire"); + let body = derived_roster_body(&files); + assert!(body.contains("data rung_drop_roster: List = [\n alpha,\n beta,\n]\n")); + assert!( + !body.contains("standing"), + "a helper module is never a member" + ); + let roster = files.iter().find(|f| f.is_derived()).unwrap(); + assert_eq!(roster.path(), ws.join("dag/gunbc/rung_drop/roster.dag")); + assert!(!roster.path().exists(), "the derived module has no file"); + // A second invocation sees, and leaves, the identical tree. + let again = acquire_rung_drop_dir(&ws).expect("acquire again"); + assert_eq!(derived_roster_body(&again), body); + assert_eq!( + tree(&ws), + before, + "acquisition never writes under a source root" + ); + let _ = std::fs::remove_dir_all(&ws); + } + + #[test] + fn adding_and_removing_a_row_file_changes_the_roster() { + let ws = rung_drop_fixture("membership"); + author(&ws, "dag/gunbc/rung_drop/gamma.dag", &drop_row("gamma")); + let body = derived_roster_body(&acquire_rung_drop_dir(&ws).unwrap()); + assert!(body.contains(" alpha,\n beta,\n gamma,\n]")); + std::fs::remove_file(ws.join("dag/gunbc/rung_drop/alpha.dag")).unwrap(); + let body = derived_roster_body(&acquire_rung_drop_dir(&ws).unwrap()); + assert!(body.contains("= [\n beta,\n gamma,\n]")); + assert!(!body.contains("alpha")); + let _ = std::fs::remove_dir_all(&ws); + } + + #[test] + fn a_legacy_physical_roster_refuses_located_and_is_neither_consumed_nor_deleted() { + let ws = rung_drop_fixture("legacy"); + let legacy = ws.join("dag/gunbc/rung_drop/roster.dag"); + std::fs::write(&legacy, "module gunbc.rung_drop.roster\n\nSTALE\n").unwrap(); + let err = acquire_rung_drop_dir(&ws).expect_err("a physical roster must refuse"); + assert!( + err.contains(&legacy.display().to_string()), + "located: {err}" + ); + assert!( + err.contains("gunbc.rung_drop.roster"), + "names the identity: {err}" + ); + assert_eq!( + std::fs::read_to_string(&legacy).unwrap(), + "module gunbc.rung_drop.roster\n\nSTALE\n", + "the loader performs no cleanup" + ); + // The tolerant walk refuses identically rather than swallowing it. + let mut files = Vec::new(); + assert!(crate::cli_run::collect_dag_files_tolerant(&ws.join("dag"), &mut files).is_err()); + let _ = std::fs::remove_dir_all(&ws); + } + + #[test] + fn an_unreadable_row_input_refuses_rather_than_shortening_the_roster() { + let ws = rung_drop_fixture("unreadable"); + let dir = ws.join("dag/gunbc/rung_drop"); + // A listed row input whose bytes cannot be read (here: listed, then gone before the read). + let bad = dir.join("broken.dag"); + let err = super::acquire_dir_files( + &dir, + vec![dir.join("alpha.dag"), bad.clone(), dir.join("beta.dag")], + ) + .err(); + let err = err.expect("an unreadable listed row refuses"); + assert!(err.contains(&bad.display().to_string()), "located: {err}"); + let _ = std::fs::remove_dir_all(&ws); + } + + #[cfg(unix)] + #[test] + fn acquisition_works_over_read_only_source_roots() { + use std::os::unix::fs::PermissionsExt; + let ws = rung_drop_fixture("readonly"); + let dir = ws.join("dag/gunbc/rung_drop"); + let set = |mode: u32| { + for d in [ws.join("dag"), ws.join("dag/gunbc"), dir.clone()] { + std::fs::set_permissions(&d, std::fs::Permissions::from_mode(mode)).unwrap(); + } + }; + set(0o555); + let result = acquire_rung_drop_dir(&ws); + set(0o755); + let body = derived_roster_body(&result.expect("read-only roots acquire")); + assert!(body.contains(" alpha,\n beta,\n]")); + assert!(!dir.join("roster.dag").exists()); + let _ = std::fs::remove_dir_all(&ws); + } + + /// THE PRODUCER -> CONSUMER WITNESS: the required-CI parse sweep, over unchanged source roots, + /// indexes the derived roster module from the in-memory contribution and leaves the tree as + /// it found it. + #[test] + fn the_parse_sweep_indexes_the_derived_roster_without_writing_it() { + let ws = rung_drop_fixture("sweep"); + let before = tree(&ws); + let sweep = crate::cli_run::run_dag_parse_sweep(&ws, &["dag"]).expect("sweep parses"); + let record = + crate::cli_run::declaration_index::index_get(&sweep.index, "gunbc.rung_drop.roster") + .expect("the derived roster module is in the sweep index"); + assert_eq!(record.rel_path, "dag/gunbc/rung_drop/roster.dag"); + assert!(record.declared.contains("rung_drop_roster")); + assert!(crate::cli_run::declaration_index::index_get( + &sweep.index, + "gunbc.rung_drop.standing" + ) + .is_some()); + assert_eq!( + tree(&ws), + before, + "the sweep writes nothing under its roots" + ); + let _ = std::fs::remove_dir_all(&ws); + } } diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 915ca8dfec5..8bf1ed91d95 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -51,7 +51,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use crate::coproduct_reflection::{decl_facts_corpus_walk, DeclFactRaw}; use crate::module_path_index::{ @@ -3123,12 +3123,21 @@ pub(crate) fn fixture_closure_union_digest(members: &BTreeMap) - /// is the compile's list followed by the emitter's (`emit_resolved_for_target_selected`), so /// the suffix past the compile's length is exactly what the emitter added. /// -/// Unmodeled rust stderr-capture channels (`gunbc.rung_drop` -/// `fixture_closure_union_unmodeled_stderr_capture`) are a typed exclusion, not this refusal: -/// those services are stripped from the emit graph and listed on the observation. A sibling -/// unmodeled key or any other emit error still refuses. +/// Rust realizes the three capture-accounting channels. An unmodeled sibling key or any +/// other emit error still refuses. The retired drop +/// `fixture_closure_union_unmodeled_stderr_capture` no longer strips a service. pub(crate) fn fixture_closure_union_emit_receipt( union: &FixtureClosureUnion, +) -> Result { + fixture_closure_union_emit_receipt_staged(union, &|_, _, _| {}) +} + +/// The receipt with a stage observer: `on_stage(stage, members, wall_ms)` is called as each stage +/// completes, BEFORE the next stage's work starts, so a run cancelled inside the phase still +/// names the stage that was running. The caller owns the printing (a library crate may not). +pub(crate) fn fixture_closure_union_emit_receipt_staged( + union: &FixtureClosureUnion, + on_stage: &dyn Fn(&str, usize, u128), ) -> Result { let digest = fixture_closure_union_digest(&union.members); let refuse = |cause: &str, what: String| { @@ -3172,7 +3181,16 @@ pub(crate) fn fixture_closure_union_emit_receipt( }) }) .collect(); + let stage_started = std::time::Instant::now(); + let stage_line = |stage: &str| { + on_stage( + stage, + union.members.len(), + stage_started.elapsed().as_millis(), + ); + }; let resolved = v1_compiler_compile::compile_to_resolved(Rc::new(sources.into())); + stage_line("compile-done"); let located = |d: &Rc| { format!( "module={} reason=`{}`", @@ -3198,12 +3216,13 @@ pub(crate) fn fixture_closure_union_emit_receipt( ), )); } - let (resolved, excluded) = union_emit_graph_excluding_unmodeled_stderr_capture(resolved); let compile_diagnostics = resolved.diagnostics.len(); + stage_line("exclusion-done"); let rendered = v1_compiler_compile::emit_resolved_for_target( resolved, crate::v1_compiler_artifact::RenderTarget::Rust, ); + stage_line("emit-done"); let emitted: Vec<&Rc> = rendered .diagnostics .iter() @@ -3229,369 +3248,10 @@ pub(crate) fn fixture_closure_union_emit_receipt( digest, files: rendered.files.len(), emit_diagnostics: emitted.len(), - excluded, - }) -} - -/// Deleting either the drop row or its typed carrier fails this compile. The union compiles -/// the carrier through `compile_to_resolved` and evaluates `exclusion` (review 77125). -const STDERR_CAPTURE_GAP_CARRIER_SOURCE: &str = - include_str!("../../../../../dag/gunbc/stderr_capture_gap_exclusion.dag"); -const STDERR_CAPTURE_POLICY_DROP_SOURCE: &str = include_str!( - "../../../../../dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag" -); - -struct CaptureGapExclusion { - drop_identity: String, - declaring_module: String, - service: String, - operation_qualified: String, - operation_bare: String, -} - -fn record_field_string( - ctx: &crate::v1_interpreter::InterpContext, - fields: &[(crate::v1_interpreter::Symbol, crate::v1_interpreter::Value)], - name: &str, -) -> Result { - use crate::v1_interpreter::Value; - match fields - .iter() - .find(|(sym, _)| ctx.sym_eq(*sym, name)) - .map(|(_, v)| v) - { - Some(Value::Str(s)) => Ok(s.to_string()), - Some(other) => Err(format!( - "cause=CaptureGapExclusionFieldNotString field={name} value={other:?}" - )), - None => Err(format!( - "cause=CaptureGapExclusionFieldMissing field={name}" - )), - } -} - -fn load_capture_gap_exclusion() -> Result { - let _drop_row_tether = STDERR_CAPTURE_POLICY_DROP_SOURCE; - let files = vec![Rc::new(v1_compiler_compile::SourceFile { - path: "dag/gunbc/stderr_capture_gap_exclusion.dag".to_string(), - content: STDERR_CAPTURE_GAP_CARRIER_SOURCE.to_string(), - })]; - let resolved = v1_compiler_compile::compile_to_resolved(Rc::new(files.into())); - let located = |d: &Rc| { - format!( - "module={} reason=`{}`", - d.module_name, - crate::v1_std_core::diagnostic_to_message(d.diagnostic.clone()) - ) - }; - if v1_compiler_compile::emittable_graph(resolved.clone()).is_none() { - let blocking: Vec = resolved - .diagnostics - .iter() - .filter(|d| { - crate::v1_std_core::is_interpreter_blocking_diagnostic(d.diagnostic.clone()) - }) - .map(located) - .collect(); - return Err(format!( - "cause=CaptureGapExclusionUncompilable drop_row_bytes={} diagnostics={}", - _drop_row_tether.len(), - blocking.join(" | ") - )); - } - let Some(graph) = resolved.graph.clone() else { - return Err(format!( - "cause=CaptureGapExclusionUncompilable drop_row_bytes={} -- resolved graph is none", - _drop_row_tether.len() - )); - }; - let ctx = make_eval_context( - &graph, - resolved.source_indices.clone(), - crate::v1_interpreter::ExecutionMode::Hermetic, - ); - let val = crate::v1_interpreter::with_active_context(&ctx, || { - match crate::v1_interpreter::eval_data_item_value(&ctx, "exclusion") { - Ok(Some(v)) => Ok(Some(v)), - Ok(None) => crate::v1_interpreter::eval_data_item_value( - &ctx, - "gunbc.stderr_capture_gap_exclusion.exclusion", - ), - Err(e) => Err(e), - } - }) - .map_err(|e| format!("cause=CaptureGapExclusionEvalFailed error={e}"))? - .ok_or_else(|| "cause=CaptureGapExclusionDataMissing name=exclusion".to_string())?; - let crate::v1_interpreter::Value::Record { - ref fields, - type_name, - } = val - else { - return Err(format!("cause=CaptureGapExclusionNotRecord value={val:?}")); - }; - if !ctx.sym_eq(type_name, "StderrCaptureGapExclusion") - && !ctx.sym_eq( - type_name, - "gunbc.stderr_capture_gap_exclusion.StderrCaptureGapExclusion", - ) - { - return Err(format!( - "cause=CaptureGapExclusionUnexpectedType type={}", - ctx.resolve(type_name) - )); - } - Ok(CaptureGapExclusion { - drop_identity: record_field_string(&ctx, fields.as_slice(), "drop_identity")?, - declaring_module: record_field_string(&ctx, fields.as_slice(), "declaring_module")?, - service: record_field_string(&ctx, fields.as_slice(), "service")?, - operation_qualified: record_field_string(&ctx, fields.as_slice(), "operation_qualified")?, - operation_bare: record_field_string(&ctx, fields.as_slice(), "operation_bare")?, - }) -} - -fn capture_gap_exclusion() -> Result<&'static CaptureGapExclusion, &'static str> { - static GAP: OnceLock> = OnceLock::new(); - match GAP.get_or_init(load_capture_gap_exclusion) { - Ok(gap) => Ok(gap), - Err(e) => Err(e.as_str()), - } -} - -/// Facts `v1.compiler.emit` `shell_emission_refusal_fact` renders for -/// `ShellChannelNotRealizedByTarget` on the three stderr-accounting channels the drop names. -/// Not every channel `shell_channel_realized_by_target` currently returns false for: a later -/// unrealized stdout-side channel must still refuse the union (review 77034). -fn rust_stderr_capture_channel_not_realized_facts() -> &'static BTreeSet { - static FACTS: OnceLock> = OnceLock::new(); - FACTS.get_or_init(|| { - use crate::v1_compiler_artifact::RenderTarget; - use crate::v1_compiler_emit::{ - render_target_name, shell_channel_realized_by_target, shell_emission_refusal_fact, - shell_result_channel_key, ShellEmissionRefusal, ShellResultChannel, - }; - let target = RenderTarget::Rust; - let target_name = render_target_name(target); - [ - ShellResultChannel::ShellChanStderrTruncated, - ShellResultChannel::ShellChanStderrTotalBytes, - ShellResultChannel::ShellChanStderrRetainedBytes, - ] - .into_iter() - .filter(|channel| !shell_channel_realized_by_target(*channel, target)) - .map(|channel| { - shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: shell_result_channel_key(channel), - target_name: target_name.clone(), - }, - )) - }) - .collect() - }) -} - -/// The drop population: rust shell TransportEmissionNotModeled on exactly -/// `extdeps.gunbc` `WitnessBin.Run` whose fact equals a ShellChannelNotRealizedByTarget -/// fact for an unrealized rust channel. Any other module, service, or operation stays rendered. -fn stderr_capture_policy_gap_service(d: &Rc) -> Option<(String, String)> { - let Ok(gap) = capture_gap_exclusion() else { - return None; - }; - match &*d.diagnostic { - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind, - service, - operation, - declaring_module, - target, - missing_realization_fact, - .. - } if transport_kind == "shell" - && target == "rust" - && declaring_module.as_str() == gap.declaring_module - && service.as_str() == gap.service - && is_drop_run_operation(operation) - && rust_stderr_capture_channel_not_realized_facts() - .contains(missing_realization_fact) => - { - Some((declaring_module.clone(), service.clone())) - } - _ => None, - } -} - -fn is_drop_run_operation(operation: &str) -> bool { - let Ok(gap) = capture_gap_exclusion() else { - return false; - }; - operation == gap.operation_bare || operation == gap.operation_qualified -} - -fn transport_emission_run(d: &Rc) -> Option<(String, String)> { - match &*d.diagnostic { - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - service, - operation, - declaring_module, - .. - } if is_drop_run_operation(operation) => Some((declaring_module.clone(), service.clone())), - _ => None, - } -} - -/// The selection fold: among supplied unmodeled-transport rows, keep `(module, service)` -/// only when every Run row for that pair is a capture-gap fact. Production feeds it the -/// three emit diagnostic streams; tests supply rows. -fn select_run_operations_excluded_for_stderr_capture_gap( - unmodeled: &[Rc], -) -> BTreeSet<(String, String)> { - let mut gap_runs: BTreeSet<(String, String)> = BTreeSet::new(); - for d in unmodeled { - if let Some(key) = stderr_capture_policy_gap_service(d) { - gap_runs.insert(key); - } - } - gap_runs - .into_iter() - .filter(|key| { - unmodeled - .iter() - .filter(|d| transport_emission_run(d).as_ref() == Some(key)) - .all(|d| stderr_capture_policy_gap_service(d).is_some()) - }) - .collect() -} - -/// `extdeps.gunbc` `gunbc.WitnessBin.Run` when that operation's unmodeled-transport refusals -/// are solely the rust stderr-capture gap. Other operations on the same service are not members. -fn run_operations_excluded_for_stderr_capture_gap( - typed: &Rc, -) -> BTreeSet<(String, String)> { - let target = crate::v1_compiler_artifact::RenderTarget::Rust; - let mut unmodeled = Vec::new(); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_file_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_shell_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_rest_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - select_run_operations_excluded_for_stderr_capture_gap(&unmodeled) -} - -fn strip_excluded_run_operations( - typed: Rc, - excluded: &BTreeSet<(String, String)>, -) -> Rc { - if excluded.is_empty() { - return typed; - } - let modules = Rc::new( - typed - .modules - .iter() - .map(|tm| { - let module_name = crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - tm.module.clone(), - ); - let items = Rc::new( - tm.items - .iter() - .filter_map(|item| { - if item.module_item_kind - != crate::v1_std_core::ParsedModuleItemKind::ModuleItemService - { - return Some((*item).clone()); - } - let service = crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - (*item).clone(), - ); - if !excluded.contains(&(module_name.clone(), service)) { - return Some((*item).clone()); - } - let kept: im::Vector<_> = item - .children - .iter() - .filter(|op| { - !is_drop_run_operation( - &crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - (*op).clone(), - ), - ) - }) - .cloned() - .collect(); - if kept.is_empty() { - return None; - } - Some(Rc::new({ - let mut node = (**item).clone(); - node.children = Rc::new(kept); - node - })) - }) - .collect::>(), - ); - Rc::new(crate::v1_compiler_infer_items::TypedModule { - items, - ..(**tm).clone() - }) - }) - .collect::>(), - ); - Rc::new(crate::v1_compiler_infer_items::ResolvedGraph { - modules, - ..(*typed).clone() + excluded: Vec::new(), }) } -/// Compile stays the full closure. Emit strips only `gunbc.WitnessBin.Run` when its rust -/// refusals are solely the capture-policy gap. Other operations on that service stay. -fn union_emit_graph_excluding_unmodeled_stderr_capture( - resolved: Rc, -) -> (Rc, Vec) { - let Some(typed) = resolved.graph.clone() else { - return (resolved, Vec::new()); - }; - let excluded_keys = run_operations_excluded_for_stderr_capture_gap(&typed); - if excluded_keys.is_empty() { - return (resolved, Vec::new()); - } - let Ok(gap) = capture_gap_exclusion() else { - return (resolved, Vec::new()); - }; - let excluded: Vec = excluded_keys - .iter() - .map(|(module, service)| { - format!( - "module={module} service={service} operation={} \ - cause=ShellChannelNotRealizedByTarget \ - fact=stderr_capture_policy_unrealized drop={}", - gap.operation_qualified, gap.drop_identity, - ) - }) - .collect(); - let graph = strip_excluded_run_operations(typed, &excluded_keys); - ( - Rc::new(v1_compiler_compile::ResolvedPipelineResult { - graph: Some(graph), - ..(*resolved).clone() - }), - excluded, - ) -} - /// The red control's member: a non-tail effectful self-call, which the rust emitter refuses /// (`EffectfulSelfRecursionUnrealized`, the derived form exercised by /// `test.claim.effectful_item_kind_collapse_witness_test`). @@ -3708,8 +3368,8 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { ))) } } - // A fixture whose closure reaches extdeps.gunbc is admitted with the typed capture-policy - // exclusion, not as FixtureClosureUnionEmitRefused (the #13420 floor after #13437). + // A fixture whose closure reaches extdeps.gunbc is admitted by rust emit: WitnessBin.Run + // is realized, and the retired capture-gap strip is gone (review 77726). let gunbc_observed = fixture_closure_union_emit_receipt( &fixture_closure_union_control_union(FIXTURE_CLOSURE_GUNBC_REACH_MEMBER) .map_err(&refuse)?, @@ -3719,21 +3379,13 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { "a fixture whose closure reaches extdeps.gunbc refused: {refusal}" )) })?; - if !gunbc_observed.excluded.iter().any(|row| { - let Ok(gap) = capture_gap_exclusion() else { - return false; - }; - row.contains(&format!("module={}", gap.declaring_module)) - && row.contains(&format!("operation={}", gap.operation_qualified)) - && row.contains("cause=ShellChannelNotRealizedByTarget") - && row.contains(&gap.drop_identity) - }) { + if !gunbc_observed.excluded.is_empty() { return Err(refuse(format!( - "a fixture whose closure reaches extdeps.gunbc was admitted without the typed exclusion: {gunbc_observed:?}" + "the retired capture-gap strip must not still exclude a member: {gunbc_observed:?}" ))); } // A real emit error in a member the union still renders still refuses, even when the same - // closure also reaches the excluded service. + // closure also reaches extdeps.gunbc. match fixture_closure_union_emit_receipt( &fixture_closure_union_control_union(FIXTURE_CLOSURE_GUNBC_AND_REAL_EMIT_ERROR) .map_err(&refuse)?, @@ -3748,7 +3400,7 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { } Ok(observed) => { return Err(refuse(format!( - "a real emit error beside the excluded gunbc service did not refuse the union: {observed:?}" + "a real emit error beside a gunbc-reaching closure did not refuse the union: {observed:?}" ))) } } @@ -3778,10 +3430,6 @@ mod fixture_closure_union_tests { /// The recorder and the union are process-wide; tests that touch them run one at a time. static UNION_TEST_LOCK: Mutex<()> = Mutex::new(()); - fn gap_excl() -> &'static CaptureGapExclusion { - capture_gap_exclusion().unwrap_or_else(|e| panic!("{e}")) - } - /// An empty union is a bypassed recording seam and refuses (review 76399). #[test] fn an_empty_union_refuses() { @@ -3886,340 +3534,477 @@ mod fixture_closure_union_tests { assert!(union.conflicts.contains("dag/a.dag")); } + const STDERR_CAPTURE_MEMBER: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_UNMODELED_SIBLING: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n operation Weird {\n input { bin_path: String }\n output { digest: String from \"stderr_digest_hex\" }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_WITHOUT_POLICY: &str = "module efr_member\nservice Bin {\n operation Run {\n input { bin_path: String }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_STRING_POLICY: &str = "module efr_member\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: String\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_OPTIONAL_POLICY: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy?\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_COLLECTION_POLICY: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: List\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_HOMONYM_POLICY: &str = "module efr_member\ntype WitnessStderrCapturePolicy { mark: String }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const GUNBC_MODULE_REACH_MEMBER: &str = + "module efr_member\nimport extdeps.gunbc { packages }\nfn ignore() -> Int { 0 }\n"; + + /// RED: extending a fixture through the process-shared index left that fixture's + /// both-closure in the caches the claim fold reads. GREEN: the same walk still + /// closes (syllogism reaches its reference provider) and the shared typed cache + /// and both-closure edge map do not grow. #[test] - fn capture_gap_keys_on_shell_channel_not_realized_fact_equality() { - use crate::v1_compiler_emit::{shell_emission_refusal_fact, ShellEmissionRefusal}; - use crate::v1_std_core::{make_error_node, CompilerDiagnostic}; - let span = crate::v1_std_core::kernel_span("probe".to_string()); - let mk = |fact: String| { - let gap = gap_excl(); - make_error_node( - Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: gap.service.to_string(), - operation: gap.operation_qualified.to_string(), - declaring_module: gap.declaring_module.to_string(), - target: "rust".to_string(), - missing_realization_fact: fact, - span: span.clone(), - }), - gap.declaring_module.to_string(), - ) - }; - let gap = shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stderr_truncated".to_string(), - target_name: "rust".to_string(), - }, - )); - let unmodeled_key = - shell_emission_refusal_fact(Rc::new(ShellEmissionRefusal::ShellOutputKeyNotModeled { - key: "not_a_channel".to_string(), - })); - let substring_poison = - "unmodeled key 'not_a_channel' implements no stderr capture policy".to_string(); - let stdout_unrealized = shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stdout".to_string(), - target_name: "rust".to_string(), - }, - )); - assert!(stderr_capture_policy_gap_service(&mk(gap.clone())).is_some()); - assert!(stderr_capture_policy_gap_service(&mk(unmodeled_key)).is_none()); - assert!(stderr_capture_policy_gap_service(&mk(substring_poison)).is_none()); - assert!(stderr_capture_policy_gap_service(&mk(stdout_unrealized)).is_none()); - let other_module = make_error_node( - Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: gap_excl().service.to_string(), - operation: gap_excl().operation_qualified.to_string(), - declaring_module: "extdeps.other".to_string(), - target: "rust".to_string(), - missing_realization_fact: gap, - span: span.clone(), - }), - "extdeps.other".to_string(), + fn fixture_closure_extension_does_not_populate_the_process_shared_index() { + let _serial = UNION_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let layers = crate::cli_run::witness_layer_roots(); + let shared = super::entry_resolve::try_process_shared_index(&layers) + .unwrap_or_else(|e| panic!("process-shared index is the subject of this control: {e}")); + let typed_before = shared.typed_module_cache.borrow().len(); + let edges_before = shared + .both_closure_edges + .borrow() + .as_ref() + .map(|e| e.ref_out.len()) + .unwrap_or(0); + let admissions_before = shared.bare_reference_admission.borrow().len(); + let module_index = crate::cli_run::build_module_path_index_from_witness_roots(); + let sources = crate::cli_run::resolve_virtual_source_with_imports( + FIXTURE_SOURCE_PATH, + FIXTURE_CLOSURE_REFERENCE_REACH_MEMBER, + &module_index, + ) + .unwrap_or_else(|e| panic!("fixture closure must still close: {e}")); + assert!( + sources.len() > 1, + "the syllogism specimen must pull its provider, got {}", + sources.len() + ); + let typed_after = shared.typed_module_cache.borrow().len(); + let edges_after = shared + .both_closure_edges + .borrow() + .as_ref() + .map(|e| e.ref_out.len()) + .unwrap_or(0); + let admissions_after = shared.bare_reference_admission.borrow().len(); + assert_eq!( + typed_before, typed_after, + "fixture closure must not admit typed-cache rows on the process-shared index" + ); + assert_eq!( + edges_before, edges_after, + "fixture closure must not grow both_closure_edges on the process-shared index" + ); + assert_eq!( + admissions_before, admissions_after, + "fixture closure must not grow bare-reference admission on the process-shared index" ); - assert!(stderr_capture_policy_gap_service(&other_module).is_none()); - } - - fn gap_fact() -> String { - use crate::v1_compiler_emit::{shell_emission_refusal_fact, ShellEmissionRefusal}; - shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stderr_truncated".to_string(), - target_name: "rust".to_string(), - }, - )) } - fn transport_row( - module: &str, - service: &str, - operation: &str, - fact: String, - ) -> Rc { - crate::v1_std_core::make_error_node( - Rc::new( - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: service.to_string(), - operation: operation.to_string(), - declaring_module: module.to_string(), - target: "rust".to_string(), - missing_realization_fact: fact, - span: crate::v1_std_core::kernel_span("probe".to_string()), - }, - ), - module.to_string(), + /// THE DISCRIMINATING RED of the class: the pre-fix loader + /// (`try_index_for_run_or_owned_pool` over the layer roots) grows `both_closure_edges` + /// on the process-shared index. If this greens, the green control above has no red. + #[test] + fn fixture_closure_extension_via_shared_index_grows_both_closure_edges() { + let _serial = UNION_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let layers = crate::cli_run::witness_layer_roots(); + let shared = super::entry_resolve::try_process_shared_index(&layers) + .unwrap_or_else(|e| panic!("process-shared index is the subject of this control: {e}")); + let edges_before = shared + .both_closure_edges + .borrow() + .as_ref() + .map(|e| e.ref_out.len()) + .unwrap_or(0); + let module_index = crate::cli_run::build_module_path_index_from_witness_roots(); + let sources = crate::cli_run::extend_fixture_imports_on_process_shared_index( + FIXTURE_CLOSURE_REFERENCE_REACH_MEMBER, + &module_index, ) + .unwrap_or_else(|e| panic!("pre-fix shared-index extension must still close: {e}")); + assert!( + sources.len() > 1, + "the syllogism specimen must pull its provider, got {}", + sources.len() + ); + let edges_after = shared + .both_closure_edges + .borrow() + .as_ref() + .map(|e| e.ref_out.len()) + .unwrap_or(0); + assert!( + edges_after > edges_before, + "the pre-fix route must grow both_closure_edges (before={edges_before} after={edges_after})" + ); } + /// PLAN-TIME CLOSURE of the forged-probe witness module (the changed-witness seed), not + /// the in-memory probe string. If MegaRAC production paths appear, planning that witness + /// compiled the string's imports as both-closure edges and the pin at fold-start is that + /// increment. If they do not, the increment is the typed graph of this module itself. #[test] - fn selection_fold_allows_only_run_capture_rows_and_vetoes_mixed_refusal() { - let gap = gap_fact(); - let gap_row = gap_excl(); - let allowed = transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), + fn forged_probe_witness_module_both_closure_excludes_string_literal_imports() { + let _serial = UNION_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let layers = crate::cli_run::witness_layer_roots(); + let shared = super::entry_resolve::try_process_shared_index(&layers) + .unwrap_or_else(|e| panic!("process-shared index is the subject of this control: {e}")); + let scratch = super::entry_resolve::new_multi_entry_index_scratch_over( + shared.source_files.clone(), + &shared.source_roots, ); - let selected = select_run_operations_excluded_for_stderr_capture_gap(&[allowed]); - assert_eq!( - selected.iter().cloned().collect::>(), - vec![( - gap_row.declaring_module.to_string(), - gap_row.service.to_string() - )] + let rel = "dag/test/claim/host/megarac_managed_host_forged_probe_witness_test.dag"; + let content = std::fs::read_to_string(process_workspace_root().join(rel)) + .unwrap_or_else(|e| panic!("read {rel}: {e}")); + let source = Rc::new(v1_compiler_compile::SourceFile { + path: rel.to_string(), + content, + }); + let closed = + crate::cli_run::extend_sources_to_both_closure_fixpoint(vec![source.clone()], &scratch) + .unwrap_or_else(|e| panic!("witness-module closure must close: {e}")); + let megarac: Vec = closed + .iter() + .map(|s| s.path.replace('\\', "/")) + .filter(|p| p.contains("megarac") && p != rel) + .collect(); + assert!( + megarac.is_empty(), + "planning the witness must not both-close MegaRAC production named only inside \ + forged_probe_source; pulled {megarac:?} (closure_len={})", + closed.len() ); + } - let mixed_key = crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( - crate::v1_compiler_emit::ShellEmissionRefusal::ShellOutputKeyNotModeled { - key: "not_a_channel".to_string(), - }, - )); - let mixed = vec![ - transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), - ), - transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - mixed_key, - ), - ]; - assert!(select_run_operations_excluded_for_stderr_capture_gap(&mixed).is_empty()); - - let wrong_module = transport_row( - "extdeps.other", - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), + /// Scratch over an already-indexed name set is recorded as `ScratchCachesOverExistingSet` + /// and must not refuse as a second `NameSetIndex`. + #[test] + fn fixture_scratch_shell_is_not_a_second_index_of_the_name_set() { + let _serial = UNION_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let layers = crate::cli_run::witness_layer_roots(); + let shared = super::entry_resolve::try_process_shared_index(&layers) + .unwrap_or_else(|e| panic!("process-shared index is the subject of this control: {e}")); + let before = crate::cli_run::multi_entry_index_builds(); + let _ = super::entry_resolve::new_multi_entry_index_scratch_over( + shared.source_files.clone(), + &shared.source_roots, ); - assert!(select_run_operations_excluded_for_stderr_capture_gap(&[wrong_module]).is_empty()); + let _ = super::entry_resolve::new_multi_entry_index_scratch_over( + shared.source_files.clone(), + &shared.source_roots, + ); + let after = crate::cli_run::multi_entry_index_builds(); + assert_eq!( + after.len(), + before.len() + 2, + "scratches are countable constructions" + ); + crate::cli_run::multi_entry_index_sharing_control(&after).unwrap_or_else(|e| { + panic!("scratch kind must not refuse as a second name-set index: {e}") + }); + } - let nonmember = transport_row(&gap_row.declaring_module, &gap_row.service, "Sibling", gap); - assert!(select_run_operations_excluded_for_stderr_capture_gap(&[nonmember]).is_empty()); + #[test] + fn declared_stderr_capture_channels_do_not_refuse_the_union() { + let union = fixture_closure_union_control_union(STDERR_CAPTURE_MEMBER) + .expect("capture member resolves"); + fixture_closure_union_emit_receipt(&union) + .expect("the rust shell handler realizes the declared capture channels"); } - fn kernel_named( - name: &str, - kind: crate::v1_std_core::ParsedModuleItemKind, - children: im::Vector>, - ) -> Rc { - let mut node = (*crate::v1_std_core::leaf_node_with_span( - Rc::new(crate::std_occurrence_identity::NodeOccurrenceIdentity::OccurrenceSynthetic), - name.to_string(), - crate::v1_std_core::kernel_span(name.to_string()), - )) - .clone(); - node.module_item_kind = kind; - node.children = Rc::new(children); - Rc::new(node) + #[test] + fn an_unmodeled_shell_channel_still_refuses_the_union() { + let union = fixture_closure_union_control_union(STDERR_CAPTURE_UNMODELED_SIBLING) + .expect("mixed member resolves"); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("unmodeled stderr_digest_hex must still refuse"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("stderr_digest_hex"), + "{refusal}" + ); } - fn supplied_graph_with_run_and_sibling() -> Rc { - use crate::v1_compiler_infer_items::{ - ModuleInterface, ModuleTypecheckProgress, ResolvedGraph, TypedModule, - }; - use crate::v1_std_core::ParsedModuleItemKind; - let env = crate::v1_compiler_infer_env::empty_type_env(); - let cache = crate::v1_compiler_infer_env::empty_type_env_cache(); - let gap = gap_excl(); - let run = kernel_named( - &gap.operation_bare, - ParsedModuleItemKind::NotAModuleItem, - im::vector![], + #[test] + fn capture_channels_without_stderr_capture_input_refuse_the_union() { + // Real absent-policy route: rust emit refuses at + // `unmodeled_shell_transport_operation_diagnostics` / + // `ShellCapturePolicyInputAbsent` before any program is emitted. + // `emit_shell_stderr_policy_binding`'s else-arm is not reachable from + // this harness (the wall fires first; the generated bind fn is not on + // this mirror). The rustc-decoy that compiled a hand-built `return Err` + // before spawn was deleted (review 78366). + let union = fixture_closure_union_control_union(STDERR_CAPTURE_WITHOUT_POLICY) + .expect("member without policy input resolves"); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("a capture channel without stderr_capture must refuse"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("stderr_capture") + && refusal.contains("transport emission is not modeled"), + "{refusal}" ); - let sibling = kernel_named( - "Sibling", - ParsedModuleItemKind::NotAModuleItem, - im::vector![], + } + + fn capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + source: &str, + label: &str, + ) { + let union = fixture_closure_union_control_union(source).unwrap_or_else(|e| { + panic!("{label} must resolve so emit can refuse the typed policy: {e}") + }); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("wrong stderr_capture shape must refuse at emit, not rustc"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("transport emission is not modeled") + && refusal.contains("stderr_capture"), + "{label}: {refusal}" ); - let other_fn = kernel_named( - "unrelated_fn", - ParsedModuleItemKind::ModuleItemFunction, - im::vector![], + assert!( + !refusal.contains("error[E") && !refusal.contains("mismatched types"), + "typed emission must refuse before rustc: {label}: {refusal}" ); - let service = kernel_named( - &gap.service, - ParsedModuleItemKind::ModuleItemService, - im::vector![run, sibling], + } + + #[test] + fn capture_channels_with_string_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_STRING_POLICY, + "String", ); - let module = kernel_named( - &gap.declaring_module, - ParsedModuleItemKind::NotAModuleItem, - im::vector![], + } + + #[test] + fn capture_channels_with_optional_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_OPTIONAL_POLICY, + "optional", ); - let other_module_node = kernel_named( - "other.mod", - ParsedModuleItemKind::NotAModuleItem, - im::vector![], + } + + #[test] + fn capture_channels_with_collection_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_COLLECTION_POLICY, + "collection", ); - let other_item = kernel_named( - "KeepMe", - ParsedModuleItemKind::ModuleItemFunction, - im::vector![], + } + + #[test] + fn capture_channels_with_homonym_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_HOMONYM_POLICY, + "homonym", ); - let interface = |e: Rc, - c: Rc, - path: &str| { - Rc::new(ModuleInterface { - summary: Rc::new(crate::std_interface_summary::InterfaceSummary { - module_path: path.to_string(), - exports: Rc::new(im::vector![]), - interface_hash: crate::std_interface_summary::interface_summary_rollup( - Rc::new(im::vector![]), + } + + /// Drain fragments from `v1.compiler.emit_rust` (`shell_capture_drain_start`, + /// `shell_capture_join_project`). These tests inhabit those strings. + /// + /// `__stderr_complete_limit` is planted in the specimen. The production + /// Complete bind (`emit_shell_stderr_policy_binding` → + /// `v1_rt::read_host_budget_bytes()`) is not compiled here: a standalone + /// rustc specimen cannot link the crate, and this harness does not call that + /// function. Deleting the bind's `read_host_budget_bytes` call does not turn + /// these tests red. That host-budget route is uncovered (review 78366, + /// review 78373). + fn rustc_and_run_emitted_capture( + stem: &str, + complete_limit: Option, + tail_bytes: usize, + stderr_payload: &str, + stdin_payload: Option<&str>, + ) -> std::process::Output { + let limit = match complete_limit { + Some(n) => format!("Some({n})"), + None => "None".to_string(), + }; + let (script, stdin_prelude) = match stdin_payload { + Some(stdin) => { + let n = stderr_payload.len(); + let combined = format!("{}{}", stderr_payload, stdin); + ( + format!("head -c {n} 1>&2; cat >/dev/null"), + format!( + "let __stdin_bytes: Vec = ({:?}).as_bytes().to_vec();\n\ + let mut stdin_pipe = output.stdin.take();\n\ + let __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {{\n\ + use std::io::Write;\n\ + if let Some(mut stdin) = stdin_pipe {{\n\ + stdin.write_all(&__stdin_bytes)?;\n\ + }}\n\ + Ok(())\n\ + }});\n", + combined ), - }), - env: e, - cache: c, - }) + ) + } + None => ( + "printf %s '{payload}' 1>&2".to_string(), + "let mut stdin_pipe = output.stdin.take();\n\ + let __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n\ + drop(stdin_pipe);\n\ + Ok(())\n\ + });\n" + .to_string(), + ), }; - let tm = Rc::new(TypedModule { - module, - items: Rc::new(im::vector![service, other_fn]), - progress: ModuleTypecheckProgress::ItemsChecked, - type_env: env.clone(), - type_env_cache: cache.clone(), - interface: interface(env.clone(), cache.clone(), &gap.declaring_module), - func_env: Rc::new(crate::v1_compiler_infer_sigs::ResolvedFuncEnv { - name: gap.declaring_module.to_string(), - local: crate::v1_rt::rc_empty_map(), - parents: Rc::new(im::vector![]), - }), - item_registry: crate::v1_rt::rc_empty_map(), - occurrence_transport: None, - }); - let other = Rc::new(TypedModule { - module: other_module_node, - items: Rc::new(im::vector![other_item]), - progress: ModuleTypecheckProgress::ItemsChecked, - type_env: env.clone(), - type_env_cache: cache.clone(), - interface: interface(env, cache, "other.mod"), - func_env: Rc::new(crate::v1_compiler_infer_sigs::ResolvedFuncEnv { - name: "other.mod".to_string(), - local: crate::v1_rt::rc_empty_map(), - parents: Rc::new(im::vector![]), - }), - item_registry: crate::v1_rt::rc_empty_map(), - occurrence_transport: None, - }); - Rc::new(ResolvedGraph { - modules: Rc::new(im::vector![tm, other]), - item_registry: crate::v1_rt::rc_empty_map(), - item_leaf_owner_modules: crate::v1_rt::rc_empty_map(), - diagnostics: Rc::new(im::vector![]), - }) + let drain = format!( + "{}{}{}", + crate::v1_compiler_emit_rust::shell_capture_drain_start(), + stdin_prelude, + crate::v1_compiler_emit_rust::shell_capture_join_project() + ); + let program = format!( + "fn main() -> Result<(), Box> {{\n\ + let __stderr_complete_limit: Option = {limit};\n\ + let __stderr_complete_source: String = {source};\n\ + let __stderr_tail_bytes: usize = {tail};\n\ + let mut output = std::process::Command::new(\"sh\")\n\ + .args([\"-c\", \"{script}\"])\n\ + .stdin(std::process::Stdio::piped())\n\ + .stdout(std::process::Stdio::piped())\n\ + .stderr(std::process::Stdio::piped())\n\ + .spawn()?;\n\ + {drain}\n\ + print!(\"{{stderr_truncated}}|{{stderr_total_bytes}}|{{stderr_retained_bytes}}|{{stderr}}\");\n\ + Ok(())\n\ + }}\n", + script = script.replace("{payload}", stderr_payload), + limit = limit, + source = if complete_limit.is_some() { + "\"planted complete limit\".to_string()".to_string() + } else { + "String::new()".to_string() + }, + tail = tail_bytes, + drain = drain, + ); + let root = std::env::temp_dir().join(format!( + "gunbc-emitted-capture-{}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + stem + )); + std::fs::create_dir_all(&root).expect("scratch dir"); + let src = root.join("main.rs"); + let exe = root.join("specimen"); + std::fs::write(&src, program).expect("write emitted capture harness"); + let compiled = std::process::Command::new("rustc") + .args(["--edition=2021", "-o"]) + .arg(&exe) + .arg(&src) + .output() + .expect("invoke rustc"); + assert!( + compiled.status.success(), + "emitted capture body refused to compile: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + let run = std::process::Command::new(&exe) + .output() + .expect("run emitted capture specimen"); + let _ = std::fs::remove_dir_all(&root); + run } - fn service_op_names( - graph: &crate::v1_compiler_infer_items::ResolvedGraph, - module: &str, - service: &str, - ) -> Vec { - let env = crate::v1_compiler_infer_env::empty_type_env(); - graph - .modules - .iter() - .find(|tm| { - crate::v1_compiler_infer_env::authored_name(tm.type_env.clone(), tm.module.clone()) - == module - }) - .into_iter() - .flat_map(|tm| tm.items.iter()) - .filter(|item| { - item.module_item_kind == crate::v1_std_core::ParsedModuleItemKind::ModuleItemService - && crate::v1_compiler_infer_env::authored_name(env.clone(), (*item).clone()) - == service - }) - .flat_map(|item| { - item.children - .iter() - .map(|op| crate::v1_compiler_infer_env::authored_name(env.clone(), op.clone())) - }) - .collect() + #[test] + fn emitted_bounded_tail_truncates_and_keeps_the_tail() { + let run = rustc_and_run_emitted_capture( + "over-tail", + None, + 16, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + None, + ); + assert!( + run.status.success(), + "over-tail specimen failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "true|36|16|UVWXYZ0123456789" + ); } #[test] - fn strip_removes_only_run_and_keeps_sibling_and_unrelated_items() { - let gap = gap_excl(); - let graph = supplied_graph_with_run_and_sibling(); - let mut excluded = BTreeSet::new(); - excluded.insert((gap.declaring_module.to_string(), gap.service.to_string())); - let stripped = strip_excluded_run_operations(graph.clone(), &excluded); + fn emitted_bounded_tail_under_cap_is_complete() { + let run = rustc_and_run_emitted_capture("under-cap", None, 16, "abcdefghij", None); + assert!( + run.status.success(), + "under-cap specimen failed: {}", + String::from_utf8_lossy(&run.stderr) + ); assert_eq!( - service_op_names(&stripped, &gap.declaring_module, &gap.service), - vec!["Sibling".to_string()] + String::from_utf8_lossy(&run.stdout), + "false|10|10|abcdefghij" ); - let env = crate::v1_compiler_infer_env::empty_type_env(); - let gunbc_item_names: Vec = stripped - .modules - .iter() - .find(|tm| { - crate::v1_compiler_infer_env::authored_name(tm.type_env.clone(), tm.module.clone()) - == gap.declaring_module - }) - .unwrap() - .items - .iter() - .map(|item| crate::v1_compiler_infer_env::authored_name(env.clone(), item.clone())) - .collect(); + } + + #[test] + fn emitted_complete_over_budget_refuses() { + let run = + rustc_and_run_emitted_capture("complete-over", Some(8), 0, "abcdefghijklmnop", None); + assert!( + !run.status.success(), + "Complete overflow must refuse, got stdout {:?}", + String::from_utf8_lossy(&run.stdout) + ); + let err = String::from_utf8_lossy(&run.stderr); + assert!( + err.contains("WitnessStderrCaptureCompleteBudgetExceeded") + && err.contains("16") + && err.contains("8"), + "{err}" + ); + } + + #[test] + fn emitted_complete_under_budget_retains_all() { + let run = rustc_and_run_emitted_capture("complete-under", Some(64), 0, "abcdefghij", None); assert!( - gunbc_item_names.contains(&"unrelated_fn".to_string()), - "{gunbc_item_names:?}" + run.status.success(), + "under-budget Complete failed: {}", + String::from_utf8_lossy(&run.stderr) ); - assert_eq!(stripped.modules.len(), 2, "unrelated module must remain"); - let empty = strip_excluded_run_operations(graph, &BTreeSet::new()); assert_eq!( - service_op_names(&empty, &gap.declaring_module, &gap.service), - vec![gap.operation_bare.to_string(), "Sibling".to_string()] + String::from_utf8_lossy(&run.stdout), + "false|10|10|abcdefghij" ); } #[test] - fn capture_gap_exclusion_reads_typed_fields_from_the_drop_module() { - let gap = gap_excl(); - assert!(!gap.drop_identity.is_empty()); - assert!(!gap.declaring_module.is_empty()); - assert!(!gap.service.is_empty()); - assert!(!gap.operation_bare.is_empty()); + fn emitted_stdin_and_long_stderr_does_not_deadlock() { + let stderr = "Y".repeat(80_000); + let stdin = "X".repeat(80_000); + let run = + rustc_and_run_emitted_capture("stdin-long-stderr", None, 16, &stderr, Some(&stdin)); assert!( - gap.operation_qualified == gap.operation_bare - || gap - .operation_qualified - .ends_with(&format!(".{}", gap.operation_bare)), - "qualified={} bare={}", - gap.operation_qualified, - gap.operation_bare + run.status.success(), + "stdin+stderr specimen deadlocked or failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + format!("true|80000|16|{}", "Y".repeat(16)) ); } + + #[test] + fn reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels() { + let union = fixture_closure_union_control_union(GUNBC_MODULE_REACH_MEMBER) + .expect("extdeps.gunbc reach resolves"); + fixture_closure_union_emit_receipt(&union).unwrap_or_else(|refusal| { + panic!("a compile-probe reach of extdeps.gunbc must emit: {refusal}"); + }); + } } #[cfg(test)] diff --git a/src/v1/stage0/src/cli_run/entry_resolve.rs b/src/v1/stage0/src/cli_run/entry_resolve.rs index 380d45bb3b4..8570ec5ff15 100644 --- a/src/v1/stage0/src/cli_run/entry_resolve.rs +++ b/src/v1/stage0/src/cli_run/entry_resolve.rs @@ -364,12 +364,14 @@ fn broken() -> Int { no_such_function_anywhere() }\n"; let Some(candidates) = index.get(&module_path) else { continue; }; - for path in candidates { + for source in candidates { + let path = source.path(); let rel = normalize_repo_path(&module_index_path_key(path)); if !seen.insert(rel) { continue; } - let file_content = std::fs::read_to_string(path) + let file_content = source + .read() .map_err(|e| format!("read imported module {}: {e}", path.display()))?; queue.push(file_content); } @@ -521,12 +523,14 @@ fn broken() -> Int { no_such_function_anywhere() }\n"; let Some(candidates) = index.get(&module_path) else { continue; }; - for path in candidates { + for source in candidates { + let path = source.path(); let rel = normalize_repo_path(&module_index_path_key(path)); if seen.contains_key(&rel) { continue; } - let file_content = std::fs::read_to_string(path) + let file_content = source + .read() .map_err(|e| format!("read imported module {}: {e}", path.display()))?; seen.insert( rel.clone(), @@ -1008,7 +1012,7 @@ pub fn resolve_entry_graph( // wave 1) an entry's dependencies are name-derived, and the old // `load_sources_for_entry_with_index` walk only follows import edges — a // stripped fixed entry (e.g. the floor runner) failed to resolve at all. - let index = process_shared_index(source_roots); + let index = try_process_shared_index(source_roots)?; resolve_entry_with_index(&index, entry_file) } @@ -1499,12 +1503,39 @@ pub fn resolved_graph_memo_keys_for_test(index: &MultiEntryIndex) -> Vec index.resolved_graph_memo.borrow().keys().cloned().collect() } +/// Empty caches over an already-indexed `source_files` map. Recorded as +/// `ScratchCachesOverExistingSet` so the sharing control can count it without treating it as a +/// second `NameSetIndex` (`MultiEntryIndexBuiltTwiceForOneNameSet` remains two name-set indexes). +#[track_caller] +pub(crate) fn new_multi_entry_index_scratch_over( + source_files: ModuleSourceIndex, + source_roots: &[String], +) -> MultiEntryIndex { + record_multi_entry_index_site( + std::panic::Location::caller(), + &source_files, + MultiEntryIndexBuildKind::ScratchCachesOverExistingSet, + ); + multi_entry_index_shell_body(source_files, source_roots) +} + #[track_caller] pub(crate) fn new_multi_entry_index_shell( source_files: ModuleSourceIndex, source_roots: &[String], ) -> MultiEntryIndex { - record_multi_entry_index_site(std::panic::Location::caller(), &source_files); + record_multi_entry_index_site( + std::panic::Location::caller(), + &source_files, + MultiEntryIndexBuildKind::NameSetIndex, + ); + multi_entry_index_shell_body(source_files, source_roots) +} + +fn multi_entry_index_shell_body( + source_files: ModuleSourceIndex, + source_roots: &[String], +) -> MultiEntryIndex { MultiEntryIndex { generation: next_index_generation(), source_files, @@ -1532,6 +1563,7 @@ pub(crate) fn new_multi_entry_index_shell( pool_bare_census: RefCell::new(None), entry_closure_sources: RefCell::new(HashMap::new()), both_closure_edges: RefCell::new(None), + scratch_underlay: RefCell::new(None), closure_name_censuses: RefCell::new(HashMap::new()), bare_reference_admission: RefCell::new(HashMap::new()), pool_module_names: std::cell::OnceCell::new(), @@ -2894,16 +2926,19 @@ pub(crate) fn import_resolution_facts_with_observation( if !root_path.is_dir() { continue; } - let mut dag_files: Vec = Vec::new(); - collect_dag_files_tolerant(root_path, &mut dag_files); + let mut dag_files = Vec::new(); + if let Err(cause) = collect_dag_files_tolerant(root_path, &mut dag_files) { + read_refusals.push((workspace_relative_repo_path(root), cause)); + continue; + } dag_files.sort(); for file in dag_files { - let rel = rel_path_for_layer_import(&file); + let rel = rel_path_for_layer_import(file.path()); if is_excluded_import_path(&rel, exclude_substrings) { continue; } observed_paths.insert(workspace_relative_repo_path(&rel)); - let content = match std::fs::read_to_string(&file) { + let content = match file.read() { Ok(c) => c, Err(e) => { read_refusals.push((workspace_relative_repo_path(&rel), e.to_string())); @@ -3136,12 +3171,14 @@ pub(crate) fn reference_pool_names(pool_roots: &[String]) -> Rc = Vec::new(); - collect_dag_files_tolerant(root_path, &mut files); + let mut files = Vec::new(); + // A ledger row directory whose roster cannot be derived is not a tolerable read miss: + // a short pool-name set would silently drop the roster module's names. + collect_dag_files_tolerant(root_path, &mut files).unwrap_or_else(|cause| panic!("{cause}")); files.sort(); for file in files { - let rel = rel_path_for_layer_import(&file); - let Ok(content) = std::fs::read_to_string(&file) else { + let rel = rel_path_for_layer_import(file.path()); + let Ok(content) = file.read() else { continue; }; let Some(module_name) = extract_module_path(&content) else { @@ -3583,8 +3620,8 @@ pub(crate) fn reference_edges_for_file_on_demand< // declaration or a name the file imports (the import edge already carries that), so // guessing a pool declarer for it is a heuristic the closed substrate never needs. It // manufactured a phantom edge from `std.optional { Present }` importers to a fixture that - // merely declares the same spelling. The proximity tier below stays for import-less files, - // where it already applied. + // merely declares the same spelling. Import-less files use the same on-chain UniqueBare + // rule below — not a leftover proximity rank. if !imports.is_empty() { continue; } @@ -3597,29 +3634,14 @@ pub(crate) fn reference_edges_for_file_on_demand< if mods.contains(&self_module) { continue; } - // Proximity disambiguation (namespace-only "nearest in the containment tree"): - // among declarers, prefer the one sharing the longest module-path prefix with the - // referencing module. A single nearest → UniqueBare; a tie at the nearest depth → - // AmbiguousBare (a genuine homonym the source must qualify — the bright-cat lane). - let mut best_len = 0usize; - let mut winners: Vec<&String> = Vec::new(); - for m in mods.iter() { - let shared = module_prefix_shared_len(&self_module, m); - if winners.is_empty() || shared > best_len { - best_len = shared; - winners.clear(); - winners.push(m); - } else if shared == best_len { - winners.push(m); - } - } - match winners.len() { - 0 => {} - 1 => upgrade(winners[0].clone(), RefEdgeResolution::UniqueBare), - _ => { - // Homonym-qualification worklist dump (bright-cat lane (c) seed): each - // AmbiguousBare is a bare ref, in a file that does not declare it, whose - // nearest declarers tie — the definitive "needs qualification" site. + // On-chain unique → UniqueBare. Census-unique off-chain → UniqueBare, because + // UniqueBinding still accepts that name on the compile path (dropping it is an + // undercount). Homonyms with no unique on-chain binder → no UniqueBare (proximity + // deleted); two or more on-chain → AmbiguousBare. + match pick_importless_bare(&self_module, mods) { + ImportlessBarePick::None => {} + ImportlessBarePick::Unique(m) => upgrade(m.clone(), RefEdgeResolution::UniqueBare), + ImportlessBarePick::Ambiguous(winners) => { if std::env::var("REFAMBIG_DUMP").is_ok() { let is_witness = rel.contains("/test/") || rel.ends_with("_test.dag"); let cands: Vec = winners.iter().map(|s| (*s).clone()).collect(); @@ -3681,15 +3703,21 @@ pub fn reference_resolution_facts( if !root_path.is_dir() { continue; } - let mut files: Vec = Vec::new(); - collect_dag_files_tolerant(root_path, &mut files); + let mut files = Vec::new(); + if let Err(cause) = collect_dag_files_tolerant(root_path, &mut files) { + unaccounted.push(ReferenceAccountingRefusal { + path: cause, + cause: "the pool root's ledger roster could not be derived", + }); + continue; + } files.sort(); for file in files { - let rel = rel_path_for_layer_import(&file); + let rel = rel_path_for_layer_import(file.path()); if is_excluded_import_path(&rel, exclude_substrings) { continue; } - let content = std::fs::read_to_string(&file).ok(); + let content = file.read().ok(); match reference_edges_for_file(&rel, content.as_deref(), &names) { FileReferenceEdges::Edges(file_edges) => edges.extend(file_edges), FileReferenceEdges::Unaccounted(cause) => { diff --git a/src/v1/stage0/src/cli_run/native_lane_runner.rs b/src/v1/stage0/src/cli_run/native_lane_runner.rs index 08060f2d008..5c79cf77704 100644 --- a/src/v1/stage0/src/cli_run/native_lane_runner.rs +++ b/src/v1/stage0/src/cli_run/native_lane_runner.rs @@ -80,8 +80,9 @@ const MALFORMED_SPECIMEN_COMMITTED: &str = "fixtures/native_lane_malformed/poiso /// workspace-relative like every path this harness hands the binary (its working directory is /// the workspace root). The receipt records the observed refusal path; the admission authority /// requires it non-empty, and this harness requires it to name the materialized specimen. -const MALFORMED_CONTROL_ROOT: &str = "target/v2-native-lane/malformed-control-root"; -const MALFORMED_MATERIALIZED_PATH: &str = "target/v2-native-lane/malformed-control-root/poison.dag"; +const MALFORMED_CONTROL_ROOT: &str = "target/v2-native-lane/malformed-control-root/dag"; +const MALFORMED_MATERIALIZED_PATH: &str = + "target/v2-native-lane/malformed-control-root/dag/poison.dag"; /// THE TWO DOORS' CONTROL INPUTS, AS THE THREE FACTS THEIR ARGV IS BUILT FROM. /// @@ -101,7 +102,12 @@ const MALFORMED_MATERIALIZED_PATH: &str = "target/v2-native-lane/malformed-contr /// the text not Rust (gunbc.recurring_failure_mode closure_emit_renders_an_arrow_without_its_body). /// So an exit 0 now has to carry the declaration's NAME and its VALUE in text a Rust compiler /// ACCEPTS; anything less is that class returning, not an emission. -const WELL_FORMED_CONTROL_ROOT: &str = "fixtures/native_cli_door"; +const WELL_FORMED_CONTROL_ROOT: &str = "fixtures/native_cli_door/dag"; +const NATIVE_INGEST_COPIED_SINGLE_ROOT: &str = "fixtures/native_ingest_copied_single_root"; +const NATIVE_INGEST_SRC_IN_DAG_ROOT: &str = "fixtures/native_ingest_src_in_dag/dag"; +const NATIVE_INGEST_TWO_ROOTS_DAG: &str = "fixtures/native_ingest_two_roots/dag"; +const NATIVE_INGEST_TWO_ROOTS_V2: &str = "fixtures/native_ingest_two_roots/src/v2"; +const NATIVE_INGEST_DUPLICATE_MODULE_ROOT: &str = "fixtures/native_ingest_duplicate_module/dag"; const CLI_DOOR_ENTRY_MODULE: &str = "fixture.native_cli_door.door_probe"; const CLI_DOOR_EMITTED_WITNESS: &str = "native_cli_door_probe_value"; const CLI_DOOR_EMITTED_VALUE: &str = "606060"; @@ -884,10 +890,9 @@ fn materialize_malformed_specimen(workspace: &Path) -> Result { /// census run's only consumed output. struct NativeFileRefusalObserved { path: String, - /// The FIRST diagnostic of the file's chain -- often an advisory that rode along (the - /// grammar-construction residue `parse_grammar_choice_overlap_residue` heads every file, - /// clean ones included). Carried so the summary can show it AS the head, beside the cause, - /// rather than leaving a reader to mistake it for one. + /// The FIRST diagnostic of the file's chain -- possibly a non-fatal advisory that + /// `rejected_with_pending` prepended. Carried so the summary can show it AS the head, beside + /// the cause, rather than leaving a reader to mistake it for one. head_reason: String, fatal_reason: String, /// Where the FATAL link points, rendered from the row's `chain` @@ -2065,8 +2070,7 @@ struct CliRefusalRendering { /// /// `v2.cli.compile_cli` renders `REFUSED: the closure did not emit; diagnostic chain: | /// FATAL AT `, and `diagnostics_fatal` folds the chain to its LAST link -- that module's -/// annotation states why: the head answers `parse_grammar_choice_overlap_residue` for every entry -/// tried, a grammar-global ADVISORY that is never the fatal. +/// annotation states why: a head can be a non-fatal advisory, and the fatal is the last link. /// /// EVERY LINK IS NOW LOCATED, AND THIS DECODER DID NOT KNOW THAT. gunbc#11965/#11985 moved the chain /// to `lens_verdict_diagnostics_located_chain_text`, so a link is @@ -2568,11 +2572,129 @@ fn walk_cli_door( } } eprintln!("v2-native-cli: filesystem controls refused absent root and unreadable source with typed, located causes"); + walk_native_ingest_layout_controls(binary, workspace)?; let refusal_status = i64::from(refused.status.unwrap_or_default()); eprintln!("v2-native-cli: door refused as cli_no_entry — exit {refusal_status}"); Ok((door_exit_status, emitted_bytes, refusal_status)) } +/// THE LAYOUT REDS THE PARENT ASKED THE NATIVE DOOR TO EXECUTE. Claims over `v2_cli_run` supply +/// ingest at that fold; these walks are the inhabitance that the discovering --source-root, not +/// the file spelling, tags the tree — deleting any of the four fixture roots makes this control +/// fail. +/// +/// SEED DELTA (ctrl hand-Rust receipt): extend `walk_cli_door`; no new production route and no +/// deleted scaffold. Accounted on `gunbc.source_root_eval_driver_seed_growth` +/// `source_root_eval_driver_seed_growth_justification` (lane `v1-hand-queue-drain`). Net seed is +/// this inhabitance walk over existing `run_cli_door`. +fn walk_native_ingest_layout_controls(binary: &Path, workspace: &Path) -> Result<(), String> { + let copied = workspace.join(NATIVE_INGEST_COPIED_SINGLE_ROOT); + let src_in_dag = workspace.join(NATIVE_INGEST_SRC_IN_DAG_ROOT); + let two_dag = workspace.join(NATIVE_INGEST_TWO_ROOTS_DAG); + let two_v2 = workspace.join(NATIVE_INGEST_TWO_ROOTS_V2); + let dup = workspace.join(NATIVE_INGEST_DUPLICATE_MODULE_ROOT); + for (label, path) in [ + ("copied-single-root", copied.as_path()), + ("src-in-dag", src_in_dag.as_path()), + ("two-roots-dag", two_dag.as_path()), + ("two-roots-v2", two_v2.as_path()), + ("duplicate-module", dup.as_path()), + ] { + if !path.is_dir() { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeIngestLayoutRootAbsent — {label} {} is not a directory", + path.display() + )); + } + } + + let copied_obs = run_cli_door( + binary, + &[ + "emit".into(), + "--entry".into(), + "fixture.native_ingest.copied_dag".into(), + "--source-root".into(), + copied.display().to_string(), + ], + )?; + if copied_obs.status != Some(CLI_DOOR_USAGE_REFUSAL_EXIT) + || !copied_obs.stdout.is_empty() + || !copied_obs + .stderr + .contains("source root is not a dag or src/v2 tree") + || !copied_obs.stderr.contains(&copied.display().to_string()) + { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeIngestCopiedRootNotUnrecognized — status={:?} stderr={}", + copied_obs.status, + copied_obs.stderr.trim() + )); + } + + let src_obs = run_cli_door( + binary, + &[ + "emit".into(), + "--entry".into(), + "fixture.native_ingest.src_in_dag".into(), + "--source-root".into(), + src_in_dag.display().to_string(), + ], + )?; + if src_obs.status != Some(0) || src_obs.stdout.is_empty() { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeIngestSrcInDagPathMisclassified — status={:?} stderr={}", + src_obs.status, + src_obs.stderr.trim() + )); + } + + let two_obs = run_cli_door( + binary, + &[ + "emit".into(), + "--entry".into(), + "fixture.native_ingest.dag_entry".into(), + "--source-root".into(), + two_dag.display().to_string(), + "--source-root".into(), + two_v2.display().to_string(), + ], + )?; + if two_obs.status != Some(0) || two_obs.stdout.is_empty() { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeIngestTwoRootsDidNotEmit — status={:?} stderr={}", + two_obs.status, + two_obs.stderr.trim() + )); + } + + let dup_obs = run_cli_door( + binary, + &[ + "emit".into(), + "--entry".into(), + "fixture.native_ingest.collision".into(), + "--source-root".into(), + dup.display().to_string(), + ], + )?; + if dup_obs.status != Some(CLI_DOOR_REFUSAL_EXIT) + || !dup_obs.stderr.contains("closure_census_duplicate_module") + || !dup_obs.stderr.contains("collision_a.dag") + || !dup_obs.stderr.contains("collision_b.dag") + { + return Err(format!( + "V2-NATIVE REFUSAL cause=NativeIngestDuplicateDidNotNameBothFiles — status={:?} stderr={}", + dup_obs.status, + dup_obs.stderr.trim() + )); + } + eprintln!("v2-native-cli: native-ingest layout controls held (unrecognized copied root, /src/ under dag, two roots, both-files duplicate)"); + Ok(()) +} + /// THE GENERATION-ONE EXECUTABLE OUTLIVES THE RUN, because generation two is that executable /// emitting `v2.compiler.compile` and nothing else may stand in for it. The probe root is NOT /// retained: once the executable is copied out and its copy re-hashes to the identity the run @@ -2723,6 +2845,8 @@ pub struct NativeServeProgramRun { pub seed_identity: String, pub warning_count: i64, pub announcement: String, + pub peer_announcement: String, + pub peer_port: i64, pub responses: Vec, pub refused_status: Option, pub refused_stderr: String, @@ -2815,9 +2939,8 @@ pub fn run_native_serve_program( release_revision: &str, refused_revision: &str, request_deadline_ms: &str, - requests: &[String], + requests_for_peer_port: &dyn Fn(i64) -> Result, String>, ) -> Result { - use std::io::{BufRead, Read}; let prepared = prepare_emitted_compiler_for_entry(source_roots, entry)?; eprintln!( "native-serve: {entry} built (closure {}) -- starting {}", @@ -2826,7 +2949,132 @@ pub fn run_native_serve_program( ); let (refused_status, refused_stderr) = native_serve_refused_launch(&prepared.binary_path, refused_revision, request_deadline_ms)?; - let mut child = Command::new(&prepared.binary_path) + // The PEER is a second instance of the same entry: the local server the entry's bound REST + // handler is pointed at, so the answered arm is produced by a real exchange. Its port is the + // one fact only it can publish, so the reader module is asked for the requests only after it. + let mut peer = native_serve_start( + &prepared.binary_path, + entry, + release_revision, + request_deadline_ms, + )?; + let peer_port = match peer + .address + .as_deref() + .and_then(|address| address.rsplit(':').next()) + .and_then(|port| port.parse::().ok()) + { + Some(port) => port, + None => { + let peer_stderr = peer.stop(); + return Err(format!( + "NATIVE-SERVE REFUSAL cause=PeerUnannounced entry={entry} announcement={:?} — the peer instance printed no bound address, so no request can name its port{}", + peer.announcement, + if peer_stderr.is_empty() { String::new() } else { format!(" (stderr: {})", peer_stderr.trim_end()) } + )); + } + }; + let requests = match requests_for_peer_port(peer_port) { + Ok(requests) => requests, + Err(cause) => { + peer.stop(); + return Err(cause); + } + }; + let mut subject = match native_serve_start( + &prepared.binary_path, + entry, + release_revision, + request_deadline_ms, + ) { + Ok(subject) => subject, + Err(cause) => { + peer.stop(); + return Err(cause); + } + }; + let responses = match &subject.address { + Some(address) => requests + .iter() + .map(|request| native_serve_exchange(address, request)) + .collect(), + None => Vec::new(), + }; + // The last case drives the subject past its stuck-worker ceiling, after which it exits by + // itself; wait for that (bounded) and carry the status to the reader unjudged. Only a process + // still running at the deadline is killed, and it reports no status. The peer serves no budget + // case and is stopped. + let (served_exit_status, stderr) = subject.await_exit(); + let peer_stderr = peer.stop(); + Ok(NativeServeProgramRun { + closure_identity: prepared.closure_identity, + binary_identity: prepared.binary_identity, + seed_identity: prepared.seed_identity, + warning_count: prepared.build.warning_count, + announcement: subject.announcement, + peer_announcement: peer.announcement, + peer_port, + responses, + refused_status, + refused_stderr, + served_exit_status, + stderr: format!("{stderr}{peer_stderr}"), + }) +} + +/// One started instance of a served entry: its announcement, the address read off it, and the +/// process to stop. The host reads the address because the service binds port 0. +struct NativeServeInstance { + child: std::process::Child, + reader: Option>, + announcement: String, + address: Option, +} + +impl NativeServeInstance { + /// Wait up to the run deadline for the instance to exit by itself, killing it only past that; + /// the status is None when it had to be killed. + fn await_exit(&mut self) -> (Option, String) { + let started = std::time::Instant::now(); + let status = loop { + match self.child.try_wait() { + Ok(Some(status)) => break status.code(), + Ok(None) if started.elapsed() < NATIVE_SERVE_DEADLINE => { + std::thread::sleep(std::time::Duration::from_millis(20)) + } + _ => { + let _ = self.child.kill(); + let _ = self.child.wait(); + break None; + } + } + }; + let stderr = self + .reader + .take() + .and_then(|reader| reader.join().ok()) + .unwrap_or_default(); + (status, stderr) + } + + fn stop(&mut self) -> String { + let _ = self.child.kill(); + let _ = self.child.wait(); + self.reader + .take() + .and_then(|reader| reader.join().ok()) + .unwrap_or_default() + } +} + +fn native_serve_start( + binary: &Path, + entry: &str, + release_revision: &str, + request_deadline_ms: &str, +) -> Result { + use std::io::{BufRead, Read}; + let mut child = Command::new(binary) .args(native_serve_launch_args( release_revision, request_deadline_ms, @@ -2837,10 +3085,14 @@ pub fn run_native_serve_program( .map_err(|cause| { format!("NATIVE-SERVE REFUSAL cause=SpawnFailed entry={entry} — {cause}") })?; - let pipe = child - .stderr - .take() - .ok_or("NATIVE-SERVE REFUSAL cause=NoStderrPipe")?; + let pipe = match child.stderr.take() { + Some(pipe) => pipe, + None => { + let _ = child.kill(); + let _ = child.wait(); + return Err("NATIVE-SERVE REFUSAL cause=NoStderrPipe".to_string()); + } + }; let (lines, announced) = std::sync::mpsc::channel::(); let reader = std::thread::spawn(move || { let mut pipe = std::io::BufReader::new(pipe); @@ -2858,42 +3110,11 @@ pub fn run_native_serve_program( .strip_prefix("native-serve listening on ") .and_then(|rest| rest.split(' ').next()) .map(str::to_string); - let responses = match &address { - Some(address) => requests - .iter() - .map(|request| native_serve_exchange(address, request)) - .collect(), - None => Vec::new(), - }; - // The last case drives the service past its stuck-worker ceiling, after which it exits by - // itself; wait for that (bounded) and carry the status to the reader unjudged. Only a process - // still running at the deadline is killed, and it reports no status. - let started = std::time::Instant::now(); - let served_exit_status = loop { - match child.try_wait() { - Ok(Some(status)) => break status.code(), - Ok(None) if started.elapsed() < NATIVE_SERVE_DEADLINE => { - std::thread::sleep(std::time::Duration::from_millis(20)) - } - _ => { - let _ = child.kill(); - let _ = child.wait(); - break None; - } - } - }; - let stderr = reader.join().unwrap_or_default(); - Ok(NativeServeProgramRun { - closure_identity: prepared.closure_identity, - binary_identity: prepared.binary_identity, - seed_identity: prepared.seed_identity, - warning_count: prepared.build.warning_count, + Ok(NativeServeInstance { + child, + reader: Some(reader), announcement, - responses, - refused_status, - refused_stderr, - served_exit_status, - stderr, + address, }) } @@ -2983,19 +3204,60 @@ pub fn run_v2_native_frontier( /// carries the terminal marker's counts and decides nothing about them. pub struct NativeCensusRun { pub cause_groups: u64, + pub roots: u64, pub file_refusals: u64, pub residual_rows: u64, pub modules: u64, pub advised_files: u64, + pub inferred: u64, + pub infer_refused: u64, + pub type_census: TypeCensusVerdictWord, +} + +/// THE TYPE CENSUS'S VERDICT AS THE TERMINAL CARRIES IT: one of the three words the rendered main +/// writes from `std.compiler_entry` `NativeClaimTerminal`, and nothing else. Any other value refuses +/// rather than being copied into the receipt. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypeCensusVerdictWord { + Held, + NotHeld, + NoObservation, +} + +impl TypeCensusVerdictWord { + fn decode(word: &str) -> Option { + match word { + "held" => Some(Self::Held), + "not_held" => Some(Self::NotHeld), + "no_observation" => Some(Self::NoObservation), + _ => None, + } + } + + pub fn word(self) -> &'static str { + match self { + Self::Held => "held", + Self::NotHeld => "not_held", + Self::NoObservation => "no_observation", + } + } } -/// `gunbc test //gunbc/instruments:v2-native-census`: the emitted compiler's `census-resolve` verb -/// over the given roots. The child's stdout -- one `file_refusal`, `accepted_file_advisories`, `census_residual` and -/// `cause_group` line per row, then the terminal -- is relayed whole, because those rows ARE the +/// `gunbc test //gunbc/instruments:v2-native-census`: the emitted compiler's `census-infer` verb +/// over the given roots (census part 2: front-end, resolve and infer refusals in one carrier, one +/// walk shared with the type-declaration census). The child's stdout -- one `file_refusal`, +/// `accepted_file_advisories`, `census_residual`, `cause_group` and `census_root` line per row, the +/// type census's own lines, then the terminal -- is relayed whole, because those rows ARE the /// census; only the terminal is decoded, and every field it must carry is required, none defaulted. +/// The child's exit status is the TYPE census's verdict, a different fact, so it is carried in the +/// terminal's `type_census` field and never read as this census's standing. pub fn run_v2_native_census(source_roots: &[String]) -> Result { + eprintln!( + "{}", + host_memory_budget_receipt_line(&crate::memory_governor::read_host_budget_resolution()) + ); let preparation = prepare_emitted_compiler(source_roots)?; - let mut args = vec!["census-resolve".to_string()]; + let mut args = vec!["census-infer".to_string()]; args.extend(source_roots.iter().cloned()); let mut command = Command::new(&preparation.binary_path); command.args(&args); @@ -3011,6 +3273,41 @@ pub fn run_v2_native_census(source_roots: &[String]) -> Result String { + use crate::memory_governor::HostBudgetResolution; + match resolution { + HostBudgetResolution::Resolved { + effective_bytes, + requested_bytes, + observation, + } => format!( + "[v2-native-census] host_memory_budget source=observed observed_by=\"{}\" \ + bounds_this_process={} effective_bytes={effective_bytes} declared_bytes={}", + observation.source.label(), + observation.source.bounds_this_process(), + requested_bytes.map_or("none".to_string(), |b| b.to_string()) + ), + HostBudgetResolution::DeclaredUnverified { + requested_bytes, + reason, + } => format!( + "[v2-native-census] host_memory_budget source=declared_unverified \ + declared_bytes={requested_bytes} reason=\"{reason}\"" + ), + HostBudgetResolution::Unreadable { reason } => { + format!("[v2-native-census] host_memory_budget source=unreadable reason=\"{reason}\"") + } + } +} + /// THE CHILD'S STREAMS, RELAYED AS IT WRITES THEM AND STILL COLLECTED WHOLE. `Command::output()` /// forwarded nothing until the child exited, so a multi-hour census was silent throughout and a /// cancelled run lost every row it had already printed (run 37229309848: 3h48m, empty stdout). @@ -3053,7 +3350,7 @@ fn run_relaying_as_it_runs(mut command: Command) -> std::io::Result) -> Result) -> Result", ), @@ -3384,13 +3693,13 @@ mod tests { summary.contains("fatal_cause 1x body_lowering_reason_x"), "{summary}" ); - assert!(!summary.contains("fatal_cause 2x parse_grammar_choice_overlap_residue")); + assert!(!summary.contains("fatal_cause 2x advisory_head_x")); // EVERY SUPPLIED FILE, EXACTLY ONCE, AS ITS WHOLE LINE. Asserting a and c alone let a // rendering that dropped b pass (review on #13005); the expected line is derived per row, // so the head appears only where it differs from the cause. let expected = [ - " refused a.dag at=12:5 fatal=parse_g0_tokens_remain head(advisory)=parse_grammar_choice_overlap_residue", - " refused b.dag at= fatal=body_lowering_reason_x head(advisory)=parse_grammar_choice_overlap_residue", + " refused a.dag at=12:5 fatal=parse_g0_tokens_remain head(advisory)=advisory_head_x", + " refused b.dag at= fatal=body_lowering_reason_x head(advisory)=advisory_head_x", " refused c.dag at= fatal=parse_g0_tokens_remain", ]; assert_eq!( @@ -3703,15 +4012,12 @@ mod tests { fn a_file_refusal_row_decodes_its_fatal_line_and_byte_column() { let marker = "{\"_terminal\":\"complete\",\"mode\":\"adjudicate\",\"rows\":0,\"universe\":0,\"file_refusals\":1,\"advised_files\":0,\"admitted\":false,\"summary\":\"s\",\"frontier\":\"held\"}"; let located = format!( - "{{\"file_refusal\":{{\"path\":\"a.dag\",\"module\":{{\"_variant\":\"Absent\"}},\"chain\":{{\"head\":{{\"reason\":\"parse_grammar_choice_overlap_residue\",\"at\":{{\"_variant\":\"FileRefusalAtInvariant\",\"invariant\":\"grammar_choice_overlap\"}}}},\"tail\":[{{\"reason\":\"parse_g0_tokens_remain\",\"at\":{{\"_variant\":\"FileRefusalAtLine\",\"line\":12,\"byte_column\":{{\"value\":5}}}}}}]}}}}}}\n{marker}\n" + "{{\"file_refusal\":{{\"path\":\"a.dag\",\"module\":{{\"_variant\":\"Absent\"}},\"chain\":{{\"head\":{{\"reason\":\"advisory_head_x\",\"at\":{{\"_variant\":\"FileRefusalAtInvariant\",\"invariant\":\"grammar_choice_overlap\"}}}},\"tail\":[{{\"reason\":\"parse_g0_tokens_remain\",\"at\":{{\"_variant\":\"FileRefusalAtLine\",\"line\":12,\"byte_column\":{{\"value\":5}}}}}}]}}}}}}\n{marker}\n" ); let parsed = parse_native_run_output(&located).expect("a located row parses"); assert_eq!(parsed.file_refusals[0].fatal_at, "12:5"); // Both reasons are DERIVED from the chain: the head link's and the last link's. - assert_eq!( - parsed.file_refusals[0].head_reason, - "parse_grammar_choice_overlap_residue" - ); + assert_eq!(parsed.file_refusals[0].head_reason, "advisory_head_x"); assert_eq!( parsed.file_refusals[0].fatal_reason, "parse_g0_tokens_remain" @@ -3773,27 +4079,51 @@ mod tests { } #[test] - fn census_resolve_advisory_rows_reach_the_receipt() { - let terminal = "{\"_terminal\":\"complete\",\"mode\":\"census-resolve\",\"modules\":3,\ - \"file_refusals\":0,\"advised_files\":1,\"residual_rows\":0,\"cause_groups\":0}\n"; + fn census_infer_advisory_rows_reach_the_receipt() { + let terminal = "{\"_terminal\":\"complete\",\"mode\":\"census-infer\",\"modules\":3,\ + \"file_refusals\":0,\"advised_files\":1,\"residual_rows\":0,\"cause_groups\":0,\ + \"roots\":0,\"inferred\":3,\"infer_refused\":0,\"type_census\":\"held\"}\n"; let with_row = format!( "{{\"accepted_file_advisories\":{{\"path\":\"a.dag\",\"head\":\"r\",\"tail\":[]}}}}\n{terminal}" ); let run = decode_native_census_output(&with_row, Some(0)).expect("rows match the count"); assert_eq!(run.advised_files, 1); - // DISCRIMINATING: the same terminal without its row -- the receipt census-resolve wrote + // DISCRIMINATING: the same terminal without its row -- the receipt the census verb wrote // before it printed advisories -- refuses rather than carrying a count with no population. let cause = decode_native_census_output(terminal, Some(0)) .err() .expect("must refuse"); assert!(cause.contains("AdvisoryRowsDisagree"), "got: {cause}"); // And a terminal with no advised_files at all (the old verb's marker) refuses too. - let old = "{\"_terminal\":\"complete\",\"mode\":\"census-resolve\",\"modules\":3,\ + let old = "{\"_terminal\":\"complete\",\"mode\":\"census-infer\",\"modules\":3,\ \"file_refusals\":0,\"residual_rows\":0,\"cause_groups\":0}\n"; let cause = decode_native_census_output(old, Some(0)) .err() .expect("must refuse"); assert!(cause.contains("no advised_files"), "got: {cause}"); + // DISCRIMINATING: a census-resolve terminal is part 1's grain, not this census, and refuses. + let resolve_grain = terminal.replace("census-infer", "census-resolve"); + let with_row = format!( + "{{\"accepted_file_advisories\":{{\"path\":\"a.dag\",\"head\":\"r\",\"tail\":[]}}}}\n{resolve_grain}" + ); + let cause = decode_native_census_output(&with_row, Some(0)) + .err() + .expect("must refuse"); + assert!(cause.contains("TerminalNotComplete"), "got: {cause}"); + // DISCRIMINATING: a type_census word outside the three the main writes refuses. + let odd = with_row + .replace("census-resolve", "census-infer") + .replace("\"held\"", "\"hold\""); + let cause = decode_native_census_output(&odd, Some(0)) + .err() + .expect("must refuse"); + assert!(cause.contains("TypeCensusWordUnknown"), "got: {cause}"); + let run = decode_native_census_output( + &with_row.replace("census-resolve", "census-infer"), + Some(0), + ) + .expect("a known word decodes"); + assert_eq!(run.type_census, TypeCensusVerdictWord::Held); // A malformed row refuses here exactly as it does under `census`: one decoder. let malformed = format!("{{\"accepted_file_advisories\":{{\"path\":\"a.dag\"}}}}\n{terminal}"); @@ -3803,6 +4133,46 @@ mod tests { assert!(cause.contains("AdvisoryRowMalformed"), "got: {cause}"); } + /// THE THREE BUDGET SOURCES RENDER AS THREE DIFFERENT RECEIPT WORDS: a declared ceiling is never + /// printed as an observed one, and an unreadable budget never as a number. + #[test] + fn host_memory_budget_receipt_line_names_its_source() { + use crate::memory_governor::{ + HostBudgetObservation, HostBudgetResolution, HostBudgetSource, + }; + let observed = host_memory_budget_receipt_line(&HostBudgetResolution::Resolved { + effective_bytes: 7, + requested_bytes: None, + observation: HostBudgetObservation { + source: HostBudgetSource::CgroupMemoryMax { + cgroup_dir: "/x".to_string(), + }, + bytes: 7, + }, + }); + assert!(observed.contains("source=observed"), "got: {observed}"); + assert!( + observed.contains("cgroup memory.max (/x)"), + "got: {observed}" + ); + let declared = host_memory_budget_receipt_line(&HostBudgetResolution::DeclaredUnverified { + requested_bytes: 9, + reason: "no cgroup".to_string(), + }); + assert!( + declared.contains("source=declared_unverified declared_bytes=9"), + "got: {declared}" + ); + let unreadable = host_memory_budget_receipt_line(&HostBudgetResolution::Unreadable { + reason: "none".to_string(), + }); + assert!( + unreadable.contains("source=unreadable"), + "got: {unreadable}" + ); + assert!(!unreadable.contains("bytes="), "got: {unreadable}"); + } + /// THE MARKER'S COUNT AND THE ROWS MUST AGREE. A run that reports more advised files than it /// printed rows dropped some, and the decode refuses rather than reporting the short list. #[test] @@ -4099,6 +4469,7 @@ mod cli_emit_probe_tests { let main = crate::v1_compiler_emit_rust::emit_native_cli_driver_main_rs( "crate_x".to_string(), "pipeline_x".to_string(), + std::rc::Rc::new(im::Vector::new()), ); let source = main.content.as_str(); let write = "print!(\"{text}\");"; @@ -4323,9 +4694,11 @@ mod run_relaying_as_it_runs_tests { // same counts as the same bytes decoded directly. #[test] fn a_relayed_census_stdout_decodes_to_the_same_verdict() { - let out = "{\"accepted_file_advisories\":{\"path\":\"a.dag\",\"head\":\"r\",\"tail\":[]}}\n\ - {\"_terminal\":\"complete\",\"mode\":\"census-resolve\",\"modules\":3,\ - \"file_refusals\":0,\"advised_files\":1,\"residual_rows\":0,\"cause_groups\":0}\n"; + let out = + "{\"accepted_file_advisories\":{\"path\":\"a.dag\",\"head\":\"r\",\"tail\":[]}}\n\ + {\"_terminal\":\"complete\",\"mode\":\"census-infer\",\"modules\":3,\ + \"file_refusals\":0,\"advised_files\":1,\"residual_rows\":0,\"cause_groups\":0,\ + \"roots\":0,\"inferred\":3,\"infer_refused\":0,\"type_census\":\"held\"}\n"; let mut c = Command::new("printf"); c.arg("%s").arg(out); let relayed = run_relaying_as_it_runs(c).expect("spawn"); diff --git a/src/v1/stage0/src/cli_run/owned_data.rs b/src/v1/stage0/src/cli_run/owned_data.rs index 4a5fc1c67b1..2d9f07f72b6 100644 --- a/src/v1/stage0/src/cli_run/owned_data.rs +++ b/src/v1/stage0/src/cli_run/owned_data.rs @@ -96,20 +96,15 @@ use serde::Serialize; pub(crate) fn owned_data_initializer_from_body( graph: &ResolvedGraph, source_indices: &HashMap>, - variant_to_enum: &im::HashMap, + emit_info: &crate::v1_compiler_infer_emit_info::EmitGraphInfo, entry_path: &str, decl_name: &str, body: &Rc, type_annotation: Option<&Rc>, ) -> Result { - let resolved_initializer = resolved_initializer_decl_ref( - graph, - source_indices, - variant_to_enum, - body, - type_annotation, - ) - .map_err(|e| format!("{entry_path}: owned data '{decl_name}': {e}"))?; + let resolved_initializer = + resolved_initializer_decl_ref(graph, source_indices, emit_info, body, type_annotation) + .map_err(|e| format!("{entry_path}: owned data '{decl_name}': {e}"))?; if is_resolved_bool_witness_claim(&resolved_initializer) { let (witness_entry, witness_function) = extract_bool_witness_transport(body, source_indices); @@ -135,7 +130,7 @@ pub(crate) fn owned_data_initializer_from_body( pub fn owned_data_decls_for_entry( graph: &ResolvedGraph, source_indices: &HashMap>, - variant_to_enum: &im::HashMap, + emit_info: &crate::v1_compiler_infer_emit_info::EmitGraphInfo, entry_path: &str, entry_module: &str, ) -> Result, String> { @@ -195,7 +190,7 @@ pub fn owned_data_decls_for_entry( let initializer = owned_data_initializer_from_body( graph, source_indices, - variant_to_enum, + emit_info, entry_path, &decl_name, body, diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index 415badd7f77..8a6ecaadd46 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -13875,9 +13875,26 @@ pub fn run_required_floor( "[floor-receipt] receipt=fixture-closure-union-controls state=held red_wall_ms={control_red_ms} \ clean_wall_ms={control_clean_ms}" ); + // Printed BEFORE the compile so a run cancelled inside the phase still names the union's size + // and its identity join against the prepared subject (a censored run otherwise has no members). + eprintln!( + "[floor-phase] phase=fixture-closure-union-emit state=begin members={} \ + outside_prepared={outside_prepared} prepared_paths={} fixture_compiles={}", + union.members.len(), + prepared_source_paths.len(), + union.fixture_compiles + ); let union_started = std::time::Instant::now(); let union_cpu_started = v1_interpreter::thread_cpu_nanos(); - let union_observed = crate::cli_run::fixture_closure_union_emit_receipt(&union)?; + let union_observed = crate::cli_run::fixture_closure_union_emit_receipt_staged( + &union, + &|stage, members, wall_ms| { + eprintln!( + "[floor-phase] phase=fixture-closure-union-emit stage={stage} members={members} \ + wall_ms={wall_ms}" + ); + }, + )?; eprintln!( "[floor-phase] phase=fixture-closure-union-emit state=held fixture_compiles={} \ memo_hits={} members={} digest={} outside_prepared={outside_prepared} files={} emit_diagnostics={} \ diff --git a/src/v1/stage0/src/cli_run/rostered_row_join.rs b/src/v1/stage0/src/cli_run/rostered_row_join.rs index 17b0490d8c5..ab651956543 100644 --- a/src/v1/stage0/src/cli_run/rostered_row_join.rs +++ b/src/v1/stage0/src/cli_run/rostered_row_join.rs @@ -376,19 +376,31 @@ fn unaccounted_sources(accounted: &[(String, String)]) -> Vec { let mut out = Vec::new(); for root in JOIN_POOL_ROOTS { let mut files = Vec::new(); - crate::cli_run::collect_dag_files_tolerant(&ws.join(root), &mut files); + if let Err(cause) = crate::cli_run::collect_dag_files_tolerant(&ws.join(root), &mut files) { + out.push(JoinFinding { + kind: JoinFindingKind::SourceUnaccounted, + row_type: "(no row type — the pool root could not be acquired)".to_string(), + subject: root.to_string(), + detail: format!( + "acquiring this pool root refused ({cause}), so no source under it can be \ + joined to the declared population" + ), + }); + continue; + } files.sort(); for file in files { let rel = file + .path() .strip_prefix(&ws) - .unwrap_or(&file) + .unwrap_or(file.path()) .to_string_lossy() .replace('\\', "/"); if accounted_paths.contains(rel.as_str()) { continue; } - let digest = match std::fs::read(&file) { - Ok(bytes) => content_digest(&bytes), + let digest = match file.read() { + Ok(text) => content_digest(text.as_bytes()), Err(e) => format!("unreadable: {e}"), }; out.push(JoinFinding { @@ -408,7 +420,8 @@ fn unaccounted_sources(accounted: &[(String, String)]) -> Vec { } /// File stems under `dag/gunbc/recurring_failure_mode/` versus roster member names. -/// Independent of `derived_row_roster::row_stems`: a skip in the writer must still refuse here. +/// Independent of `derived_row_roster::acquire_dir_files`' row selection: a skip in the derivation +/// must still refuse here. fn join_recurring_failure_mode_files_to_roster( rostered_names: &BTreeSet, ) -> Vec { @@ -692,9 +705,9 @@ pub fn run_rostered_row_join(index: &DeclarationIndex) -> Result, ) { - crate::cli_run::derived_row_roster::ensure_if_row_dir_or_panic(dir); if let Ok(entries) = std::fs::read_dir(dir) { + let mut here = Vec::new(); for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { collect_dag_recursive(&path, root, out); } else if path.extension().map_or(false, |e| e == "dag") { - let rel = path - .strip_prefix(root) - .unwrap() - .to_string_lossy() - .to_string(); - // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules. - if rel.contains("/tests/") { - continue; - } - let content = std::fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e)); - out.push((rel, content)); + here.push(path); + } + } + // Admitted per directory, so a ledger row directory contributes its derived roster + // in memory (`derived_row_roster::acquire_dir_files`) rather than as a written file. + let acquired = crate::cli_run::derived_row_roster::acquire_dir_files(dir, here) + .unwrap_or_else(|e| panic!("{}", e)); + for source in acquired { + let path = source.path(); + let rel = path + .strip_prefix(root) + .unwrap() + .to_string_lossy() + .to_string(); + // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules. + if rel.contains("/tests/") { + continue; } + let content = source + .read() + .unwrap_or_else(|e| panic!("failed to read {}: {}", path.display(), e)); + out.push((rel, content)); } } } @@ -2698,7 +2707,7 @@ mod compiler_tests { // row, and the call is STILL refused, because the receiver does not // fail to resolve in the way the row was measured on. A (module, method) // key passed this input; that is the fail-open the third component closes. - let listed_wrong_shape = compile_one("listed.dag", frontier_src("extdeps.dns.domain_name", "list_push")); + let listed_wrong_shape = compile_one("listed.dag", frontier_src("v1.compiler.trace", "map")); assert!( listed_wrong_shape.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. })), "a declared row must NOT admit a new call in the same module on a receiver whose shape it was never measured on, got: {:?}", @@ -4332,10 +4341,25 @@ mod compiler_tests { ) ); assert!( - crate::v1_compiler_emit_rust::is_grounded_coproduct_native_alias( + crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias( + "v2.std.diagnostic".to_string(), "Diagnostics".to_string() ) ); + // The host option is a DECLARATION, not a name: the kernel optional's declaration is it, and a + // coproduct another module declares as Optional is not. + assert!( + crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias( + "std.optional".to_string(), + "Optional".to_string() + ) + ); + assert!( + !crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias( + "gunbc.instruments.native_emission_controls_optional_collision".to_string(), + "Optional".to_string() + ) + ); let empty_shared = std::rc::Rc::new(im::OrdSet::new()); assert_eq!( crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied( @@ -5353,7 +5377,7 @@ mod compiler_tests { // A QUALIFIED REFERENCE IS NOT DECIDED BY THE LOCAL DECLARER. hom.local declares its own Slot // and ALSO names no.such.module.Slot, which resolve cannot bind, so the reference reaches the // emitter unstamped while two modules declare Slot. The referencing-module arm - // (v1.compiler.infer_emit_info type_decl_referencing_module_declarer) would pick the LOCAL Slot: + // (v1.compiler.infer_emit_info type_decl_occurrence_binding) would pick the LOCAL Slot: // the wrong-declarer class itself. It is admitted only for a bare spelling, so this refuses. // THE CONTROL is the same module without the second declarer: the spelling names a sole // declarer, and nothing refuses. @@ -5388,6 +5412,154 @@ mod compiler_tests { ); } + // A SAME-LEAF TYPE ANYWHERE MUST NOT CHANGE ANOTHER MODULE'S EMISSION. hom.user returns the kernel + // optional's Absent; hom.edge declares an unrelated enum that also has an Absent variant. With the + // leaf-keyed type summaries, adding hom.coll (a coproduct it declares as Optional) replaced the + // kernel Optional's summary, so Absent stopped being an Optional variant and hom.user emitted + // Rc::new(Lookup::Absent) where it had emitted None. Keyed by declaration, both summaries stand: + // hom.user's file is byte-identical with and without hom.coll, and its Absent is the host None. + #[test] + fn same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged() { + use crate::v1_compiler_compile::SourceFile; + let src = |path: &str, content: &str| { + std::rc::Rc::new(SourceFile { + path: path.to_string(), + content: content.to_string(), + }) + }; + let optional = || { + src( + "fixtures/same_leaf_optional/optional.dag", + "module std.optional\n\ntype Optional\n = Present { value: T }\n | Absent\n", + ) + }; + let edge = || { + src( + "fixtures/same_leaf_optional/edge.dag", + "module hom.edge\n\ntype Lookup\n = Found { at: Int }\n | Absent\n", + ) + }; + let user = || { + src("fixtures/same_leaf_optional/user.dag", "module hom.user\nimport std.optional { Optional, Present, Absent }\n\nfn nothing_here(x: Int) -> Optional {\n Absent\n}\n") + }; + let coll = || { + src("fixtures/same_leaf_optional/coll.dag", "module hom.coll\n\ntype Optional\n = CollisionWrapped { value: Int }\n | CollisionEmpty\n") + }; + let emitted_user = |sources: Vec>| -> String { + let result = crate::v1_compiler_compile::compile_sources( + std::rc::Rc::new(sources.into()), + crate::v1_compiler_artifact::RenderTarget::Rust, + ); + result + .files + .iter() + .find(|f| f.path.contains("hom_user")) + .map(|f| f.content.clone()) + .unwrap_or_default() + }; + let without = emitted_user(vec![optional(), edge(), user()]); + let with = emitted_user(vec![optional(), edge(), user(), coll()]); + assert!( + without.contains("None") && !without.contains("Lookup::Absent"), + "the control: hom.user's Absent is the host None:\n{without}" + ); + assert_eq!( + with, without, + "adding a same-leaf Optional elsewhere changed hom.user's emission" + ); + } + + // AN UNSTAMPED BARE `Optional` FOLLOWS THE TYPECHECKER'S lookup_binding ORDER: own file, then + // the file's named import (followed through re-exports), then the kernel + // (v1.compiler.infer_env bare_occurrence_binding). The member references of a coproduct body reach + // the emitter from the type environment's copy with no Node.declaration, so the census route is + // the emitter's own decision. THE RED is the importer: with the kernel ahead of the named import + // (the order direct_import_export_precedence_note states) its payload routes kernel_mint to + // std.optional, measured on gunbc#13454 at 57551c4b. The control keeps a module that names + // Optional without importing it on the kernel arm. + #[test] + fn unstamped_bare_optional_follows_the_lookup_binding_order() { + use crate::v1_compiler_compile::SourceFile; + let sources = vec![ + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/optional.dag".to_string(), content: "module std.optional\n\ntype Optional\n = Present { value: T }\n | Absent\n".to_string() }), + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/collision.dag".to_string(), content: "module hom.collision\n\ntype Optional\n = CollisionWrapped { value: Int }\n | CollisionEmpty\n".to_string() }), + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/importer.dag".to_string(), content: "module hom.importer\n\nimport hom.collision { Optional }\n\ntype HeldChoice\n = HeldOptional { held: Optional }\n | HeldNothing\n".to_string() }), + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/plain.dag".to_string(), content: "module hom.plain\n\ntype PlainChoice\n = PlainHeld { held: Optional }\n | PlainNothing\n".to_string() }), + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/user.dag".to_string(), content: "module hom.user\n\nimport hom.importer { HeldChoice }\nimport hom.plain { PlainChoice }\n\nfn pick(h: HeldChoice, p: PlainChoice) -> Int {\n 1\n}\n".to_string() }), + ]; + let receipt = crate::v1_tests_claim_carrier_realization_census::typed_census_from_sources( + std::rc::Rc::new(sources.into()), + ); + assert!(!receipt.starts_with("REFUSED"), "{receipt}"); + let routes = |enclosing: &str| -> Vec { + receipt + .lines() + .skip(1) + .map(|l| l.split('\t').collect::>()) + .filter(|c| c[1] == enclosing && c[2] == "declaration_field" && c[3] == "Optional") + .map(|c| c[13].to_string()) + .collect() + }; + let held = routes("HeldChoice"); + assert!( + !held.is_empty() + && held + .iter() + .all(|r| r == "referencing_module_import:Resolved:hom.collision.Optional"), + "the imported Optional must route to the import, never the kernel mint:\n{receipt}" + ); + let plain = routes("PlainChoice"); + assert!( + !plain.is_empty() + && plain + .iter() + .all(|r| r == "kernel_mint:Resolved:std.optional.Optional"), + "an unimported Optional is the kernel's:\n{receipt}" + ); + assert!(!receipt.contains("LeafAmbiguous:Optional"), "{receipt}"); + } + + // A COPRODUCT NAMED Optional EMITS AS ITS OWN ENUM. still-raven-321's fixture, + // fixtures/native_emission_controls_optional_collision.dag, is a non-kernel declaration that shares + // the kernel mint's leaf. Host-Option is decided by declaration (v1.compiler.emit_rust + // declaration_owns_host_option over gunbc.structural_realization_bindings kernel_mint_declaration_rows), + // so its positions name its own enum. THE RED is the leaf rule this replaced: every position rendered + // as Option<..> and the variants were never emitted (E0425 cannot find CollisionWrapped). The fixture + // lives outside the source roots, because a second Optional in the accepted corpus is a leaf-name fork, + // so it is compiled here, from its file, beside a kernel std.optional. Its one import line names a + // kernel type and is dropped so the pair compiles standalone. + #[test] + fn a_coproduct_named_optional_emits_as_its_own_enum() { + use crate::v1_compiler_compile::SourceFile; + let collision = read_dag("fixtures/native_emission_controls_optional_collision.dag") + .replace("import std.types { Int }\n", ""); + let sources = vec![ + std::rc::Rc::new(SourceFile { path: "fixtures/kernel_shadow/optional.dag".to_string(), content: "module std.optional\n\ntype Optional\n = Present { value: T }\n | Absent\n".to_string() }), + std::rc::Rc::new(SourceFile { path: "fixtures/native_emission_controls_optional_collision.dag".to_string(), content: collision }), + ]; + let result = crate::v1_compiler_compile::compile_sources( + std::rc::Rc::new(sources.into()), + crate::v1_compiler_artifact::RenderTarget::Rust, + ); + let emitted = result + .files + .iter() + .find(|f| { + f.path + .contains("native_emission_controls_optional_collision") + }) + .map(|f| f.content.clone()) + .unwrap_or_default(); + assert!( + emitted.contains("pub enum Optional") && emitted.contains("CollisionWrapped"), + "the collision coproduct must emit as its own enum:\n{emitted}" + ); + assert!( + !emitted.contains("Option<") && !emitted.contains("compile_error!"), + "no position of the collision coproduct may realize as the host Option:\n{emitted}" + ); + } + // A REPEATED TYPE PARAMETER IN ONE HEADER REFUSES, LOCATED, AND SAYS WHICH NAME. The refusal // predates this row. Its message once blamed a value parameter and called a type a fn, so the // row asserts the NAME and the OWNER, not merely that some diagnostic fired. The control header diff --git a/src/v1/stage0/src/coproduct_reflection.rs b/src/v1/stage0/src/coproduct_reflection.rs index a8183fab4ea..9db4ed78f83 100644 --- a/src/v1/stage0/src/coproduct_reflection.rs +++ b/src/v1/stage0/src/coproduct_reflection.rs @@ -585,10 +585,38 @@ fn decls_parse_only_from_disk( } modules.sort(); let module_count = modules.len(); + // THE ACQUIRED POPULATION, so a module the index names but no file holds -- a derived ledger + // roster, contributed in memory by `derived_row_roster::acquire_dir_files` -- is parsed from + // the bytes acquisition contributed rather than from a path that has no file behind it. + let mut acquired: std::collections::HashMap< + PathBuf, + crate::cli_run::derived_row_roster::AcquiredDag, + > = std::collections::HashMap::new(); + for root in roots { + let root_path = ws.join(root); + if root_path.is_dir() { + let mut files = Vec::new(); + collect_dag_files_tolerant(&root_path, &mut files) + .map_err(|cause| format!("{caller}: acquiring {root}: {cause}"))?; + for file in files { + acquired.insert(file.path().to_path_buf(), file); + } + } + } let mut out = Vec::new(); for (module_path, rel_path) in modules { let abs = ws.join(&rel_path); - let parsed = parse_dag_file(&abs).ok_or_else(|| { + let parsed = match acquired.get(&abs) { + Some(source) => { + let content = source.read().map_err(|e| { + format!("{caller}: read `{rel_path}`: {e} (fail-closed; no silent skip)") + })?; + let filename = abs.file_name().and_then(|s| s.to_str()).unwrap_or("?"); + parse_dag_content(&content, filename) + } + None => parse_dag_file(&abs), + } + .ok_or_else(|| { format!("{caller}: failed to parse `{rel_path}` (fail-closed; no silent skip)") })?; let si = parsed.source_indices; @@ -624,9 +652,11 @@ fn dag_files_under_abs_roots(abs_roots: &[String]) -> Vec { for root in abs_roots { let root_path = PathBuf::from(root); if root_path.is_dir() { - collect_dag_files_tolerant(&root_path, &mut files); + collect_dag_files_tolerant(&root_path, &mut files) + .unwrap_or_else(|cause| panic!("acquiring {root}: {cause}")); } } + let mut files: Vec = files.iter().map(|f| f.path().to_path_buf()).collect(); files.sort(); files } @@ -1764,13 +1794,16 @@ pub fn pool_root_refusal_message( ) } -fn corpus_dag_files_for_roots(roots: &[String]) -> Vec { +fn corpus_dag_files_for_roots( + roots: &[String], +) -> Vec { let ws = workspace_root(); let mut files = Vec::new(); for root in roots { let root_path = ws.join(root); if root_path.is_dir() { - collect_dag_files_tolerant(&root_path, &mut files); + collect_dag_files_tolerant(&root_path, &mut files) + .unwrap_or_else(|cause| panic!("acquiring {root}: {cause}")); } } files.sort(); @@ -1797,17 +1830,25 @@ pub fn decl_facts_corpus_walk(pool_roots: &[String]) -> DeclFactsCorpusWalk { let mut files_scanned = 0usize; let mut files_parsed = 0usize; for file in corpus_dag_files_for_roots(pool_roots) { - let rel = repo_rel(&file); + let rel = repo_rel(file.path()); if is_test_dag(&rel) { continue; } files_scanned += 1; - let content = std::fs::read_to_string(&file).ok(); + let content = file.read().ok(); let module_path = content .as_ref() .and_then(|c| extract_module_path(c)) .unwrap_or_default(); - let Some(parsed) = parse_dag_file(&file) else { + // The acquired bytes, not a second read of the path: a derived roster has no file. + let filename = file + .path() + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("?"); + let Some(parsed) = content.as_deref().and_then(|c| { + crate::module_path_index::parsed_dag_file::parse_dag_content(c, filename) + }) else { continue; }; files_parsed += 1; diff --git a/src/v1/stage0/src/declaration_index.rs b/src/v1/stage0/src/declaration_index.rs index 144f733d1f0..2f88bb06e68 100644 --- a/src/v1/stage0/src/declaration_index.rs +++ b/src/v1/stage0/src/declaration_index.rs @@ -3519,13 +3519,14 @@ mod import_binding_authority_tests { /// The declaration must sit in a PARENT env: `callable_lookup_over_candidates` checks /// `func_env.local` first and returns immediately on a hit, so a local declaration never - /// reaches the authored-membership branch. `v2.std.collection` / `map_get` is not an arbitrary + /// reaches the authored-membership branch. `v2.std.collection` / `map_get_checked` is not an arbitrary /// choice -- it is the specimen carrying `DivergentProjection` fidelity, which is the condition /// `declared_candidate_rivals_the_builtin` requires. - fn parent_env_declaring_map_get() -> Rc { + fn parent_env_declaring_map_get_checked() -> Rc + { use crate::v1_compiler_infer_sigs::{ResolvedFormals, ResolvedFuncEnv, ResolvedFuncSig}; let sig = Rc::new(ResolvedFuncSig { - name: "map_get".to_string(), + name: "map_get_checked".to_string(), params: crate::v1_std_core::empty_node_list(), // This synthetic declaration deliberately has no parameters. Its empty formal // population is kernel-grounded, not awaiting a module-context resolution pass. @@ -3540,7 +3541,7 @@ mod import_binding_authority_tests { name: "v2.std.collection".to_string(), local: Rc::new(crate::v1_rt::map_insert( crate::v1_rt::empty_map(), - "map_get".to_string(), + "map_get_checked".to_string(), sig, )), parents: Rc::new(im::Vector::new()), @@ -3565,7 +3566,7 @@ mod import_binding_authority_tests { /// That is an OBSERVATION-BOUNDARY defect, not a bad fixture spelling. /// /// AND IT DOES NOT MEAN THE POLICY IS DEAD. That convenient reading is refuted by a specimen in - /// the tree: `map_get` is declared at `v2.std.collection` with `DivergentProjection` fidelity in + /// the tree: `map_get_checked` is declared at `v2.std.collection` with `DivergentProjection` fidelity in /// `std.primitive_projection`, which is exactly the condition /// `declared_candidate_rivals_the_builtin` tests. So the collision is real, reachable from /// authored source, and already witnessed by `callable_candidate_ambiguity_witness_test`. @@ -3577,16 +3578,16 @@ mod import_binding_authority_tests { #[test] fn authored_membership_alone_decides_the_callable_verdict() { use crate::v1_compiler_infer_lookup::callable_lookup_over_candidates; - use crate::v1_compiler_infer_sigs::{CallableIdentity, FuncSigLookup}; + use crate::v1_compiler_infer_sigs::FuncSigLookup; - let env = parent_env_declaring_map_get(); + let env = parent_env_declaring_map_get_checked(); let named = callable_lookup_over_candidates( env.clone(), - type_env_with_authored(&["map_get"]), - "map_get".to_string(), + type_env_with_authored(&["map_get_checked"]), + "map_get_checked".to_string(), ); - let omitted = callable_lookup_over_candidates( + let bare = callable_lookup_over_candidates( env, type_env_with_authored(&["cut_unrelated_marker"]), "map_get".to_string(), @@ -3598,7 +3599,7 @@ mod import_binding_authority_tests { match &*named { FuncSigLookup::FuncSigResolved { declared, .. } => { assert_eq!(declared.owner_module_path, "v2.std.collection"); - assert_eq!(declared.decl_name, "map_get"); + assert_eq!(declared.decl_name, "map_get_checked"); } other => panic!( "naming the callable must suppress the builtin co-candidate and resolve to the \ @@ -3606,38 +3607,112 @@ mod import_binding_authority_tests { ), } - // AND THE AMBIGUOUS ARM IS CHECKED BY ITS EXACT IDENTITY SET, never by a count and never by - // list order. A count would pass on a substitution, and an order-sensitive comparison would - // make candidate ordering -- which nothing declares to be semantic -- part of the assertion. - match &*omitted { - FuncSigLookup::FuncSigAmbiguous { candidates, .. } => { - let mut observed: Vec = candidates - .iter() - .map(|c| match &*c.identity { - CallableIdentity::DeclaredCallable { identity } => format!( - "declared:{}:{}", - identity.owner_module_path, identity.decl_name - ), - CallableIdentity::BuiltinCallable { primitive_name } => { - format!("builtin:{primitive_name}") - } - }) - .collect(); - observed.sort(); - observed.dedup(); - assert_eq!( - observed, - vec![ - "builtin:map_get".to_string(), - "declared:v2.std.collection:map_get".to_string(), - ], - "the two rival authorities must both be present, and only those two" - ); - } + // THE OLD map_get FORK IS DISSOLVED, AND THIS ARM IS ITS PERMANENT REGRESSION CONTROL, + // checked by its exact identity set rather than a count or list order. Before the rename, + // omitting the declaration from the authored list while calling the bare name minted TWO + // rival authorities -- `builtin:map_get` and `declared:v2.std.collection:map_get` -- and + // went ambiguous. The rename moved the declaration to `map_get_checked`, so the bare + // spelling now has no declared candidate at all: only the builtin remains, which this + // route leaves unresolved (builtins reach inference through the method path). + // + // HONESTY ABOUT WHAT THIS ARM CAN AND CANNOT SEE ANY MORE (review 77428). The former + // rival-join -- a rival declaration omitted from the authored list joining the builtin + // into FuncSigAmbiguous -- is NOT reachable at this seam any more, and that absence is + // the repair, not a hole: the only DivergentProjection row in the compiled roster + // (std_primitive_projection.rs) answers to the declaration `map_get_checked`, which is + // not a primitive-registry key, while the registry keys the primitive as `map_get` + // (v1_compiler_infer_method.rs). parent_closure_callable_candidates takes the declared + // identity's decl_name from the LOOKUP KEY, so no lookup name can carry both a rival + // declaration and a builtin co-candidate; a synthetic fixture that "declares map_get" + // mints the identity (v2.std.collection, map_get), which the roster does not cover. A + // re-widening that reclaimed the bare spelling under a registry-keyed name would + // re-ambient the join, but THIS arm would still green until a lookup name carried both + // authorities -- so the rival predicate's own discriminating control + // (declared_candidate_rivals_the_builtin_discriminates_by_row_fidelity, below) stays + // enrolled as the executing evidence for the mechanism, per DESIGN 4b(4), and the census + // wall w_projection_census_declares_no_divergent_shared_spelling holds the no-shared- + // spelling fact at the tree grain. + match &*bare { + FuncSigLookup::FuncSigUnresolved => {} + FuncSigLookup::FuncSigAmbiguous { candidates } => panic!( + "the dissolved map_get fork must not return: the bare spelling minted rivals \ + {:?}", + candidates + ), other => panic!( - "omitting it from a NONEMPTY authored list must admit the builtin rival and go \ - ambiguous, got {other:?}" + "the dissolved map_get fork must leave the bare spelling a single builtin \ + authority -- no declared candidate, never ambiguous -- got {other:?}" ), } } + + /// THE RIVAL PREDICATE'S OWN DISCRIMINATING CONTROL, ENROLLED AFTER THE RENAME DISSOLVED THE + /// SEAM'S SHARED SPELLING (review 77428; DESIGN 4b(4): the class's executing evidence stays + /// enrolled). Checked against the COMPILED roster's actual rows, so a row that loses its + /// DivergentProjection fidelity, a predicate that stops consulting the roster, or a roster + /// that stops carrying the divergent specimen all fail here. The identity values are the + /// mechanism's own data: the divergent declaration must rival, a modeled projection must + /// not, an unprojected declaration must not, and a builtin candidate is never a rival to + /// itself. + #[test] + fn declared_candidate_rivals_the_builtin_discriminates_by_row_fidelity() { + use crate::v1_compiler_infer_lookup::declared_candidate_rivals_the_builtin; + use crate::v1_compiler_infer_sigs::CallableIdentity::{BuiltinCallable, DeclaredCallable}; + use crate::v1_compiler_infer_sigs::{CallableCandidate, DeclaredCallableIdentity}; + + let candidate_for = |owner: &str, decl: &str| { + Rc::new(CallableCandidate { + identity: Rc::new(DeclaredCallable { + identity: Rc::new(DeclaredCallableIdentity { + owner_module_path: owner.to_string(), + decl_name: decl.to_string(), + }), + }), + sig: parent_env_declaring_map_get_checked() + .parents + .iter() + .next() + .expect("fixture parent") + .local + .values() + .next() + .expect("fixture signature") + .clone(), + }) + }; + + // The compiled roster's one DivergentProjection row: the declaration must rival. + assert!(declared_candidate_rivals_the_builtin(candidate_for( + "v2.std.collection", + "map_get_checked" + ))); + // A modeled projection is a faithful alias, never a rival. + assert!(!declared_candidate_rivals_the_builtin(candidate_for( + "v2.std.collection", + "empty_map" + ))); + // An unprojected declaration is never a rival. + assert!(!declared_candidate_rivals_the_builtin(candidate_for( + "cut.unrelated.module", + "map_get_checked" + ))); + // A builtin candidate is never a rival to itself. + assert!(!declared_candidate_rivals_the_builtin(Rc::new( + CallableCandidate { + identity: Rc::new(BuiltinCallable { + primitive_name: "map_get".to_string(), + }), + sig: parent_env_declaring_map_get_checked() + .parents + .iter() + .next() + .expect("fixture parent") + .local + .values() + .next() + .expect("fixture signature") + .clone(), + } + ))); + } } diff --git a/src/v1/stage0/src/emitted_closure_compile_host.rs b/src/v1/stage0/src/emitted_closure_compile_host.rs index 62b3f13107c..5c08e9a2722 100644 --- a/src/v1/stage0/src/emitted_closure_compile_host.rs +++ b/src/v1/stage0/src/emitted_closure_compile_host.rs @@ -2480,9 +2480,10 @@ pub fn emit_compile_selection(source_roots: &[String]) -> EmitCompileSelection { let mut universe: Vec = Vec::new(); for root in source_roots { let mut files = Vec::new(); - super::collect_dag_files_tolerant(std::path::Path::new(root), &mut files); + super::collect_dag_files_tolerant(std::path::Path::new(root), &mut files) + .unwrap_or_else(|cause| panic!("emit compile selection over {root}: {cause}")); for file in files { - universe.push(file.to_string_lossy().to_string()); + universe.push(file.path().to_string_lossy().to_string()); } } universe.sort(); diff --git a/src/v1/stage0/src/extdeps_container_oci_digest.rs b/src/v1/stage0/src/extdeps_container_oci_digest.rs index 60344d27275..71788f3a266 100644 --- a/src/v1/stage0/src/extdeps_container_oci_digest.rs +++ b/src/v1/stage0/src/extdeps_container_oci_digest.rs @@ -259,7 +259,7 @@ pub fn oci_wire_digest_parts(raw: String) -> Option> { std::option::Option::None } else { match parts.clone().first().cloned() { - Some(algorithm) => match parts.clone().iter().cloned().skip(1 as usize).next() { + Some(algorithm) => match parts.clone().get((1) as usize).cloned() { Some(encoded) => Some(oci_wire_digest_parts_from_split( algorithm.clone(), encoded.clone(), diff --git a/src/v1/stage0/src/extdeps_languages_rust_emit.rs b/src/v1/stage0/src/extdeps_languages_rust_emit.rs index 0ec6134ed39..edcdd4fdc23 100644 --- a/src/v1/stage0/src/extdeps_languages_rust_emit.rs +++ b/src/v1/stage0/src/extdeps_languages_rust_emit.rs @@ -90,7 +90,7 @@ pub fn rust_container_templates() -> Rc> { pub fn rust_simple_method_specs() -> Rc>> { thread_local! { static CACHED: Rc>> = { - serde_json::from_str("[{\"method_name\": \"count\", \"template\": \"({recv}.len() as i64)\", \"wraps_result\": false}, {\"method_name\": \"join\", \"template\": \"{recv}.join(&{arg})\", \"wraps_result\": false}, {\"method_name\": \"split\", \"template\": \"{recv}.split(&{arg}).map(|s| s.to_string()).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"last\", \"template\": \"{recv}.last().cloned()\", \"wraps_result\": false}, {\"method_name\": \"first\", \"template\": \"{recv}.first().cloned()\", \"wraps_result\": false}, {\"method_name\": \"enumerate\", \"template\": \"{recv}.iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"chars\", \"template\": \"{recv}.chars().map(|c| c as i64).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"skip\", \"template\": \"{recv}.iter().cloned().skip({arg} as usize).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"take\", \"template\": \"{recv}.iter().cloned().take({arg} as usize).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"is_empty\", \"template\": \"{recv}.is_empty()\", \"wraps_result\": false}]") + serde_json::from_str("[{\"method_name\": \"count\", \"template\": \"({recv}.len() as i64)\", \"wraps_result\": false}, {\"method_name\": \"join\", \"template\": \"{recv}.join(&{arg})\", \"wraps_result\": false}, {\"method_name\": \"split\", \"template\": \"{recv}.split(&{arg}).map(|s| s.to_string()).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"last\", \"template\": \"{recv}.last().cloned()\", \"wraps_result\": false}, {\"method_name\": \"first\", \"template\": \"{recv}.first().cloned()\", \"wraps_result\": false}, {\"method_name\": \"enumerate\", \"template\": \"{recv}.iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"chars\", \"template\": \"{recv}.chars().map(|c| c as i64).collect::>()\", \"wraps_result\": true}, {\"method_name\": \"skip\", \"template\": \"v1_rt::list_skip(&{recv}, {arg})\", \"wraps_result\": true}, {\"method_name\": \"take\", \"template\": \"v1_rt::list_take(&{recv}, {arg})\", \"wraps_result\": true}, {\"method_name\": \"is_empty\", \"template\": \"{recv}.is_empty()\", \"wraps_result\": false}]") .expect("valid data definition") }; } diff --git a/src/v1/stage0/src/extdeps_uri.rs b/src/v1/stage0/src/extdeps_uri.rs index 303138dbac4..919c2a4bb70 100644 --- a/src/v1/stage0/src/extdeps_uri.rs +++ b/src/v1/stage0/src/extdeps_uri.rs @@ -118,18 +118,15 @@ pub fn uri_from_wire(url: String) -> Option> { ParsedHrefScheme::UnknownHref => std::option::Option::None, ParsedHrefScheme::HrefScheme { scheme: scheme, .. } => { let prefix = uri_scheme_wire(scheme.clone()); - let locator = Rc::new( - Rc::new( + let locator = Rc::new(v1_rt::list_skip( + &Rc::new( s.clone() .split(&prefix.clone()) .map(|s| s.to_string()) .collect::>(), - ) - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + ), + 1, + )) .join(&prefix.clone()); if (v1_rt::starts_with(s.clone(), prefix.clone()) && !(locator.clone() == "".to_string())) diff --git a/src/v1/stage0/src/extdeps_uri_path.rs b/src/v1/stage0/src/extdeps_uri_path.rs index 1c6f7ff7508..83385bc9570 100644 --- a/src/v1/stage0/src/extdeps_uri_path.rs +++ b/src/v1/stage0/src/extdeps_uri_path.rs @@ -176,13 +176,7 @@ pub fn parse_segment_tokens(seg: String) -> Rc { }) } }; - let after_open = match before_and_rest - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + let after_open = match before_and_rest.clone().get((1) as usize).cloned() { Some(r) => r.clone(), std::option::Option::None => { return Rc::new(PathSegmentTokensResult::MalformedPathSegment { @@ -216,13 +210,7 @@ pub fn parse_segment_tokens(seg: String) -> Rc { }) } }; - let suffix = match name_and_suffix - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + let suffix = match name_and_suffix.clone().get((1) as usize).cloned() { Some(s) => s.clone(), std::option::Option::None => { return Rc::new(PathSegmentTokensResult::MalformedPathSegment { @@ -304,14 +292,7 @@ pub fn match_path_segments(path_only: String) -> Rc> { match raw_segs.clone().first().cloned() { Some(lead) => { if (lead.clone() == "".to_string()) { - Rc::new( - raw_segs - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_skip(&raw_segs.clone(), 1)) } else { raw_segs.clone() } @@ -337,21 +318,8 @@ pub fn match_path_tokens( Some(tok) => match segs.clone().first().cloned() { std::option::Option::None => Rc::new(PathTemplateMatch::PathNotMatched), Some(seg) => match (*match_path_tokens( - Rc::new( - tokens - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - segs.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&tokens.clone(), 1)), + Rc::new(v1_rt::list_skip(&segs.clone(), 1)), )) .clone() { @@ -461,51 +429,48 @@ pub fn parse_path_template(raw: String) -> Rc { tokens: first_tokens, .. } => { - let parsed = Rc::new( - segments - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - Rc::new(PathTemplateParseResult::ParsedPathTemplate { - template: Rc::new(PathTemplate { - tokens: first_tokens.clone(), - }), - }), - |acc: Rc, seg: String| match (*acc.clone()).clone() - { - PathTemplateParseResult::MalformedPathTemplate { .. } => acc.clone(), - PathTemplateParseResult::ParsedPathTemplate { - template: path, .. - } => match (*parse_segment_tokens(seg.clone())).clone() { - PathSegmentTokensResult::MalformedPathSegment { - segment: s, - reason: r, - .. - } => Rc::new(PathTemplateParseResult::MalformedPathTemplate { - raw: raw.clone(), - segment: s.clone(), - reason: r.clone(), + let parsed = Rc::new(v1_rt::list_skip(&segments.clone(), 1)) + .iter() + .cloned() + .fold( + Rc::new(PathTemplateParseResult::ParsedPathTemplate { + template: Rc::new(PathTemplate { + tokens: first_tokens.clone(), }), - PathSegmentTokensResult::ParsedSegmentTokens { - tokens: seg_tokens, + }), + |acc: Rc, seg: String| match (*acc.clone()) + .clone() + { + PathTemplateParseResult::MalformedPathTemplate { .. } => { + acc.clone() + } + PathTemplateParseResult::ParsedPathTemplate { + template: path, .. - } => Rc::new(PathTemplateParseResult::ParsedPathTemplate { - template: Rc::new(PathTemplate { - tokens: v1_rt::concat( - path.tokens.clone(), - seg_tokens.clone(), - ), + } => match (*parse_segment_tokens(seg.clone())).clone() { + PathSegmentTokensResult::MalformedPathSegment { + segment: s, + reason: r, + .. + } => Rc::new(PathTemplateParseResult::MalformedPathTemplate { + raw: raw.clone(), + segment: s.clone(), + reason: r.clone(), }), - }), + PathSegmentTokensResult::ParsedSegmentTokens { + tokens: seg_tokens, + .. + } => Rc::new(PathTemplateParseResult::ParsedPathTemplate { + template: Rc::new(PathTemplate { + tokens: v1_rt::concat( + path.tokens.clone(), + seg_tokens.clone(), + ), + }), + }), + }, }, - }, - ); + ); parsed } }, @@ -520,10 +485,8 @@ pub fn uri_query_string(path: String) -> String { .map(|s| s.to_string()) .collect::>(), ) - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(q) => q.clone(), std::option::Option::None => "".to_string(), @@ -560,10 +523,8 @@ pub fn uri_query_param(path: String, key: String) -> String { .map(|s| s.to_string()) .collect::>(), ) - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(x) => x.clone(), std::option::Option::None => "".to_string(), diff --git a/src/v1/stage0/src/extdeps_version.rs b/src/v1/stage0/src/extdeps_version.rs index c9fc71c64fc..66f7b5c8ee8 100644 --- a/src/v1/stage0/src/extdeps_version.rs +++ b/src/v1/stage0/src/extdeps_version.rs @@ -1,6 +1,8 @@ // Generated by v1 compiler -- do not edit. // Source module: extdeps.version +use self::ComparisonSide::*; +use self::VersionComparison::*; pub use crate::extdeps_external_authority::ExternalAuthority; use crate::extdeps_uri::UriScheme::Https; pub use crate::extdeps_uri::{Uri, UriScheme}; @@ -31,9 +33,36 @@ pub fn extdeps_external_authority_anchor() -> Rc { pub type VersionIdentity = String; +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] +#[serde(tag = "_variant")] +pub enum ComparisonSide { + LeftOperand, + RightOperand, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum VersionComparison { + VersionCompared { + ordering: Ordering, + }, + VersionComparisonRefused { + side: ComparisonSide, + identity: String, + cause: String, + }, +} + #[derive(Clone)] pub struct VersionScheme { - pub compare: Rc Ordering>, + pub compare: Rc Rc>, } pub type VersionConstraint = String; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct LeftOperand; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RightOperand; diff --git a/src/v1/stage0/src/extdeps_version_semver.rs b/src/v1/stage0/src/extdeps_version_semver.rs index da6901c2ac6..962a3df984c 100644 --- a/src/v1/stage0/src/extdeps_version_semver.rs +++ b/src/v1/stage0/src/extdeps_version_semver.rs @@ -5,13 +5,15 @@ use self::SemVerIdentifier::*; pub use crate::extdeps_external_authority::ExternalAuthority; use crate::extdeps_uri::UriScheme::Https; pub use crate::extdeps_uri::{Uri, UriScheme}; -pub use crate::extdeps_version::{VersionConstraint, VersionIdentity, VersionScheme}; +use crate::extdeps_version::ComparisonSide::{LeftOperand, RightOperand}; +use crate::extdeps_version::VersionComparison::{VersionCompared, VersionComparisonRefused}; +pub use crate::extdeps_version::{ + ComparisonSide, VersionComparison, VersionConstraint, VersionIdentity, VersionScheme, +}; pub use crate::std_algebra::trim; pub use crate::std_algebra::Ordering; use crate::std_algebra::Ordering::{Equal, Greater, Less}; -pub use crate::std_checked_arithmetic::checked_int_to_nat; pub use crate::std_integer::NonNegativeInt; -pub use crate::std_nat::nat_compare; pub use crate::std_types::{List, NonEmptyStr}; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; @@ -38,37 +40,140 @@ pub type SemVerIdentity = NonEmptyStr; pub type SemVerConstraint = NonEmptyStr; -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Serialize)] +pub struct SemVerNumericField { + digits: Rc>, +} +impl SemVerNumericField { + pub fn digits(&self) -> Rc> { + self.digits.clone() + } +} + +pub fn semver_numeric_field(cps: Rc>) -> Option> { + if ((cps.clone().len() as i64) == 0) { + std::option::Option::None + } else { + if !{ + let mut __all = true; + for cp in cps.iter().cloned() { + if !((cp.clone() >= 48) && (cp.clone() <= 57)) { + __all = false; + break; + } + } + __all + } { + std::option::Option::None + } else { + if ((cps.clone().len() as i64) > 1) { + match cps.clone().first().cloned() { + Some(48) => std::option::Option::None, + Some(_) => Some(Rc::new(SemVerNumericField { + digits: cps.clone(), + })), + std::option::Option::None => std::option::Option::None, + } + } else { + Some(Rc::new(SemVerNumericField { + digits: cps.clone(), + })) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize)] #[serde(tag = "_variant")] pub enum SemVerIdentifier { - SemVerNumericIdentifier { value: NonNegativeInt }, + SemVerNumericIdentifier { digits: Rc }, SemVerAlphanumericIdentifier { label: NonEmptyStr }, } -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Serialize)] pub struct SemVerVersion { - pub major: NonNegativeInt, - pub minor: NonNegativeInt, - pub patch: NonNegativeInt, + pub major: Rc, + pub minor: Rc, + pub patch: Rc, pub pre_release: Rc>>, pub build: Rc>>, } -pub fn semver_compare_non_negative_int(a: NonNegativeInt, b: NonNegativeInt) -> Ordering { - crate::std_nat::nat_compare(a.clone(), b.clone()) +pub fn semver_compare_numeric_digits( + a: Rc, + b: Rc, +) -> Ordering { + semver_compare_digit_runs(a.digits(), b.digits()) +} + +pub fn semver_compare_digit_runs(a: Rc>, b: Rc>) -> Ordering { + if ((a.clone().len() as i64) < (b.clone().len() as i64)) { + Ordering::Less + } else { + if ((a.clone().len() as i64) > (b.clone().len() as i64)) { + Ordering::Greater + } else { + semver_compare_digits_lexical(a.clone(), b.clone()) + } + } +} + +pub fn semver_compare_digits_lexical( + mut __tco_loop_a: Rc>, + mut __tco_loop_b: Rc>, +) -> Ordering { + loop { + #[allow(unused_mut)] + let mut a = __tco_loop_a; + #[allow(unused_mut)] + let mut b = __tco_loop_b; + if ((a.clone().len() as i64) == 0) { + break Ordering::Equal; + } else { + match a.clone().first().cloned() { + Some(ac) => match b.clone().first().cloned() { + Some(bc) => { + if (ac.clone() < bc.clone()) { + break Ordering::Less; + } else { + if (ac.clone() > bc.clone()) { + break Ordering::Greater; + } else { + { + let __tco_0 = Rc::new(v1_rt::list_skip(&a, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&b, 1)); + __tco_loop_a = __tco_0; + __tco_loop_b = __tco_1; + continue; + } + } + } + } + std::option::Option::None => { + break Ordering::Greater; + } + }, + std::option::Option::None => { + break Ordering::Less; + } + } + } + } } pub fn semver_compare_identifier(a: Rc, b: Rc) -> Ordering { match (*a.clone()).clone() { - SemVerIdentifier::SemVerNumericIdentifier { value: av, .. } => match (*b.clone()).clone() { - SemVerIdentifier::SemVerNumericIdentifier { value: bv, .. } => { - semver_compare_non_negative_int(av.clone(), bv.clone()) + SemVerIdentifier::SemVerNumericIdentifier { digits: ad, .. } => { + match (*b.clone()).clone() { + SemVerIdentifier::SemVerNumericIdentifier { digits: bd, .. } => { + semver_compare_numeric_digits(ad.clone(), bd.clone()) + } + SemVerIdentifier::SemVerAlphanumericIdentifier { label: _, .. } => Ordering::Less, } - SemVerIdentifier::SemVerAlphanumericIdentifier { label: _, .. } => Ordering::Less, - }, + } SemVerIdentifier::SemVerAlphanumericIdentifier { label: al, .. } => { match (*b.clone()).clone() { - SemVerIdentifier::SemVerNumericIdentifier { value: _, .. } => Ordering::Greater, + SemVerIdentifier::SemVerNumericIdentifier { digits: _, .. } => Ordering::Greater, SemVerIdentifier::SemVerAlphanumericIdentifier { label: bl, .. } => { if (al.clone() < bl.clone()) { Ordering::Less @@ -107,12 +212,8 @@ pub fn semver_compare_identifiers( Some(ah) => match b.clone().first().cloned() { Some(bh) => match semver_compare_identifier(ah.clone(), bh.clone()) { Ordering::Equal => { - let __tco_0 = Rc::new( - a.iter().cloned().skip(1 as usize).collect::>(), - ); - let __tco_1 = Rc::new( - b.iter().cloned().skip(1 as usize).collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&a, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&b, 1)); __tco_loop_a = __tco_0; __tco_loop_b = __tco_1; continue; @@ -155,20 +256,18 @@ pub fn semver_compare_pre_release( } pub fn semver_compare(a: Rc, b: Rc) -> Ordering { - match semver_compare_non_negative_int(a.major.clone(), b.major.clone()) { - Ordering::Equal => { - match semver_compare_non_negative_int(a.minor.clone(), b.minor.clone()) { - Ordering::Equal => { - match semver_compare_non_negative_int(a.patch.clone(), b.patch.clone()) { - Ordering::Equal => { - semver_compare_pre_release(a.pre_release.clone(), b.pre_release.clone()) - } - other => other.clone(), + match semver_compare_numeric_digits(a.major.clone(), b.major.clone()) { + Ordering::Equal => match semver_compare_numeric_digits(a.minor.clone(), b.minor.clone()) { + Ordering::Equal => { + match semver_compare_numeric_digits(a.patch.clone(), b.patch.clone()) { + Ordering::Equal => { + semver_compare_pre_release(a.pre_release.clone(), b.pre_release.clone()) } + other => other.clone(), } - other => other.clone(), } - } + other => other.clone(), + }, other => other.clone(), } } @@ -190,13 +289,21 @@ pub fn semver_core_of_dotted(text: String) -> Option> { std::option::Option::None => std::option::Option::None, Some(minor) => { if ((parts.clone().len() as i64) == 2) { - Some(Rc::new(SemVerVersion { - major: major.clone(), - minor: minor.clone(), - patch: 0, - pre_release: Rc::new(vec![]), - build: Rc::new(vec![]), - })) + match semver_numeric_field(Rc::new( + "0".to_string() + .chars() + .map(|c| c as i64) + .collect::>(), + )) { + std::option::Option::None => std::option::Option::None, + Some(zero) => Some(Rc::new(SemVerVersion { + major: major.clone(), + minor: minor.clone(), + patch: zero.clone(), + pre_release: Rc::new(vec![]), + build: Rc::new(vec![]), + })), + } } else { match semver_dotted_part(parts.clone(), 2) { std::option::Option::None => std::option::Option::None, @@ -216,19 +323,12 @@ pub fn semver_core_of_dotted(text: String) -> Option> { } } -pub fn semver_dotted_part(parts: Rc>, index: i64) -> Option { - match parts - .clone() - .iter() - .cloned() - .skip(index.clone() as usize) - .next() - { +pub fn semver_dotted_part(parts: Rc>, index: i64) -> Option> { + match parts.clone().get((index.clone()) as usize).cloned() { std::option::Option::None => std::option::Option::None, - Some(w) => match v1_rt::parse_int(w.clone()) { - std::option::Option::None => std::option::Option::None, - Some(n) => crate::std_checked_arithmetic::checked_int_to_nat(n.clone()), - }, + Some(w) => semver_numeric_field(Rc::new( + w.clone().chars().map(|c| c as i64).collect::>(), + )), } } @@ -245,7 +345,9 @@ pub fn semver_minimum_of_constraint(constraint: NonEmptyStr) -> Option) -> String { match (*id.clone()).clone() { - SemVerIdentifier::SemVerNumericIdentifier { value: v, .. } => format!("{}", v.clone()), + SemVerIdentifier::SemVerNumericIdentifier { digits: d, .. } => { + semver_label_from_code_points(d.digits()) + } SemVerIdentifier::SemVerAlphanumericIdentifier { label: s, .. } => s.clone(), } } @@ -264,12 +366,15 @@ pub fn semver_identifiers_label(ids: Rc>>) -> String { pub fn semver_version_label(v: Rc) -> String { { let core = v1_rt::concat( - (v.major.clone()).to_string(), + semver_label_from_code_points(v.major.clone().digits()), v1_rt::concat( ".".to_string(), v1_rt::concat( - (v.minor.clone()).to_string(), - v1_rt::concat(".".to_string(), (v.patch.clone()).to_string()), + semver_label_from_code_points(v.minor.clone().digits()), + v1_rt::concat( + ".".to_string(), + semver_label_from_code_points(v.patch.clone().digits()), + ), ), ), ); @@ -299,15 +404,320 @@ pub fn semver_to_version_identity(v: Rc) -> NonEmptyStr { semver_version_label(v.clone()) } -pub fn semver_identity_compare(a: NonEmptyStr, b: NonEmptyStr) -> Ordering { - if (a.clone() == b.clone()) { - Ordering::Equal +pub fn semver_identity_compare(a: NonEmptyStr, b: NonEmptyStr) -> Rc { + match semver_identity_parse(a.clone()) { + Some(av) => match semver_identity_parse(b.clone()) { + Some(bv) => Rc::new(VersionComparison::VersionCompared { + ordering: semver_compare(av.clone(), bv.clone()), +}), + std::option::Option::None => Rc::new(VersionComparison::VersionComparisonRefused { + side: ComparisonSide::RightOperand, + identity: b.clone(), + cause: "label does not satisfy the SemVer 2.0.0 version grammar, so section 11 precedence is undefined for it".to_string(), +}), +}, + std::option::Option::None => Rc::new(VersionComparison::VersionComparisonRefused { + side: ComparisonSide::LeftOperand, + identity: a.clone(), + cause: "label does not satisfy the SemVer 2.0.0 version grammar, so section 11 precedence is undefined for it".to_string(), +}), +} +} + +pub fn semver_identity_code_points(label: NonEmptyStr) -> Rc> { + Rc::new(label.clone().chars().map(|c| c as i64).collect::>()) +} + +pub fn semver_is_ascii_digit(cp: i64) -> bool { + ((cp.clone() >= 48) && (cp.clone() <= 57)) +} + +pub fn semver_is_ascii_letter(cp: i64) -> bool { + (((cp.clone() >= 65) && (cp.clone() <= 90)) || ((cp.clone() >= 97) && (cp.clone() <= 122))) +} + +pub fn semver_is_identifier_char(cp: i64) -> bool { + ((semver_is_ascii_digit(cp.clone()) || semver_is_ascii_letter(cp.clone())) + || (cp.clone() == 45)) +} + +pub fn semver_index_of(cp: i64, cps: Rc>) -> Option { + stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { + if ((cps.clone().len() as i64) == 0) { + std::option::Option::None + } else { + match cps.clone().first().cloned() { + Some(h) => { + if (h.clone() == cp.clone()) { + Some(0) + } else { + match semver_index_of( + cp.clone(), + Rc::new(v1_rt::list_skip(&cps.clone(), 1)), + ) { + Some(k) => Some(v1_rt::int_add(k.clone(), 1)), + std::option::Option::None => std::option::Option::None, + } + } + } + std::option::Option::None => std::option::Option::None, + } + } + }) +} + +pub fn semver_split_on( + mut __tco_loop_cp: i64, + mut __tco_loop_cps: Rc>, + mut __tco_loop_acc: Rc>>>, +) -> Rc>>> { + loop { + #[allow(unused_mut)] + let mut cp = __tco_loop_cp; + #[allow(unused_mut)] + let mut cps = __tco_loop_cps; + #[allow(unused_mut)] + let mut acc = __tco_loop_acc; + match semver_index_of(cp.clone(), cps.clone()) { + Some(i) => { + let __tco_0 = cp; + let __tco_1 = Rc::new(v1_rt::list_skip(&cps.clone(), v1_rt::int_add(i.clone(), 1))); + let __tco_2 = v1_rt::concat( + acc, + Rc::new(vec![Rc::new(v1_rt::list_take(&cps.clone(), i.clone()))]), + ); + __tco_loop_cp = __tco_0; + __tco_loop_cps = __tco_1; + __tco_loop_acc = __tco_2; + continue; + } + std::option::Option::None => { + break v1_rt::concat(acc.clone(), Rc::new(vec![cps.clone()])); + } + } + } +} + +pub fn semver_all_digits(cps: Rc>) -> bool { + (((cps.clone().len() as i64) > 0) && { + let mut __all = true; + for cp in cps.iter().cloned() { + if !(semver_is_ascii_digit(cp.clone())) { + __all = false; + break; + } + } + __all + }) +} + +pub fn semver_numeric_run_without_leading_zero(cps: Rc>) -> bool { + if !semver_all_digits(cps.clone()) { + false } else { - if (a.clone() < b.clone()) { - Ordering::Less + match cps.clone().first().cloned() { + Some(h) => (((cps.clone().len() as i64) == 1) || (h.clone() != 48)), + std::option::Option::None => false, + } + } +} + +pub fn semver_label_from_code_points(cps: Rc>) -> String { + Rc::new({ + let mut __result = Vec::new(); + for cp in cps.iter().cloned() { + __result.push(v1_rt::from_code_point(cp.clone())); + } + __result + }) + .join(&"".to_string()) +} + +pub fn semver_core_fields_parse( + cps: Rc>, + remaining: i64, +) -> Option>>> { + stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { + if (remaining.clone() == 0) { + if ((cps.clone().len() as i64) == 0) { + Some(Rc::new(vec![])) + } else { + std::option::Option::None + } } else { - Ordering::Greater + if (remaining.clone() == 1) { + match semver_numeric_field(cps.clone()) { + std::option::Option::None => std::option::Option::None, + Some(v) => Some(Rc::new(vec![v.clone()])), + } + } else { + match semver_index_of(46, cps.clone()) { + Some(i) => match semver_numeric_field(Rc::new(v1_rt::list_take( + &cps.clone(), + i.clone(), + ))) { + std::option::Option::None => std::option::Option::None, + Some(v) => match semver_core_fields_parse( + Rc::new(v1_rt::list_skip(&cps.clone(), v1_rt::int_add(i.clone(), 1))), + v1_rt::int_sub(remaining.clone(), 1), + ) { + std::option::Option::None => std::option::Option::None, + Some(rest) => { + Some(v1_rt::concat(Rc::new(vec![v.clone()]), rest.clone())) + } + }, + }, + std::option::Option::None => std::option::Option::None, + } + } } + }) +} + +pub fn semver_core_parse(cps: Rc>) -> Option> { + match semver_core_fields_parse(cps.clone(), 3) { + std::option::Option::None => std::option::Option::None, + Some(fields) => { + if ((fields.clone().len() as i64) != 3) { + std::option::Option::None + } else { + match fields.clone().first().cloned() { + std::option::Option::None => std::option::Option::None, + Some(major) => match fields.clone().get((1) as usize).cloned() { + std::option::Option::None => std::option::Option::None, + Some(minor) => match fields.clone().get((2) as usize).cloned() { + std::option::Option::None => std::option::Option::None, + Some(patch) => Some(Rc::new(SemVerVersion { + major: major.clone(), + minor: minor.clone(), + patch: patch.clone(), + pre_release: Rc::new(vec![]), + build: Rc::new(vec![]), + })), + }, + }, + } + } + } + } +} + +pub fn semver_identifier_parse( + cps: Rc>, + strict_numeric: bool, +) -> Option> { + if ((cps.clone().len() as i64) == 0) { + std::option::Option::None + } else { + if !{ + let mut __all = true; + for cp in cps.iter().cloned() { + if !(semver_is_identifier_char(cp.clone())) { + __all = false; + break; + } + } + __all + } { + std::option::Option::None + } else { + if semver_numeric_run_without_leading_zero(cps.clone()) { + match semver_numeric_field(cps.clone()) { + Some(f) => Some(Rc::new(SemVerIdentifier::SemVerNumericIdentifier { + digits: f.clone(), + })), + std::option::Option::None => std::option::Option::None, + } + } else { + if (strict_numeric.clone() && semver_all_digits(cps.clone())) { + std::option::Option::None + } else { + Some(Rc::new(SemVerIdentifier::SemVerAlphanumericIdentifier { + label: semver_label_from_code_points(cps.clone()), + })) + } + } + } + } +} + +pub fn semver_identifier_list_parse( + segments: Rc>>>, + strict_numeric: bool, +) -> Option>>> { + segments + .iter() + .cloned() + .fold(Some(Rc::new(vec![])), |st: _, seg: Rc>| { + match st.clone() { + std::option::Option::None => std::option::Option::None, + Some(acc) => match semver_identifier_parse(seg.clone(), strict_numeric.clone()) { + std::option::Option::None => std::option::Option::None, + Some(id) => Some(v1_rt::concat(acc.clone(), Rc::new(vec![id.clone()]))), + }, + } + }) +} + +pub fn semver_pre_release_parse(cps: Rc>) -> Option>>> { + semver_identifier_list_parse(semver_split_on(46, cps.clone(), Rc::new(vec![])), true) +} + +pub fn semver_build_parse(cps: Rc>) -> Option>>> { + semver_identifier_list_parse(semver_split_on(46, cps.clone(), Rc::new(vec![])), false) +} + +pub fn semver_identity_head_parse( + cps: Rc>, + build: Rc>>, +) -> Option> { + match semver_index_of(45, cps.clone()) { + Some(pi) => match semver_core_parse(Rc::new(v1_rt::list_take(&cps.clone(), pi.clone()))) { + std::option::Option::None => std::option::Option::None, + Some(core) => match semver_pre_release_parse(Rc::new(v1_rt::list_skip( + &cps.clone(), + v1_rt::int_add(pi.clone(), 1), + ))) { + std::option::Option::None => std::option::Option::None, + Some(pre) => Some(Rc::new(SemVerVersion { + major: core.major.clone(), + minor: core.minor.clone(), + patch: core.patch.clone(), + pre_release: pre.clone(), + build: build.clone(), + })), + }, + }, + std::option::Option::None => match semver_core_parse(cps.clone()) { + std::option::Option::None => std::option::Option::None, + Some(core) => Some(Rc::new(SemVerVersion { + major: core.major.clone(), + minor: core.minor.clone(), + patch: core.patch.clone(), + pre_release: Rc::new(vec![]), + build: build.clone(), + })), + }, + } +} + +pub fn semver_identity_parse(label: NonEmptyStr) -> Option> { + semver_version_parse(semver_identity_code_points(label.clone())) +} + +pub fn semver_version_parse(cps: Rc>) -> Option> { + match semver_index_of(43, cps.clone()) { + Some(bi) => match semver_build_parse(Rc::new(v1_rt::list_skip( + &cps.clone(), + v1_rt::int_add(bi.clone(), 1), + ))) { + std::option::Option::None => std::option::Option::None, + Some(build) => semver_identity_head_parse( + Rc::new(v1_rt::list_take(&cps.clone(), bi.clone())), + build.clone(), + ), + }, + std::option::Option::None => semver_identity_head_parse(cps.clone(), Rc::new(vec![])), } } diff --git a/src/v1/stage0/src/gunbc_cli_dispatch_generated.rs b/src/v1/stage0/src/gunbc_cli_dispatch_generated.rs index d837f9e9a8d..4622202c860 100644 --- a/src/v1/stage0/src/gunbc_cli_dispatch_generated.rs +++ b/src/v1/stage0/src/gunbc_cli_dispatch_generated.rs @@ -62,7 +62,7 @@ pub enum Commands { #[arg(long)] claim_run: bool, /// Named argument for the entry function, repeatable: `--arg name=value`. - /// Values enter as String; a missing `=` refuses rather than guessing. + /// Bound against the parameter's resolved type identity: kernel String, Int, and Bool, and std.types NonEmptyStr (String where string_non_empty). Other types refuse. #[arg(long = "arg")] args: Vec, }, diff --git a/src/v1/stage0/src/gunbc_cli_dispatch_surface.rs b/src/v1/stage0/src/gunbc_cli_dispatch_surface.rs index 74b95589a8f..d5f31f4a167 100644 --- a/src/v1/stage0/src/gunbc_cli_dispatch_surface.rs +++ b/src/v1/stage0/src/gunbc_cli_dispatch_surface.rs @@ -563,7 +563,7 @@ pub fn gunbc_cli_subcommands() -> Rc>> { text_default: std::option::Option::None, }), arity: CliOptionArity::CliRepeated, - doc: Rc::new(vec!["Named argument for the entry function, repeatable: `--arg name=value`.".to_string(), "Values enter as String; a missing `=` refuses rather than guessing.".to_string()]), + doc: Rc::new(vec!["Named argument for the entry function, repeatable: `--arg name=value`.".to_string(), "Bound against the parameter's resolved type identity: kernel String, Int, and Bool, and std.types NonEmptyStr (String where string_non_empty). Other types refuse.".to_string()]), emission: CliSurfaceEmission::CarriedByGeneratedDispatch, })]), realization: Rc::new(CliArmRealization::CliDelegatesToHostFn { diff --git a/src/v1/stage0/src/gunbc_rust_source_type_bindings.rs b/src/v1/stage0/src/gunbc_rust_source_type_bindings.rs index 2627adc50ca..98256d0f36e 100644 --- a/src/v1/stage0/src/gunbc_rust_source_type_bindings.rs +++ b/src/v1/stage0/src/gunbc_rust_source_type_bindings.rs @@ -135,7 +135,7 @@ pub fn checkpoint_row_migration_rows() -> Rc>> { }), Rc::new(CheckpointRowMigration { dag_name: "String".to_string(), disposition: Rc::new(CheckpointRowDisposition::ProvenUniqueKernelBinding { - proof: "The kernel string_type is minted by v1.compiler.types for every string literal and carries no declaration; both .dag declarations of the spelling (v2.std.text, std.string_type) are gated structural by v1.compiler.coercion structural_declaration_modules_for and never reach this row. THE VERDICT STANDS AND ITS ORIGINAL ARGUMENT WAS EXACTLY INVERTED, corrected here rather than softened because the inverted form would justify the wrong things if reused. It read: the bare spelling cannot be captured because the only import that could shadow it binds the SAME token, so in a module importing v2.std.text String every `String` reference IS the text carrier and the kernel spelling is unreachable there. The opposite is true. KERNEL NAMES ARE NEVER OVERRIDDEN BY IMPORTS (v1.compiler.infer direct_import_export_precedence_note, overlay_skips_kernel_name), so in such a module the import does not shadow at all and a bare `String` is ALWAYS the kernel -- the spelling the old argument called unreachable is the only one you reach. Three routes exist and all three are sound for this row: a bare reference in an importing module resolves to the kernel and IS this row's binding; a bare reference INSIDE v2.std.text resolves LOCALLY (locals beat kernel) and is gated structural, never reaching the row; and a QUALIFIED v2.std.text.String resolves through the alias to the dotted name std.algebra.FreeMonoid, which matches no bare row. Symbol was captured precisely because its token differed from its spelling, and that clause survives unchanged. ESTABLISHED BY EXECUTION, not by reading precedence -- AND THE SUBJECT NAMED HERE IS CORRECTED: this sentence previously said the census was run over A FIXTURE MODULE importing both v2.std.text String and v2.std.node Node. IT WAS NOT. The receipts carry no fixture rows at all; the subjects were LIVE CORPUS MODULES, and naming a subject that was not run is the same citation defect this row exists to correct. What v1.tests.claim.carrier_realization_census actually reports: in std.algebra, a module that imports the spelling, a bare String carries the kernel identity at declaration_field AND at fn_signature_param; inside v2.std.text the same spelling carries its own declaring module and diverges with exact identity, which is route 2 executed rather than argued; and v2.std.node Node carries its real declaring module at both position kinds as the positive control. RE-DERIVATION IS POSITION-KIND SENSITIVE AND THE OBVIOUS INSTRUCTION IS A TRAP. Running that census over a closure re-derives routes 1 and 2 soundly. It does NOT re-derive route 3: at fn_signature_param the census resolves a QUALIFIED v2.std.text.String through the alias onto the alias declaration's own name, which is the spelling String, and attributes it to -- confidently naming the very carrier the qualification was written to move away from. That is measured by vivid-deer-102 and filed as recurring failure mode alias_resolution_collides_with_kernel_spelling (gunbc#9939); the same instrument is correct for qualified aliases at declaration_field. ROUTE 3 THEREFORE RESTS ON STRUCTURAL RESOLUTION PLUS A RUSTC MEASUREMENT -- the emitted crate carries an E0308 on the bare arm and not on the qualified one -- and explicitly NOT on this census, which cannot answer at that position. The instrument is named rather than its rows transcribed (DESIGN section 6). SINCE gunbc#12760 THE KERNEL HOST TEXT HAS A DECLARATION: v2.std.node String, opaque, with its own exact row above (RustStdString). So the kernel spelling now has one declaration, and this bare row and that exact row render the same carrier, std::string::String; the uniqueness this row proves is unchanged.".to_string(), + proof: "The kernel string_type is minted by v1.compiler.types for every string literal and carries no declaration; both .dag declarations of the spelling (v2.std.text, std.string_type) are gated structural by v1.compiler.coercion structural_declaration_modules_for and never reach this row. THE VERDICT STANDS AND ITS ORIGINAL ARGUMENT WAS EXACTLY INVERTED, corrected here rather than softened because the inverted form would justify the wrong things if reused. It read: the bare spelling cannot be captured because the only import that could shadow it binds the SAME token, so in a module importing v2.std.text String every `String` reference IS the text carrier and the kernel spelling is unreachable there. The opposite is true. KERNEL NAMES ARE NEVER OVERRIDDEN BY IMPORTS (v1.compiler.infer direct_import_export_precedence_note; v1.compiler.infer_env overlay_skips_kernel_name), so in such a module the import does not shadow at all and a bare `String` is ALWAYS the kernel -- the spelling the old argument called unreachable is the only one you reach. Three routes exist and all three are sound for this row: a bare reference in an importing module resolves to the kernel and IS this row's binding; a bare reference INSIDE v2.std.text resolves LOCALLY (locals beat kernel) and is gated structural, never reaching the row; and a QUALIFIED v2.std.text.String resolves through the alias to the dotted name std.algebra.FreeMonoid, which matches no bare row. Symbol was captured precisely because its token differed from its spelling, and that clause survives unchanged. ESTABLISHED BY EXECUTION, not by reading precedence -- AND THE SUBJECT NAMED HERE IS CORRECTED: this sentence previously said the census was run over A FIXTURE MODULE importing both v2.std.text String and v2.std.node Node. IT WAS NOT. The receipts carry no fixture rows at all; the subjects were LIVE CORPUS MODULES, and naming a subject that was not run is the same citation defect this row exists to correct. What v1.tests.claim.carrier_realization_census actually reports: in std.algebra, a module that imports the spelling, a bare String carries the kernel identity at declaration_field AND at fn_signature_param; inside v2.std.text the same spelling carries its own declaring module and diverges with exact identity, which is route 2 executed rather than argued; and v2.std.node Node carries its real declaring module at both position kinds as the positive control. RE-DERIVATION IS POSITION-KIND SENSITIVE AND THE OBVIOUS INSTRUCTION IS A TRAP. Running that census over a closure re-derives routes 1 and 2 soundly. It does NOT re-derive route 3: at fn_signature_param the census resolves a QUALIFIED v2.std.text.String through the alias onto the alias declaration's own name, which is the spelling String, and attributes it to -- confidently naming the very carrier the qualification was written to move away from. That is measured by vivid-deer-102 and filed as recurring failure mode alias_resolution_collides_with_kernel_spelling (gunbc#9939); the same instrument is correct for qualified aliases at declaration_field. ROUTE 3 THEREFORE RESTS ON STRUCTURAL RESOLUTION PLUS A RUSTC MEASUREMENT -- the emitted crate carries an E0308 on the bare arm and not on the qualified one -- and explicitly NOT on this census, which cannot answer at that position. The instrument is named rather than its rows transcribed (DESIGN section 6). SINCE gunbc#12760 THE KERNEL HOST TEXT HAS A DECLARATION: v2.std.node String, opaque, with its own exact row above (RustStdString). So the kernel spelling now has one declaration, and this bare row and that exact row render the same carrier, std::string::String; the uniqueness this row proves is unchanged.".to_string(), }), }), Rc::new(CheckpointRowMigration { dag_name: "Int".to_string(), diff --git a/src/v1/stage0/src/main.rs b/src/v1/stage0/src/main.rs index afe2583a7e3..f2881923667 100644 --- a/src/v1/stage0/src/main.rs +++ b/src/v1/stage0/src/main.rs @@ -73,7 +73,7 @@ enum RetainedCommands { #[arg(long)] claim_run: bool, /// Named argument for the entry function, repeatable: `--arg name=value`. - /// Values enter as String; a missing `=` refuses rather than guessing. + /// Bound against the parameter's resolved type identity: kernel String, Int, and Bool, and std.types NonEmptyStr (String where string_non_empty). Other types refuse. #[arg(long = "arg")] args: Vec, }, @@ -147,13 +147,16 @@ fn is_cargo_target_output_dir(parent: &std::path::Path, child: &std::path::Path) /// RUNG, HONESTLY: this restores a guard, it does not prove a live bug -- `find target -name /// '*.dag'` returns 0 in this worktree today. The hazard is evidenced by the deleted code's own /// comment naming its case: a corpus copy under `target/func_env_semantic_baseline_corpus/dag/**`. -fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) { - cli_run::derived_row_roster::ensure_if_row_dir_or_panic(dir); +fn collect_dag_files( + dir: &std::path::Path, + files: &mut Vec, +) { let mut entries: Vec<_> = std::fs::read_dir(dir) .unwrap_or_else(|e| panic!("failed to read dir {:?}: {}", dir, e)) .map(|e| e.unwrap_or_else(|e| panic!("failed to read dir entry in {:?}: {}", dir, e))) .collect(); entries.sort_by_key(|e| e.file_name()); + let mut here = Vec::new(); for entry in entries { let path = entry.path(); if path.is_dir() { @@ -162,9 +165,12 @@ fn collect_dag_files(dir: &std::path::Path, files: &mut Vec) } collect_dag_files(&path, files); } else if path.extension().map(|e| e == "dag").unwrap_or(false) { - files.push(path); + here.push(path); } } + files.extend( + cli_run::derived_row_roster::acquire_dir_files(dir, here).unwrap_or_else(|e| panic!("{e}")), + ); } #[derive(Clone, Copy, PartialEq, Eq)] @@ -656,8 +662,10 @@ fn retained_dispatch(command: RetainedCommands, dry_run: bool) -> ! { let mut dag_paths = Vec::new(); collect_dag_files(std::path::Path::new(&dir), &mut dag_paths); let mut sources = Vec::new(); - for path in &dag_paths { - let content = std::fs::read_to_string(path) + for source in &dag_paths { + let path = source.path(); + let content = source + .read() .unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e)); let filename = path.file_name().unwrap().to_string_lossy().to_string(); sources.push(Rc::new(v1_compiler_compile::SourceFile { @@ -1048,21 +1056,8 @@ mod tests { /// driver's subject. /// A missing `=` REFUSES with the offending spec rather than guessing a positional — /// the deleted handler's own rule, kept because it is right, not because it was there. -fn decode_run_args( - raw: &[String], -) -> Result, v1_compiler::v1_interpreter::Value)>, String> { - raw.iter() - .map(|spec| match spec.split_once('=') { - Some((name, value)) if !name.is_empty() => Ok(( - Some(name.to_string()), - v1_compiler::v1_interpreter::str_value(value), - )), - _ => Err(format!( - "--arg expects name=value, got `{spec}` (a missing `=` is refused, not \ - interpreted as a positional argument)" - )), - }) - .collect() +fn decode_run_args(raw: &[String]) -> Result, String> { + cli_run::parse_run_arg_specs(raw) } /// `gunbc run` -- argv -> modeled intent -> the RETAINED resolve/eval engine -> exit code. @@ -1422,11 +1417,19 @@ fn run_one_function( ctx: &v1_compiler::v1_interpreter::InterpContext, function: &str, entry_file: &str, - run_args: &[(Option, v1_compiler::v1_interpreter::Value)], + run_args: &[(String, String)], claim_run: bool, ) -> Verdict { - match v1_compiler::v1_interpreter::run_in_context_with_args(ctx, function, run_args, !claim_run) - { + let bound = match cli_run::bind_run_args_for_entry(ctx, function, run_args) { + Ok(bound) => bound, + Err(message) => { + return Verdict { + status: 2, + message: Some(format!("error: {message}")), + }; + } + }; + match v1_compiler::v1_interpreter::run_in_context_with_args(ctx, function, &bound, !claim_run) { // A claim run's Bool is the verdict: false is a FAILED claim, exit 1. Outside a // claim run a Bool is an ordinary value and says nothing about success. Ok(v1_compiler::v1_interpreter::Value::Bool(false)) if claim_run => Verdict { diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 3f38f189c31..bc9eadf2827 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1218,17 +1218,10 @@ pub fn host_budget_unreadable_reason() -> String { ) } -/// Resolve the host budget from OBSERVATIONS, so every arm — including the refusal — is -/// reachable from a test on any machine. `read_host_budget_resolution` is this function -/// applied to the real reads; nothing else composes the precedence. -/// -/// The effective planning ceiling is the minimum of the operator request and every observed -/// applicable cgroup line. An operator request alone is `DeclaredUnverified`: an integer in an -/// environment variable constrains no allocation and is not evidence of executor provisioning. -/// There is no meminfo arm: MemAvailable and MemTotal describe a MACHINE, and -/// on a kernel that can express a private limit, substituting one for the limit this process -/// failed to read is DESIGN §5's absorbing fallback (answering with a superset). Authority: -/// `gunbc.host_budget_source` `host_budget_source_admissible_as_bound_on_kernel`. +/// Typed view of `v1_rt::resolve_host_budget_join`. That function is the one composer +/// (`gunbc.host_budget_source`); this maps its result onto `HostBudgetResolution` so +/// existing governor consumers keep a typed source. Tests plant observations here +/// and still exercise the runtime join. pub fn resolve_host_budget( env_override: Option, cgroup_high: Option<(String, u64)>, @@ -1236,99 +1229,94 @@ pub fn resolve_host_budget( cgroup_v1_limit: Option<(String, CgroupV1MemoryLimitValue)>, darwin_physical: Option, ) -> HostBudgetResolution { - // A v1 memory hierarchy holds this process but its limit could not be read: that limit may - // be the tightest one, so no other observation may stand in for it (DESIGN §5). - let cgroup_v1_limit = match cgroup_v1_limit { - Some((dir, CgroupV1MemoryLimitValue::Unparseable(body))) => { - return HostBudgetResolution::Unreadable { - reason: format!( - "cgroup v1 memory hierarchy at {dir} holds this process but its \ - hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ - tightest cannot be replaced by another reading" - ), - }; - } - Some((dir, CgroupV1MemoryLimitValue::Limited(bytes))) => Some((dir, bytes)), - Some((_, CgroupV1MemoryLimitValue::Unlimited)) | None => None, - }; - // Every observed process-scoped line is a candidate; the tightest one is the planning - // ceiling. On a hybrid host the unified hierarchy carries no memory files, so the v2 and - // v1 readings do not normally coexist; when they do, the minimum is still the honest bound. - let observation = [ - cgroup_high.map(|(cgroup_dir, b)| (HostBudgetSource::CgroupMemoryHigh { cgroup_dir }, b)), - cgroup_max.map(|(cgroup_dir, b)| (HostBudgetSource::CgroupMemoryMax { cgroup_dir }, b)), - cgroup_v1_limit.map(|(cgroup_dir, b)| { - ( - HostBudgetSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, - b, - ) - }), - ] - .into_iter() - .flatten() - .fold(None::<(HostBudgetSource, u64)>, |best, cand| match best { - Some(cur) if cur.1 <= cand.1 => Some(cur), - _ => Some(cand), + let v1 = cgroup_v1_limit.map(|(dir, v)| { + ( + dir, + match v { + CgroupV1MemoryLimitValue::Limited(bytes) => { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) + } + CgroupV1MemoryLimitValue::Unlimited => crate::v1_rt::HostBudgetCgroupV1::Unlimited, + CgroupV1MemoryLimitValue::Unparseable(body) => { + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) + } + }, + ) }); - if let Some((source, observed_bytes)) = observation { - return HostBudgetResolution::Resolved { - effective_bytes: env_override - .map(|requested| requested.min(observed_bytes)) - .unwrap_or(observed_bytes), - requested_bytes: env_override, + match crate::v1_rt::resolve_host_budget_join( + env_override, + cgroup_high, + cgroup_max, + v1, + darwin_physical, + ) { + crate::v1_rt::HostBudgetJoin::Resolved { + effective_bytes, + requested_bytes, + source, + observed_bytes, + } => HostBudgetResolution::Resolved { + effective_bytes, + requested_bytes, observation: HostBudgetObservation { - source, + source: match source { + crate::v1_rt::HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => { + HostBudgetSource::CgroupMemoryHigh { cgroup_dir } + } + crate::v1_rt::HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => { + HostBudgetSource::CgroupMemoryMax { cgroup_dir } + } + crate::v1_rt::HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { + cgroup_dir, + } => HostBudgetSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, + crate::v1_rt::HostBudgetJoinSource::DarwinPhysicalMemory => { + HostBudgetSource::DarwinPhysicalMemory + } + }, bytes: observed_bytes, }, - }; - } - // Darwin only. `darwin_physical_memory_bytes` is `None` on every other target, so this - // arm cannot be reached on a kernel that has cgroups — which is precisely the wall - // `host_budget_source_admissible_as_bound_on_kernel` states: a host-shared reading may - // serve as the budget only where no private-limit mechanism exists. - if let Some(bytes) = darwin_physical { - return HostBudgetResolution::Resolved { - effective_bytes: env_override - .map(|requested| requested.min(bytes)) - .unwrap_or(bytes), - requested_bytes: env_override, - observation: HostBudgetObservation { - source: HostBudgetSource::DarwinPhysicalMemory, - bytes, - }, - }; - } - if let Some(requested_bytes) = env_override { - return HostBudgetResolution::DeclaredUnverified { + }, + crate::v1_rt::HostBudgetJoin::DeclaredUnverified { requested_bytes, - reason: "no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string(), - }; - } - HostBudgetResolution::Unreadable { - reason: host_budget_unreadable_reason(), + reason, + } => HostBudgetResolution::DeclaredUnverified { + requested_bytes, + reason, + }, + crate::v1_rt::HostBudgetJoin::Unreadable { reason } => { + HostBudgetResolution::Unreadable { reason } + } } } -/// The host memory planning ceiling, as a typed resolution. It does not cap RSS. Single authority shared -/// by the MemoryGovernor (which SCHEDULES against it), the typed-module cache cap (which -/// bounds an estimated ENTRY COUNT with it) and the P4 realize advisory (which PREDICTS against it) — no -/// consumer may re-read a partial version of this precedence (§3 single authority). +/// The host memory planning ceiling, as a typed resolution. Live observations and +/// precedence are `v1_rt::resolve_host_budget_join`; this is the typed wrapper. pub fn read_host_budget_resolution() -> HostBudgetResolution { let env_override = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") .ok() .and_then(|s| s.trim().parse::().ok()); - let cgroup_high = binding_high_cgroup_dir().and_then(|dir| { - read_cgroup_u64(&dir, "memory.high").map(|v| (dir.display().to_string(), v)) - }); - let cgroup_max = binding_cap_cgroup_dir().and_then(|dir| { - read_cgroup_u64(&dir, "memory.max").map(|v| (dir.display().to_string(), v)) + let cgroup_high = crate::v1_rt::host_budget_tightest_cgroup("memory.high"); + let cgroup_max = crate::v1_rt::host_budget_tightest_cgroup("memory.max"); + let cgroup_v1_limit = crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) }); resolve_host_budget( env_override, cgroup_high, cgroup_max, - read_cgroup_v1_hierarchical_limit(), - darwin_physical_memory_bytes(), + cgroup_v1_limit, + crate::v1_rt::host_budget_darwin_physical(), ) } @@ -1387,29 +1375,8 @@ pub fn tightest_cgroup_dir_for(limit_file: &str) -> Option { /// `tightest_cgroup_dir_for` over supplied `/proc/self/cgroup` content and unified mount root, /// so a fixture hierarchy exercises the same walk. pub fn tightest_cgroup_dir_under(self_cg: &str, root: &Path, limit_file: &str) -> Option { - let rel = self_cg - .lines() - .find_map(|l| l.strip_prefix("0::")) - .map(|p| p.trim().trim_start_matches('/').to_string())?; - let mut dir = root.join(&rel); - let mut best: Option<(u64, PathBuf)> = None; - loop { - if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) { - let s = s.trim(); - if s != "max" { - if let Ok(v) = s.parse::() { - let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true); - if take { - best = Some((v, dir.clone())); - } - } - } - } - if dir == root || !dir.pop() { - break; - } - } - best.map(|(_, d)| d) + crate::v1_rt::host_budget_tightest_cgroup_under(self_cg, root, limit_file) + .map(|(dir, _)| PathBuf::from(dir)) } /// The process's own deepest (leaf) cgroup from `/proc/self/cgroup`. @@ -1453,41 +1420,13 @@ pub fn memory_pressure_some_avg10(content: &str) -> Option { /// mounted, the process has no line on it, or its path is outside the mounted subtree — each of /// which leaves the budget to the other sources or to `Unreadable`, never to a guess. pub fn cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { - let (mount_root, mount_point) = mountinfo.lines().find_map(|line| { - let fields: Vec<&str> = line.split(' ').collect(); - let dash = fields.iter().position(|f| *f == "-")?; - let fstype = fields.get(dash + 1)?; - let super_opts = fields.get(dash + 3)?; - if *fstype == "cgroup" && super_opts.split(',').any(|o| o == "memory") { - Some((fields.get(3)?.to_string(), fields.get(4)?.to_string())) - } else { - None - } - })?; - let path = self_cg.lines().find_map(|l| { - let mut parts = l.splitn(3, ':'); - let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?); - controllers - .split(',') - .any(|c| c == "memory") - .then(|| path.trim().to_string()) - })?; - let rel = if mount_root == "/" { - path.as_str() - } else { - let rest = path.strip_prefix(mount_root.as_str())?; - if !(rest.is_empty() || rest.starts_with('/')) { - return None; - } - rest - }; - Some(Path::new(&mount_point).join(rel.trim_start_matches('/'))) + crate::v1_rt::host_budget_cgroup_v1_memory_dir(self_cg, mountinfo).map(PathBuf::from) } /// Mirror of `extdeps.linux.cgroup_v1_memory` `cgroup_v1_unlimited_bytes`: the kernel's /// PAGE_COUNTER_MAX (LONG_MAX / PAGE_SIZE) reported in bytes, so the sentinel follows the page size. pub fn cgroup_v1_unlimited_bytes(page_size: u64) -> u64 { - (i64::MAX as u64 / page_size) * page_size + crate::v1_rt::host_budget_cgroup_v1_unlimited_bytes(page_size) } /// Mirror of `extdeps.linux.cgroup_v1_memory` `CgroupV1MemoryLimitValue`: three states, because @@ -1504,20 +1443,14 @@ pub fn cgroup_v1_hierarchical_limit_from_stat( memory_stat: &str, page_size: u64, ) -> CgroupV1MemoryLimitValue { - let hits: Vec<&str> = memory_stat - .lines() - .filter_map(|l| l.trim().strip_prefix("hierarchical_memory_limit ")) - .collect(); - let [body] = hits.as_slice() else { - return CgroupV1MemoryLimitValue::Unparseable(memory_stat.to_string()); - }; - match body.trim().parse::() { - Ok(n) if n < 0 => CgroupV1MemoryLimitValue::Unparseable(body.to_string()), - Ok(n) if n >= cgroup_v1_unlimited_bytes(page_size) as i128 => { - CgroupV1MemoryLimitValue::Unlimited + match crate::v1_rt::host_budget_cgroup_v1_from_stat(memory_stat, page_size) { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) } - Ok(n) => CgroupV1MemoryLimitValue::Limited(n as u64), - Err(_) => CgroupV1MemoryLimitValue::Unparseable(body.to_string()), } } @@ -1531,27 +1464,41 @@ pub fn cgroup_v1_hierarchical_limit_under( mountinfo: &str, page_size: u64, ) -> Option<(String, CgroupV1MemoryLimitValue)> { - let dir = cgroup_v1_memory_dir(self_cg, mountinfo)?; - let dir = fs_root.join(dir.strip_prefix("/").unwrap_or(&dir)); - let value = match std::fs::read_to_string(dir.join("memory.stat")) { - Ok(stat) => cgroup_v1_hierarchical_limit_from_stat(&stat, page_size), - Err(e) => CgroupV1MemoryLimitValue::Unparseable(format!("memory.stat: {e}")), - }; - Some((dir.display().to_string(), value)) + crate::v1_rt::host_budget_cgroup_v1_under(fs_root, self_cg, mountinfo, page_size).map( + |(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => { + CgroupV1MemoryLimitValue::Unlimited + } + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) + }, + ) } pub fn read_cgroup_v1_hierarchical_limit() -> Option<(String, CgroupV1MemoryLimitValue)> { - let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; - let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; - // SAFETY: sysconf has no preconditions. - let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; - if page_size <= 0 { - return Some(( - "/proc/self/mountinfo".to_string(), - CgroupV1MemoryLimitValue::Unparseable("sysconf(_SC_PAGESIZE) unreadable".to_string()), - )); - } - cgroup_v1_hierarchical_limit_under(Path::new("/"), &self_cg, &mountinfo, page_size as u64) + crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) + }) } pub fn read_cgroup_u64(dir: &Path, file: &str) -> Option { @@ -1575,32 +1522,8 @@ pub fn read_cgroup_raw(dir: &Path, file: &str) -> Option { /// /// Exists because the governor previously had NO source on Darwin and fell back to the most /// permissive cap it could name; macOS's memory facts were never asked for. -#[cfg(target_os = "macos")] -pub fn darwin_physical_memory_bytes() -> Option { - let name = c"hw.memsize"; - let mut value: u64 = 0; - let mut len: libc::size_t = std::mem::size_of::() as libc::size_t; - // SAFETY: `name` is a NUL-terminated literal, `value`/`len` are live locals sized to - // match, and `newp`/`newlen` are null/0 for a read-only query per sysctl(3). - let rc = unsafe { - libc::sysctlbyname( - name.as_ptr(), - (&mut value as *mut u64).cast::(), - &mut len, - std::ptr::null_mut(), - 0, - ) - }; - if rc == 0 && value > 0 { - Some(value) - } else { - None - } -} - -#[cfg(not(target_os = "macos"))] pub fn darwin_physical_memory_bytes() -> Option { - None + crate::v1_rt::host_budget_darwin_physical() } /// The bind request an executor may carry, read from `GUNBC_BIND_MEMORY_CGROUP_BYTES`. diff --git a/src/v1/stage0/src/namespace_baseline.rs b/src/v1/stage0/src/namespace_baseline.rs index 7484ba98df1..2608a225e9f 100644 --- a/src/v1/stage0/src/namespace_baseline.rs +++ b/src/v1/stage0/src/namespace_baseline.rs @@ -1390,10 +1390,11 @@ pub(crate) fn reconstruct_base_index( // A DERIVED ARTIFACT IS NEVER IN THE DIFF, SO IT MUST NOT BE INHERITED FROM THE HEAD. // The baseline is reconstructed by carrying every head record the diff did not touch and - // re-reading the rest from the base tree. `roster.dag` is gitignored and written on the read - // path, so the diff can never name it — and carrying it made the HEAD's roster stand as the - // BASE's. Its base side is not read from git either (the tree does not carry it); it is - // DERIVED from the base tree's row membership, below, by the same renderer the writer uses. + // re-reading the rest from the base tree. The roster module has no file -- source acquisition + // contributes it in memory -- so the diff can never name it, and carrying it made the HEAD's + // roster stand as the BASE's. Its base side is not read from git either (the tree does not carry + // it, unless the base predates the derived fold); it is DERIVED from the base tree's row + // membership, below, by the same renderer head acquisition uses. // // CARRYING UNTOUCHED HEAD RECORDS IS VALID ONLY WHILE THE GRAMMARS AGREE. The reconstruction // below keeps every head record the diff did not touch, which assumes an untouched FILE has an @@ -1531,7 +1532,7 @@ pub(crate) fn reconstruct_base_index( let _ = std::fs::remove_dir_all(&base_tree); // THE DERIVED ROSTER'S BASE SIDE, from the base tree's row membership. `base_paths` is the - // authoritative listing already in hand, so this asks the same question the writer asks of a + // authoritative listing already in hand, so this asks the same question head acquisition asks of a // directory. A base tree carrying no row files under that root has no roster module at all, // and `roster_from_path_listing` answers `None` rather than fabricating a present empty list. let base_path_refs: Vec<&str> = base_paths.iter().map(|p| p.as_str()).collect(); @@ -1547,7 +1548,7 @@ pub(crate) fn reconstruct_base_index( continue; } // Membership is selected by declared type, so the base side reads each listed row file's - // content at the base -- the same question the writer asks of the head's files. + // content at the base -- the same question head acquisition asks of the head's files. let content = match crate::cli_run::derived_row_roster::roster_from_path_listing( &record.rel_path, base_path_refs.iter().copied(), @@ -1561,7 +1562,7 @@ pub(crate) fn reconstruct_base_index( Err(reason) => return Ok(BaselineReconstruction::NotEvaluated { reason }), }; // SYNTHESIZED BY THE CURRENT RENDERER, SO PARSED UNDER THE CURRENT GRAMMAR. This content is - // not bytes read from the base tree; it is new source the head's roster writer produced from + // not bytes read from the base tree; it is new source the head's roster renderer produced from // the base tree's path membership. The base environment is reserved for bytes that actually // came out of the base revision -- sending renderer output through an older grammar could // refuse text no historical source ever carried. diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 6a089b2999c..5fc446a6ac7 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -2979,6 +2979,8 @@ pub struct RegenRoundCostOutcome { } pub const REGEN_ROUND_COST_RECEIPT_REL: &str = "target/stage0-regen-round-cost.txt"; +/// Set on the single re-exec after the seed build replaced the running executable. +const REGEN_ROUND_COST_REEXEC_ENV: &str = "GUNBC_REGEN_ROUND_COST_REEXEC"; const REGEN_ROUND_COST_PRODUCER: &str = "claim_executor --regen-round-cost"; const REGEN_ROUND_COST_ENTRY_UNDER_ROOT: &str = "gunbc/regen_round_cost.dag"; @@ -3699,6 +3701,235 @@ fn journal_stage0_paths_for_subject( fs::write(&manifest_path, bytes).map_err(|e| format!("write {}: {e}", manifest_path.display())) } +/// The seed build's cost as observed by one process image. +#[derive(Clone, Debug, PartialEq, Eq)] +struct SeedBuildCost { + compiled_crates: u64, + wall_ms: u64, + /// `None` is `gunbc.regen_round_cost` `CpuUnreadable`, never zero. + cpu_ms: Option, +} + +/// What the replaced image hands to the built one: its seed-build cost, the source identity it +/// built (HEAD plus the digest of the uncommitted tracked diff: a dirty tree stays legal, a +/// same-commit source change is visible), and the digest of the artifact it built. +#[derive(Clone, Debug, PartialEq, Eq)] +struct SeedHandoff { + cost: SeedBuildCost, + source: String, + built_exe: String, +} + +/// Why the seed handoff refuses. Standing: runtime mitigation (guarantee-ladder rung 1) -- a +/// typed refusal at the point the invalid state is observed; the invalid state stays writable. +#[derive(Clone, Debug, PartialEq, Eq)] +enum SeedHandoffRefusal { + /// The built image was replaced by another path replacement after the handoff. Claims + /// nothing about idempotence: only that the executable changed again. + ExecutableReplacedAfterHandoff { + before: String, + after: String, + }, + /// The source the first image built is not the source this image sees (another commit, or + /// the same commit with a different uncommitted tracked diff). + SourceChangedAcrossHandoff { + carried: String, + observed: String, + }, + /// The descriptor opened for the exec holds other bytes than the build produced. + OpenedTargetNotBuildOutput { + built: String, + opened: String, + }, + /// This image is not the artifact the first image built. + WrongArtifactAcrossHandoff { + built: String, + running: String, + }, + /// The composed ledger holds other than exactly one `round.seed_build` measurement, so the + /// carried cost cannot be added exactly once. + SeedBuildMeasurementCount { + found: usize, + }, + /// The carried observation does not decode. + HandoffUndecodable { + value: String, + }, + ReExecFailed { + path: String, + cause: String, + }, +} + +impl std::fmt::Display for SeedHandoffRefusal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let p = REGEN_ROUND_COST_PRODUCER; + match self { + Self::ExecutableReplacedAfterHandoff { before, after } => write!( + f, + "refusal: ExecutableReplacedAfterHandoff: {p}: the seed build replaced the \ + running executable again after the one handoff ({before} -> {after}); the tree \ + is the carried one, so another path is replacing the executable" + ), + Self::SourceChangedAcrossHandoff { carried, observed } => write!( + f, + "refusal: SourceChangedAcrossHandoff: {p}: first image built source {carried}, \ + this image sees {observed}" + ), + Self::OpenedTargetNotBuildOutput { built, opened } => write!( + f, + "refusal: OpenedTargetNotBuildOutput: {p}: the build produced {built} but the \ + descriptor opened for the exec holds {opened}" + ), + Self::WrongArtifactAcrossHandoff { built, running } => write!( + f, + "refusal: WrongArtifactAcrossHandoff: {p}: first image built {built}, this \ + image is {running}" + ), + Self::SeedBuildMeasurementCount { found } => write!( + f, + "refusal: SeedBuildMeasurementCount: {p}: expected exactly one round.seed_build \ + measurement to receive the carried cost, found {found}" + ), + Self::HandoffUndecodable { value } => write!( + f, + "refusal: HandoffUndecodable: {p}: {REGEN_ROUND_COST_REEXEC_ENV}={value:?}" + ), + Self::ReExecFailed { path, cause } => write!( + f, + "refusal: ReExecFailed: {p}: the seed build replaced the running executable and \ + re-exec of {path} failed: {cause}" + ), + } + } +} + +enum SeedHandoffDecision { + Proceed, + ReExec(SeedHandoff), +} + +impl SeedHandoff { + fn encode(&self) -> String { + let cpu = self + .cost + .cpu_ms + .map_or("unreadable".to_string(), |c| c.to_string()); + format!( + "crates={};wall_ms={};cpu_ms={};source={};built_exe={}", + self.cost.compiled_crates, self.cost.wall_ms, cpu, self.source, self.built_exe + ) + } + + fn decode(value: &str) -> Result { + let bad = || SeedHandoffRefusal::HandoffUndecodable { + value: value.to_string(), + }; + let mut it = value.split(';').map(|kv| kv.split_once('=')); + let mut field = |key: &str| -> Result { + match it.next() { + Some(Some((k, v))) if k == key => Ok(v.to_string()), + _ => Err(bad()), + } + }; + let compiled_crates = field("crates")?.parse::().map_err(|_| bad())?; + let wall_ms = field("wall_ms")?.parse::().map_err(|_| bad())?; + let cpu = field("cpu_ms")?; + let cpu_ms = if cpu == "unreadable" { + None + } else { + Some(cpu.parse::().map_err(|_| bad())?) + }; + let source = field("source")?; + let built_exe = field("built_exe")?; + Ok(SeedHandoff { + cost: SeedBuildCost { + compiled_crates, + wall_ms, + cpu_ms, + }, + source, + built_exe, + }) + } + + fn from_env() -> Result, SeedHandoffRefusal> { + match std::env::var(REGEN_ROUND_COST_REEXEC_ENV) { + Ok(v) => Self::decode(&v).map(Some), + Err(std::env::VarError::NotPresent) => Ok(None), + Err(std::env::VarError::NotUnicode(v)) => Err(SeedHandoffRefusal::HandoffUndecodable { + value: v.to_string_lossy().into_owned(), + }), + } + } +} + +fn decide_seed_handoff( + incoming: Option<&SeedHandoff>, + exe_before: &str, + exe_after: &str, + source: &str, + cost: &SeedBuildCost, +) -> Result { + if let Some(h) = incoming { + if h.source != source { + return Err(SeedHandoffRefusal::SourceChangedAcrossHandoff { + carried: h.source.clone(), + observed: source.to_string(), + }); + } + if h.built_exe != exe_before { + return Err(SeedHandoffRefusal::WrongArtifactAcrossHandoff { + built: h.built_exe.clone(), + running: exe_before.to_string(), + }); + } + if exe_before != exe_after { + return Err(SeedHandoffRefusal::ExecutableReplacedAfterHandoff { + before: exe_before.to_string(), + after: exe_after.to_string(), + }); + } + return Ok(SeedHandoffDecision::Proceed); + } + if exe_before == exe_after { + return Ok(SeedHandoffDecision::Proceed); + } + Ok(SeedHandoffDecision::ReExec(SeedHandoff { + cost: cost.clone(), + source: source.to_string(), + built_exe: exe_after.to_string(), + })) +} + +/// Compose the carried pre-handoff seed build into this image's observation: crate count and +/// the `round.seed_build` ledger row, summed exactly once. A carried CPU that is unreadable +/// makes the composed CPU unreadable, never zero. +fn compose_seed_build_cost( + carried: Option<&SeedBuildCost>, + this_image_crates: u64, + mut marks: std::vec::Vec, +) -> Result<(u64, std::vec::Vec), SeedHandoffRefusal> { + let Some(c) = carried else { + return Ok((this_image_crates, marks)); + }; + let found = marks + .iter() + .filter(|r| r.label == "round.seed_build") + .count(); + if found != 1 { + return Err(SeedHandoffRefusal::SeedBuildMeasurementCount { found }); + } + for row in marks.iter_mut().filter(|r| r.label == "round.seed_build") { + row.wall_ms += c.wall_ms; + row.cpu_ms = match (row.cpu_ms, c.cpu_ms) { + (Some(a), Some(b)) => Some(a + b), + _ => None, + }; + } + Ok((this_image_crates + c.compiled_crates, marks)) +} + fn seed_cargo_build(workspace: &Path, label: &str) -> Result { v1_rt::trace_mark(format!("{label}.begin")); let output = Command::new("cargo") @@ -3752,12 +3983,103 @@ fn current_exe_on_disk() -> Result { )) } +/// Open the path ONCE and digest the bytes read through that descriptor. The returned file is +/// the exec target: `/proc/self/fd/N` names this inode, so a later replacement of the path +/// cannot make the executed image differ from the hashed one. +fn open_exec_target(path: &Path) -> Result<(fs::File, String), String> { + let mut file = fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?; + let digest = digest_open_file(&mut file)?; + Ok((file, digest)) +} + +/// Admit the exec target: the carry keeps the BUILD-OUTPUT digest, and the opened descriptor's +/// digest must equal it. Never overwrites the carry from the open. +fn admit_exec_target( + carry: SeedHandoff, + opened_digest: &str, +) -> Result { + if carry.built_exe != opened_digest { + return Err(SeedHandoffRefusal::OpenedTargetNotBuildOutput { + built: carry.built_exe, + opened: opened_digest.to_string(), + }); + } + Ok(carry) +} + +/// Digest of the bytes of an open file, read from its start. +fn digest_open_file(file: &mut fs::File) -> Result { + use std::io::{Read, Seek}; + file.rewind().map_err(|e| format!("rewind: {e}"))?; + let mut bytes = std::vec::Vec::new(); + file.read_to_end(&mut bytes) + .map_err(|e| format!("read: {e}"))?; + Ok(bytes_digest(&bytes)) +} + +/// The RUNNING image: `/proc/self/exe` opened directly, no installed-pathname reopen and no +/// ` (deleted)` strip. Three identities stay separate: the build output (`current_exe_digest`, +/// the installed path), the opened exec target (`open_exec_target`, carried as `built_exe`), +/// and this one. +fn running_image_digest() -> Result { + let mut file = + fs::File::open("/proc/self/exe").map_err(|e| format!("open /proc/self/exe: {e}"))?; + digest_open_file(&mut file) +} + fn current_exe_digest() -> Result { let on_disk = current_exe_on_disk()?; let bytes = fs::read(&on_disk).map_err(|e| format!("read {}: {e}", on_disk.display()))?; Ok(bytes_digest(&bytes)) } +/// HEAD, the digest of the uncommitted tracked diff, and path+content digests of untracked, +/// non-ignored files (a new module or source the build consumes). +fn source_identity(workspace: &Path, head: &str) -> Result { + let run = |args: &[&str]| -> Result, String> { + let output = Command::new("git") + .args(args) + .current_dir(workspace) + .output() + .map_err(|e| format!("git {}: {e}", args.join(" ")))?; + if !output.status.success() { + return Err(String::from_utf8_lossy(&output.stderr).to_string()); + } + Ok(output.stdout) + }; + let diff = run(&["diff", "HEAD", "--binary"])?; + let listing = run(&["ls-files", "--others", "--exclude-standard", "-z"])?; + let mut untracked = std::vec::Vec::new(); + for rel in listing.split(|b| *b == 0).filter(|p| !p.is_empty()) { + use std::os::unix::ffi::OsStrExt; + let full = workspace.join(std::ffi::OsStr::from_bytes(rel)); + let content = fs::read(&full).map_err(|e| format!("read {}: {e}", full.display()))?; + untracked.push((rel.to_vec(), content)); + } + Ok(compose_source_identity(head, &diff, &mut untracked)) +} + +fn compose_source_identity( + head: &str, + tracked_diff: &[u8], + untracked: &mut [(std::vec::Vec, std::vec::Vec)], +) -> String { + untracked.sort_by(|a, b| a.0.cmp(&b.0)); + // Length-prefixed raw path bytes: no filename byte (`=`, newline, NUL) can move a + // boundary between entries. + let mut payload = std::vec::Vec::new(); + for (path, content) in untracked.iter() { + payload.extend_from_slice(&(path.len() as u64).to_le_bytes()); + payload.extend_from_slice(path); + payload.extend_from_slice(bytes_digest(content).as_bytes()); + } + format!( + "{head}+{}+{}", + bytes_digest(tracked_diff), + bytes_digest(&payload) + ) +} + fn git_tree_dirty(workspace: &Path) -> Result { let output = Command::new("git") .args(["status", "--porcelain", "--untracked-files=no"]) @@ -5467,22 +5789,67 @@ pub fn run_regen_round_cost( let host = host_name(); let tree = git_head_sha(&workspace)?; let tree_dirty = git_tree_dirty(&workspace)?; - let exe_before = current_exe_digest()?; + let source_identity = source_identity(&workspace, &tree)?; + let exe_before = running_image_digest()?; // Recover an interrupted transaction before attempting to build or observe a new subject. restore_regen_convergence_journal(&workspace)?; v1_rt::trace_ledger_arm(); let rustfmt_spawns_before = rustfmt_spawn_count(); + let seed_cpu_before = v1_rt::trace_process_tree_cpu_ms(); + let seed_wall_before = std::time::Instant::now(); let seed_build = seed_cargo_build(&workspace, "round.seed_build")?; + let seed_build_wall_ms = seed_wall_before.elapsed().as_millis() as u64; + let seed_build_cpu_ms = match (seed_cpu_before, v1_rt::trace_process_tree_cpu_ms()) { + (Some(b), Some(a)) => Some(a.saturating_sub(b)), + _ => None, + }; let exe_after = current_exe_digest()?; - if exe_before != exe_after { - return Err(format!( - "refusal: the seed build replaced the running executable ({exe_before} -> \ - {exe_after}), so an emit from this process would measure a seed the build did not \ - produce. Re-run {REGEN_ROUND_COST_PRODUCER} so the emitting seed is the built one." - )); + let handoff_in = SeedHandoff::from_env().map_err(|cause| cause.to_string())?; + let seed_build_cost = SeedBuildCost { + compiled_crates: seed_build.compiled_crates, + wall_ms: seed_build_wall_ms, + cpu_ms: seed_build_cpu_ms, + }; + match decide_seed_handoff( + handoff_in.as_ref(), + &exe_before, + &exe_after, + &source_identity, + &seed_build_cost, + ) + .map_err(|cause| cause.to_string())? + { + SeedHandoffDecision::Proceed => {} + SeedHandoffDecision::ReExec(carry) => { + // ROOT CAUSE: the build's output path IS this process's image path, so a build that + // relinks replaces the executable that is running it. The emit below runs in this + // process, so it must be the built seed: hand the observation to the built binary, + // which composes it into the receipt exactly once. + use std::os::fd::AsRawFd; + use std::os::unix::process::CommandExt; + let on_disk = current_exe_on_disk()?; + // `carry.built_exe` is the build-output digest; the descriptor we exec must match + // it before the exec, and the carry is never re-identified from the open. + let (target, target_digest) = open_exec_target(&on_disk)?; + let carry = + admit_exec_target(carry, &target_digest).map_err(|cause| cause.to_string())?; + let err = Command::new(format!("/proc/self/fd/{}", target.as_raw_fd())) + .arg0(&on_disk) + .args(std::env::args_os().skip(1)) + .env(REGEN_ROUND_COST_REEXEC_ENV, carry.encode()) + .exec(); + return Err(SeedHandoffRefusal::ReExecFailed { + path: on_disk.display().to_string(), + cause: err.to_string(), + } + .to_string()); + } } + // Pre-handoff cost joins this image's no-op rebuild: summed once, here, before the + // ledger rows and the crate count reach the receipt. + let carried_cost = handoff_in.as_ref().map(|h| h.cost.clone()); let ( basename_to_module, @@ -5775,13 +6142,19 @@ pub fn run_regen_round_cost( // The runtime's `Vec` is the persistent vector its emitted programs use; the receipt // renderer takes a slice, so the rows are collected once here. - let marks: std::vec::Vec = v1_rt::trace_ledger_drain() + let drained_marks: std::vec::Vec = v1_rt::trace_ledger_drain() .ok_or_else(|| { "refusal: the trace ledger was not armed, so no phase was recorded".to_string() })? .iter() .cloned() .collect(); + let (seed_build_crates_total, marks) = compose_seed_build_cost( + carried_cost.as_ref(), + seed_build.compiled_crates, + drained_marks, + ) + .map_err(|cause| cause.to_string())?; let host_shell_modules = super::emitted_closure_compile_host::closure_modules(&stage0_src.join("lib.rs"))?; let rendered = render_round_cost_receipt( @@ -5789,7 +6162,7 @@ pub fn run_regen_round_cost( &host, &tree, tree_dirty, - seed_build.compiled_crates, + seed_build_crates_total, rebuild_compiled_crates, rustfmt_spawn_count() - rustfmt_spawns_before, &marks, @@ -5818,6 +6191,280 @@ pub fn run_regen_round_cost( mod regen_round_cost_tests { use super::*; + fn cost(crates: u64, wall: u64, cpu: Option) -> SeedBuildCost { + SeedBuildCost { + compiled_crates: crates, + wall_ms: wall, + cpu_ms: cpu, + } + } + + fn row(label: &str, wall: u64, cpu: Option) -> v1_rt::TraceLedgerRow { + v1_rt::TraceLedgerRow { + label: label.to_string(), + wall_ms: wall, + cpu_ms: cpu, + } + } + + /// P1 discriminator: a supplied nonzero pre-handoff cost and crate count survive the + /// production composition and the production render, once. + #[test] + fn pre_handoff_seed_build_cost_is_composed_exactly_once_into_the_receipt() { + let carried = cost(655, 180_000, Some(170_000)); + let marks = vec![ + row("round.seed_build", 96, Some(100)), + row("corpus_load", 7, Some(7)), + ]; + let (crates, composed) = compose_seed_build_cost(Some(&carried), 0, marks.clone()).unwrap(); + assert_eq!(crates, 655); + assert_eq!(composed[0].wall_ms, 180_096); + assert_eq!(composed[0].cpu_ms, Some(170_100)); + assert_eq!(composed[1].wall_ms, 7, "other phases are untouched"); + // no carried cost: identity (no omission invented, nothing added) + let (c0, m0) = compose_seed_build_cost(None, 3, marks.clone()).unwrap(); + assert_eq!((c0, m0[0].wall_ms), (3, 96)); + // unreadable carried CPU is unreadable, never zero + let (_, un) = compose_seed_build_cost(Some(&cost(1, 1, None)), 0, marks.clone()).unwrap(); + assert_eq!(un[0].cpu_ms, None); + // zero rows: the carried duration would vanish while the crates stayed + assert!(matches!( + compose_seed_build_cost(Some(&carried), 0, vec![row("corpus_load", 7, Some(7))]), + Err(SeedHandoffRefusal::SeedBuildMeasurementCount { found: 0 }) + )); + // two rows: the carried duration would be counted twice + let two = vec![ + row("round.seed_build", 96, Some(1)), + row("round.seed_build", 4, Some(1)), + ]; + assert!(matches!( + compose_seed_build_cost(Some(&carried), 0, two), + Err(SeedHandoffRefusal::SeedBuildMeasurementCount { found: 2 }) + )); + + let workspace = workspace_root(); + let roots = vec![ + workspace.join("dag").display().to_string(), + workspace.join("src/v2").display().to_string(), + ]; + let rendered = render_round_cost_receipt( + &roots, + "h", + "t", + false, + crates, + 0, + 0, + &composed, + &[], + &[], + &[], + &[], + &[], + "e", + "c", + ) + .expect("the production renderer accepts the composed rows"); + assert!( + rendered.contains("seed_build_compiled_crates=655"), + "{rendered}" + ); + assert!( + rendered.contains("phase=seed_build wall_ms=180096"), + "{rendered}" + ); + assert_eq!(rendered.matches("seed_build_compiled_crates=").count(), 1); + } + + #[test] + fn source_identity_sees_untracked_files() { + let mut none: Vec<(Vec, Vec)> = vec![]; + let base = compose_source_identity("H", b"d", &mut none); + let mut added = vec![(b"new.dag".to_vec(), b"x".to_vec())]; + let with_new = compose_source_identity("H", b"d", &mut added); + let mut edited = vec![(b"new.dag".to_vec(), b"y".to_vec())]; + let edited_id = compose_source_identity("H", b"d", &mut edited); + assert_ne!( + base, with_new, + "an added untracked file changes the identity" + ); + assert_ne!( + with_new, edited_id, + "an edited untracked file changes the identity" + ); + let c = cost(1, 1, None); + let carry = match decide_seed_handoff(None, "a", "b", &with_new, &c).unwrap() { + SeedHandoffDecision::ReExec(h) => h, + SeedHandoffDecision::Proceed => panic!(), + }; + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "b", &edited_id, &c), + Err(SeedHandoffRefusal::SourceChangedAcrossHandoff { .. }) + )); + } + + #[test] + fn untracked_framing_distinguishes_ambiguous_populations() { + let c = cost(1, 1, None); + // old framing: path '=' digest '\n'. Two files vs one file whose NAME embeds the + // first file's entry. + let mut two = vec![ + (b"a.dag".to_vec(), b"x".to_vec()), + (b"b.dag".to_vec(), b"y".to_vec()), + ]; + let forged = format!("a.dag={}\nb.dag", bytes_digest(b"x")).into_bytes(); + let mut one = vec![(forged, b"y".to_vec())]; + let id_two = compose_source_identity("H", b"d", &mut two); + let id_one = compose_source_identity("H", b"d", &mut one); + assert_ne!(id_two, id_one); + let carry = match decide_seed_handoff(None, "a", "b", &id_two, &c).unwrap() { + SeedHandoffDecision::ReExec(h) => h, + SeedHandoffDecision::Proceed => panic!(), + }; + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "b", &id_one, &c), + Err(SeedHandoffRefusal::SourceChangedAcrossHandoff { .. }) + )); + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "b", &id_two, &c), + Ok(SeedHandoffDecision::Proceed) + )); + } + + #[test] + fn exec_target_digest_is_of_the_opened_inode_not_the_path() { + let dir = std::env::temp_dir().join(format!("rrc-exec-target-{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("img"); + fs::write(&path, b"built").unwrap(); + let (file, digest) = open_exec_target(&path).unwrap(); + // the path is replaced after the open: the descriptor still names the hashed bytes + fs::remove_file(&path).unwrap(); + fs::write(&path, b"replaced").unwrap(); + use std::io::{Read, Seek}; + let mut f = file; + f.rewind().unwrap(); + let mut got = std::vec::Vec::new(); + f.read_to_end(&mut got).unwrap(); + assert_eq!(got, b"built"); + assert_eq!(digest, bytes_digest(b"built")); + assert_ne!(digest, current_digest_of(&path)); + let _ = fs::remove_dir_all(&dir); + } + + fn current_digest_of(p: &Path) -> String { + bytes_digest(&fs::read(p).unwrap()) + } + + #[test] + fn exec_target_admission_compares_with_the_build_output_and_never_overwrites() { + let c = cost(1, 1, None); + // production decision: build output B is what the carry names + let carry = match decide_seed_handoff(None, "old", "B", "S", &c).unwrap() { + SeedHandoffDecision::ReExec(h) => h, + SeedHandoffDecision::Proceed => panic!(), + }; + assert_eq!(carry.built_exe, "B"); + // build=B, install replaced with C before the open: opened=C refuses + assert!(matches!( + admit_exec_target(carry.clone(), "C"), + Err(SeedHandoffRefusal::OpenedTargetNotBuildOutput { .. }) + )); + // build=B, opened=B (a byte-identical replacement included): proceeds, carry intact + let admitted = admit_exec_target(carry.clone(), "B").unwrap(); + assert_eq!(admitted, carry); + // and the receiver still proceeds on the admitted carry + assert!(matches!( + decide_seed_handoff(Some(&admitted), "B", "B", "S", &c), + Ok(SeedHandoffDecision::Proceed) + )); + } + + #[test] + fn running_image_is_read_from_proc_self_exe_not_the_installed_path() { + let direct = bytes_digest(&fs::read("/proc/self/exe").unwrap()); + assert_eq!(running_image_digest().unwrap(), direct); + } + + #[test] + fn modify_after_verify_and_installed_running_mismatch_refuse() { + let c = cost(1, 1, None); + // the first image hashed inode I = B and carried it; another writer then rewrote I + let dir = std::env::temp_dir().join(format!("rrc-mav-{}", std::process::id())); + fs::create_dir_all(&dir).unwrap(); + let path = dir.join("img"); + fs::write(&path, b"B").unwrap(); + let (_target, built) = open_exec_target(&path).unwrap(); + let carry = SeedHandoff { + cost: c.clone(), + source: "S".into(), + built_exe: built.clone(), + }; + // rewrite the same inode, then put B back at the installed path + let mut rewritten = fs::OpenOptions::new().write(true).open(&path).unwrap(); + use std::io::Write as _; + rewritten.write_all(b"C").unwrap(); + let running = digest_open_file(&mut fs::File::open(&path).unwrap()).unwrap(); + assert_ne!( + running, built, + "the rewrite is visible through the running inode" + ); + // receiver: running image C, installed path B again -> the old path-reopen said Proceed + assert!(matches!( + decide_seed_handoff(Some(&carry), &running, &built, "S", &c), + Err(SeedHandoffRefusal::WrongArtifactAcrossHandoff { .. }) + )); + // installed/running mismatch with the carried artifact running + assert!(matches!( + decide_seed_handoff(Some(&carry), &built, &running, "S", &c), + Err(SeedHandoffRefusal::ExecutableReplacedAfterHandoff { .. }) + )); + // same artifact everywhere proceeds + assert!(matches!( + decide_seed_handoff(Some(&carry), &built, &built, "S", &c), + Ok(SeedHandoffDecision::Proceed) + )); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn handoff_decisions_are_typed_and_round_trip() { + let c = cost(4, 5, Some(6)); + let carry = match decide_seed_handoff(None, "a", "b", "T", &c).unwrap() { + SeedHandoffDecision::ReExec(h) => h, + SeedHandoffDecision::Proceed => panic!("replaced exe must hand off"), + }; + assert_eq!(carry.built_exe, "b", "the carry names the built artifact"); + assert_eq!(SeedHandoff::decode(&carry.encode()).unwrap(), carry); + assert!(matches!( + decide_seed_handoff(None, "a", "a", "T", &c), + Ok(SeedHandoffDecision::Proceed) + )); + // positive: the re-exec'd image is the built artifact, same source, build is a no-op + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "b", "T", &c), + Ok(SeedHandoffDecision::Proceed) + )); + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "c", "T", &c), + Err(SeedHandoffRefusal::ExecutableReplacedAfterHandoff { .. }) + )); + // same commit, different uncommitted source + assert!(matches!( + decide_seed_handoff(Some(&carry), "b", "b", "T-other-diff", &c), + Err(SeedHandoffRefusal::SourceChangedAcrossHandoff { .. }) + )); + // wrong artifact: running image is not the one the first image built + assert!(matches!( + decide_seed_handoff(Some(&carry), "z", "z", "T", &c), + Err(SeedHandoffRefusal::WrongArtifactAcrossHandoff { .. }) + )); + assert!(matches!( + SeedHandoff::decode("garbage"), + Err(SeedHandoffRefusal::HandoffUndecodable { .. }) + )); + } + #[test] fn assembled_seed_includes_foundation_and_layered_partitions_not_emit_consumer() { let workspace = workspace_root(); diff --git a/src/v1/stage0/src/std_compiler_entry.rs b/src/v1/stage0/src/std_compiler_entry.rs index 875b1f1704b..0f30d0a9015 100644 --- a/src/v1/stage0/src/std_compiler_entry.rs +++ b/src/v1/stage0/src/std_compiler_entry.rs @@ -7,6 +7,10 @@ use self::NativeDriverChildStanding::*; use self::NativeDriverCostAccounting::*; use self::NativeDriverCostRowStanding::*; use self::NativeDriverExclusiveRowKey::*; +use self::NativeEffectClass::*; +use self::NativeEffectScope::*; +pub use crate::std_decl_ref::decl_ref; +pub use crate::std_decl_ref::DeclarationRef; pub use crate::std_measure::Nanosecond; pub use crate::std_measure::{ millisecond, millisecond_to_nanosecond, nanosecond, nanosecond_count, @@ -54,6 +58,92 @@ pub enum CompilerEntryDriver { NativeServeDriver, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectClass { + NativeEffectDeclaration { declaration: Rc }, + NativeOutgoingHttp, +} +impl NativeEffectClass { + pub fn declaration(&self) -> Rc { + match self { + NativeEffectClass::NativeEffectDeclaration { + declaration: __val, .. + } => __val.clone(), + NativeEffectClass::NativeOutgoingHttp => panic!("no declaration on unit variant"), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectScope { + NativeAdmitAllExcept { + excluded: Rc>>, + }, + NativeAdmitOnly { + admitted: Rc>>, + }, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NativeEffectDenominator { + pub route: CompilerEntryDriver, + pub ruling: Rc, + pub scope: Rc, +} + +pub fn native_effect_declared(module_path: String, decl_name: String) -> Rc { + Rc::new(NativeEffectClass::NativeEffectDeclaration { + declaration: crate::std_decl_ref::decl_ref(module_path.clone(), decl_name.clone()), + }) +} + +pub fn native_compiler_route_denominator() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(NativeEffectDenominator { + route: CompilerEntryDriver::NativeCliDriver, + ruling: crate::std_decl_ref::decl_ref("gunbc.plans.demand_engine_program".to_string(), "demand_engine_program_plan".to_string()), + scope: Rc::new(NativeEffectScope::NativeAdmitAllExcept { + excluded: Rc::new(vec![native_effect_declared("std.resources".to_string(), "Clock".to_string()), native_effect_declared("std.resources".to_string(), "Network".to_string()), native_effect_declared("std.resources".to_string(), "Entropy".to_string()), native_effect_declared("std.resources".to_string(), "AuthContext".to_string()), native_effect_declared("extdeps.clock".to_string(), "Clock".to_string()), Rc::new(NativeEffectClass::NativeOutgoingHttp)]), + }), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + +pub fn native_service_route_denominator() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(NativeEffectDenominator { + route: CompilerEntryDriver::NativeServeDriver, + ruling: crate::std_decl_ref::decl_ref("gunbc.plans.demand_engine_program".to_string(), "demand_engine_program_plan".to_string()), + scope: Rc::new(NativeEffectScope::NativeAdmitAllExcept { + excluded: Rc::new(vec![native_effect_declared("std.resources".to_string(), "Entropy".to_string()), native_effect_declared("std.resources".to_string(), "AuthContext".to_string())]), + }), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + +pub fn native_claim_route_denominator() -> Rc { + thread_local! { + static CACHED: Rc = { + Rc::new(NativeEffectDenominator { + route: CompilerEntryDriver::NativeClaimDriver, + ruling: crate::std_decl_ref::decl_ref("std.compiler_entry".to_string(), "NativeClaimDriver".to_string()), + scope: Rc::new(NativeEffectScope::NativeAdmitOnly { + admitted: Rc::new(vec![native_effect_declared("extdeps.filesystem.filesystem_io".to_string(), "Filesystem".to_string())]), + }), + }) + }; + } + CACHED.with(|c: &Rc| c.clone()) +} + #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] diff --git a/src/v1/stage0/src/std_graph.rs b/src/v1/stage0/src/std_graph.rs index a813e4db5eb..12b3c64d336 100644 --- a/src/v1/stage0/src/std_graph.rs +++ b/src/v1/stage0/src/std_graph.rs @@ -371,13 +371,7 @@ pub fn is_lexicographic_descent(mut __tco_loop_evidence: Rc break true; } DescentEvidence::NonIncreasing => { - let __tco_0 = Rc::new( - evidence - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&evidence, 1)); __tco_loop_evidence = __tco_0; continue; } diff --git a/src/v1/stage0/src/std_integer.rs b/src/v1/stage0/src/std_integer.rs index bbdca20220c..dea273c3614 100644 --- a/src/v1/stage0/src/std_integer.rs +++ b/src/v1/stage0/src/std_integer.rs @@ -129,13 +129,7 @@ pub fn first_uint8_out_of_range(mut __tco_loop_remaining: Rc>) -> Optio break Some(o.clone()); } UInt8Result::UInt8Ready { value: _, .. } => { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); __tco_loop_remaining = __tco_0; continue; } diff --git a/src/v1/stage0/src/std_occurrence_identity.rs b/src/v1/stage0/src/std_occurrence_identity.rs index 299c52ca17a..454b38f5d1b 100644 --- a/src/v1/stage0/src/std_occurrence_identity.rs +++ b/src/v1/stage0/src/std_occurrence_identity.rs @@ -549,14 +549,10 @@ pub fn occurrence_id_list_is_prefix_of( Some(head) => { if occurrence_id_eq(head.clone(), expected.clone()) { Rc::new(OccurrenceIdListPrefixAcc { - path_remaining: Rc::new( - acc.path_remaining - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + path_remaining: Rc::new(v1_rt::list_skip( + &acc.path_remaining.clone(), + 1, + )), ok: true, }) } else { diff --git a/src/v1/stage0/src/std_primitive_projection.rs b/src/v1/stage0/src/std_primitive_projection.rs index 7f23a1abc04..0dde8b18b5d 100644 --- a/src/v1/stage0/src/std_primitive_projection.rs +++ b/src/v1/stage0/src/std_primitive_projection.rs @@ -422,7 +422,7 @@ pub fn primitive_projection_roster() -> Rc>> { primitive_projection_row( primitive_map_get(), "v2.std.collection".to_string(), - "map_get".to_string(), + "map_get_checked".to_string(), Rc::new(ProjectionFidelity::DivergentProjection { divergence: "declared return Outcome> against the primitive's Optional" diff --git a/src/v1/stage0/src/std_realization_schedule.rs b/src/v1/stage0/src/std_realization_schedule.rs index 452e02181d4..8f1847f187c 100644 --- a/src/v1/stage0/src/std_realization_schedule.rs +++ b/src/v1/stage0/src/std_realization_schedule.rs @@ -831,10 +831,8 @@ pub fn string_list_eq( break false; } else { { - let __tco_0 = - Rc::new(left.iter().cloned().skip(1 as usize).collect::>()); - let __tco_1 = - Rc::new(right.iter().cloned().skip(1 as usize).collect::>()); + let __tco_0 = Rc::new(v1_rt::list_skip(&left, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&right, 1)); __tco_loop_left = __tco_0; __tco_loop_right = __tco_1; continue; @@ -867,12 +865,8 @@ pub fn schedule_witness_entry_list_eq( break false; } else { { - let __tco_0 = Rc::new( - left.iter().cloned().skip(1 as usize).collect::>(), - ); - let __tco_1 = Rc::new( - right.iter().cloned().skip(1 as usize).collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&left, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&right, 1)); __tco_loop_left = __tco_0; __tco_loop_right = __tco_1; continue; @@ -980,12 +974,8 @@ pub fn runnable_batch_eq( break false; } else { { - let __tco_0 = Rc::new( - left.iter().cloned().skip(1 as usize).collect::>(), - ); - let __tco_1 = Rc::new( - right.iter().cloned().skip(1 as usize).collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&left, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&right, 1)); __tco_loop_left = __tco_0; __tco_loop_right = __tco_1; continue; @@ -1024,12 +1014,8 @@ pub fn schedule_eq(mut __tco_loop_left: Schedule, mut __tco_loop_right: Schedule break false; } else { { - let __tco_0 = Rc::new( - left.iter().cloned().skip(1 as usize).collect::>(), - ); - let __tco_1 = Rc::new( - right.iter().cloned().skip(1 as usize).collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&left, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&right, 1)); __tco_loop_left = __tco_0; __tco_loop_right = __tco_1; continue; diff --git a/src/v1/stage0/src/std_repair_input_origin.rs b/src/v1/stage0/src/std_repair_input_origin.rs index 283034fcc6e..e49ccbd854c 100644 --- a/src/v1/stage0/src/std_repair_input_origin.rs +++ b/src/v1/stage0/src/std_repair_input_origin.rs @@ -95,14 +95,10 @@ pub fn candidate_spelling_from_text(name: String) -> Rc { ); if ((segments.clone().len() as i64) > 1) { Rc::new(CandidateSpelling::QualifiedCandidate { - authored_qualifier: Rc::new( - segments - .clone() - .iter() - .cloned() - .take(v1_rt::int_sub((segments.clone().len() as i64), 1) as usize) - .collect::>(), - ) + authored_qualifier: Rc::new(v1_rt::list_take( + &segments.clone(), + v1_rt::int_sub((segments.clone().len() as i64), 1), + )) .join(&".".to_string()), member: match segments.clone().last().cloned() { Some(member) => member.clone(), diff --git a/src/v1/stage0/src/std_source_annotation.rs b/src/v1/stage0/src/std_source_annotation.rs index 64e9feefea5..687ac4e8be5 100644 --- a/src/v1/stage0/src/std_source_annotation.rs +++ b/src/v1/stage0/src/std_source_annotation.rs @@ -141,15 +141,7 @@ pub fn admit_annotations(result: Rc) -> Rc Rc::new(AnnotationAdmission::AnnotationsRefused { refusals: Rc::new(FreeSemigroup { head: head.clone(), - tail: Rc::new( - result - .refusals - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + tail: Rc::new(v1_rt::list_skip(&result.refusals.clone(), 1)), _phantom: std::marker::PhantomData, }), }), @@ -407,14 +399,7 @@ pub fn keyed_annotation_rows_agree( agreed: false, }), Some(other) => Rc::new(KeyedAnnotationRowWalk { - rest: Rc::new( - acc.rest - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + rest: Rc::new(v1_rt::list_skip(&acc.rest.clone(), 1)), agreed: ((acc.agreed.clone() && (row.subject_key.clone() == other.subject_key.clone())) && (row.text.clone() == other.text.clone())), diff --git a/src/v1/stage0/src/target_invocation_host.rs b/src/v1/stage0/src/target_invocation_host.rs index 30c0268b5f5..2341e08c7b3 100644 --- a/src/v1/stage0/src/target_invocation_host.rs +++ b/src/v1/stage0/src/target_invocation_host.rs @@ -248,7 +248,7 @@ pub fn parse_target_pattern(text: &str) -> Result Vec<(Label, TargetProducer)> { ( instrument_label("v2-native-census"), TargetProducer::V2NativeCensus { - reading: V2NativeCensusReading::ResolveRefusal, + reading: V2NativeCensusReading::RefusalCensus, }, ), ( @@ -847,7 +847,7 @@ fn run_producer(producer: TargetProducer) -> InvocationOutcome { TargetProducer::V2NativeCli => run_v2_native_cli(&v2_native_cli_source_roots()), TargetProducer::V2NativeFrontier => run_v2_native_frontier(&self_host_source_roots()), TargetProducer::V2NativeCensus { - reading: V2NativeCensusReading::ResolveRefusal, + reading: V2NativeCensusReading::RefusalCensus, } => run_v2_native_census(&self_host_source_roots()), TargetProducer::V2NativeCensus { reading: V2NativeCensusReading::TypeDeclarationUse, @@ -1230,12 +1230,18 @@ fn run_v2_native_census(source_roots: &[String]) -> InvocationOutcome { termination: Termination::ObservationHeld, message: format!( "v2-native-census: modules={} file_refusals={} advised_files={} residual_rows={} \ - cause_groups={}; the rows grouped by fatal reason are the cause_group lines above", + inferred={} infer_refused={} cause_groups={} roots={} type_census={}; the rows \ + grouped by fatal reason are the cause_group lines above, ranked by closure fan-out \ + in the census_root lines", run.modules, run.file_refusals, run.advised_files, run.residual_rows, - run.cause_groups + run.inferred, + run.infer_refused, + run.cause_groups, + run.roots, + run.type_census.word() ), }, Err(cause) => InvocationOutcome { @@ -1646,23 +1652,39 @@ fn run_native_serve_program(entry: &'static str) -> InvocationOutcome { _ => None, } }; - let plan = (|| -> Result<(Vec, String, String, String), String> { - let requests = match &read("native_serve_probe_requests")? { + // The requests depend on the peer's bound port, which only exists once the peer is up, so they + // are read from the reader module with that port as its argument -- still every byte there. + let requests_for_peer_port = |peer_port: i64| -> Result, String> { + let value = crate::v1_interpreter::run_in_context_with_args( + &ctx, + "native_serve_probe_requests", + &[( + Some("peer_port".to_string()), + crate::v1_interpreter::Value::Int(peer_port), + )], + true, + ) + .map_err(|cause| { + format!("{label_name}: {READER} native_serve_probe_requests failed: {cause}") + })?; + match &value { crate::v1_interpreter::Value::List(items) => items .iter() .map(|item| text(item).ok_or("a request is not a String".to_string())) - .collect::, String>>()?, - _ => return Err("native_serve_probe_requests is not a List".to_string()), - }; + .collect::, String>>(), + _ => Err("native_serve_probe_requests is not a List".to_string()), + } + }; + let plan = (|| -> Result<(String, String, String), String> { let release = text(&read("native_serve_probe_release_revision")?) .ok_or("the release revision is not a String")?; let refused = text(&read("native_serve_probe_refused_revision")?) .ok_or("the refused revision is not a String")?; let deadline = text(&read("native_serve_probe_request_deadline_ms")?) .ok_or("the request deadline is not a String")?; - Ok((requests, release, refused, deadline)) + Ok((release, refused, deadline)) })(); - let (requests, release, refused, deadline) = match plan { + let (release, refused, deadline) = match plan { Ok(plan) => plan, Err(cause) => { return InvocationOutcome { @@ -1677,7 +1699,7 @@ fn run_native_serve_program(entry: &'static str) -> InvocationOutcome { &release, &refused, &deadline, - &requests, + &requests_for_peer_port, ) { Ok(run) => run, Err(cause) => { @@ -1693,6 +1715,14 @@ fn run_native_serve_program(entry: &'static str) -> InvocationOutcome { Some("announcement".to_string()), crate::v1_interpreter::Value::Str(run.announcement.clone().into()), ), + ( + Some("peer_announcement".to_string()), + crate::v1_interpreter::Value::Str(run.peer_announcement.clone().into()), + ), + ( + Some("peer_port".to_string()), + crate::v1_interpreter::Value::Int(run.peer_port), + ), ( Some("responses".to_string()), crate::v1_interpreter::Value::List(std::rc::Rc::new( @@ -3651,7 +3681,7 @@ fn run_interpolation_hole_census(source_roots: &[String]) -> InvocationOutcome { for line in standing.lines() { println!("interpolation-hole-census: {line}"); } - let mut paths: Vec = Vec::new(); + let mut paths = Vec::new(); for root in source_roots { if let Err(detail) = cli_run::collect_dag_files_result(std::path::Path::new(root), &mut paths) @@ -3660,9 +3690,9 @@ fn run_interpolation_hole_census(source_roots: &[String]) -> InvocationOutcome { } } let mut corpus: Vec> = Vec::new(); - for path in &paths { - let path = path.to_string_lossy().to_string(); - match std::fs::read_to_string(&path) { + for source in &paths { + let path = source.path().to_string_lossy().to_string(); + match source.read() { Ok(content) => corpus.push(Rc::new(SourceFile { path, content })), Err(err) => return unreached(format!("corpus file {path}: {err}")), } diff --git a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs index c5c5b0b9a60..978adcdc7c8 100644 --- a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs +++ b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs @@ -33,7 +33,7 @@ pub fn ct_workspace_helpers() -> String { } pub fn ct_file_discovery_helpers() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" /// Discover all .dag files under a directory, returning (relative_path, content) pairs.\n".to_string(), " /// The relative path is relative to the workspace root.\n".to_string()), " fn discover_dag_files(dir: &str) -> Vec<(String, String)> {\n".to_string()), " let root = workspace_root();\n".to_string()), " let base = root.join(dir);\n".to_string()), " let mut results = Vec::new();\n".to_string()), " collect_dag_recursive(&base, &root, &mut results);\n".to_string()), " results.sort_by(|a, b| a.0.cmp(&b.0));\n".to_string()), " results\n".to_string()), " }\n\n".to_string()), " fn collect_dag_recursive(\n".to_string()), " dir: &std::path::Path,\n".to_string()), " root: &std::path::Path,\n".to_string()), " out: &mut Vec<(String, String)>,\n".to_string()), " ) {\n".to_string()), " crate::cli_run::derived_row_roster::ensure_if_row_dir_or_panic(dir);\n".to_string()), " if let Ok(entries) = std::fs::read_dir(dir) {\n".to_string()), " for entry in entries.flatten() {\n".to_string()), " let path = entry.path();\n".to_string()), " if path.is_dir() {\n".to_string()), " collect_dag_recursive(&path, root, out);\n".to_string()), " } else if path.extension().map_or(false, |e| e == \"dag\") {\n".to_string()), " let rel = path\n".to_string()), " .strip_prefix(root)\n".to_string()), " .unwrap()\n".to_string()), " .to_string_lossy()\n".to_string()), " .to_string();\n".to_string()), " // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules.\n".to_string()), " if rel.contains(\"/tests/\") {\n".to_string()), " continue;\n".to_string()), " }\n".to_string()), " let content = std::fs::read_to_string(&path)\n".to_string()), " .unwrap_or_else(|e| panic!(\"failed to read {}: {}\", path.display(), e));\n".to_string()), " out.push((rel, content));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" /// Discover all .dag files under a directory, returning (relative_path, content) pairs.\n".to_string(), " /// The relative path is relative to the workspace root.\n".to_string()), " fn discover_dag_files(dir: &str) -> Vec<(String, String)> {\n".to_string()), " let root = workspace_root();\n".to_string()), " let base = root.join(dir);\n".to_string()), " let mut results = Vec::new();\n".to_string()), " collect_dag_recursive(&base, &root, &mut results);\n".to_string()), " results.sort_by(|a, b| a.0.cmp(&b.0));\n".to_string()), " results\n".to_string()), " }\n\n".to_string()), " fn collect_dag_recursive(\n".to_string()), " dir: &std::path::Path,\n".to_string()), " root: &std::path::Path,\n".to_string()), " out: &mut Vec<(String, String)>,\n".to_string()), " ) {\n".to_string()), " if let Ok(entries) = std::fs::read_dir(dir) {\n".to_string()), " let mut here = Vec::new();\n".to_string()), " for entry in entries.flatten() {\n".to_string()), " let path = entry.path();\n".to_string()), " if path.is_dir() {\n".to_string()), " collect_dag_recursive(&path, root, out);\n".to_string()), " } else if path.extension().map_or(false, |e| e == \"dag\") {\n".to_string()), " here.push(path);\n".to_string()), " }\n".to_string()), " }\n".to_string()), " // Admitted per directory, so a ledger row directory contributes its derived roster\n".to_string()), " // in memory (`derived_row_roster::acquire_dir_files`) rather than as a written file.\n".to_string()), " let acquired = crate::cli_run::derived_row_roster::acquire_dir_files(dir, here)\n".to_string()), " .unwrap_or_else(|e| panic!(\"{}\", e));\n".to_string()), " for source in acquired {\n".to_string()), " let path = source.path();\n".to_string()), " let rel = path\n".to_string()), " .strip_prefix(root)\n".to_string()), " .unwrap()\n".to_string()), " .to_string_lossy()\n".to_string()), " .to_string();\n".to_string()), " // Test fixtures (e.g. fact_cardinality_split_brace.dag) are not modules.\n".to_string()), " if rel.contains(\"/tests/\") {\n".to_string()), " continue;\n".to_string()), " }\n".to_string()), " let content = source\n".to_string()), " .read()\n".to_string()), " .unwrap_or_else(|e| panic!(\"failed to read {}: {}\", path.display(), e));\n".to_string()), " out.push((rel, content));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n\n".to_string()) } pub fn ct_source_builders() -> String { @@ -152,8 +152,20 @@ pub fn ct_unbound_reference_to_a_twice_declared_leaf_refuses_test() -> String { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // A REFERENCE NO ARM RESOLVES REFUSES, AND SAYS WHICH LEAF. hom.third names Slot through a\n".to_string(), " // qualifier no module answers to, so resolve binds nothing and stamps nothing; two modules\n".to_string()), " // declare Slot, so the spelling does not decide either. The leaf-keyed index took whichever Slot\n".to_string()), " // was folded last. THE CONTROL is the same reference in a closure with ONE Slot: the spelling\n".to_string()), " // then names a sole declarer and the holder emits with that declaration's bound.\n".to_string()), " #[test]\n".to_string()), " fn unbound_reference_to_a_twice_declared_leaf_refuses() {\n".to_string()), " use crate::v1_compiler_compile::SourceFile;\n".to_string()), " let wide = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_refusal/wide.dag\".to_string(), content: \"module hom.wide\\n\\ntype Inner {\\n value: C\\n}\\n\\ntype Slot {\\n first: Inner\\n second: Inner\\n}\\n\".to_string() });\n".to_string()), " let narrow = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_refusal/narrow.dag\".to_string(), content: \"module hom.narrow\\n\\ntype Slot {\\n held: T\\n}\\n\".to_string() });\n".to_string()), " let third = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_refusal/third.dag\".to_string(), content: \"module hom.third\\n\\ntype Odd {\\n slot: no.such.module.Slot\\n}\\n\".to_string() });\n".to_string()), " let emitted = |sources: Vec>| -> String {\n".to_string()), " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n".to_string()), " result.files.iter().find(|f| f.path.contains(\"hom_third\")).map(|f| f.content.clone()).unwrap_or_default()\n".to_string()), " };\n".to_string()), " let red = emitted(vec![wide(), narrow(), third()]);\n".to_string()), " assert!(red.contains(\"compile_error!\") && red.contains(\"item 'Odd' references type 'Slot', which more than one module of this closure declares\"), \"two declarers and no identity must refuse, naming the leaf:\\n{red}\");\n".to_string()), " let control = emitted(vec![narrow(), third()]);\n".to_string()), " assert!(control.contains(\"pub struct Odd\") && !control.contains(\"more than one module of this closure declares\"), \"a sole declarer resolves by spelling:\\n{control}\");\n".to_string()), " }\n\n".to_string()) } +pub fn ct_a_coproduct_named_optional_emits_as_its_own_enum_test() -> String { + Rc::new(vec![" // A COPRODUCT NAMED Optional EMITS AS ITS OWN ENUM. still-raven-321's fixture,\n".to_string(), " // fixtures/native_emission_controls_optional_collision.dag, is a non-kernel declaration that shares\n".to_string(), " // the kernel mint's leaf. Host-Option is decided by declaration (v1.compiler.emit_rust\n".to_string(), " // declaration_owns_host_option over gunbc.structural_realization_bindings kernel_mint_declaration_rows),\n".to_string(), " // so its positions name its own enum. THE RED is the leaf rule this replaced: every position rendered\n".to_string(), " // as Option<..> and the variants were never emitted (E0425 cannot find CollisionWrapped). The fixture\n".to_string(), " // lives outside the source roots, because a second Optional in the accepted corpus is a leaf-name fork,\n".to_string(), " // so it is compiled here, from its file, beside a kernel std.optional. Its one import line names a\n".to_string(), " // kernel type and is dropped so the pair compiles standalone.\n".to_string(), " #[test]\n".to_string(), " fn a_coproduct_named_optional_emits_as_its_own_enum() {\n".to_string(), " use crate::v1_compiler_compile::SourceFile;\n".to_string(), " let collision = read_dag(\"fixtures/native_emission_controls_optional_collision.dag\").replace(\"import std.types { Int }\\n\", \"\");\n".to_string(), " let sources = vec![\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/optional.dag\".to_string(), content: \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\".to_string() }),\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/native_emission_controls_optional_collision.dag\".to_string(), content: collision }),\n".to_string(), " ];\n".to_string(), " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n".to_string(), " let emitted = result.files.iter().find(|f| f.path.contains(\"native_emission_controls_optional_collision\")).map(|f| f.content.clone()).unwrap_or_default();\n".to_string(), " assert!(emitted.contains(\"pub enum Optional\") && emitted.contains(\"CollisionWrapped\"), \"the collision coproduct must emit as its own enum:\\n{emitted}\");\n".to_string(), " assert!(!emitted.contains(\"Option<\") && !emitted.contains(\"compile_error!\"), \"no position of the collision coproduct may realize as the host Option:\\n{emitted}\");\n".to_string(), " }\n".to_string(), "\n".to_string()]).join(&"".to_string()) +} + +pub fn ct_unstamped_bare_optional_follows_the_lookup_binding_order_test() -> String { + Rc::new(vec![" // AN UNSTAMPED BARE `Optional` FOLLOWS THE TYPECHECKER'S lookup_binding ORDER: own file, then\n".to_string(), " // the file's named import (followed through re-exports), then the kernel\n".to_string(), " // (v1.compiler.infer_env bare_occurrence_binding). The member references of a coproduct body reach\n".to_string(), " // the emitter from the type environment's copy with no Node.declaration, so the census route is\n".to_string(), " // the emitter's own decision. THE RED is the importer: with the kernel ahead of the named import\n".to_string(), " // (the order direct_import_export_precedence_note states) its payload routes kernel_mint to\n".to_string(), " // std.optional, measured on gunbc#13454 at 57551c4b. The control keeps a module that names\n".to_string(), " // Optional without importing it on the kernel arm.\n".to_string(), " #[test]\n".to_string(), " fn unstamped_bare_optional_follows_the_lookup_binding_order() {\n".to_string(), " use crate::v1_compiler_compile::SourceFile;\n".to_string(), " let sources = vec![\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/optional.dag\".to_string(), content: \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\".to_string() }),\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/collision.dag\".to_string(), content: \"module hom.collision\\n\\ntype Optional\\n = CollisionWrapped { value: Int }\\n | CollisionEmpty\\n\".to_string() }),\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/importer.dag\".to_string(), content: \"module hom.importer\\n\\nimport hom.collision { Optional }\\n\\ntype HeldChoice\\n = HeldOptional { held: Optional }\\n | HeldNothing\\n\".to_string() }),\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/plain.dag\".to_string(), content: \"module hom.plain\\n\\ntype PlainChoice\\n = PlainHeld { held: Optional }\\n | PlainNothing\\n\".to_string() }),\n".to_string(), " std::rc::Rc::new(SourceFile { path: \"fixtures/kernel_shadow/user.dag\".to_string(), content: \"module hom.user\\n\\nimport hom.importer { HeldChoice }\\nimport hom.plain { PlainChoice }\\n\\nfn pick(h: HeldChoice, p: PlainChoice) -> Int {\\n 1\\n}\\n\".to_string() }),\n".to_string(), " ];\n".to_string(), " let receipt = crate::v1_tests_claim_carrier_realization_census::typed_census_from_sources(std::rc::Rc::new(sources.into()));\n".to_string(), " assert!(!receipt.starts_with(\"REFUSED\"), \"{receipt}\");\n".to_string(), " let routes = |enclosing: &str| -> Vec {\n".to_string(), " receipt.lines().skip(1).map(|l| l.split('\\t').collect::>())\n".to_string(), " .filter(|c| c[1] == enclosing && c[2] == \"declaration_field\" && c[3] == \"Optional\")\n".to_string(), " .map(|c| c[13].to_string()).collect()\n".to_string(), " };\n".to_string(), " let held = routes(\"HeldChoice\");\n".to_string(), " assert!(!held.is_empty() && held.iter().all(|r| r == \"referencing_module_import:Resolved:hom.collision.Optional\"), \"the imported Optional must route to the import, never the kernel mint:\\n{receipt}\");\n".to_string(), " let plain = routes(\"PlainChoice\");\n".to_string(), " assert!(!plain.is_empty() && plain.iter().all(|r| r == \"kernel_mint:Resolved:std.optional.Optional\"), \"an unimported Optional is the kernel's:\\n{receipt}\");\n".to_string(), " assert!(!receipt.contains(\"LeafAmbiguous:Optional\"), \"{receipt}\");\n".to_string(), " }\n".to_string(), "\n".to_string()]).join(&"".to_string()) +} + +pub fn ct_same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged_test() -> String { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // A SAME-LEAF TYPE ANYWHERE MUST NOT CHANGE ANOTHER MODULE'S EMISSION. hom.user returns the kernel\n".to_string(), " // optional's Absent; hom.edge declares an unrelated enum that also has an Absent variant. With the\n".to_string()), " // leaf-keyed type summaries, adding hom.coll (a coproduct it declares as Optional) replaced the\n".to_string()), " // kernel Optional's summary, so Absent stopped being an Optional variant and hom.user emitted\n".to_string()), " // Rc::new(Lookup::Absent) where it had emitted None. Keyed by declaration, both summaries stand:\n".to_string()), " // hom.user's file is byte-identical with and without hom.coll, and its Absent is the host None.\n".to_string()), " #[test]\n".to_string()), " fn same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged() {\n".to_string()), " use crate::v1_compiler_compile::SourceFile;\n".to_string()), " let src = |path: &str, content: &str| std::rc::Rc::new(SourceFile { path: path.to_string(), content: content.to_string() });\n".to_string()), " let optional = || src(\"fixtures/same_leaf_optional/optional.dag\", \"module std.optional\\n\\ntype Optional\\n = Present { value: T }\\n | Absent\\n\");\n".to_string()), " let edge = || src(\"fixtures/same_leaf_optional/edge.dag\", \"module hom.edge\\n\\ntype Lookup\\n = Found { at: Int }\\n | Absent\\n\");\n".to_string()), " let user = || src(\"fixtures/same_leaf_optional/user.dag\", \"module hom.user\\nimport std.optional { Optional, Present, Absent }\\n\\nfn nothing_here(x: Int) -> Optional {\\n Absent\\n}\\n\");\n".to_string()), " let coll = || src(\"fixtures/same_leaf_optional/coll.dag\", \"module hom.coll\\n\\ntype Optional\\n = CollisionWrapped { value: Int }\\n | CollisionEmpty\\n\");\n".to_string()), " let emitted_user = |sources: Vec>| -> String {\n".to_string()), " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n".to_string()), " result.files.iter().find(|f| f.path.contains(\"hom_user\")).map(|f| f.content.clone()).unwrap_or_default()\n".to_string()), " };\n".to_string()), " let without = emitted_user(vec![optional(), edge(), user()]);\n".to_string()), " let with = emitted_user(vec![optional(), edge(), user(), coll()]);\n".to_string()), " assert!(without.contains(\"None\") && !without.contains(\"Lookup::Absent\"), \"the control: hom.user's Absent is the host None:\\n{without}\");\n".to_string()), " assert_eq!(with, without, \"adding a same-leaf Optional elsewhere changed hom.user's emission\");\n".to_string()), " }\n".to_string()), "\n".to_string()) +} + pub fn ct_qualified_reference_is_not_decided_by_the_local_declarer_test() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // A QUALIFIED REFERENCE IS NOT DECIDED BY THE LOCAL DECLARER. hom.local declares its own Slot\n".to_string(), " // and ALSO names no.such.module.Slot, which resolve cannot bind, so the reference reaches the\n".to_string()), " // emitter unstamped while two modules declare Slot. The referencing-module arm\n".to_string()), " // (v1.compiler.infer_emit_info type_decl_referencing_module_declarer) would pick the LOCAL Slot:\n".to_string()), " // the wrong-declarer class itself. It is admitted only for a bare spelling, so this refuses.\n".to_string()), " // THE CONTROL is the same module without the second declarer: the spelling names a sole\n".to_string()), " // declarer, and nothing refuses.\n".to_string()), " #[test]\n".to_string()), " fn qualified_reference_is_not_decided_by_the_local_declarer() {\n".to_string()), " use crate::v1_compiler_compile::SourceFile;\n".to_string()), " let wide = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_qualified/wide.dag\".to_string(), content: \"module hom.wide\\n\\ntype Inner {\\n value: C\\n}\\n\\ntype Slot {\\n first: Inner\\n second: Inner\\n}\\n\".to_string() });\n".to_string()), " let local = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_qualified/local.dag\".to_string(), content: \"module hom.local\\n\\ntype Slot {\\n held: T\\n}\\n\\ntype Odd {\\n slot: no.such.module.Slot\\n}\\n\".to_string() });\n".to_string()), " let emitted = |sources: Vec>| -> String {\n".to_string()), " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n".to_string()), " result.files.iter().find(|f| f.path.contains(\"hom_local\")).map(|f| f.content.clone()).unwrap_or_default()\n".to_string()), " };\n".to_string()), " let red = emitted(vec![wide(), local()]);\n".to_string()), " assert!(red.contains(\"compile_error!\") && red.contains(\"item 'Odd' references type 'Slot', which more than one module of this closure declares\"), \"a qualified unstamped reference must refuse, not take the local Slot:\\n{red}\");\n".to_string()), " let control = emitted(vec![local()]);\n".to_string()), " assert!(control.contains(\"pub struct Odd\") && !control.contains(\"more than one module of this closure declares\"), \"a sole declarer resolves by spelling:\\n{control}\");\n".to_string()), " }\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // A QUALIFIED REFERENCE IS NOT DECIDED BY THE LOCAL DECLARER. hom.local declares its own Slot\n".to_string(), " // and ALSO names no.such.module.Slot, which resolve cannot bind, so the reference reaches the\n".to_string()), " // emitter unstamped while two modules declare Slot. The referencing-module arm\n".to_string()), " // (v1.compiler.infer_emit_info type_decl_occurrence_binding) would pick the LOCAL Slot:\n".to_string()), " // the wrong-declarer class itself. It is admitted only for a bare spelling, so this refuses.\n".to_string()), " // THE CONTROL is the same module without the second declarer: the spelling names a sole\n".to_string()), " // declarer, and nothing refuses.\n".to_string()), " #[test]\n".to_string()), " fn qualified_reference_is_not_decided_by_the_local_declarer() {\n".to_string()), " use crate::v1_compiler_compile::SourceFile;\n".to_string()), " let wide = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_qualified/wide.dag\".to_string(), content: \"module hom.wide\\n\\ntype Inner {\\n value: C\\n}\\n\\ntype Slot {\\n first: Inner\\n second: Inner\\n}\\n\".to_string() });\n".to_string()), " let local = || std::rc::Rc::new(SourceFile { path: \"fixtures/same_leaf_qualified/local.dag\".to_string(), content: \"module hom.local\\n\\ntype Slot {\\n held: T\\n}\\n\\ntype Odd {\\n slot: no.such.module.Slot\\n}\\n\".to_string() });\n".to_string()), " let emitted = |sources: Vec>| -> String {\n".to_string()), " let result = crate::v1_compiler_compile::compile_sources(std::rc::Rc::new(sources.into()), crate::v1_compiler_artifact::RenderTarget::Rust);\n".to_string()), " result.files.iter().find(|f| f.path.contains(\"hom_local\")).map(|f| f.content.clone()).unwrap_or_default()\n".to_string()), " };\n".to_string()), " let red = emitted(vec![wide(), local()]);\n".to_string()), " assert!(red.contains(\"compile_error!\") && red.contains(\"item 'Odd' references type 'Slot', which more than one module of this closure declares\"), \"a qualified unstamped reference must refuse, not take the local Slot:\\n{red}\");\n".to_string()), " let control = emitted(vec![local()]);\n".to_string()), " assert!(control.contains(\"pub struct Odd\") && !control.contains(\"more than one module of this closure declares\"), \"a sole declarer resolves by spelling:\\n{control}\");\n".to_string()), " }\n\n".to_string()) } pub fn ct_duplicate_type_parameter_refuses_test() -> String { @@ -174,7 +186,7 @@ pub fn ct_coercion_tests() -> String { } __result }); - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // =========================================================================\n".to_string(), " // Coercion registry tests (auto-generated from data declarations)\n".to_string()), " // =========================================================================\n\n".to_string()), test_fns.clone().join(&"\n".to_string())), ct_rust_btree_set_ord_eligibility_test()), ct_unresolved_variant_pattern_refuses_test()), ct_diagnostics_carrier_grounding_test()), ct_std_nat_realization_agreement_test()), ct_std_nat_operators_refuse_overflow_test()), ct_render_rust_applied_type_qualified_base_test()), ct_measure_unit_variant_exact_one_test()), ct_renderer_hop_identity_keying_test()), ct_generic_param_declines_fail_closed_unwrap_test()), ct_witness_carrier_declines_non_witness_expected_type_test()), ct_kernel_identity_single_authority_test()), ct_type_reference_identity_split_test()), ct_declaration_field_reference_identity_test()), ct_same_leaf_type_declarations_emit_by_identity_test()), ct_unbound_reference_to_a_twice_declared_leaf_refuses_test()), ct_qualified_reference_is_not_decided_by_the_local_declarer_test()), ct_duplicate_type_parameter_refuses_test()), ct_resource_item_is_not_a_type_item_test()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" // =========================================================================\n".to_string(), " // Coercion registry tests (auto-generated from data declarations)\n".to_string()), " // =========================================================================\n\n".to_string()), test_fns.clone().join(&"\n".to_string())), ct_rust_btree_set_ord_eligibility_test()), ct_unresolved_variant_pattern_refuses_test()), ct_diagnostics_carrier_grounding_test()), ct_std_nat_realization_agreement_test()), ct_std_nat_operators_refuse_overflow_test()), ct_render_rust_applied_type_qualified_base_test()), ct_measure_unit_variant_exact_one_test()), ct_renderer_hop_identity_keying_test()), ct_generic_param_declines_fail_closed_unwrap_test()), ct_witness_carrier_declines_non_witness_expected_type_test()), ct_kernel_identity_single_authority_test()), ct_type_reference_identity_split_test()), ct_declaration_field_reference_identity_test()), ct_same_leaf_type_declarations_emit_by_identity_test()), ct_unbound_reference_to_a_twice_declared_leaf_refuses_test()), ct_qualified_reference_is_not_decided_by_the_local_declarer_test()), ct_same_leaf_type_elsewhere_leaves_other_modules_emission_unchanged_test()), ct_unstamped_bare_optional_follows_the_lookup_binding_order_test()), ct_a_coproduct_named_optional_emits_as_its_own_enum_test()), ct_duplicate_type_parameter_refuses_test()), ct_resource_item_is_not_a_type_item_test()) } } @@ -207,7 +219,7 @@ pub fn ct_std_nat_operators_refuse_overflow_test() -> String { } pub fn ct_diagnostics_carrier_grounding_test() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn diagnostics_carrier_grounds_to_native_option() {\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Diagnostics\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Optional\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_grounded_coproduct_native_alias(\"Diagnostics\".to_string()));\n".to_string()), " let empty_shared = std::rc::Rc::new(im::OrdSet::new());\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied(empty_shared.clone()),\n".to_string()), " \"Option\"\n".to_string()), " );\n".to_string()), " let mut shared_ned_inner = im::OrdSet::new();\n".to_string()), " shared_ned_inner.insert(\"NonEmptyDiagnostics\".to_string());\n".to_string()), " let shared_ned = std::rc::Rc::new(shared_ned_inner);\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied(shared_ned),\n".to_string()), " \"Option>\"\n".to_string()), " );\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Some\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Present\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_some_like_variant_name(\"None\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Absent\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Diagnostics\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Optional\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Witness\".to_string()));\n".to_string()), " let diagnostics_node = named_type_node(\"Diagnostics\");\n".to_string()), " let source_indices = std::rc::Rc::new(HashMap::new());\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_type(diagnostics_node.clone(), source_indices.clone()));\n".to_string()), " let empty_emit = crate::v1_compiler_infer_emit_info::empty_emit_graph_info();\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_type(\n".to_string()), " diagnostics_node,\n".to_string()), " empty_shared,\n".to_string()), " source_indices,\n".to_string()), " empty_emit\n".to_string()), " ),\n".to_string()), " \"Option\"\n".to_string()), " );\n".to_string()), " }\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn diagnostics_carrier_grounds_to_native_option() {\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Diagnostics\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_alias(\"Optional\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"v2.std.diagnostic\".to_string(), \"Diagnostics\".to_string()));\n".to_string()), " // The host option is a DECLARATION, not a name: the kernel optional's declaration is it, and a\n".to_string()), " // coproduct another module declares as Optional is not.\n".to_string()), " assert!(crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"std.optional\".to_string(), \"Optional\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::declaration_is_grounded_coproduct_native_alias(\"gunbc.instruments.native_emission_controls_optional_collision\".to_string(), \"Optional\".to_string()));\n".to_string()), " let empty_shared = std::rc::Rc::new(im::OrdSet::new());\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied(empty_shared.clone()),\n".to_string()), " \"Option\"\n".to_string()), " );\n".to_string()), " let mut shared_ned_inner = im::OrdSet::new();\n".to_string()), " shared_ned_inner.insert(\"NonEmptyDiagnostics\".to_string());\n".to_string()), " let shared_ned = std::rc::Rc::new(shared_ned_inner);\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_diagnostics_carrier_applied(shared_ned),\n".to_string()), " \"Option>\"\n".to_string()), " );\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Some\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Present\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_some_like_variant_name(\"None\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_some_like_variant_name(\"Absent\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Diagnostics\".to_string()));\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Optional\".to_string()));\n".to_string()), " assert!(!crate::v1_compiler_emit_rust::is_optional_like_parent_name(\"Witness\".to_string()));\n".to_string()), " let diagnostics_node = named_type_node(\"Diagnostics\");\n".to_string()), " let source_indices = std::rc::Rc::new(HashMap::new());\n".to_string()), " assert!(crate::v1_compiler_emit_rust::is_host_diagnostics_carrier_type(diagnostics_node.clone(), source_indices.clone()));\n".to_string()), " let empty_emit = crate::v1_compiler_infer_emit_info::empty_emit_graph_info();\n".to_string()), " assert_eq!(\n".to_string()), " crate::v1_compiler_emit_rust::render_rust_type(\n".to_string()), " diagnostics_node,\n".to_string()), " empty_shared,\n".to_string()), " source_indices,\n".to_string()), " empty_emit\n".to_string()), " ),\n".to_string()), " \"Option\"\n".to_string()), " );\n".to_string()), " }\n\n".to_string()) } pub fn ct_rendered_leaf_occurrence_identity_test() -> String { @@ -271,7 +283,7 @@ pub fn first_or_empty(items: Rc>) -> String { } pub fn second_or_empty(items: Rc>) -> String { - match items.clone().iter().cloned().skip(1 as usize).next() { + match items.clone().get((1) as usize).cloned() { Some(v) => v.clone(), std::option::Option::None => "".to_string(), } @@ -390,7 +402,7 @@ pub fn ct_function_value_field_method_known_hole_probe_test() -> String { } pub fn ct_method_existence_wall_witness_test() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn method_existence_wall_witness() {\n".to_string()), " // DISCRIMINATING RED for method_existence_wall_note. Before the wall an\n".to_string()), " // unresolved method inherited the RECEIVER's type with no diagnostic, so\n".to_string()), " // `xs |> filter_map(..)` on List typed as List, compiled clean, and\n".to_string()), " // died at InterpError::Unimplemented in live dispatch (#7479, HTTP 500).\n".to_string()), " let result = std::thread::Builder::new()\n".to_string()), " .stack_size(8 * 1024 * 1024)\n".to_string()), " .spawn(|| {\n".to_string()), " let red = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"red.dag\".to_string(),\n".to_string()), " content: \"module red\\nfn f(xs: List) -> List { xs |> filter_map(x => x) }\\nfn g(xs: List) -> Bool { xs |> starts_with(\\\"x\\\") }\\nfn h(xs: List) -> String { xs |> to_upper() }\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let red_result = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![red]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " let missing: Vec<_> = red_result.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " missing.len() >= 3,\n".to_string()), " \"expected MethodNotFound for the unresolved method AND for rostered names on a receiver that does not offer them (starts_with / to_upper on List — codex review 45327: a name-grain predicate admitted these), got: {:?}\",\n".to_string()), " red_result.diagnostics\n".to_string()), " );\n".to_string()), " // POSITIVE CONTROLS the wall must not touch: algebra method templates\n".to_string()), " // resolve at tier0, and `count` is in the declared std.methods roster\n".to_string()), " // even though free_monoid_scalar_templates omits it (the measured fork).\n".to_string()), " let green = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"green.dag\".to_string(),\n".to_string()), " content: \"module green\\nfn p(xs: List) -> List { xs |> filter(x => x > 1) |> map(x => x + 1) }\\nfn q(xs: List) -> Int { xs |> fold(0, (a, x) => a + x) }\\nfn r(s: String) -> Int { s |> count }\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let green_result = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![green]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " // The positive control asserts ZERO diagnostics, not merely no\n".to_string()), " // MethodNotFound. Filtering to the blocking variant let an ADVISORY\n".to_string()), " // MethodExistenceUndecided pass unnoticed, which is how `String |> count`\n".to_string()), " // was reported as a passing control while it was actually resolving\n".to_string()), " // through the non-blocking arm (codex review 45357).\n".to_string()), " assert!(\n".to_string()), " green_result.diagnostics.is_empty(),\n".to_string()), " \"legitimate methods must resolve with NO diagnostic of any severity — an advisory here means the call is not actually resolving, got: {:?}\",\n".to_string()), " green_result.diagnostics\n".to_string()), " );\n".to_string()), " // DISCRIMINATING RED for the declared frontier (codex reviews 45357,\n".to_string()), " // 45383). An undecided method existence must BLOCK, so the graph is\n".to_string()), " // never emitted on an unestablished judgment; the only non-blocking\n".to_string()), " // path is a DECLARED frontier row, which is countable and carries its\n".to_string()), " // own dissolution trigger. The two halves must be discriminated by\n".to_string()), " // MODULE NAME on otherwise identical source — an admission that fires\n".to_string()), " // for any module is an escape hatch, not a frontier.\n".to_string()), " let frontier_src = |module: &str, method: &str| {\n".to_string()), " format!(\"module {}\\ntype T {{ a: Int }}\\nfn f(t: T) -> Int {{ t |> {}(1) }}\\n\", module, method)\n".to_string()), " };\n".to_string()), " let compile_one = |path: &str, content: String| {\n".to_string()), " crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![std::rc::Rc::new(\n".to_string()), " crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: path.to_string(),\n".to_string()), " content,\n".to_string()), " }\n".to_string()), " )]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " )\n".to_string()), " };\n".to_string()), " let unlisted = compile_one(\"unlisted.dag\", frontier_src(\"unlisted.module\", \"list_push\"));\n".to_string()), " let undecided: Vec<_> = unlisted.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " !undecided.is_empty(),\n".to_string()), " \"a method whose existence is undecided in a module with NO declared frontier row must refuse, got: {:?}\",\n".to_string()), " unlisted.diagnostics\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " undecided.iter().all(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone())\n".to_string()), " && crate::v1_std_core::is_interpreter_blocking_diagnostic(d.diagnostic.clone())),\n".to_string()), " \"MethodExistenceUndecided must BLOCK both typecheck and the interpreter — a non-blocking undecided judgment is exactly the fail-open the wall exists to close\"\n".to_string()), " );\n".to_string()), " // The SHAPE is part of the key, and this is the half that answers\n".to_string()), " // codex review 45398: the module and the method both match a declared\n".to_string()), " // row, and the call is STILL refused, because the receiver does not\n".to_string()), " // fail to resolve in the way the row was measured on. A (module, method)\n".to_string()), " // key passed this input; that is the fail-open the third component closes.\n".to_string()), " let listed_wrong_shape = compile_one(\"listed.dag\", frontier_src(\"extdeps.dns.domain_name\", \"list_push\"));\n".to_string()), " assert!(\n".to_string()), " listed_wrong_shape.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. })),\n".to_string()), " \"a declared row must NOT admit a new call in the same module on a receiver whose shape it was never measured on, got: {:?}\",\n".to_string()), " listed_wrong_shape.diagnostics\n".to_string()), " );\n".to_string()), " // The admission half, checked against the frontier data itself rather\n".to_string()), " // than a synthetic source, because the residual shapes (an unnamed\n".to_string()), " // lambda-parameter receiver, a coproduct payload typed as its variant)\n".to_string()), " // are upstream resolution defects that cannot be conjured on demand.\n".to_string()), " // Every declared row must be admitted by its own exact key, and\n".to_string()), " // perturbing ANY of the three components must withdraw the admission.\n".to_string()), " let rows = crate::v1_compiler_infer::unresolved_method_frontier();\n".to_string()), " assert!(!rows.is_empty(), \"the frontier must be a declared, countable roster\");\n".to_string()), " for r in rows.iter() {\n".to_string()), " assert!(\n".to_string()), " crate::v1_compiler_infer::unresolved_method_frontier_trigger(\n".to_string()), " r.module_name.clone(), r.method.clone(), r.receiver_shape.clone()).is_some(),\n".to_string()), " \"row {}/{}/{} must be admitted by its own key\", r.module_name, r.method, r.receiver_shape\n".to_string()), " );\n".to_string()), " for perturbed in [\n".to_string()), " (format!(\"{}.other\", r.module_name), r.method.clone(), r.receiver_shape.clone()),\n".to_string()), " (r.module_name.clone(), format!(\"{}_other\", r.method), r.receiver_shape.clone()),\n".to_string()), " (r.module_name.clone(), r.method.clone(), format!(\"Product(Other{})\", r.receiver_shape.len())),\n".to_string()), " ] {\n".to_string()), " assert!(\n".to_string()), " crate::v1_compiler_infer::unresolved_method_frontier_trigger(\n".to_string()), " perturbed.0.clone(), perturbed.1.clone(), perturbed.2.clone()).is_none(),\n".to_string()), " \"perturbing the key to {:?} must WITHDRAW the admission — each component is load-bearing\", perturbed\n".to_string()), " );\n".to_string()), " }\n".to_string()), " }\n".to_string()), " // DISCRIMINATING RED for where_refinement_receiver_peel_note. A\n".to_string()), " // where-refinement alias reached method lookup as Product()\n".to_string()), " // because resolve_method_receiver_type short-circuits on Conj, so the\n".to_string()), " // base's algebra profile was never consulted. The peel must make the\n".to_string()), " // receiver decidable in BOTH directions on the SAME alias — a green\n".to_string()), " // that only proves the refusal stopped is indistinguishable from\n".to_string()), " // deleting the wall.\n".to_string()), " let peel_src = |call: &str| {\n".to_string()), " format!(\"module peel\\ntype Tight = String where string_non_empty\\nfn f(s: Tight) -> Int {{ s |> {} }}\\n\", call)\n".to_string()), " };\n".to_string()), " let peel_green = compile_one(\"peel_green.dag\", peel_src(\"count\"));\n".to_string()), " assert!(\n".to_string()), " peel_green.diagnostics.is_empty(),\n".to_string()), " \"a method on the refinement's String base must RESOLVE once the base is peeled — no diagnostic of any severity, got: {:?}\",\n".to_string()), " peel_green.diagnostics\n".to_string()), " );\n".to_string()), " // DISCRIMINATING RED for the two special-case arms (codex review\n".to_string()), " // 45430). The wall was reached only from the FINAL else, so an\n".to_string()), " // unresolved map_keys / map_values still returned the RECEIVER type\n".to_string()), " // with an empty diagnostic list — the exact success-shaped fallback\n".to_string()), " // this PR deletes, surviving in the two branches that ran before it.\n".to_string()), " let special = compile_one(\"special.dag\",\n".to_string()), " \"module special\\nfn f(xs: List) -> List {{ xs |> map_keys }}\\nfn g(xs: List) -> List {{ xs |> map_values }}\\n\".replace(\"{{\", \"{\").replace(\"}}\", \"}\"));\n".to_string()), " let special_missing: Vec<_> = special.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " special_missing.len() >= 2,\n".to_string()), " \"map_keys and map_values must route through the SAME refusal as every other method when the receiver is not a keyed collection — a wall reachable only from the final else is not a wall, got: {:?}\",\n".to_string()), " special.diagnostics\n".to_string()), " );\n".to_string()), " // ReceiverTypeUnestablished CLASSIFICATION control. The behavioural\n".to_string()), " // evidence for this class is the corpus census, not a synthetic\n".to_string()), " // source: 18 sites whole-tree, counted as advisories with the gate at\n".to_string()), " // zero blocking diagnostics. A synthetic reproducer WAS attempted --\n".to_string()), " // an untyped lambda parameter inside a record-field fn, the shape the\n".to_string()), " // real sites have -- and it produced no diagnostic at all, so it did\n".to_string()), " // not reproduce the shape and is not asserted here as though it did.\n".to_string()), " // ADMISSION is roster-gated, not automatic: an anonymous receiver is\n".to_string()), " // admitted only where a declared row names (module, method,\n".to_string()), " // Primitive()), and the perturbation loop above proves that gate for\n".to_string()), " // every row. Admitting on the CAUSE alone was the earlier version and\n".to_string()), " // was an unbounded green path — sound about decidability, wrong about\n".to_string()), " // admission, since it made every future anonymous-receiver call pass\n".to_string()), " // including one whose method does not exist (codex review 45459).\n".to_string()), " // What THIS control pins is the remaining half, the classification of\n".to_string()), " // an admitted one: COUNTED and NON-BLOCKING. Blocking it fabricates a\n".to_string()), " // refusal over the 18 measured sites; dropping it restores #7479 silence.\n".to_string()), " let unestablished_diag = std::rc::Rc::new(\n".to_string()), " crate::v1_std_core::CompilerDiagnostic::ReceiverTypeUnestablished {\n".to_string()), " method: \"probe\".to_string(),\n".to_string()), " span: std::rc::Rc::new(crate::std_types::SourceSpan {\n".to_string()), " file: \"probe.dag\".to_string(), start: 0, end: 0,\n".to_string()), " }),\n".to_string()), " });\n".to_string()), " assert!(\n".to_string()), " !crate::v1_std_core::is_error_diagnostic(unestablished_diag.clone())\n".to_string()), " && !crate::v1_std_core::is_interpreter_blocking_diagnostic(unestablished_diag.clone()),\n".to_string()), " \"ReceiverTypeUnestablished must not block — the receiver's type is an upstream deficit, so refusing here fabricates a refusal over correct code\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " crate::v1_std_core::is_discovery_corpus_advisory_typecheck_diagnostic(unestablished_diag.clone()),\n".to_string()), " \"...and it must be a COUNTED advisory, never absent — an uncounted degradation is the absorbing fallback DESIGN §5 forbids\"\n".to_string()), " );\n".to_string()), " // DISCRIMINATING CONTROL for the occurrence ratchet (codex reviews\n".to_string()), " // 45464 and 45491). A (module, method, receiver_shape) key bounds\n".to_string()), " // WHERE an unresolved call may live but not HOW MANY, and the rows\n".to_string()), " // need not be singletons (the multi-occurrence rows this once named\n".to_string()), " // were deleted at zero observed — see frontier_occurrence_budget_note —\n".to_string()), " // so the control takes whichever row survives and counts against\n".to_string()), " // its declared receiver shape rather than against Primitive()).\n".to_string()), " // The comparison is EQUALITY, not a ceiling: a ceiling lets seven\n".to_string()), " // shrink to six and a seventh come back silently, which is a static\n".to_string()), " // limit rather than a ratchet. Equality is safe because the check runs\n".to_string()), " // per MODULE, and every occurrence in a module is present whenever\n".to_string()), " // that module is typechecked at all — a closure that omits the module\n".to_string()), " // never runs its rows. Exercised at the mechanism so the boundary is\n".to_string()), " // exact on both sides.\n".to_string()), " let budget_row = rows.iter()\n".to_string()), " .next()\n".to_string()), " .expect(\"the frontier must hold at least one row to bound\");\n".to_string()), " let probe_span = || std::rc::Rc::new(crate::std_types::SourceSpan {\n".to_string()), " file: \"probe.dag\".to_string(), start: 0, end: 0,\n".to_string()), " });\n".to_string()), " let unestablished_at = |n: usize| {\n".to_string()), " std::rc::Rc::new((0..n).map(|_| std::rc::Rc::new(crate::v1_std_core::ErrorNode {\n".to_string()), " diagnostic: std::rc::Rc::new(crate::v1_std_core::CompilerDiagnostic::MethodExistenceFrontierAdmitted {\n".to_string()), " method: budget_row.method.clone(),\n".to_string()), " receiver_type: budget_row.receiver_shape.clone(),\n".to_string()), " trigger: String::new(),\n".to_string()), " span: probe_span(),\n".to_string()), " }),\n".to_string()), " module_name: budget_row.module_name.clone(),\n".to_string()), " })).collect::>())\n".to_string()), " };\n".to_string()), " let budget_at = |n: usize| crate::v1_compiler_infer::frontier_occurrence_budget_diags(\n".to_string()), " budget_row.module_name.clone(), probe_span(), unestablished_at(n));\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize).is_empty(),\n".to_string()), " \"exactly the declared occurrence count must pass\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize + 1).iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { .. })),\n".to_string()), " \"one MORE than the declared count must refuse — otherwise the row bounds where an unresolved call may live but not how many\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize + 1).iter().all(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone())),\n".to_string()), " \"...and that refusal must BLOCK, or the ratchet is decorative\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(0).iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { .. })),\n".to_string()), " \"FEWER than declared must ALSO refuse, and this is the half that makes it a ratchet rather than a ceiling: fixing a call forces the declared count DOWN, so a later reintroduction has no headroom to slip back into (codex review 45491)\"\n".to_string()), " );\n".to_string()), " // DISCRIMINATING PAIR for the early-return walk. An early return is a\n".to_string()), " // second exit from the same declaration and must meet its declared\n".to_string()), " // type; a return inside a nested LAMBDA belongs to the lambda's own\n".to_string()), " // callable return, so checking it against the enclosing declaration\n".to_string()), " // fabricates a refusal — the mirror image of the hole the walk closes\n".to_string()), " // (codex reviews 45472 and 45481, both confirmed by execution).\n".to_string()), " let early_return = compile_one(\"early_return.dag\",\n".to_string()), " \"module early_return\\nfn f(cond: Bool) -> Int { if cond { return \\\"wrong\\\" } 1 }\\n\".to_string());\n".to_string()), " assert!(\n".to_string()), " early_return.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::TypeMismatch { .. })),\n".to_string()), " \"an early return of the wrong type must refuse — the trailing expression conforming says nothing about the other exit, got: {:?}\",\n".to_string()), " early_return.diagnostics\n".to_string()), " );\n".to_string()), " let lambda_return = compile_one(\"lambda_return.dag\",\n".to_string()), " \"module lambda_return\\nfn apply_it(g: fn(Int) -> String, v: Int) -> String { g(v) }\\nfn outer(v: Int) -> Int { apply_it(g: x => { return \\\"inner\\\" }, v: v) 1 }\\n\".to_string());\n".to_string()), " assert!(\n".to_string()), " lambda_return.diagnostics.iter().all(|d| !matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::TypeMismatch { .. })),\n".to_string()), " \"a return inside a nested lambda belongs to the LAMBDA's declared return, not the enclosing function's — checking it here fabricates a refusal, got: {:?}\",\n".to_string()), " lambda_return.diagnostics\n".to_string()), " );\n".to_string()), " let peel_red = compile_one(\"peel_red.dag\", peel_src(\"filter_map(x => x)\"));\n".to_string()), " assert!(\n".to_string()), " peel_red.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. })),\n".to_string()), " \"peeling must make the receiver DECIDABLE, not merely quiet: a method absent from the peeled base must refuse as MethodNotFound rather than resting in the frontier, got: {:?}\",\n".to_string()), " peel_red.diagnostics\n".to_string()), " );\n".to_string()), " })\n".to_string()), " .expect(\"failed to spawn thread\")\n".to_string()), " .join();\n".to_string()), " result.expect(\"method_existence_wall_witness panicked\");\n".to_string()), " }\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn method_existence_wall_witness() {\n".to_string()), " // DISCRIMINATING RED for method_existence_wall_note. Before the wall an\n".to_string()), " // unresolved method inherited the RECEIVER's type with no diagnostic, so\n".to_string()), " // `xs |> filter_map(..)` on List typed as List, compiled clean, and\n".to_string()), " // died at InterpError::Unimplemented in live dispatch (#7479, HTTP 500).\n".to_string()), " let result = std::thread::Builder::new()\n".to_string()), " .stack_size(8 * 1024 * 1024)\n".to_string()), " .spawn(|| {\n".to_string()), " let red = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"red.dag\".to_string(),\n".to_string()), " content: \"module red\\nfn f(xs: List) -> List { xs |> filter_map(x => x) }\\nfn g(xs: List) -> Bool { xs |> starts_with(\\\"x\\\") }\\nfn h(xs: List) -> String { xs |> to_upper() }\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let red_result = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![red]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " let missing: Vec<_> = red_result.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " missing.len() >= 3,\n".to_string()), " \"expected MethodNotFound for the unresolved method AND for rostered names on a receiver that does not offer them (starts_with / to_upper on List — codex review 45327: a name-grain predicate admitted these), got: {:?}\",\n".to_string()), " red_result.diagnostics\n".to_string()), " );\n".to_string()), " // POSITIVE CONTROLS the wall must not touch: algebra method templates\n".to_string()), " // resolve at tier0, and `count` is in the declared std.methods roster\n".to_string()), " // even though free_monoid_scalar_templates omits it (the measured fork).\n".to_string()), " let green = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"green.dag\".to_string(),\n".to_string()), " content: \"module green\\nfn p(xs: List) -> List { xs |> filter(x => x > 1) |> map(x => x + 1) }\\nfn q(xs: List) -> Int { xs |> fold(0, (a, x) => a + x) }\\nfn r(s: String) -> Int { s |> count }\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let green_result = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![green]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " // The positive control asserts ZERO diagnostics, not merely no\n".to_string()), " // MethodNotFound. Filtering to the blocking variant let an ADVISORY\n".to_string()), " // MethodExistenceUndecided pass unnoticed, which is how `String |> count`\n".to_string()), " // was reported as a passing control while it was actually resolving\n".to_string()), " // through the non-blocking arm (codex review 45357).\n".to_string()), " assert!(\n".to_string()), " green_result.diagnostics.is_empty(),\n".to_string()), " \"legitimate methods must resolve with NO diagnostic of any severity — an advisory here means the call is not actually resolving, got: {:?}\",\n".to_string()), " green_result.diagnostics\n".to_string()), " );\n".to_string()), " // DISCRIMINATING RED for the declared frontier (codex reviews 45357,\n".to_string()), " // 45383). An undecided method existence must BLOCK, so the graph is\n".to_string()), " // never emitted on an unestablished judgment; the only non-blocking\n".to_string()), " // path is a DECLARED frontier row, which is countable and carries its\n".to_string()), " // own dissolution trigger. The two halves must be discriminated by\n".to_string()), " // MODULE NAME on otherwise identical source — an admission that fires\n".to_string()), " // for any module is an escape hatch, not a frontier.\n".to_string()), " let frontier_src = |module: &str, method: &str| {\n".to_string()), " format!(\"module {}\\ntype T {{ a: Int }}\\nfn f(t: T) -> Int {{ t |> {}(1) }}\\n\", module, method)\n".to_string()), " };\n".to_string()), " let compile_one = |path: &str, content: String| {\n".to_string()), " crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![std::rc::Rc::new(\n".to_string()), " crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: path.to_string(),\n".to_string()), " content,\n".to_string()), " }\n".to_string()), " )]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " )\n".to_string()), " };\n".to_string()), " let unlisted = compile_one(\"unlisted.dag\", frontier_src(\"unlisted.module\", \"list_push\"));\n".to_string()), " let undecided: Vec<_> = unlisted.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " !undecided.is_empty(),\n".to_string()), " \"a method whose existence is undecided in a module with NO declared frontier row must refuse, got: {:?}\",\n".to_string()), " unlisted.diagnostics\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " undecided.iter().all(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone())\n".to_string()), " && crate::v1_std_core::is_interpreter_blocking_diagnostic(d.diagnostic.clone())),\n".to_string()), " \"MethodExistenceUndecided must BLOCK both typecheck and the interpreter — a non-blocking undecided judgment is exactly the fail-open the wall exists to close\"\n".to_string()), " );\n".to_string()), " // The SHAPE is part of the key, and this is the half that answers\n".to_string()), " // codex review 45398: the module and the method both match a declared\n".to_string()), " // row, and the call is STILL refused, because the receiver does not\n".to_string()), " // fail to resolve in the way the row was measured on. A (module, method)\n".to_string()), " // key passed this input; that is the fail-open the third component closes.\n".to_string()), " let listed_wrong_shape = compile_one(\"listed.dag\", frontier_src(\"v1.compiler.trace\", \"map\"));\n".to_string()), " assert!(\n".to_string()), " listed_wrong_shape.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodExistenceUndecided { .. })),\n".to_string()), " \"a declared row must NOT admit a new call in the same module on a receiver whose shape it was never measured on, got: {:?}\",\n".to_string()), " listed_wrong_shape.diagnostics\n".to_string()), " );\n".to_string()), " // The admission half, checked against the frontier data itself rather\n".to_string()), " // than a synthetic source, because the residual shapes (an unnamed\n".to_string()), " // lambda-parameter receiver, a coproduct payload typed as its variant)\n".to_string()), " // are upstream resolution defects that cannot be conjured on demand.\n".to_string()), " // Every declared row must be admitted by its own exact key, and\n".to_string()), " // perturbing ANY of the three components must withdraw the admission.\n".to_string()), " let rows = crate::v1_compiler_infer::unresolved_method_frontier();\n".to_string()), " assert!(!rows.is_empty(), \"the frontier must be a declared, countable roster\");\n".to_string()), " for r in rows.iter() {\n".to_string()), " assert!(\n".to_string()), " crate::v1_compiler_infer::unresolved_method_frontier_trigger(\n".to_string()), " r.module_name.clone(), r.method.clone(), r.receiver_shape.clone()).is_some(),\n".to_string()), " \"row {}/{}/{} must be admitted by its own key\", r.module_name, r.method, r.receiver_shape\n".to_string()), " );\n".to_string()), " for perturbed in [\n".to_string()), " (format!(\"{}.other\", r.module_name), r.method.clone(), r.receiver_shape.clone()),\n".to_string()), " (r.module_name.clone(), format!(\"{}_other\", r.method), r.receiver_shape.clone()),\n".to_string()), " (r.module_name.clone(), r.method.clone(), format!(\"Product(Other{})\", r.receiver_shape.len())),\n".to_string()), " ] {\n".to_string()), " assert!(\n".to_string()), " crate::v1_compiler_infer::unresolved_method_frontier_trigger(\n".to_string()), " perturbed.0.clone(), perturbed.1.clone(), perturbed.2.clone()).is_none(),\n".to_string()), " \"perturbing the key to {:?} must WITHDRAW the admission — each component is load-bearing\", perturbed\n".to_string()), " );\n".to_string()), " }\n".to_string()), " }\n".to_string()), " // DISCRIMINATING RED for where_refinement_receiver_peel_note. A\n".to_string()), " // where-refinement alias reached method lookup as Product()\n".to_string()), " // because resolve_method_receiver_type short-circuits on Conj, so the\n".to_string()), " // base's algebra profile was never consulted. The peel must make the\n".to_string()), " // receiver decidable in BOTH directions on the SAME alias — a green\n".to_string()), " // that only proves the refusal stopped is indistinguishable from\n".to_string()), " // deleting the wall.\n".to_string()), " let peel_src = |call: &str| {\n".to_string()), " format!(\"module peel\\ntype Tight = String where string_non_empty\\nfn f(s: Tight) -> Int {{ s |> {} }}\\n\", call)\n".to_string()), " };\n".to_string()), " let peel_green = compile_one(\"peel_green.dag\", peel_src(\"count\"));\n".to_string()), " assert!(\n".to_string()), " peel_green.diagnostics.is_empty(),\n".to_string()), " \"a method on the refinement's String base must RESOLVE once the base is peeled — no diagnostic of any severity, got: {:?}\",\n".to_string()), " peel_green.diagnostics\n".to_string()), " );\n".to_string()), " // DISCRIMINATING RED for the two special-case arms (codex review\n".to_string()), " // 45430). The wall was reached only from the FINAL else, so an\n".to_string()), " // unresolved map_keys / map_values still returned the RECEIVER type\n".to_string()), " // with an empty diagnostic list — the exact success-shaped fallback\n".to_string()), " // this PR deletes, surviving in the two branches that ran before it.\n".to_string()), " let special = compile_one(\"special.dag\",\n".to_string()), " \"module special\\nfn f(xs: List) -> List {{ xs |> map_keys }}\\nfn g(xs: List) -> List {{ xs |> map_values }}\\n\".replace(\"{{\", \"{\").replace(\"}}\", \"}\"));\n".to_string()), " let special_missing: Vec<_> = special.diagnostics.iter()\n".to_string()), " .filter(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. }))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " special_missing.len() >= 2,\n".to_string()), " \"map_keys and map_values must route through the SAME refusal as every other method when the receiver is not a keyed collection — a wall reachable only from the final else is not a wall, got: {:?}\",\n".to_string()), " special.diagnostics\n".to_string()), " );\n".to_string()), " // ReceiverTypeUnestablished CLASSIFICATION control. The behavioural\n".to_string()), " // evidence for this class is the corpus census, not a synthetic\n".to_string()), " // source: 18 sites whole-tree, counted as advisories with the gate at\n".to_string()), " // zero blocking diagnostics. A synthetic reproducer WAS attempted --\n".to_string()), " // an untyped lambda parameter inside a record-field fn, the shape the\n".to_string()), " // real sites have -- and it produced no diagnostic at all, so it did\n".to_string()), " // not reproduce the shape and is not asserted here as though it did.\n".to_string()), " // ADMISSION is roster-gated, not automatic: an anonymous receiver is\n".to_string()), " // admitted only where a declared row names (module, method,\n".to_string()), " // Primitive()), and the perturbation loop above proves that gate for\n".to_string()), " // every row. Admitting on the CAUSE alone was the earlier version and\n".to_string()), " // was an unbounded green path — sound about decidability, wrong about\n".to_string()), " // admission, since it made every future anonymous-receiver call pass\n".to_string()), " // including one whose method does not exist (codex review 45459).\n".to_string()), " // What THIS control pins is the remaining half, the classification of\n".to_string()), " // an admitted one: COUNTED and NON-BLOCKING. Blocking it fabricates a\n".to_string()), " // refusal over the 18 measured sites; dropping it restores #7479 silence.\n".to_string()), " let unestablished_diag = std::rc::Rc::new(\n".to_string()), " crate::v1_std_core::CompilerDiagnostic::ReceiverTypeUnestablished {\n".to_string()), " method: \"probe\".to_string(),\n".to_string()), " span: std::rc::Rc::new(crate::std_types::SourceSpan {\n".to_string()), " file: \"probe.dag\".to_string(), start: 0, end: 0,\n".to_string()), " }),\n".to_string()), " });\n".to_string()), " assert!(\n".to_string()), " !crate::v1_std_core::is_error_diagnostic(unestablished_diag.clone())\n".to_string()), " && !crate::v1_std_core::is_interpreter_blocking_diagnostic(unestablished_diag.clone()),\n".to_string()), " \"ReceiverTypeUnestablished must not block — the receiver's type is an upstream deficit, so refusing here fabricates a refusal over correct code\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " crate::v1_std_core::is_discovery_corpus_advisory_typecheck_diagnostic(unestablished_diag.clone()),\n".to_string()), " \"...and it must be a COUNTED advisory, never absent — an uncounted degradation is the absorbing fallback DESIGN §5 forbids\"\n".to_string()), " );\n".to_string()), " // DISCRIMINATING CONTROL for the occurrence ratchet (codex reviews\n".to_string()), " // 45464 and 45491). A (module, method, receiver_shape) key bounds\n".to_string()), " // WHERE an unresolved call may live but not HOW MANY, and the rows\n".to_string()), " // need not be singletons (the multi-occurrence rows this once named\n".to_string()), " // were deleted at zero observed — see frontier_occurrence_budget_note —\n".to_string()), " // so the control takes whichever row survives and counts against\n".to_string()), " // its declared receiver shape rather than against Primitive()).\n".to_string()), " // The comparison is EQUALITY, not a ceiling: a ceiling lets seven\n".to_string()), " // shrink to six and a seventh come back silently, which is a static\n".to_string()), " // limit rather than a ratchet. Equality is safe because the check runs\n".to_string()), " // per MODULE, and every occurrence in a module is present whenever\n".to_string()), " // that module is typechecked at all — a closure that omits the module\n".to_string()), " // never runs its rows. Exercised at the mechanism so the boundary is\n".to_string()), " // exact on both sides.\n".to_string()), " let budget_row = rows.iter()\n".to_string()), " .next()\n".to_string()), " .expect(\"the frontier must hold at least one row to bound\");\n".to_string()), " let probe_span = || std::rc::Rc::new(crate::std_types::SourceSpan {\n".to_string()), " file: \"probe.dag\".to_string(), start: 0, end: 0,\n".to_string()), " });\n".to_string()), " let unestablished_at = |n: usize| {\n".to_string()), " std::rc::Rc::new((0..n).map(|_| std::rc::Rc::new(crate::v1_std_core::ErrorNode {\n".to_string()), " diagnostic: std::rc::Rc::new(crate::v1_std_core::CompilerDiagnostic::MethodExistenceFrontierAdmitted {\n".to_string()), " method: budget_row.method.clone(),\n".to_string()), " receiver_type: budget_row.receiver_shape.clone(),\n".to_string()), " trigger: String::new(),\n".to_string()), " span: probe_span(),\n".to_string()), " }),\n".to_string()), " module_name: budget_row.module_name.clone(),\n".to_string()), " })).collect::>())\n".to_string()), " };\n".to_string()), " let budget_at = |n: usize| crate::v1_compiler_infer::frontier_occurrence_budget_diags(\n".to_string()), " budget_row.module_name.clone(), probe_span(), unestablished_at(n));\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize).is_empty(),\n".to_string()), " \"exactly the declared occurrence count must pass\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize + 1).iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { .. })),\n".to_string()), " \"one MORE than the declared count must refuse — otherwise the row bounds where an unresolved call may live but not how many\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(budget_row.occurrences as usize + 1).iter().all(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone())),\n".to_string()), " \"...and that refusal must BLOCK, or the ratchet is decorative\"\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " budget_at(0).iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { .. })),\n".to_string()), " \"FEWER than declared must ALSO refuse, and this is the half that makes it a ratchet rather than a ceiling: fixing a call forces the declared count DOWN, so a later reintroduction has no headroom to slip back into (codex review 45491)\"\n".to_string()), " );\n".to_string()), " // DISCRIMINATING PAIR for the early-return walk. An early return is a\n".to_string()), " // second exit from the same declaration and must meet its declared\n".to_string()), " // type; a return inside a nested LAMBDA belongs to the lambda's own\n".to_string()), " // callable return, so checking it against the enclosing declaration\n".to_string()), " // fabricates a refusal — the mirror image of the hole the walk closes\n".to_string()), " // (codex reviews 45472 and 45481, both confirmed by execution).\n".to_string()), " let early_return = compile_one(\"early_return.dag\",\n".to_string()), " \"module early_return\\nfn f(cond: Bool) -> Int { if cond { return \\\"wrong\\\" } 1 }\\n\".to_string());\n".to_string()), " assert!(\n".to_string()), " early_return.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::TypeMismatch { .. })),\n".to_string()), " \"an early return of the wrong type must refuse — the trailing expression conforming says nothing about the other exit, got: {:?}\",\n".to_string()), " early_return.diagnostics\n".to_string()), " );\n".to_string()), " let lambda_return = compile_one(\"lambda_return.dag\",\n".to_string()), " \"module lambda_return\\nfn apply_it(g: fn(Int) -> String, v: Int) -> String { g(v) }\\nfn outer(v: Int) -> Int { apply_it(g: x => { return \\\"inner\\\" }, v: v) 1 }\\n\".to_string());\n".to_string()), " assert!(\n".to_string()), " lambda_return.diagnostics.iter().all(|d| !matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::TypeMismatch { .. })),\n".to_string()), " \"a return inside a nested lambda belongs to the LAMBDA's declared return, not the enclosing function's — checking it here fabricates a refusal, got: {:?}\",\n".to_string()), " lambda_return.diagnostics\n".to_string()), " );\n".to_string()), " let peel_red = compile_one(\"peel_red.dag\", peel_src(\"filter_map(x => x)\"));\n".to_string()), " assert!(\n".to_string()), " peel_red.diagnostics.iter().any(|d| matches!(*d.diagnostic, crate::v1_std_core::CompilerDiagnostic::MethodNotFound { .. })),\n".to_string()), " \"peeling must make the receiver DECIDABLE, not merely quiet: a method absent from the peeled base must refuse as MethodNotFound rather than resting in the frontier, got: {:?}\",\n".to_string()), " peel_red.diagnostics\n".to_string()), " );\n".to_string()), " })\n".to_string()), " .expect(\"failed to spawn thread\")\n".to_string()), " .join();\n".to_string()), " result.expect(\"method_existence_wall_witness panicked\");\n".to_string()), " }\n\n".to_string()) } pub fn ct_declared_type_conformance_witness_test() -> String { diff --git a/src/v1/stage0/src/v1_compiler_complexity.rs b/src/v1/stage0/src/v1_compiler_complexity.rs index fcbdbcfd653..7d32976ba45 100644 --- a/src/v1/stage0/src/v1_compiler_complexity.rs +++ b/src/v1/stage0/src/v1_compiler_complexity.rs @@ -358,12 +358,7 @@ pub fn iteration_element_name( { let params = crate::v1_std_core::lambda_param_names_at(lambda.clone(), si.clone()); match method_callback_element_position(method_semantics.clone()) { - Some(pos) => params - .clone() - .iter() - .cloned() - .skip(pos.clone() as usize) - .next(), + Some(pos) => params.clone().get((pos.clone()) as usize).cloned(), std::option::Option::None => std::option::Option::None, } } @@ -710,14 +705,10 @@ pub fn parser_block_state_progress( match stmts.clone().last().cloned() { std::option::Option::None => DescentEvidence::DescentUnknown, Some(last_stmt) => { - let leading = Rc::new( - stmts - .clone() - .iter() - .cloned() - .take(v1_rt::int_sub((stmts.clone().len() as i64), 1) as usize) - .collect::>(), - ); + let leading = Rc::new(v1_rt::list_take( + &stmts.clone(), + v1_rt::int_sub((stmts.clone().len() as i64), 1), + )); let final_env = leading.iter().cloned().fold( env.clone(), |acc_env: Rc, stmt: Rc| match (*stmt.expr_data.clone()) @@ -1364,13 +1355,10 @@ pub fn parser_success_progress( } } ExprData::ExprBlock => { - let prefix = v1_rt::reverse(Rc::new( - v1_rt::reverse(expr.children.clone()) - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - )); + let prefix = v1_rt::reverse(Rc::new(v1_rt::list_skip( + &v1_rt::reverse(expr.children.clone()), + 1, + ))); let acc = prefix.iter().cloned().fold( Rc::new(ParserProgressAcc { edges: Rc::new(vec![]), @@ -1486,14 +1474,7 @@ pub fn infer_parser_always_advancing_members_worklist( let mut si = __tco_loop_si; match queue.clone().first().cloned() { Some(name) => { - let rest = Rc::new( - queue - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&queue.clone(), 1)); if set_has(proven.clone(), name.clone()) { { let __tco_0 = rest.clone(); @@ -10220,10 +10201,8 @@ pub fn merge_param_evidence( DescentEvidence::Strict, |acc: DescentEvidence, call_evidence: Rc>>| match call_evidence .clone() - .iter() + .get((param_index.clone()) as usize) .cloned() - .skip(param_index.clone() as usize) - .next() { Some(rel) => crate::std_termination::descent_evidence_lattice_meet( acc.clone(), @@ -10246,10 +10225,8 @@ pub fn extract_shrink_factor( std::option::Option::None => std::option::Option::None, Some(prev) => match call_evidence .clone() - .iter() + .get((param_index.clone()) as usize) .cloned() - .skip(param_index.clone() as usize) - .next() { Some(rel) => match (*rel.clone()).clone() { SubValueRelation::StrictSubValue { factor: f, .. } => { @@ -10301,10 +10278,8 @@ pub fn max_path_descending( match de.clone() { Some(evidence) => match evidence .clone() - .iter() + .get((param_index.clone()) as usize) .cloned() - .skip(param_index.clone() as usize) - .next() { Some(rel) => match (*rel.clone()).clone() { SubValueRelation::StrictSubValue { .. } => 1, @@ -10420,10 +10395,8 @@ pub fn distinct_descended_fields( |acc: Rc>, call_evidence: Rc>>| { match call_evidence .clone() - .iter() + .get((param_index.clone()) as usize) .cloned() - .skip(param_index.clone() as usize) - .next() { Some(rel) => match (*rel.clone()).clone() { SubValueRelation::StrictSubValue { field: f, .. } => { diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 7700517388f..c0bf7f72bff 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -9,6 +9,11 @@ use self::FileResultChannel::*; use self::FileVerb::*; use self::FuncBodyShape::*; use self::JsonFragmentsAccum::*; +use self::NativeEffectContract::*; +use self::NativeEffectHandler::*; +use self::NativeEffectRealizationAdmission::*; +use self::NativeEffectResolution::*; +use self::NativeEffectUnavailableCause::*; use self::ShellEmissionRefusal::*; use self::ShellResultChannel::*; use self::TargetEmissionMode::*; @@ -21,6 +26,17 @@ pub use crate::std_coercion::TypeDeclarationProvenance; use crate::std_coercion::TypeDeclarationProvenance::DeclarationIdentityAbsent; pub use crate::std_coercion::TypeRealizationDecision; use crate::std_coercion::TypeRealizationDecision::*; +use crate::std_compiler_entry::CompilerEntryDriver::{ + DirectIngestDriver, NativeClaimDriver, NativeCliDriver, NativeServeDriver, + RetainedHostCliKernel, SourceRootEvalDriver, +}; +use crate::std_compiler_entry::NativeEffectClass::{NativeEffectDeclaration, NativeOutgoingHttp}; +use crate::std_compiler_entry::NativeEffectScope::{NativeAdmitAllExcept, NativeAdmitOnly}; +pub use crate::std_compiler_entry::{ + CompilerEntryDriver, NativeEffectClass, NativeEffectDenominator, NativeEffectScope, +}; +pub use crate::std_decl_ref::DeclarationRef; +pub use crate::std_decl_ref::{decl_ref, declaration_ref_eq}; use crate::std_induction::SubValueRelation::SubValueUnknown; pub use crate::std_induction::{InductiveField, SubValueRelation}; pub use crate::std_occurrence_identity::occurrence_id_eq; @@ -55,6 +71,7 @@ pub use crate::v1_compiler_emit_core_support::{EmitResult, TestProjection}; pub use crate::v1_compiler_infer::InferScope; pub use crate::v1_compiler_infer::{ build_params_scope, call_param_caller_labels, extend_scope, is_where_refinement_type, + resolved_type_name, }; use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::{ DataVariantBareString, DataVariantInternalTagged, DataVariantSpellingRefused, @@ -62,9 +79,14 @@ use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::{ }; pub use crate::v1_compiler_infer_emit_info::{DataVariantWireSpelling, EmitGraphInfo, TypeSummary}; use crate::v1_compiler_infer_env::GlobalBareLookupState::*; +use crate::v1_compiler_infer_env::TypeReferenceDeclarationReading::TypeReferenceNamesDeclaration; pub use crate::v1_compiler_infer_env::UnitVariantContribution; -pub use crate::v1_compiler_infer_env::{authored_name, empty_symbol_index, lookup_type_for}; -pub use crate::v1_compiler_infer_env::{GlobalBareLookupState, TypeBinding, TypeEnv}; +pub use crate::v1_compiler_infer_env::{ + authored_name, empty_symbol_index, lookup_type_for, type_reference_declaration_reading, +}; +pub use crate::v1_compiler_infer_env::{ + GlobalBareLookupState, TypeBinding, TypeEnv, TypeReferenceDeclarationReading, +}; pub use crate::v1_compiler_infer_items::{item_is_effectful_callee, item_resource_names}; pub use crate::v1_compiler_infer_items::{ItemInfo, ResolvedGraph, TypedModule}; pub use crate::v1_compiler_infer_lookup::lookup_func_sig; @@ -110,7 +132,7 @@ use crate::v1_std_core::CallSemantics::ResolvedDirectCallSemantics; use crate::v1_std_core::CallTargetIdentity::{ CallableTargetUndetermined, LocallyBoundCall, RuntimePrimitiveCall, SourceDeclarationCall, }; -use crate::v1_std_core::Cardinality::CardOptional; +use crate::v1_std_core::Cardinality::{CardOptional, Required}; use crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled; use crate::v1_std_core::Connective::{Arrow, Conj, Disj, NoConnective}; use crate::v1_std_core::ExprData::{ @@ -840,16 +862,12 @@ pub fn order_typed_call_args_from_semantics( for app in plan.iter().cloned() { __result.extend( (*match app.matched_argument_index.clone() { - Some(argument_index) => match args - .clone() - .iter() - .cloned() - .skip(argument_index.clone() as usize) - .next() - { - Some(arg) => Rc::new(vec![arg.clone()]), - std::option::Option::None => Rc::new(vec![]), - }, + Some(argument_index) => { + match args.clone().get((argument_index.clone()) as usize).cloned() { + Some(arg) => Rc::new(vec![arg.clone()]), + std::option::Option::None => Rc::new(vec![]), + } + } std::option::Option::None => Rc::new(vec![]), }) .iter() @@ -895,7 +913,7 @@ pub fn has_nested_records_node( source_indices.clone(), ); if is_map.clone() { - match n.children.clone().iter().cloned().skip(1 as usize).next() { + match n.children.clone().get((1) as usize).cloned() { Some(val_child) => { let __tco_0 = crate::v1_compiler_infer_types::child_type_node(val_child.clone()); @@ -1026,6 +1044,64 @@ pub fn emit_data_fields_json( ) } +pub fn emit_declared_optional_row_json( + value: Rc, + inner_type_name: String, + source_indices: Rc>>, + variant_wire: Rc>>, +) -> Rc { + match (*value.expr_data.clone()).clone() { + ExprData::ExprLiteral { ref value, .. } + if matches!(value.as_ref(), LiteralValue::LitNull) => + { + let LiteralValue::LitNull = value.as_ref() else { + unreachable!() + }; + Rc::new(EmitterOutcome::Emitted { + json: "null".to_string(), + }) + } + ExprData::ExprRecordLit { + parent_enum: pe, .. + } => match pe.clone() { + Some(parent) => { + if (crate::v1_std_core::qualified_last_segment(parent.clone()) + == crate::v1_std_core::qualified_last_segment(inner_type_name.clone())) + { + Rc::new(EmitterOutcome::Refused { + reason: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("a data row declared ".to_string(), inner_type_name.clone()), "? is initialized with a bare ".to_string()), inner_type_name.clone()), " literal: no declared coercion lifts T into T? (DESIGN 4, 2026-09-26), so spell it Present { value: .. } or none".to_string()), +}) + } else { + match value.children.clone().first().cloned() { + std::option::Option::None => Rc::new(EmitterOutcome::Emitted { + json: "null".to_string(), + }), + Some(payload_field) => { + if ((value.children.clone().len() as i64) == 1) { + emit_data_value_json( + crate::v1_std_core::field_init_node_value( + payload_field.clone(), + ), + source_indices.clone(), + variant_wire.clone(), + ) + } else { + Rc::new(EmitterOutcome::Refused { + reason: v1_rt::concat(v1_rt::concat("an optional row's present literal carries ".to_string(), crate::v1_compiler_emit_core_support::to_string((value.children.clone().len() as i64))), " fields; the option's present arm carries exactly its payload".to_string()), +}) + } + } + } + } + } + std::option::Option::None => { + emit_data_value_json(value.clone(), source_indices.clone(), variant_wire.clone()) + } + }, + _ => emit_data_value_json(value.clone(), source_indices.clone(), variant_wire.clone()), + } +} + pub fn emit_data_value_json( value: Rc, source_indices: Rc>>, @@ -1964,29 +2040,26 @@ pub fn render_node_type( } std::option::Option::None => "_".to_string(), }; - let second_child = - match n.children.clone().iter().cloned().skip(1 as usize).next() { - Some(c) => { - if (c.inferred.clone() != std::option::Option::None) { - render_node_type( - crate::v1_compiler_infer_types::resolved_type( - c.clone(), - ), - target.clone(), - shared_types.clone(), - source_indices.clone(), - ) - } else { - render_node_type( - c.clone(), - target.clone(), - shared_types.clone(), - source_indices.clone(), - ) - } + let second_child = match n.children.clone().get((1) as usize).cloned() { + Some(c) => { + if (c.inferred.clone() != std::option::Option::None) { + render_node_type( + crate::v1_compiler_infer_types::resolved_type(c.clone()), + target.clone(), + shared_types.clone(), + source_indices.clone(), + ) + } else { + render_node_type( + c.clone(), + target.clone(), + shared_types.clone(), + source_indices.clone(), + ) } - std::option::Option::None => "_".to_string(), - }; + } + std::option::Option::None => "_".to_string(), + }; let tuple_str = render_tuple_parts( Rc::new(vec![first_child.clone(), second_child.clone()]), target.clone(), @@ -2144,7 +2217,7 @@ pub fn render_node_type( ), std::option::Option::None => "_".to_string(), }; - let v = match n.children.clone().iter().cloned().skip(1 as usize).next() { + let v = match n.children.clone().get((1) as usize).cloned() { Some(vn) => render_node_type( crate::v1_compiler_infer_types::child_type_node(vn.clone()), target.clone(), @@ -2271,7 +2344,7 @@ pub fn render_tuple_parts(parts: Rc>, target: RenderTarget) -> Strin if ((parts.clone().len() as i64) > 0) { if ((parts.clone().len() as i64) == 2) { match parts.clone().first().cloned() { - Some(p0) => match parts.clone().iter().cloned().skip(1 as usize).next() { + Some(p0) => match parts.clone().get((1) as usize).cloned() { Some(p1) => crate::v1_compiler_emit_core_support::apply_type_template2( ts.pair_template.clone(), p0.clone(), @@ -2827,6 +2900,7 @@ pub struct ShellResultField { pub enum ShellEmissionRefusal { ShellOutputKeyNotModeled { key: String }, ShellChannelNotRealizedByTarget { key: String, target_name: String }, + ShellCapturePolicyInputAbsent { key: String }, } impl ShellEmissionRefusal { pub fn key(&self) -> String { @@ -2835,6 +2909,7 @@ impl ShellEmissionRefusal { ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: __val, .. } => { __val.clone() } + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: __val, .. } => __val.clone(), } } } @@ -2891,12 +2966,27 @@ pub fn shell_result_channel_key(c: ShellResultChannel) -> String { } } -pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { +pub fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> bool { match c.clone() { - ShellResultChannel::ShellChanStderrTruncated => false, - ShellResultChannel::ShellChanStderrTotalBytes => false, - ShellResultChannel::ShellChanStderrRetainedBytes => false, - _ => target_renders_shell_transport(target.clone()), + ShellResultChannel::ShellChanStderrTruncated => true, + ShellResultChannel::ShellChanStderrTotalBytes => true, + ShellResultChannel::ShellChanStderrRetainedBytes => true, + _ => false, + } +} + +pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { + if shell_channel_is_capture_accounting(c.clone()) { + target_is_rust(target.clone()) + } else { + target_renders_shell_transport(target.clone()) + } +} + +pub fn target_is_rust(target: RenderTarget) -> bool { + match target.clone() { + RenderTarget::Rust => true, + _ => false, } } @@ -2911,6 +3001,7 @@ pub fn shell_emission_refusal_fact(refusal: Rc) -> String match (*refusal.clone()).clone() { ShellEmissionRefusal::ShellOutputKeyNotModeled { key: k, .. } => v1_rt::concat(v1_rt::concat("shell transport output key '".to_string(), k.clone()), "' has no modeled channel -- the modeled channels are stdout, stderr, exit_success, success, exists, exit_code, stdout_lines, stderr_truncated, stderr_total_bytes and stderr_retained_bytes".to_string()), ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: k, target_name: tn, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a modeled channel that target ".to_string()), tn.clone()), " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran".to_string()), + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: k, .. } => v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a capture-accounting channel and this operation does not declare a required scalar stderr_capture: WitnessStderrCapturePolicy -- answering it would apply a policy nobody declared".to_string()), } } @@ -3077,6 +3168,531 @@ pub fn bind_operation_transport( } } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectContract { + NativeServiceEffect { + declaration: Rc, + }, + NativeResourceEffect { + binding: String, + declaration: Rc, + }, +} +impl NativeEffectContract { + pub fn declaration(&self) -> Rc { + match self { + NativeEffectContract::NativeServiceEffect { + declaration: __val, .. + } => __val.clone(), + NativeEffectContract::NativeResourceEffect { + declaration: __val, .. + } => __val.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectHandler { + NativeTransportHandler { transports: Rc> }, + NativeCapabilityToken, +} +impl NativeEffectHandler { + pub fn transports(&self) -> Rc> { + match self { + NativeEffectHandler::NativeTransportHandler { + transports: __val, .. + } => __val.clone(), + NativeEffectHandler::NativeCapabilityToken => panic!("no transports on unit variant"), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectUnavailableCause { + NativeOperationBindingRefused { + operation: String, + refusal: Rc, + }, + NativeResourceTraitHasNoImplementor { + capability_count: i64, + }, + NativeEffectDeclarationAbsent, + NativeEffectDeclarationAmbiguous { + candidates: i64, + }, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectRealizationAdmission { + NativeEffectHandlerBound { + effect_contract: Rc, + handler_identity: Rc, + }, + NativeEffectOutsideProductionDenominator { + effect_contract: Rc, + denominator: Rc, + }, + NativeEffectRealizationUnavailable { + effect_contract: Rc, + cause: Rc, + }, +} +impl NativeEffectRealizationAdmission { + pub fn effect_contract(&self) -> Rc { + match self { + NativeEffectRealizationAdmission::NativeEffectHandlerBound { + effect_contract: __val, + .. + } => __val.clone(), + NativeEffectRealizationAdmission::NativeEffectOutsideProductionDenominator { + effect_contract: __val, + .. + } => __val.clone(), + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: __val, + .. + } => __val.clone(), + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NativeEffectDeclSite { + pub item: Rc, + pub module_name: String, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum NativeEffectResolution { + NativeEffectResolved { + site: Rc, + }, + NativeEffectUnresolved { + cause: Rc, + }, +} + +pub fn native_effect_resolution( + sites: Rc>>, +) -> Rc { + match sites.clone().first().cloned() { + std::option::Option::None => Rc::new(NativeEffectResolution::NativeEffectUnresolved { + cause: Rc::new(NativeEffectUnavailableCause::NativeEffectDeclarationAbsent), + }), + Some(site) => { + if ((sites.clone().len() as i64) == 1) { + Rc::new(NativeEffectResolution::NativeEffectResolved { site: site.clone() }) + } else { + Rc::new(NativeEffectResolution::NativeEffectUnresolved { + cause: Rc::new( + NativeEffectUnavailableCause::NativeEffectDeclarationAmbiguous { + candidates: (sites.clone().len() as i64), + }, + ), + }) + } + } + } +} + +pub fn native_effect_class_matches( + class: Rc, + declaration: Rc, + transports: Rc>, +) -> bool { + match (*class.clone()).clone() { + NativeEffectClass::NativeEffectDeclaration { declaration: d, .. } => { + ((d.module_path.clone() == declaration.module_path.clone()) + && (d.decl_name.clone() == declaration.decl_name.clone())) + } + NativeEffectClass::NativeOutgoingHttp => { + let mut __found = false; + for t in transports.iter().cloned() { + if (t.clone() == TransportKind::RestTransport) { + __found = true; + break; + } + } + __found + } + } +} + +pub fn native_effect_outside_denominator( + denominator: Rc, + declaration: Rc, + transports: Rc>, +) -> bool { + match (*denominator.scope.clone()).clone() { + NativeEffectScope::NativeAdmitAllExcept { excluded: xs, .. } => { + let mut __found = false; + for x in xs.iter().cloned() { + if native_effect_class_matches(x.clone(), declaration.clone(), transports.clone()) { + __found = true; + break; + } + } + __found + } + NativeEffectScope::NativeAdmitOnly { admitted: xs, .. } => { + ({ + let mut __found = false; + for x in xs.iter().cloned() { + if native_effect_class_matches( + x.clone(), + declaration.clone(), + transports.clone(), + ) { + __found = true; + break; + } + } + __found + } == false) + } + } +} + +pub fn bound_operation_transport_kind(bound: Rc) -> Option { + match (*bound.clone()).clone() { + BoundOperation::RestBound { transport: _, .. } => Some(TransportKind::RestTransport), + BoundOperation::ShellBound { .. } => Some(TransportKind::ShellTransport), + BoundOperation::FileBound { .. } => Some(TransportKind::FileTransport), + BoundOperation::LocalBound => Some(TransportKind::LocalTransport), + BoundOperation::BindingRefused { cause: _, .. } => std::option::Option::None, + } +} + +pub fn native_transport_kinds_insert( + kinds: Rc>, + kind: TransportKind, +) -> Rc> { + if { + let mut __found = false; + for k in kinds.iter().cloned() { + if (k.clone() == kind.clone()) { + __found = true; + break; + } + } + __found + } { + kinds.clone() + } else { + v1_rt::concat(kinds.clone(), Rc::new(vec![kind.clone()])) + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct BoundOperationAt { + pub operation: String, + pub bound: Rc, +} + +pub fn native_service_bound_operations( + svc: Rc, + source_indices: Rc>>, +) -> Rc>> { + { + let fallback = service_fallback_transport(svc.clone()); + Rc::new({ + let mut __result = Vec::new(); + for op in svc.children.clone().iter().cloned() { + __result.push(Rc::new(BoundOperationAt { + operation: crate::v1_std_core::authored_name_at( + source_indices.clone(), + op.clone(), + ), + bound: bind_operation_transport( + effective_operation_transport(op.clone(), fallback.clone()), + op.clone(), + source_indices.clone(), + ), + })); + } + __result + }) + } +} + +pub fn admit_native_service_effect( + service: String, + resolution: Rc, + denominator: Rc, + source_indices: Rc>>, +) -> Rc { + match (*resolution.clone()).clone() { + NativeEffectResolution::NativeEffectUnresolved { cause: cz, .. } => Rc::new( + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: Rc::new(NativeEffectContract::NativeServiceEffect { + declaration: crate::std_decl_ref::decl_ref( + "".to_string(), + service.clone(), + ), + }), + cause: cz.clone(), + }, + ), + NativeEffectResolution::NativeEffectResolved { site: site, .. } => { + let declaration = + crate::std_decl_ref::decl_ref(site.module_name.clone(), service.clone()); + let contract = Rc::new(NativeEffectContract::NativeServiceEffect { + declaration: declaration.clone(), + }); + let bounds = native_service_bound_operations(site.item.clone(), source_indices.clone()); + let transports = bounds.iter().cloned().fold( + Rc::new(vec![]), + |acc: Rc>, b: Rc| { + match bound_operation_transport_kind(b.bound.clone()) { + Some(k) => native_transport_kinds_insert(acc.clone(), k.clone()), + std::option::Option::None => acc.clone(), + } + }, + ); + if native_effect_outside_denominator( + denominator.clone(), + declaration.clone(), + transports.clone(), + ) { + Rc::new( + NativeEffectRealizationAdmission::NativeEffectOutsideProductionDenominator { + effect_contract: contract.clone(), + denominator: denominator.clone(), + }, + ) + } else { + match Rc::new({ + let mut __result = Vec::new(); + for b in bounds.iter().cloned() { + if (bound_operation_transport_kind(b.bound.clone()) + == std::option::Option::None) + { + __result.push(b); + } + } + __result + }) + .first() + .cloned() + { + Some(r) => match (*r.bound.clone()).clone() { + BoundOperation::BindingRefused { cause: cz, .. } => Rc::new( + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: contract.clone(), + cause: Rc::new( + NativeEffectUnavailableCause::NativeOperationBindingRefused { + operation: r.operation.clone(), + refusal: cz.clone(), + }, + ), + }, + ), + _ => Rc::new( + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: contract.clone(), + cause: Rc::new( + NativeEffectUnavailableCause::NativeEffectDeclarationAbsent, + ), + }, + ), + }, + std::option::Option::None => { + Rc::new(NativeEffectRealizationAdmission::NativeEffectHandlerBound { + effect_contract: contract.clone(), + handler_identity: Rc::new( + NativeEffectHandler::NativeTransportHandler { + transports: transports.clone(), + }, + ), + }) + } + } + } + } + } +} + +pub fn admit_native_resource_effect( + binding: String, + resource: String, + resolution: Rc, + denominator: Rc, +) -> Rc { + match (*resolution.clone()).clone() { + NativeEffectResolution::NativeEffectUnresolved { cause: cz, .. } => Rc::new( + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: Rc::new(NativeEffectContract::NativeResourceEffect { + binding: binding.clone(), + declaration: crate::std_decl_ref::decl_ref( + "".to_string(), + resource.clone(), + ), + }), + cause: cz.clone(), + }, + ), + NativeEffectResolution::NativeEffectResolved { site: site, .. } => { + let declaration = + crate::std_decl_ref::decl_ref(site.module_name.clone(), resource.clone()); + let contract = Rc::new(NativeEffectContract::NativeResourceEffect { + binding: binding.clone(), + declaration: declaration.clone(), + }); + let capability_count = (site.item.clone().children.clone().len() as i64); + if native_effect_outside_denominator( + denominator.clone(), + declaration.clone(), + Rc::new(vec![]), + ) { + Rc::new( + NativeEffectRealizationAdmission::NativeEffectOutsideProductionDenominator { + effect_contract: contract.clone(), + denominator: denominator.clone(), + }, + ) + } else { + if (capability_count.clone() == 0) { + Rc::new(NativeEffectRealizationAdmission::NativeEffectHandlerBound { + effect_contract: contract.clone(), + handler_identity: Rc::new(NativeEffectHandler::NativeCapabilityToken), + }) + } else { + Rc::new( + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: contract.clone(), + cause: Rc::new( + NativeEffectUnavailableCause::NativeResourceTraitHasNoImplementor { + capability_count: capability_count.clone(), + }, + ), + }, + ) + } + } + } + } +} + +pub fn native_declaration_label(d: Rc) -> String { + v1_rt::concat( + d.module_path.clone(), + v1_rt::concat(" ".to_string(), d.decl_name.clone()), + ) +} + +pub fn native_effect_contract_label(c: Rc) -> String { + match (*c.clone()).clone() { + NativeEffectContract::NativeServiceEffect { declaration: d, .. } => { + v1_rt::concat("service ".to_string(), native_declaration_label(d.clone())) + } + NativeEffectContract::NativeResourceEffect { + binding: b, + declaration: d, + .. + } => v1_rt::concat( + "resource ".to_string(), + v1_rt::concat( + native_declaration_label(d.clone()), + v1_rt::concat( + " (uses ".to_string(), + v1_rt::concat(b.clone(), ")".to_string()), + ), + ), + ), + } +} + +pub fn native_route_label(route: CompilerEntryDriver) -> String { + match route.clone() { + CompilerEntryDriver::NativeCliDriver => "NativeCliDriver".to_string(), + CompilerEntryDriver::NativeClaimDriver => "NativeClaimDriver".to_string(), + CompilerEntryDriver::NativeServeDriver => "NativeServeDriver".to_string(), + CompilerEntryDriver::SourceRootEvalDriver => "SourceRootEvalDriver".to_string(), + CompilerEntryDriver::DirectIngestDriver => "DirectIngestDriver".to_string(), + CompilerEntryDriver::RetainedHostCliKernel => "RetainedHostCliKernel".to_string(), + } +} + +pub fn native_effect_admission_refusal(a: Rc) -> Option { + match (*a.clone()).clone() { + NativeEffectRealizationAdmission::NativeEffectHandlerBound { .. } => { + std::option::Option::None + } + NativeEffectRealizationAdmission::NativeEffectOutsideProductionDenominator { + effect_contract: c, + denominator: d, + .. + } => Some(v1_rt::concat( + native_effect_contract_label(c.clone()), + v1_rt::concat( + " is outside the ".to_string(), + v1_rt::concat( + native_route_label(d.route.clone()), + v1_rt::concat( + " denominator (ruling: ".to_string(), + v1_rt::concat(native_declaration_label(d.ruling.clone()), ")".to_string()), + ), + ), + ), + )), + NativeEffectRealizationAdmission::NativeEffectRealizationUnavailable { + effect_contract: c, + cause: cz, + .. + } => Some(v1_rt::concat( + native_effect_contract_label(c.clone()), + v1_rt::concat( + " has no native realization: ".to_string(), + native_effect_unavailable_fact(cz.clone()), + ), + )), + } +} + +pub fn native_effect_unavailable_fact(cause: Rc) -> String { + match (*cause.clone()).clone() { + NativeEffectUnavailableCause::NativeOperationBindingRefused { + operation: op, + refusal: r, + .. + } => v1_rt::concat( + "operation '".to_string(), + v1_rt::concat( + op.clone(), + v1_rt::concat( + "' binds no transport: ".to_string(), + transport_binding_refusal_fact(r.clone()), + ), + ), + ), + NativeEffectUnavailableCause::NativeResourceTraitHasNoImplementor { + capability_count: n, + .. + } => v1_rt::concat( + "it is a resource trait with ".to_string(), + v1_rt::concat( + crate::v1_compiler_emit_core_support::to_string(n.clone()), + " capabilities and no native implementor".to_string(), + ), + ), + NativeEffectUnavailableCause::NativeEffectDeclarationAbsent => { + "its declaration is not in the emitted closure".to_string() + } + NativeEffectUnavailableCause::NativeEffectDeclarationAmbiguous { + candidates: n, .. + } => v1_rt::concat( + crate::v1_compiler_emit_core_support::to_string(n.clone()), + " declarations in the closure carry its name, so which one it demands is not decided" + .to_string(), + ), + } +} + pub fn emit_unified_transport_dispatch( bound: Rc, op_name: String, @@ -3474,14 +4090,10 @@ pub fn block_stmts_init(stmts: Rc>>) -> Rc>> { if ((stmts.clone().len() as i64) <= 1) { Rc::new(vec![]) } else { - Rc::new( - stmts - .clone() - .iter() - .cloned() - .take(v1_rt::int_sub((stmts.clone().len() as i64), 1) as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_take( + &stmts.clone(), + v1_rt::int_sub((stmts.clone().len() as i64), 1), + )) } } @@ -5407,6 +6019,62 @@ pub fn file_operation_has_content_input( ) } +pub fn required_stderr_capture_policy_declaration() -> Rc { + crate::std_decl_ref::decl_ref( + "std.shell_stream_capture".to_string(), + "WitnessStderrCapturePolicy".to_string(), + ) +} + +pub fn param_type_is_stderr_capture_policy_declaration(p: Rc, env: Rc) -> bool { + match (*crate::v1_compiler_infer_env::type_reference_declaration_reading( + crate::v1_std_core::param_node_type_expr(p.clone()), + env.source_indices.clone(), + env.clone(), + )) + .clone() + { + TypeReferenceDeclarationReading::TypeReferenceNamesDeclaration { + declaration: d, .. + } => crate::std_decl_ref::declaration_ref_eq( + d.clone(), + required_stderr_capture_policy_declaration(), + ), + _ => false, + } +} + +pub fn param_is_required_stderr_capture_policy(p: Rc, env: Rc) -> bool { + { + let ty = crate::v1_std_core::param_node_type_expr(p.clone()); + (((((crate::v1_std_core::param_node_name_at(p.clone(), env.source_indices.clone()) + == "stderr_capture".to_string()) + && (p.return_cardinality.clone() == Cardinality::Required)) + && (ty.return_cardinality.clone() == Cardinality::Required)) + && !crate::v1_compiler_infer_types::node_is_collection( + ty.clone(), + env.source_indices.clone(), + )) + && param_type_is_stderr_capture_policy_declaration(p.clone(), env.clone())) + } +} + +pub fn operation_declares_required_stderr_capture_policy( + op_node: Rc, + env: Rc, +) -> bool { + { + let mut __found = false; + for p in op_node.params.clone().iter().cloned() { + if param_is_required_stderr_capture_policy(p.clone(), env.clone()) { + __found = true; + break; + } + } + __found + } +} + pub fn is_modeled_file_output_channel(key: String) -> bool { match file_result_channel_of_key(key.clone()) { Some(_) => true, @@ -5732,21 +6400,35 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { span: ch.span.clone(), }), module_name.clone())]) }, - Some(c) => if shell_channel_realized_by_target(c.clone(), target.clone()) { - Rc::new(vec![]) - } else { + Some(c) => if ((shell_channel_is_capture_accounting(c.clone()) && target_is_rust(target.clone())) && !operation_declares_required_stderr_capture_policy(op_node.clone(), env.clone())) { Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { transport_kind: "shell".to_string(), service: service_name.clone(), operation: crate::v1_compiler_infer_env::authored_name(env.clone(), op_node.clone()), declaring_module: module_name.clone(), target: render_target_name(target.clone()), + missing_realization_fact: shell_emission_refusal_fact(Rc::new(ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: shell_result_channel_key(c.clone()), +})), + span: ch.span.clone(), +}), module_name.clone())]) + } else { + if shell_channel_realized_by_target(c.clone(), target.clone()) { + Rc::new(vec![]) + } else { + Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { + transport_kind: "shell".to_string(), + service: service_name.clone(), + operation: crate::v1_compiler_infer_env::authored_name(env.clone(), op_node.clone()), + declaring_module: module_name.clone(), + target: render_target_name(target.clone()), missing_realization_fact: shell_emission_refusal_fact(Rc::new(ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: shell_result_channel_key(c.clone()), target_name: render_target_name(target.clone()), })), span: ch.span.clone(), }), module_name.clone())]) + } }, }).iter().cloned()); } __result }), _ => Rc::new(vec![]), @@ -6816,13 +7498,7 @@ pub fn emit_block_stmts_shared( }; let next_scope = scope_after_expr(stmt.clone(), scope.clone()); { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = v1_rt::rc_list_push(text, line.clone()); let __tco_2 = next_scope.clone(); let __tco_3 = depth; @@ -6870,14 +7546,7 @@ pub fn emit_init_block_stmts_shared( }); } Some(stmt) => { - let rest = Rc::new( - remaining - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&remaining.clone(), 1)); match rest.clone().first().cloned() { std::option::Option::None => { break Rc::new(BlockEmitState { @@ -8620,13 +9289,7 @@ pub fn emit_typed_tco_reassign_shared( let mut __result = Vec::new(); for pair in pairs.iter().cloned() { __result.push({ - let av = match arg_values - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + let av = match arg_values.clone().get((pair.0.clone()) as usize).cloned() { Some(v) => v.clone(), std::option::Option::None => pair.1.clone(), }; diff --git a/src/v1/stage0/src/v1_compiler_emit_core_support.rs b/src/v1/stage0/src/v1_compiler_emit_core_support.rs index cd3986c091a..251e9b4f561 100644 --- a/src/v1/stage0/src/v1_compiler_emit_core_support.rs +++ b/src/v1/stage0/src/v1_compiler_emit_core_support.rs @@ -552,13 +552,7 @@ pub fn apply_named_template_nested( match keys.clone().first().cloned() { std::option::Option::None => template.clone(), Some(key) => { - let rest = Rc::new( - keys.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&keys.clone(), 1)); let placeholder = v1_rt::concat(v1_rt::concat("{".to_string(), key.clone()), "}".to_string()); match v1_rt::map_get(&bindings, key.clone()) { diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index e28b0327929..09e83e2dcb0 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -7,7 +7,6 @@ use self::FmArmAnalysis::*; use self::FmLoweringRefusal::*; use self::HostOptionArmReading::*; use self::IterOwnedReceiverCloneDisposition::*; -use self::NativeClaimEffectDemand::*; use self::SealReading::*; use self::WitnessCtorPathVerdict::*; pub use crate::extdeps_cargo::CargoFeature; @@ -78,6 +77,11 @@ use crate::std_coercion::TypeDeclarationProvenance::{ }; use crate::std_coercion::TypeRealizationDecision::*; pub use crate::std_coercion::{TypeDeclarationProvenance, TypeRealizationDecision}; +pub use crate::std_compiler_entry::NativeEffectDenominator; +pub use crate::std_compiler_entry::{ + native_claim_route_denominator, native_compiler_route_denominator, + native_service_route_denominator, +}; use crate::std_decl_ref::DeclField::WholeDeclaration; pub use crate::std_decl_ref::{decl_ref, declaration_ref_in_list}; pub use crate::std_decl_ref::{DeclField, DeclarationRef}; @@ -86,13 +90,18 @@ pub use crate::std_induction::{InductiveField, SubValueRelation}; use crate::std_literal_elaboration::KernelGroundingLookup::{ KernelGroundingAbsent, KernelGroundingAmbiguous, KernelGroundingFound, }; +use crate::std_literal_elaboration::KernelMintDeclarationLookup::{ + KernelMintDeclarationAbsent, KernelMintDeclarationAmbiguous, KernelMintDeclarationFound, +}; use crate::std_literal_elaboration::KernelMintOwnership::{ DeclarationDoesNotOwnTheMint, DeclarationOwnsTheMint, KernelMintOwnershipAmbiguous, }; use crate::std_literal_elaboration::LiteralSourceKind::KernelIntLiteral; -pub use crate::std_literal_elaboration::{kernel_grounding_for, kernel_mint_ownership}; pub use crate::std_literal_elaboration::{ - KernelGroundingLookup, KernelMintOwnership, LiteralSourceKind, + kernel_grounding_for, kernel_mint_declaration_for, kernel_mint_ownership, +}; +pub use crate::std_literal_elaboration::{ + KernelGroundingLookup, KernelMintDeclarationLookup, KernelMintOwnership, LiteralSourceKind, }; pub use crate::std_measure::millisecond_count; pub use crate::std_occurrence_identity::NodeOccurrenceIdentity; @@ -178,31 +187,43 @@ use crate::v1_compiler_emit::FileVerb::{ FileDelete, FileLinkCreateNew, FileList, FileRead, FileWrite, FileWriteCreateNew, FileWriteCreateNewWithMode, FileWriteOwnerOnly, }; -use crate::v1_compiler_emit::ShellEmissionRefusal::ShellChannelNotRealizedByTarget; +use crate::v1_compiler_emit::NativeEffectContract::{NativeResourceEffect, NativeServiceEffect}; +use crate::v1_compiler_emit::NativeEffectHandler::{NativeCapabilityToken, NativeTransportHandler}; +use crate::v1_compiler_emit::NativeEffectRealizationAdmission::{ + NativeEffectHandlerBound, NativeEffectOutsideProductionDenominator, + NativeEffectRealizationUnavailable, +}; +use crate::v1_compiler_emit::ShellEmissionRefusal::{ + ShellCapturePolicyInputAbsent, ShellChannelNotRealizedByTarget, +}; use crate::v1_compiler_emit::ShellResultChannel::{ ShellChanExitCode, ShellChanExitSuccess, ShellChanStderr, ShellChanStderrRetainedBytes, ShellChanStderrTotalBytes, ShellChanStderrTruncated, ShellChanStdout, ShellChanStdoutLines, }; pub use crate::v1_compiler_emit::{ - bind_operation_transport, child_from_key, compute_service_fields, - effective_operation_transport, emit_bin_op_symbol, emit_container, emit_data_value_json, - emit_error_expr, emit_ident, emit_keyed_container_type, emit_keyword, emit_lambda, - emit_lambda_params, emit_let_binding, emit_let_binding_annotated, emit_list_lit_expr, - emit_literal, emit_node_type, emit_null_coalesce, emit_return, emit_shared_expr, - emit_shared_tco_expr, emit_simple_expr, emit_string_literal, emit_typed_cast_shared, - emit_typed_if_shared, emit_typed_let_shared, emit_unary_op, escape_rust_interp_text, - extract_modifier_names, has_nested_records_node, has_service_items, is_null_coalesce, - is_self_recursive, is_tco_eligible, keyed_container_has_target_inhabitant, - lookup_item_by_identity, module_emit_scope, order_typed_call_args_from_semantics, + admit_native_resource_effect, admit_native_service_effect, bind_operation_transport, + child_from_key, compute_service_fields, effective_operation_transport, emit_bin_op_symbol, + emit_container, emit_data_value_json, emit_declared_optional_row_json, emit_error_expr, + emit_ident, emit_keyed_container_type, emit_keyword, emit_lambda, emit_lambda_params, + emit_let_binding, emit_let_binding_annotated, emit_list_lit_expr, emit_literal, emit_node_type, + emit_null_coalesce, emit_return, emit_shared_expr, emit_shared_tco_expr, emit_simple_expr, + emit_string_literal, emit_typed_cast_shared, emit_typed_if_shared, emit_typed_let_shared, + emit_unary_op, escape_rust_interp_text, extract_modifier_names, has_nested_records_node, + has_service_items, is_null_coalesce, is_self_recursive, is_tco_eligible, + keyed_container_has_target_inhabitant, lookup_item_by_identity, module_emit_scope, + native_effect_admission_refusal, native_effect_resolution, + operation_declares_required_stderr_capture_policy, order_typed_call_args_from_semantics, render_node_type, render_tuple_parts, rust_literal_for_pattern, scope_after_expr, seed_bindings, service_fallback_transport, service_field_ctors, service_field_decls, - shared_tco_reassign, shell_emission_refusal_fact, shell_result_channel_key, - tco_loop_iteration_lets, tco_loop_slot_name, tco_reassign_core, transport_binding_refusal_fact, + shared_tco_reassign, shell_channel_is_capture_accounting, shell_emission_refusal_fact, + shell_result_channel_key, tco_loop_iteration_lets, tco_loop_slot_name, tco_reassign_core, + transport_binding_refusal_fact, }; pub use crate::v1_compiler_emit::{ BlockEmitState, BoundOperation, EmitterOutcome, FileResultChannel, FileResultField, FileVerb, - InterpPart, ServiceFieldSet, ShellEmissionRefusal, ShellResultChannel, ShellResultField, - TcoFrame, TcoReassignInput, + InterpPart, NativeEffectContract, NativeEffectDeclSite, NativeEffectHandler, + NativeEffectRealizationAdmission, ServiceFieldSet, ShellEmissionRefusal, ShellResultChannel, + ShellResultField, TcoFrame, TcoReassignInput, }; pub use crate::v1_compiler_emit_core_support::{ apply_named_template, apply_type_template1, apply_type_template2, apply_type_template3, @@ -227,15 +248,28 @@ use crate::v1_compiler_infer_emit_info::TypeDeclResolution::{ TypeDeclKernelSpellingUndecided, TypeDeclLeafAmbiguous, TypeDeclNotDeclared, TypeDeclResolved, }; use crate::v1_compiler_infer_emit_info::TypeRepr::{EnumRepr, StructRepr}; +use crate::v1_compiler_infer_emit_info::TypeSummaryLookup::{ + TypeSummaryFound, TypeSummaryLeafAmbiguous, TypeSummaryNotDeclared, +}; +use crate::v1_compiler_infer_emit_info::TypeSummaryQuestion::{ + QuestionDecided, QuestionNameAmbiguous, QuestionNotDeclared, +}; pub use crate::v1_compiler_infer_emit_info::{ collect_type_node_import_surface_names, collect_type_node_import_surface_occurrences, emit_info_with_expected_type, emit_info_with_fn_return, emit_info_with_fn_type_context, empty_emit_graph_info, find_variant_parent, is_enum_in_summaries, is_known_variant, - lookup_emit_type_summary, resolve_type_decl_reference, type_decl_identity, + lookup_emit_type_summary, resolve_type_decl_reference, summary_is_enum_with_variant, + type_decl_identities_of_leaf, type_decl_identity, type_summary_answer, type_summary_decided, + type_summary_keys_of_leaf, type_summary_lookup, type_summary_of_reference, type_summary_values, variant_belongs_to_enum, variant_summary_key, }; pub use crate::v1_compiler_infer_emit_info::{ - EmitGraphInfo, TypeDeclIndex, TypeDeclResolution, TypeRepr, TypeSummary, TypeSurfaceOccurrence, + EmitGraphInfo, TypeDeclIndex, TypeDeclResolution, TypeRepr, TypeSummary, TypeSummaryIndex, + TypeSummaryLookup, TypeSummaryQuestion, TypeSurfaceOccurrence, +}; +use crate::v1_compiler_infer_env::BareOccurrenceBinding::{ + BareOccurrenceDeclaredInItsFile, BareOccurrenceImportedByName, BareOccurrenceIsKernelName, + BareOccurrenceUndecided, }; use crate::v1_compiler_infer_env::GlobalBareLookupState::{ GlobalBareAmbiguousBinding, GlobalBareUniqueBinding, @@ -244,12 +278,13 @@ use crate::v1_compiler_infer_env::UnitVariantPhantomLookup::{ UnitVariantPhantomAbsent, UnitVariantPhantomEvidenceUnavailable, UnitVariantPhantomPresent, }; pub use crate::v1_compiler_infer_env::{ - authored_name, binding_declares_span, empty_symbol_index, lookup_binding_by_name, - lookup_type_by_name, lookup_type_for, lookup_unit_variant_phantom_type, + authored_name, bare_occurrence_binding, binding_declares_span, + declaration_ref_of_declaration_node, empty_symbol_index, empty_type_env, + lookup_binding_by_name, lookup_type_by_name, lookup_type_for, lookup_unit_variant_phantom_type, type_reference_declaration_ref, }; pub use crate::v1_compiler_infer_env::{ - GlobalBareLookupState, TypeBinding, TypeEnv, UnitVariantPhantomLookup, + BareOccurrenceBinding, GlobalBareLookupState, TypeBinding, TypeEnv, UnitVariantPhantomLookup, }; use crate::v1_compiler_infer_items::ItemKind::{DataItem, OtherItem, TypeItem}; use crate::v1_compiler_infer_items::ItemLookup::{ItemFound, ItemLeafAmbiguous, ItemNotFound}; @@ -311,8 +346,8 @@ use crate::v1_std_core::CallTargetIdentity::{ use crate::v1_std_core::Cardinality::{CardOptional, Required}; use crate::v1_std_core::CompilerDiagnostic::{ AmbiguousAnonymousRecordLiteral, AmbiguousReference, EffectfulSelfRecursionUnrealized, - InternalError, ReferenceDerivedImportExportUnproven, ReferenceDerivedImportProviderUnknown, - UnlistedImportUse, + InternalError, NativeEffectRealizationRefused, ReferenceDerivedImportExportUnproven, + ReferenceDerivedImportProviderUnknown, UnlistedImportUse, }; use crate::v1_std_core::Connective::{Arrow, Conj, Disj, NoConnective}; use crate::v1_std_core::DeclarationMarker::Unmarked; @@ -404,7 +439,11 @@ pub fn render_rust_type( if is_host_text_carrier_type(n.clone(), source_indices.clone()) { return rust_carrier_optional_wrap(n.clone(), "String".to_string()); } - if is_host_optional_carrier_type(n.clone(), source_indices.clone()) { + if is_host_optional_carrier_type( + n.clone(), + source_indices.clone(), + emit_info.fn_type_env.clone(), + ) { return render_rust_optional_carrier_applied( n.clone(), emit_info.fn_generic_param_names.clone(), @@ -423,7 +462,10 @@ pub fn render_rust_type( tv.clone(), emit_info.fn_generic_param_names.clone(), ) { - crate::v1_compiler_emit_core_support::to_pascal(tv.clone()) + rust_carrier_optional_wrap( + n.clone(), + crate::v1_compiler_emit_core_support::to_pascal(tv.clone()), + ) } else { "_".to_string() } @@ -588,7 +630,11 @@ pub fn render_rust_type_without_applied_binding( if is_host_text_carrier_type(n.clone(), source_indices.clone()) { return rust_carrier_optional_wrap(n.clone(), "String".to_string()); } - if is_host_optional_carrier_type(n.clone(), source_indices.clone()) { + if is_host_optional_carrier_type( + n.clone(), + source_indices.clone(), + emit_info.fn_type_env.clone(), + ) { return render_rust_optional_carrier_applied( n.clone(), emit_info.fn_generic_param_names.clone(), @@ -688,14 +734,7 @@ pub fn render_rust_type_without_applied_binding( let key_node = crate::v1_compiler_infer_types::child_type_node( key_child.clone(), ); - let val_node = match n - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + let val_node = match n.children.clone().get((1) as usize).cloned() { Some(val_child) => { if rust_type_node_is_arrow(val_child.clone()) { val_child.clone() @@ -897,10 +936,6 @@ pub fn is_host_freemonoid_vec_alias(name: String) -> bool { (name.clone() == "FreeMonoid".to_string()) } -pub fn is_host_optional_carrier_alias(name: String) -> bool { - (name.clone() == kernel_optional_mint_name()) -} - pub fn is_host_diagnostics_carrier_alias(name: String) -> bool { { let mut __found = false; @@ -914,9 +949,87 @@ pub fn is_host_diagnostics_carrier_alias(name: String) -> bool { } } -pub fn is_grounded_coproduct_native_alias(name: String) -> bool { - ((is_host_freemonoid_vec_alias(name.clone()) || is_host_optional_carrier_alias(name.clone())) - || is_host_diagnostics_carrier_alias(name.clone())) +pub fn declaration_is_grounded_coproduct_native_alias( + mut __tco_loop_module_path: String, + mut __tco_loop_decl_name: String, +) -> bool { + loop { + #[allow(unused_mut)] + let mut module_path = __tco_loop_module_path; + #[allow(unused_mut)] + let mut decl_name = __tco_loop_decl_name; + if v1_rt::contains(decl_name.clone(), ".".to_string()) { + let segments = Rc::new( + decl_name + .clone() + .split(&".".to_string()) + .map(|s| s.to_string()) + .collect::>(), + ); + { + let __tco_0 = Rc::new(v1_rt::list_take( + &segments.clone(), + v1_rt::int_sub((segments.clone().len() as i64), 1), + )) + .join(&".".to_string()); + let __tco_1 = crate::v1_std_core::qualified_last_segment(decl_name); + __tco_loop_module_path = __tco_0; + __tco_loop_decl_name = __tco_1; + continue; + } + } else { + break ((is_host_freemonoid_vec_alias(decl_name.clone()) + || declaration_owns_host_option(module_path.clone(), decl_name.clone())) + || crate::std_decl_ref::declaration_ref_in_list( + crate::std_decl_ref::decl_ref(module_path.clone(), decl_name.clone()), + rust_host_option_carrier_declarations(), + )); + } + } +} + +pub fn every_declarer_is_grounded_coproduct_native_alias( + decls: Rc, + name: String, +) -> bool { + { + let identities = crate::v1_compiler_infer_emit_info::type_decl_identities_of_leaf( + decls.clone(), + crate::v1_std_core::qualified_last_segment(name.clone()), + ); + (((identities.clone().len() as i64) > 0) && { + let mut __all = true; + for identity in identities.iter().cloned() { + if !(declaration_is_grounded_coproduct_native_alias( + "".to_string(), + identity.clone(), + )) { + __all = false; + break; + } + } + __all + }) + } +} + +pub fn summary_declares_grounded_coproduct_native_alias(summary: Rc) -> bool { + { + let segments = Rc::new( + summary + .key + .clone() + .split(&".".to_string()) + .map(|s| s.to_string()) + .collect::>(), + ); + let module_path = Rc::new(v1_rt::list_take( + &segments.clone(), + v1_rt::int_sub((segments.clone().len() as i64), 1), + )) + .join(&".".to_string()); + declaration_is_grounded_coproduct_native_alias(module_path.clone(), summary.name.clone()) + } } pub fn rust_declaration_realizes_as_native_alias(module_path: String, decl_name: String) -> bool { @@ -934,13 +1047,125 @@ pub fn rust_declaration_realizes_as_native_alias(module_path: String, decl_name: } } +pub fn declaration_owns_host_option(module_path: String, decl_name: String) -> bool { + match (*crate::std_literal_elaboration::kernel_mint_ownership( + kernel_mint_declaration_rows(), + kernel_optional_mint_name(), + crate::std_decl_ref::decl_ref(module_path.clone(), decl_name.clone()), + )) + .clone() + { + KernelMintOwnership::DeclarationOwnsTheMint => true, + KernelMintOwnership::DeclarationDoesNotOwnTheMint => false, + KernelMintOwnership::KernelMintOwnershipAmbiguous { row_count: _, .. } => false, + } +} + +pub fn unstamped_reference_is_host_option( + n: Rc, + source_indices: Rc>>, + env: Rc, +) -> bool { + { + let leaf = crate::v1_std_core::authored_name_at(source_indices.clone(), n.clone()); + match (*crate::v1_compiler_infer_env::bare_occurrence_binding( + env.symbol_index.clone(), + leaf.clone(), + n.clone(), + )) + .clone() + { + BareOccurrenceBinding::BareOccurrenceDeclaredInItsFile { + declaration: decl, .. + } => { + declaration_node_owns_host_option(decl.clone(), source_indices.clone(), env.clone()) + } + BareOccurrenceBinding::BareOccurrenceImportedByName { + module_path: from_module, + declaration: decl, + .. + } => { + (declaration_owns_host_option(from_module.clone(), leaf.clone()) + || declaration_node_owns_host_option( + decl.clone(), + source_indices.clone(), + env.clone(), + )) + } + BareOccurrenceBinding::BareOccurrenceIsKernelName => { + match (*crate::std_literal_elaboration::kernel_mint_declaration_for( + kernel_mint_declaration_rows(), + leaf.clone(), + )) + .clone() + { + KernelMintDeclarationLookup::KernelMintDeclarationFound { + declaration: d, + .. + } => declaration_owns_host_option(d.module_path.clone(), d.decl_name.clone()), + KernelMintDeclarationLookup::KernelMintDeclarationAbsent => false, + KernelMintDeclarationLookup::KernelMintDeclarationAmbiguous { + row_count: _, + .. + } => false, + } + } + BareOccurrenceBinding::BareOccurrenceUndecided => false, + } + } +} + +pub fn declaration_node_owns_host_option( + decl: Rc, + source_indices: Rc>>, + env: Rc, +) -> bool { + match crate::v1_compiler_infer_env::declaration_ref_of_declaration_node( + decl.clone(), + source_indices.clone(), + env.clone(), + ) { + Some(d) => declaration_owns_host_option(d.module_path.clone(), d.decl_name.clone()), + std::option::Option::None => false, + } +} + pub fn is_host_optional_carrier_type( n: Rc, source_indices: Rc>>, + env: Rc, ) -> bool { - ((crate::v1_std_core::authored_name_at(source_indices.clone(), n.clone()) - == "Optional".to_string()) - && ((n.children.clone().len() as i64) > 0)) + match n.declaration.clone() { + Some(d) => match (*d.field.clone()).clone() { + DeclField::WholeDeclaration => { + declaration_owns_host_option(d.module_path.clone(), d.decl_name.clone()) + } + _ => false, + }, + std::option::Option::None => { + let is_declaration_structure = (((n.module_item_kind.clone() + == ParsedModuleItemKind::ModuleItemTypeDeclaration) + || (n.connective.clone() == Connective::Disj)) + && (n.ident_span.clone() != std::option::Option::None)); + let declared = if is_declaration_structure.clone() { + crate::v1_compiler_infer_env::declaration_ref_of_declaration_node( + n.clone(), + source_indices.clone(), + env.clone(), + ) + } else { + std::option::Option::None + }; + match declared.clone() { + Some(d) => declaration_owns_host_option(d.module_path.clone(), d.decl_name.clone()), + std::option::Option::None => unstamped_reference_is_host_option( + n.clone(), + source_indices.clone(), + env.clone(), + ), + } + } + } } pub fn is_host_diagnostics_carrier_type( @@ -2311,7 +2536,10 @@ pub fn render_rust_applied_type_arg( match n.inferred.clone().as_deref().cloned() { Some(InferredNode::TypeVariable { id: tv, .. }) => { if type_var_in_fn_generic_scope(tv.clone(), generic_param_names.clone()) { - crate::v1_compiler_emit_core_support::to_pascal(tv.clone()) + rust_carrier_optional_wrap( + n.clone(), + crate::v1_compiler_emit_core_support::to_pascal(tv.clone()), + ) } else { "_".to_string() } @@ -2350,7 +2578,7 @@ pub fn render_rust_applied_type( if is_host_text_carrier_type(n.clone(), source_indices.clone()) { return rust_carrier_optional_wrap(n.clone(), "String".to_string()); } - if is_host_optional_carrier_type(n.clone(), source_indices.clone()) { + if is_host_optional_carrier_type(n.clone(), source_indices.clone(), env.clone()) { return render_rust_optional_carrier_applied( n.clone(), generic_param_names.clone(), @@ -2617,7 +2845,7 @@ pub fn render_rust_decl_type( if is_host_text_carrier_type(n.clone(), source_indices.clone()) { return rust_carrier_optional_wrap(n.clone(), "String".to_string()); } - if is_host_optional_carrier_type(n.clone(), source_indices.clone()) { + if is_host_optional_carrier_type(n.clone(), source_indices.clone(), env.clone()) { return render_rust_optional_carrier_applied( n.clone(), generic_param_names.clone(), @@ -2770,7 +2998,10 @@ pub fn render_rust_decl_type( __found }) { - crate::v1_compiler_emit_core_support::to_pascal(name.clone()) + rust_carrier_optional_wrap( + n.clone(), + crate::v1_compiler_emit_core_support::to_pascal(name.clone()), + ) } else { if (bare_native_alias.clone() != std::option::Option::None) { rust_carrier_optional_wrap( @@ -2880,7 +3111,7 @@ if peel.clone() { } else { if crate::v1_compiler_infer_types::node_is_keyed_collection(n.clone(), source_indices.clone()) { match arg_list.clone().first().cloned() { - Some(k) => match arg_list.clone().iter().cloned().skip(1 as usize).next() { + Some(k) => match arg_list.clone().get((1) as usize).cloned() { Some(v) => crate::v1_compiler_emit::emit_keyed_container_type(name.clone(), k.clone(), v.clone(), base.clone(), RenderTarget::Rust), std::option::Option::None => v1_rt::concat(v1_rt::concat(v1_rt::concat(base.clone(), "<".to_string()), arg_list.clone().join(&", ".to_string())), ">".to_string()), }, @@ -2980,7 +3211,7 @@ pub fn render_rust_fn_sig_type( if is_host_text_carrier_type(n.clone(), source_indices.clone()) { return rust_carrier_optional_wrap(n.clone(), "String".to_string()); } - if is_host_optional_carrier_type(n.clone(), source_indices.clone()) { + if is_host_optional_carrier_type(n.clone(), source_indices.clone(), env.clone()) { return render_rust_optional_carrier_applied( n.clone(), generic_param_names.clone(), @@ -3295,7 +3526,7 @@ pub fn rust_alias_rhs_applied_container_or_base( ) -> String { if crate::v1_compiler_infer_types::node_is_keyed_collection(n.clone(), source_indices.clone()) { match arg_list.clone().first().cloned() { - Some(k) => match arg_list.clone().iter().cloned().skip(1 as usize).next() { + Some(k) => match arg_list.clone().get((1) as usize).cloned() { Some(v) => crate::v1_compiler_emit::emit_keyed_container_type( leaf.clone(), k.clone(), @@ -3461,7 +3692,10 @@ pub fn render_rust_alias_rhs_type( __found }) { - break crate::v1_compiler_emit_core_support::to_pascal(name.clone()); + break rust_carrier_optional_wrap( + n.clone(), + crate::v1_compiler_emit_core_support::to_pascal(name.clone()), + ); } else { if ((n.connective.clone() == Connective::NoConnective) && ((n.children.clone().len() as i64) == 0)) @@ -5951,13 +6185,7 @@ pub fn emit_rust_block_stmts( let next_scope = crate::v1_compiler_emit::scope_after_expr(stmt.clone(), scope.clone()); { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = v1_rt::rc_list_push(text, line.clone()); let __tco_2 = next_scope.clone(); let __tco_3 = registry; @@ -6010,14 +6238,7 @@ pub fn emit_rust_init_block_stmts( }); } Some(stmt) => { - let rest = Rc::new( - remaining - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&remaining.clone(), 1)); match rest.clone().first().cloned() { std::option::Option::None => { break Rc::new(BlockEmitState { @@ -6077,7 +6298,10 @@ pub fn rust_qualify_type_leaf_name( match v1_rt::map_get(&variant_to_enum, name.clone()) { Some(parent) => { if (parent.clone() != "".to_string()) { - rust_variant_path(parent.clone(), name.clone()) + rust_variant_path( + crate::v1_std_core::qualified_last_segment(parent.clone()), + name.clone(), + ) } else { name.clone() } @@ -6368,7 +6592,7 @@ pub fn maybe_mark_shared_type( .. } => (unit_only.clone() == false), }); - if is_grounded_coproduct_native_alias(summary.name.clone()) { + if summary_declares_grounded_coproduct_native_alias(summary.clone()) { acc.clone() } else { if (needs_sharing.clone() @@ -6382,27 +6606,75 @@ pub fn maybe_mark_shared_type( } } +pub fn shared_by_every_declarer( + type_summaries: Rc, + name: String, + recursive_type_set: Rc>, + target_needs_sharing: bool, +) -> bool { + { + let mut __all = true; + for key in crate::v1_compiler_infer_emit_info::type_summary_keys_of_leaf( + type_summaries.clone(), + name.clone(), + ) + .iter() + .cloned() + { + if !(match crate::v1_compiler_infer_emit_info::type_summary_decided( + type_summaries.clone(), + key.clone(), + ) { + Some(summary) => v1_rt::set_contains( + &maybe_mark_shared_type( + v1_rt::rc_empty_set::(), + summary.clone(), + recursive_type_set.clone(), + target_needs_sharing.clone(), + ), + name.clone(), + ), + std::option::Option::None => true, + }) { + __all = false; + break; + } + } + __all + } +} + pub fn build_shared_types( - type_summaries: Rc>>, + type_summaries: Rc, recursive_type_set: Rc>, target: RenderTarget, ) -> Rc> { { let sharing = crate::v1_compiler_languages::sharing_for_target(target.clone()); - let user_shared = Rc::new(v1_rt::map_values(&type_summaries)) - .iter() - .cloned() - .fold( - v1_rt::rc_empty_set::(), - |acc: Rc>, summary: Rc| { - maybe_mark_shared_type( - acc, - summary.clone(), - recursive_type_set.clone(), - sharing.needs_sharing.clone(), - ) - }, - ); + let user_shared = + crate::v1_compiler_infer_emit_info::type_summary_values(type_summaries.clone()) + .iter() + .cloned() + .fold( + v1_rt::rc_empty_set::(), + |acc: Rc>, summary: Rc| { + if shared_by_every_declarer( + type_summaries.clone(), + summary.name.clone(), + recursive_type_set.clone(), + sharing.needs_sharing.clone(), + ) { + maybe_mark_shared_type( + acc.clone(), + summary.clone(), + recursive_type_set.clone(), + sharing.needs_sharing.clone(), + ) + } else { + acc.clone() + } + }, + ); let collection_keys = Rc::new({ let mut __result = Vec::new(); for k in Rc::new(v1_rt::sorted_map_keys(&rust_container_templates())) @@ -7159,11 +7431,21 @@ pub fn emit_rust_selected( || closure_renders_async_trait_resource(typed.clone())), }, ); + let native_effects = native_entry_effects(typed.modules.clone(), registry.clone()); + let native_effect_refusals = + native_entry_effect_diagnostics(native_effects.clone(), typed.modules.clone()); + if ((native_effect_refusals.clone().len() as i64) > 0) { + return Rc::new(EmitResult { + files: Rc::new(vec![]), + diagnostics: native_effect_refusals.clone(), + emitted_edges: Rc::new(vec![]), + }); + } let main_file = emit_main_rs( typed.modules.clone(), has_services.clone(), crate_name.clone(), - ctx.registry.clone(), + native_effects.clone(), ); let cli_dispatch_file = if has_pipeline.clone() { Rc::new(vec![emit_gunbc_cli_dispatch_generated(crate_name.clone())]) @@ -7966,7 +8248,7 @@ pub fn emit_inferred_type_leaf_name( pub fn anonymous_record_lit_surface_name( n: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> String { { let lit_field_names = Rc::new({ @@ -8016,7 +8298,10 @@ pub fn anonymous_record_lit_surface_name( let rt_name = crate::v1_std_core::authored_name_at(source_indices.clone(), rt.clone()); if ((rt.ident_span.clone() != std::option::Option::None) - && v1_rt::map_contains_key(&type_summaries, rt_name.clone())) + && (crate::v1_compiler_infer_emit_info::type_summary_decided( + type_summaries.clone(), + rt_name.clone(), + ) != std::option::Option::None)) { rt_name.clone() } else { @@ -8031,7 +8316,7 @@ pub fn anonymous_record_lit_surface_name( pub fn record_lit_variant_payload_struct_surfaces( n: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> Rc> { { @@ -8053,8 +8338,8 @@ pub fn record_lit_variant_payload_struct_surfaces( if (enum_name.clone() == "".to_string()) { Rc::new(vec![]) } else { - match v1_rt::map_get( - &type_summaries, + match crate::v1_compiler_infer_emit_info::type_summary_decided( + type_summaries.clone(), crate::v1_compiler_infer_emit_info::variant_summary_key( enum_name.clone(), variant_name.clone(), @@ -8066,17 +8351,13 @@ pub fn record_lit_variant_payload_struct_surfaces( .iter() .cloned() { - if ((ft.clone() != "".to_string()) - && match v1_rt::map_get(&type_summaries, ft.clone()) { - Some(fts) => match (*fts.repr.clone()).clone() { - TypeRepr::StructRepr => true, - _ => false, - }, - std::option::Option::None => false, - }) - { - __result.push(ft); - } + if ((ft.clone() != "".to_string()) && match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), ft.clone()) { + Some(fts) => match (*fts.repr.clone()).clone() { + TypeRepr::StructRepr => true, + _ => false, +}, + std::option::Option::None => false, +}) { __result.push(ft); } } __result }), @@ -8115,7 +8396,7 @@ pub fn record_lit_field_type_hints( pub fn record_lit_resolved_ctor_import_names( type_name: String, n: Rc, - type_summaries: Rc>>, + type_summaries: Rc, source_indices: Rc>>, ) -> Rc> { { @@ -8184,7 +8465,7 @@ pub fn record_lit_resolved_ctor_import_names( pub fn record_lit_ref_names( n: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> Rc> { match (*n.expr_data.clone()).clone() { @@ -8269,7 +8550,7 @@ pub fn record_lit_ref_names( pub fn collect_anonymous_record_lit_heads( n: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc> { stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { let self_head = match (*n.expr_data.clone()).clone() { @@ -8281,7 +8562,10 @@ pub fn collect_anonymous_record_lit_heads( let inferred = emit_inferred_type_leaf_name(n.clone(), source_indices.clone()); if ((inferred.clone() != "".to_string()) - && v1_rt::map_contains_key(&type_summaries, inferred.clone())) + && (crate::v1_compiler_infer_emit_info::type_summary_decided( + type_summaries.clone(), + inferred.clone(), + ) != std::option::Option::None)) { Rc::new(vec![inferred.clone()]) } else { @@ -8415,19 +8699,19 @@ pub fn collect_items_field_import_surface_names( let mut __result = Vec::new(); for type_name in surface_names.iter().cloned() { __result.extend((*{ - let summary_lookup = match v1_rt::map_get(&type_summaries, type_name.clone()) { + let summary_lookup = match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), type_name.clone()) { Some(summary) => Some(summary.clone()), std::option::Option::None => match v1_rt::map_get(&variant_to_enum, type_name.clone()) { Some(enum_name) => if (enum_name.clone() == "".to_string()) { std::option::Option::None } else { - v1_rt::map_get(&type_summaries, crate::v1_compiler_infer_emit_info::variant_summary_key(enum_name.clone(), type_name.clone())) + crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), crate::v1_compiler_infer_emit_info::variant_summary_key(enum_name.clone(), type_name.clone())) }, std::option::Option::None => std::option::Option::None, }, }; match summary_lookup.clone() { - Some(summary) => Rc::new({ let mut __result = Vec::new(); for field_type in summary.field_import_surface_names.clone().iter().cloned() { if ((field_type.clone() != "".to_string()) && match v1_rt::map_get(&type_summaries, field_type.clone()) { + Some(summary) => Rc::new({ let mut __result = Vec::new(); for field_type in summary.field_import_surface_names.clone().iter().cloned() { if ((field_type.clone() != "".to_string()) && match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), field_type.clone()) { Some(ft) => match (*ft.repr.clone()).clone() { TypeRepr::StructRepr => true, _ => false, @@ -8445,7 +8729,7 @@ match summary_lookup.clone() { pub fn collect_pattern_ref_names( pattern: Rc, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc> { stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { match (*pattern.clone()).clone() { @@ -8496,7 +8780,7 @@ pub fn collect_pattern_ref_names( pub fn collect_value_ref_names( n: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> Rc> { stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { @@ -8759,7 +9043,7 @@ pub fn emit_scrutinee_type_name( pub fn collect_pattern_rc_prelude_parent_enums( pattern: Rc, scrut_type: String, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc> { match (*pattern.clone()).clone() { MatchPattern::VariantPattern { @@ -9018,7 +9302,7 @@ pub fn collect_record_lit_field_struct_surfaces( parent: Rc, field_init: Rc, source_indices: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> Rc> { { @@ -9039,30 +9323,23 @@ pub fn collect_record_lit_field_struct_surfaces( if (summary_key.clone() == "".to_string()) { Rc::new(vec![]) } else { - match v1_rt::map_get(&type_summaries, summary_key.clone()) { - Some(summary) => match v1_rt::map_get( - &summary.field_type_map.clone(), - field_name.clone(), - ) { - Some(ft) => { - if ((ft.clone() != "".to_string()) - && match v1_rt::map_get(&type_summaries, ft.clone()) { - Some(fts) => match (*fts.repr.clone()).clone() { - TypeRepr::StructRepr => true, - _ => false, - }, - std::option::Option::None => false, - }) - { - Rc::new(vec![ft.clone()]) - } else { - Rc::new(vec![]) - } - } - std::option::Option::None => Rc::new(vec![]), - }, - std::option::Option::None => Rc::new(vec![]), - } + match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), summary_key.clone()) { + Some(summary) => match v1_rt::map_get(&summary.field_type_map.clone(), field_name.clone()) { + Some(ft) => if ((ft.clone() != "".to_string()) && match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), ft.clone()) { + Some(fts) => match (*fts.repr.clone()).clone() { + TypeRepr::StructRepr => true, + _ => false, +}, + std::option::Option::None => false, +}) { + Rc::new(vec![ft.clone()]) + } else { + Rc::new(vec![]) + }, + std::option::Option::None => Rc::new(vec![]), +}, + std::option::Option::None => Rc::new(vec![]), +} } } _ => Rc::new(vec![]), @@ -9113,7 +9390,9 @@ pub fn collect_value_emit_type_surface_names( let from_variant = match v1_rt::map_get(&variant_to_enum, nm.clone()) { Some(parent) => { if (parent.clone() != "".to_string()) { - Rc::new(vec![parent.clone()]) + Rc::new(vec![crate::v1_std_core::qualified_last_segment( + parent.clone(), + )]) } else { Rc::new(vec![]) } @@ -9377,55 +9656,29 @@ pub fn reference_derived_use_lines_note() -> String { pub fn expand_variant_payload_struct_imports( names: Rc>, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> Rc> { Rc::new({ let mut __result = Vec::new(); for n in names.iter().cloned() { - __result.extend( - (*match v1_rt::map_get(&variant_to_enum, n.clone()) { - Some(enum_name) => { - if (enum_name.clone() == "".to_string()) { - Rc::new(vec![]) - } else { - match v1_rt::map_get( - &type_summaries, - crate::v1_compiler_infer_emit_info::variant_summary_key( - enum_name.clone(), - n.clone(), - ), - ) { - Some(summary) => Rc::new({ - let mut __result = Vec::new(); - for ft in - Rc::new(v1_rt::map_values(&summary.field_type_map.clone())) - .iter() - .cloned() - { - if ((ft.clone() != "".to_string()) - && match v1_rt::map_get(&type_summaries, ft.clone()) { - Some(fts) => match (*fts.repr.clone()).clone() { - TypeRepr::StructRepr => true, - _ => false, - }, - std::option::Option::None => false, - }) - { - __result.push(ft); - } - } - __result - }), - std::option::Option::None => Rc::new(vec![]), - } - } - } - std::option::Option::None => Rc::new(vec![]), - }) - .iter() - .cloned(), - ); + __result.extend((*match v1_rt::map_get(&variant_to_enum, n.clone()) { + Some(enum_name) => if (enum_name.clone() == "".to_string()) { + Rc::new(vec![]) + } else { + match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), crate::v1_compiler_infer_emit_info::variant_summary_key(enum_name.clone(), n.clone())) { + Some(summary) => Rc::new({ let mut __result = Vec::new(); for ft in Rc::new(v1_rt::map_values(&summary.field_type_map.clone())).iter().cloned() { if ((ft.clone() != "".to_string()) && match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), ft.clone()) { + Some(fts) => match (*fts.repr.clone()).clone() { + TypeRepr::StructRepr => true, + _ => false, +}, + std::option::Option::None => false, +}) { __result.push(ft); } } __result }), + std::option::Option::None => Rc::new(vec![]), +} + }, + std::option::Option::None => Rc::new(vec![]), +}).iter().cloned()); } __result }) @@ -9477,10 +9730,13 @@ pub fn reference_is_host_realized_builtin(name: String) -> bool { pub fn reference_derived_variant_induced_parent_spelled( module_source: String, enum_name: String, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> bool { - match v1_rt::map_get(&type_summaries, enum_name.clone()) { + match crate::v1_compiler_infer_emit_info::type_summary_decided( + type_summaries.clone(), + enum_name.clone(), + ) { Some(summary) => match (*summary.repr.clone()).clone() { TypeRepr::EnumRepr { unit_only: _, .. } => { let mut __found = false; @@ -9490,7 +9746,11 @@ pub fn reference_derived_variant_induced_parent_spelled( { if match v1_rt::map_get(&variant_to_enum, vn.clone()) { Some(parent) => { - ((parent.clone() == enum_name.clone()) + (((parent.clone() != "".to_string()) + && (crate::v1_std_core::qualified_last_segment(parent.clone()) + == crate::v1_std_core::qualified_last_segment( + enum_name.clone(), + ))) && reference_derived_candidate_spelled_in_module( module_source.clone(), vn.clone(), @@ -9513,7 +9773,7 @@ pub fn reference_derived_variant_induced_parent_spelled( pub fn reference_derived_candidate_authored( module_source: String, name: String, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, ) -> bool { (reference_derived_candidate_spelled_in_module(module_source.clone(), name.clone()) @@ -9812,7 +10072,7 @@ pub fn reference_derived_candidate_disposition( typed_modules: Rc>>, source_indices: Rc>>, module_index: Rc, - type_summaries: Rc>>, + type_summaries: Rc, variant_to_enum: Rc>, in_type_position: bool, ) -> Rc { @@ -9826,7 +10086,7 @@ pub fn reference_derived_candidate_disposition( } else { Rc::new( ReferenceDerivedCandidateDisposition::CandidateVariantDelegatedToParent { - parent_enum: parent.clone(), + parent_enum: crate::v1_std_core::qualified_last_segment(parent.clone()), }, ) } @@ -10589,16 +10849,10 @@ pub fn qualified_type_reference_rows( .map(|s| s.to_string()) .collect::>(), ); - let qualifier = Rc::new( - segments - .clone() - .iter() - .cloned() - .take( - v1_rt::int_sub((segments.clone().len() as i64), 1) as usize - ) - .collect::>(), - ) + let qualifier = Rc::new(v1_rt::list_take( + &segments.clone(), + v1_rt::int_sub((segments.clone().len() as i64), 1), + )) .join(&".".to_string()); if ((((qualifier.clone() == this_module_name.clone()) || crate::v1_compiler_infer_types::emit_map_has( @@ -10796,8 +11050,8 @@ pub fn qualified_type_reference_use_lines( .cloned() { __result.extend( - (*Rc::new( - Rc::new({ + (*Rc::new(v1_rt::list_take( + &Rc::new({ let mut __result = Vec::new(); for l in typed.iter().cloned() { if (l.text.clone() == text.clone()) { @@ -10805,12 +11059,9 @@ pub fn qualified_type_reference_use_lines( } } __result - }) - .iter() - .cloned() - .take(1 as usize) - .collect::>(), - )) + }), + 1, + ))) .iter() .cloned(), ); @@ -11217,7 +11468,8 @@ pub fn emit_module_full( for item in typed_module.items.clone().iter().cloned() { if ((((crate::v1_compiler_emit_core_support::is_type_def_item(item.clone()) && crate::v1_compiler_infer_types::is_coproduct_type(item.clone())) - && !is_grounded_coproduct_native_alias( + && !declaration_is_grounded_coproduct_native_alias( + scope.type_env.clone().module_path.clone(), crate::v1_compiler_infer_env::authored_name( scope.type_env.clone(), item.clone(), @@ -11419,7 +11671,7 @@ pub fn is_import_graph_type_name( typed_modules: Rc>>, registry: Rc>>, export_sets: Rc>>>, - type_summaries: Rc>>, + type_summaries: Rc, source_indices: Rc>>, module_index: Rc, ) -> bool { @@ -11453,10 +11705,16 @@ pub fn is_import_graph_type_name( } { true } else { - if crate::v1_compiler_infer_emit_info::is_enum_in_summaries( + if match (*crate::v1_compiler_infer_emit_info::is_enum_in_summaries( type_summaries.clone(), name.clone(), - ) { + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + TypeSummaryQuestion::QuestionNameAmbiguous { leaf: _, .. } => false, + TypeSummaryQuestion::QuestionNotDeclared => false, + } { false } else { if crate::v1_compiler_infer_emit_info::is_known_variant( @@ -12098,7 +12356,7 @@ continue; pub fn import_module_enum_scope( import_module: String, registry: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, export_sets: Rc>>>, typed_modules: Rc>>, source_indices: Rc>>, @@ -12112,56 +12370,20 @@ pub fn import_module_enum_scope( module_index.clone(), ); let reexport_parents = match v1_rt::map_get(&export_sets, import_module.clone()) { - Some(exported) => Rc::new({ - let mut __result = Vec::new(); - for p in Rc::new({ + Some(exported) => { + Rc::new({ let mut __result = Vec::new(); - for n in Rc::new({ - let mut __result = Vec::new(); - for n in Rc::new(v1_rt::sorted_map_keys(&exported)).iter().cloned() { - if (crate::v1_compiler_infer_emit_info::is_known_variant( - type_summaries.clone(), - n.clone(), - ) && (crate::v1_compiler_infer_emit_info::is_enum_in_summaries( - type_summaries.clone(), - n.clone(), - ) == false)) - { - __result.push(n); - } - } - __result - }) - .iter() - .cloned() - { - __result.push( - match reexport_variant_parent_in_import_module( - n.clone(), - import_module.clone(), - registry.clone(), - type_summaries.clone(), - typed_modules.clone(), - export_sets.clone(), - source_indices.clone(), - module_index.clone(), - ) { - Some(parent) => parent.clone(), - std::option::Option::None => "".to_string(), - }, - ); - } + for p in Rc::new({ let mut __result = Vec::new(); for n in Rc::new({ let mut __result = Vec::new(); for n in Rc::new(v1_rt::sorted_map_keys(&exported)).iter().cloned() { if (crate::v1_compiler_infer_emit_info::is_known_variant(type_summaries.clone(), n.clone()) && match (*crate::v1_compiler_infer_emit_info::is_enum_in_summaries(type_summaries.clone(), n.clone())).clone() { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => (v.clone() == false), + TypeSummaryQuestion::QuestionNotDeclared => true, + TypeSummaryQuestion::QuestionNameAmbiguous { leaf: _, .. } => false, +}) { __result.push(n); } } __result }).iter().cloned() { __result.push(match reexport_variant_parent_in_import_module(n.clone(), import_module.clone(), registry.clone(), type_summaries.clone(), typed_modules.clone(), export_sets.clone(), source_indices.clone(), module_index.clone()) { + Some(parent) => parent.clone(), + std::option::Option::None => "".to_string(), +}); } __result }).iter().cloned() { if (p.clone() != "".to_string()) { __result.push(p); } } __result }) - .iter() - .cloned() - { - if (p.clone() != "".to_string()) { - __result.push(p); - } - } - __result - }), + } std::option::Option::None => Rc::new(vec![]), }; crate::v1_compiler_emit_core_support::unique_strings(v1_rt::concat( @@ -12645,7 +12867,7 @@ pub fn reexport_variant_parent_in_import_module( mut __tco_loop_variant_name: String, mut __tco_loop_import_module: String, mut __tco_loop_registry: Rc>>, - mut __tco_loop_type_summaries: Rc>>, + mut __tco_loop_type_summaries: Rc, mut __tco_loop_typed_modules: Rc>>, mut __tco_loop_export_sets: Rc>>>, mut __tco_loop_source_indices: Rc>>, @@ -13534,7 +13756,7 @@ pub fn import_variant_parent_for_name( n: String, import_module: String, registry: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, typed_modules: Rc>>, export_sets: Rc>>>, source_indices: Rc>>, @@ -14527,7 +14749,7 @@ Rc::new(vec![Rc::new(RustUseLine { let variant_lines = Rc::new({ let mut __result = Vec::new(); for parent in parent_list.iter().cloned() { - __result.extend((*if (is_grounded_coproduct_native_alias(parent.clone()) || rust_declaration_realizes_as_native_alias(import_module.clone(), parent.clone())) { + __result.extend((*if ((declaration_is_grounded_coproduct_native_alias(import_module.clone(), parent.clone()) || every_declarer_is_grounded_coproduct_native_alias(emit_info.type_decl_items.clone(), parent.clone())) || rust_declaration_realizes_as_native_alias(import_module.clone(), parent.clone())) { Rc::new(vec![]) } else { { @@ -14600,7 +14822,10 @@ Rc::new(vec![Rc::new(RustUseLine { } __found } == false) - && !is_grounded_coproduct_native_alias(en.clone())) + && !declaration_is_grounded_coproduct_native_alias( + import_module.clone(), + en.clone(), + )) && !rust_declaration_realizes_as_native_alias( import_module.clone(), en.clone(), @@ -14667,10 +14892,8 @@ pub fn rust_use_after_crate(line: String) -> String { .map(|s| s.to_string()) .collect::>(), ) - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(rest) => rest.clone(), std::option::Option::None => "".to_string(), @@ -14815,10 +15038,8 @@ pub fn rust_pub_use_braced_names(line: String) -> Rc> { .map(|s| s.to_string()) .collect::>(), ) - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(rest) => match Rc::new( rest.clone() @@ -15014,10 +15235,8 @@ pub fn rust_use_bound_symbol(entry: String) -> String { .map(|s| s.to_string()) .collect::>(), ) - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(bound_name) => bound_name.clone(), std::option::Option::None => entry.clone(), @@ -15395,7 +15614,7 @@ pub fn data_field_struct_import_provider_note() -> String { pub fn module_data_field_struct_import_names( items: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, import_module: String, export_sets: Rc>>>, typed_modules: Rc>>, @@ -15416,54 +15635,26 @@ pub fn module_data_field_struct_import_names( .iter() .cloned() { - __result.extend( - (*{ - let type_name = match item.type_annotation.clone() { - Some(ta) => { - crate::v1_std_core::authored_name_at(source_indices.clone(), ta.clone()) - } - std::option::Option::None => "".to_string(), - }; - if (type_name.clone() == "".to_string()) { - Rc::new(vec![]) - } else { - match v1_rt::map_get(&type_summaries, type_name.clone()) { - Some(summary) => Rc::new({ - let mut __result = Vec::new(); - for field_type in - summary.field_import_surface_names.clone().iter().cloned() - { - if (((field_type.clone() != "".to_string()) - && name_in_transitive_export_surface( - field_type.clone(), - import_module.clone(), - Rc::new(vec![]), - export_sets.clone(), - typed_modules.clone(), - source_indices.clone(), - module_index.clone(), - )) - && match v1_rt::map_get(&type_summaries, field_type.clone()) - { - Some(ft) => match (*ft.repr.clone()).clone() { - TypeRepr::StructRepr => true, - _ => false, - }, - std::option::Option::None => false, - }) - { - __result.push(field_type); - } - } - __result - }), - std::option::Option::None => Rc::new(vec![]), - } - } - }) - .iter() - .cloned(), - ); + __result.extend((*{ + let type_name = match item.type_annotation.clone() { + Some(ta) => crate::v1_std_core::authored_name_at(source_indices.clone(), ta.clone()), + std::option::Option::None => "".to_string(), +}; +if (type_name.clone() == "".to_string()) { + Rc::new(vec![]) + } else { + match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), type_name.clone()) { + Some(summary) => Rc::new({ let mut __result = Vec::new(); for field_type in summary.field_import_surface_names.clone().iter().cloned() { if (((field_type.clone() != "".to_string()) && name_in_transitive_export_surface(field_type.clone(), import_module.clone(), Rc::new(vec![]), export_sets.clone(), typed_modules.clone(), source_indices.clone(), module_index.clone())) && match crate::v1_compiler_infer_emit_info::type_summary_decided(type_summaries.clone(), field_type.clone()) { + Some(ft) => match (*ft.repr.clone()).clone() { + TypeRepr::StructRepr => true, + _ => false, +}, + std::option::Option::None => false, +}) { __result.push(field_type); } } __result }), + std::option::Option::None => Rc::new(vec![]), +} + } +}).iter().cloned()); } __result })) @@ -16429,7 +16620,7 @@ pub fn emit_type_def_from_connective( } } } else { - if is_host_optional_carrier_alias(item_text.clone()) { + if declaration_owns_host_option(env.module_path.clone(), item_text.clone()) { { let type_params = emit_type_params(item.params.clone(), env.source_indices.clone()); @@ -16731,7 +16922,10 @@ pub fn rust_phantom_field_name() -> String { } pub fn type_has_fn_fields(name: String, emit_info: Rc) -> bool { - match v1_rt::map_get(&emit_info.type_summaries.clone(), name.clone()) { + match crate::v1_compiler_infer_emit_info::type_summary_decided( + emit_info.type_summaries.clone(), + name.clone(), + ) { Some(ts) => ts.has_fn_fields.clone(), std::option::Option::None => false, } @@ -18575,34 +18769,67 @@ pub fn v1_fn_body_derived_clone_param_names( emit_info.type_decl_items.clone(), si.clone(), ); - let rc_match_clone_param_names = match (*body.expr_data.clone()).clone() { + let rc_match_clone_param_names = v1_fn_body_rc_match_clone_bound_param_names( + body.clone(), + generic_param_names.clone(), + shared_types.clone(), + emit_info.clone(), + si.clone(), + ); + crate::v1_compiler_trait_bound_witness::v1_union_bound_param_names( + derived_clone_param_names.clone(), + rc_match_clone_param_names.clone(), + ) + } +} + +pub fn v1_fn_body_rc_match_clone_bound_param_names( + body: Rc, + generic_param_names: Rc>, + shared_types: Rc>, + emit_info: Rc, + source_indices: Rc>>, +) -> Rc> { + stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { + let here = match (*body.expr_data.clone()).clone() { ExprData::ExprMatch => { let scrutinee = crate::v1_std_core::match_scrutinee(body.clone()); match scrutinee.inferred.clone().as_deref().cloned() { Some(InferredNode::Resolved { node: rt, .. }) => { - let scrut_type = - crate::v1_std_core::authored_name_at(si.clone(), rt.clone()); - let arms = crate::v1_std_core::match_arm_nodes(body.clone()); let rc_match = analyze_rc_match( scrutinee.clone(), - arms.clone(), - scrut_type.clone(), + crate::v1_std_core::match_arm_nodes(body.clone()), + crate::v1_std_core::authored_name_at( + source_indices.clone(), + rt.clone(), + ), shared_types.clone(), emit_info.clone(), - si.clone(), + source_indices.clone(), ); - crate::v1_compiler_trait_bound_witness::v1_rc_match_scrutinee_clone_bound_param_names(generic_param_names.clone(), Rc::new({ let mut __result = Vec::new(); for c in rt.children.clone().iter().cloned() { __result.push(crate::v1_std_core::authored_name_at(si.clone(), c.clone())); } __result }), rc_match.needs_deref.clone()) + crate::v1_compiler_trait_bound_witness::v1_rc_match_scrutinee_clone_bound_param_names(generic_param_names.clone(), Rc::new({ let mut __result = Vec::new(); for c in rt.children.clone().iter().cloned() { __result.push(crate::v1_std_core::authored_name_at(source_indices.clone(), c.clone())); } __result }), rc_match.needs_deref.clone()) } _ => Rc::new(vec![]), } } _ => Rc::new(vec![]), }; - crate::v1_compiler_trait_bound_witness::v1_union_bound_param_names( - derived_clone_param_names.clone(), - rc_match_clone_param_names.clone(), + body.children.clone().iter().cloned().fold( + here.clone(), + |acc: Rc>, child: Rc| { + crate::v1_compiler_trait_bound_witness::v1_union_bound_param_names( + acc, + v1_fn_body_rc_match_clone_bound_param_names( + child.clone(), + generic_param_names.clone(), + shared_types.clone(), + emit_info.clone(), + source_indices.clone(), + ), + ) + }, ) - } + }) } pub fn v1_fn_bounds_by_param( @@ -19013,20 +19240,8 @@ pub fn v1_record_decl_slot_arg_name( ); } else { { - let __tco_0 = Rc::new( - decl_params - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); - let __tco_1 = Rc::new( - type_args - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&decl_params, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&type_args, 1)); let __tco_2 = slot_name; let __tco_3 = source_indices; __tco_loop_decl_params = __tco_0; @@ -19100,22 +19315,8 @@ pub fn v1_positional_bound_forwarded_names( v1_rt::concat( here.clone(), v1_positional_bound_forwarded_names( - Rc::new( - callee_names - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - wrapper_names - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&callee_names.clone(), 1)), + Rc::new(v1_rt::list_skip(&wrapper_names.clone(), 1)), callee_bound_param_names.clone(), wrapper_generic_param_names.clone(), ), @@ -21548,52 +21749,67 @@ pub fn emit_pattern( } } +pub fn rust_ambiguous_type_name_refusal(leaf: String) -> String { + v1_rt::concat( + v1_rt::concat( + "compile_error!(\"ambiguous type name '".to_string(), + leaf.clone(), + ), + "': declarations disagree\")".to_string(), + ) +} + pub fn pattern_parent_enum( name: String, parent_enum: Option, scrut_type: String, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Option { - { - let scrut_is_known_enum = ((scrut_type.clone() != "".to_string()) - && is_enum_type_name(scrut_type.clone(), type_summaries.clone())); - if (parent_enum.clone() != std::option::Option::None) { - parent_enum.clone() + if (parent_enum.clone() != std::option::Option::None) { + parent_enum.clone() + } else { + if (scrut_type.clone() == "".to_string()) { + unique_variant_parent(type_summaries.clone(), name.clone()) } else { - if scrut_is_known_enum.clone() { - Some(scrut_type.clone()) - } else { - unique_variant_parent(type_summaries.clone(), name.clone()) + match (*is_enum_type_name(scrut_type.clone(), type_summaries.clone())).clone() { + TypeSummaryQuestion::QuestionDecided { value: true, .. } => { + Some(scrut_type.clone()) + } + TypeSummaryQuestion::QuestionNameAmbiguous { leaf: leaf, .. } => { + Some(rust_ambiguous_type_name_refusal(leaf.clone())) + } + _ => unique_variant_parent(type_summaries.clone(), name.clone()), } } } } pub fn unique_variant_parent( - type_summaries: Rc>>, + type_summaries: Rc, variant_name: String, ) -> Option { { let parent_matches = Rc::new({ let mut __result = Vec::new(); - for type_name in Rc::new(v1_rt::sorted_map_keys(&type_summaries)) - .iter() - .cloned() + for summary in + crate::v1_compiler_infer_emit_info::type_summary_values(type_summaries.clone()) + .iter() + .cloned() { - if (is_enum_type_name(type_name.clone(), type_summaries.clone()) - && crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( - type_summaries.clone(), - variant_name.clone(), - type_name.clone(), - )) - { - __result.push(type_name); + if crate::v1_compiler_infer_emit_info::summary_is_enum_with_variant( + summary.clone(), + variant_name.clone(), + ) { + __result.push(summary); } } __result }); if ((parent_matches.clone().len() as i64) == 1) { - parent_matches.clone().first().cloned() + match parent_matches.clone().first().cloned() { + Some(summary) => Some(summary.name.clone()), + std::option::Option::None => std::option::Option::None, + } } else { std::option::Option::None } @@ -21615,8 +21831,30 @@ pub fn is_optional_variant_name(name: String) -> bool { } pub fn is_optional_like_parent_name(name: String) -> bool { - ((name.clone() == kernel_optional_mint_name()) - || is_host_diagnostics_carrier_alias(name.clone())) + if v1_rt::contains(name.clone(), ".".to_string()) { + { + let segments = Rc::new( + name.clone() + .split(&".".to_string()) + .map(|s| s.to_string()) + .collect::>(), + ); + let module_path = Rc::new(v1_rt::list_take( + &segments.clone(), + v1_rt::int_sub((segments.clone().len() as i64), 1), + )) + .join(&".".to_string()); + let decl_name = crate::v1_std_core::qualified_last_segment(name.clone()); + (declaration_owns_host_option(module_path.clone(), decl_name.clone()) + || crate::std_decl_ref::declaration_ref_in_list( + crate::std_decl_ref::decl_ref(module_path.clone(), decl_name.clone()), + rust_host_option_carrier_declarations(), + )) + } + } else { + ((name.clone() == kernel_optional_mint_name()) + || is_host_diagnostics_carrier_alias(name.clone())) + } } pub fn is_some_like_variant_name(name: String) -> bool { @@ -22285,13 +22523,15 @@ pub fn analyze_rc_pattern( Some(enum_name) => { v1_rt::set_contains(&shared_types, enum_name.clone()) } - std::option::Option::None => match v1_rt::map_get( - &emit_info.type_summaries.clone(), - bare_n.clone(), - ) { - Some(_) => v1_rt::set_contains(&shared_types, bare_n.clone()), - std::option::Option::None => false, - }, + std::option::Option::None => { + match crate::v1_compiler_infer_emit_info::type_summary_decided( + emit_info.type_summaries.clone(), + bare_n.clone(), + ) { + Some(_) => v1_rt::set_contains(&shared_types, bare_n.clone()), + std::option::Option::None => false, + } + } }; let ref_bound_fields = Rc::new({ let mut __result = Vec::new(); @@ -23310,7 +23550,10 @@ pub fn rust_as_ref_let_is_irrefutable( ); match resolved.clone() { Some(enum_name) => { - match v1_rt::map_get(&emit_info.type_summaries.clone(), enum_name.clone()) { + match crate::v1_compiler_infer_emit_info::type_summary_decided( + emit_info.type_summaries.clone(), + enum_name.clone(), + ) { Some(summary) => { ((Rc::new(v1_rt::sorted_map_keys(&summary.variant_name_set.clone())) .len() as i64) @@ -23708,7 +23951,9 @@ pub fn freemonoid_empty_from_variant_parent(leaf_name: String, enum_name: String pub fn freemonoid_empty_from_emit_info(leaf_name: String, emit_info: Rc) -> bool { ((leaf_name.clone() == "Empty".to_string()) && match v1_rt::map_get(&emit_info.variant_to_enum.clone(), "Empty".to_string()) { - Some(p) => (p.clone() == "FreeMonoid".to_string()), + Some(p) => { + (crate::v1_std_core::qualified_last_segment(p.clone()) == "FreeMonoid".to_string()) + } std::option::Option::None => false, }) } @@ -23732,11 +23977,16 @@ pub fn effective_variant_parent_from_enum_lookup( let rt_name = crate::v1_std_core::authored_name_at(source_indices.clone(), rt.clone()); if (((rt.ident_span.clone() != std::option::Option::None) && (rt_name.clone() != leaf_name.clone())) - && crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + && match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), leaf_name.clone(), rt_name.clone(), )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + }) { Some(rt_name.clone()) } else { @@ -23788,7 +24038,7 @@ pub fn effective_variant_parent( let cached = match v1_rt::map_get(&emit_info.variant_to_enum.clone(), leaf_name.clone()) { Some(p) => { if (p.clone() != "".to_string()) { - Some(p.clone()) + Some(crate::v1_std_core::qualified_last_segment(p.clone())) } else { std::option::Option::None } @@ -24775,14 +25025,7 @@ pub fn rust_empty_map_value_type_str( shared_types: Rc>, source_indices: Rc>>, ) -> String { - match map_type - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + match map_type.children.clone().get((1) as usize).cloned() { Some(value_child) => { let rendered = render_rust_type( crate::v1_compiler_infer_types::child_type_node(value_child.clone()), @@ -25443,11 +25686,19 @@ pub fn emit_rust_expr_record_lit( }; let concrete_peel_would_erase_optional_ctor = (is_optional_variant_name(variant_name.clone()) - && !crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + && match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), variant_name.clone(), crate::v1_std_core::authored_name_at(si.clone(), expanded_rt.clone()), - )); + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => { + (v.clone() == false) + } + TypeSummaryQuestion::QuestionNotDeclared => true, + TypeSummaryQuestion::QuestionNameAmbiguous { leaf: _, .. } => false, + }); let peeled_type_name = if ((((parent_enum.clone() == std::option::Option::None) && (expanded_rt.connective.clone() == Connective::Conj)) && (expanded_rt.ident_span.clone() != std::option::Option::None)) @@ -26141,8 +26392,8 @@ pub fn emit_cloned_arg( pub fn is_enum_type_name( type_name: String, - type_summaries: Rc>>, -) -> bool { + type_summaries: Rc, +) -> Rc { crate::v1_compiler_infer_emit_info::is_enum_in_summaries( type_summaries.clone(), type_name.clone(), @@ -26160,11 +26411,16 @@ pub fn contextual_variant_parent_from_type_name( crate::v1_std_core::authored_name_at(source_indices.clone(), resolved_type.clone()); if (((resolved_type.ident_span.clone() != std::option::Option::None) && (rt_name.clone() != variant_name.clone())) - && crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + && match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), variant_name.clone(), rt_name.clone(), )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + }) { Some(rt_name.clone()) } else { @@ -26210,11 +26466,16 @@ pub fn contextual_variant_parent( ) -> Option { match parent_enum.clone() { Some(explicit_parent) => { - if crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + if match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), variant_name.clone(), explicit_parent.clone(), - ) { + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + } { Some(explicit_parent.clone()) } else { std::option::Option::None @@ -26542,16 +26803,18 @@ pub fn rust_call_arg_fail_closed_unwrap( ) -> String { match callee.clone() { Some(info) => match function_value_params(info.params.clone()) - .iter() + .get((idx.clone()) as usize) .cloned() - .skip(idx.clone() as usize) - .next() { Some(param) => { let param_type = crate::v1_std_core::param_node_type_expr(param.clone()); let param_required = (((param_type.return_cardinality.clone() != Cardinality::CardOptional) - && !is_host_optional_carrier_type(param_type.clone(), source_indices.clone())) + && !is_host_optional_carrier_type( + param_type.clone(), + source_indices.clone(), + crate::v1_compiler_infer_env::empty_type_env(), + )) && !rust_param_type_is_type_variable( param_type.clone(), source_indices.clone(), @@ -26589,10 +26852,8 @@ pub fn rust_call_arg_function_value_adapt( } match callee.clone() { Some(info) => match function_value_params(info.params.clone()) - .iter() + .get((idx.clone()) as usize) .cloned() - .skip(idx.clone() as usize) - .next() { Some(param) => { let arity = (crate::v1_std_core::param_node_type_expr(param.clone()) @@ -26702,19 +26963,94 @@ pub fn lambda_argument_scope(arg: Rc, scope: Rc) -> Rc>(), - ), + Rc::new(v1_rt::list_skip(&arg.children.clone(), 1)), ), _ => scope.clone(), } } +pub fn rust_call_target_is_v2_algebra_length(target: Rc) -> bool { + match rust_call_target_declared_length(target.clone()) { + std::option::Option::None => false, + Some(id) => { + ((id.decl_name.clone() == "length".to_string()) + && (id.owner_module_path.clone() == "v2.std.algebra".to_string())) + } + } +} + +pub fn rust_call_target_declared_length( + target: Rc, +) -> Option> { + match (*target.clone()).clone() { + CallTargetIdentity::SourceDeclarationCall { + owner_module_path: owner, + decl_name: decl, + .. + } => Some(Rc::new(DeclaredCallableIdentity { + owner_module_path: owner.clone(), + decl_name: decl.clone(), + })), + CallTargetIdentity::RuntimePrimitiveCall { + projected_from: projected, + .. + } => projected.clone(), + CallTargetIdentity::LocallyBoundCall { name: _, .. } => std::option::Option::None, + CallTargetIdentity::CallableTargetUndetermined => std::option::Option::None, + } +} + +pub fn rust_emit_length_on_host_string( + call_target: Rc, + func: String, + args: Rc>>, + call_semantics: Option>, + registry: Rc>>, + scope: Rc, + depth: i64, + shared_types: Rc>, + emit_info: Rc, +) -> Option { + if (rust_call_target_is_v2_algebra_length(call_target.clone()) == false) { + std::option::Option::None + } else { + { + let length_args = crate::v1_compiler_emit::order_typed_call_args_from_semantics( + args.clone(), + func.clone(), + call_semantics.clone(), + scope.clone(), + ); + match length_args.clone().first().cloned() { + std::option::Option::None => std::option::Option::None, + Some(xs_arg) => { + if is_host_text_typed_expr( + crate::v1_std_core::arg_value(xs_arg.clone()), + scope.type_env.clone().source_indices.clone(), + ) { + Some(v1_rt::concat( + v1_rt::concat( + "v1_rt::string_length(&".to_string(), + emit_typed_expr_base( + crate::v1_std_core::arg_value(xs_arg.clone()), + registry.clone(), + scope.clone(), + depth.clone(), + shared_types.clone(), + emit_info.clone(), + ), + ), + ")".to_string(), + )) + } else { + std::option::Option::None + } + } + } + } + } +} + pub fn emit_typed_call( func: String, args: Rc>>, @@ -26750,7 +27086,7 @@ pub fn emit_typed_call( scope.clone(), ); let get_list = get_args.clone().first().cloned(); - let get_idx = get_args.clone().iter().cloned().skip(1 as usize).next(); + let get_idx = get_args.clone().get((1) as usize).cloned(); let get_result = match get_list.clone() { Some(list_arg) => match get_idx.clone() { Some(idx_arg) => { @@ -26814,10 +27150,8 @@ pub fn emit_typed_call( let update_arg = crate::v1_std_core::arg_value( with_args .clone() - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() .clone() .unwrap(), ); @@ -26994,6 +27328,20 @@ pub fn emit_typed_call( return disc_result; } } + match rust_emit_length_on_host_string( + call_target.clone(), + func.clone(), + args.clone(), + call_semantics.clone(), + registry.clone(), + scope.clone(), + depth.clone(), + shared_types.clone(), + emit_info.clone(), + ) { + Some(lowered) => return lowered.clone(), + std::option::Option::None => {} + } let ordered_args = crate::v1_compiler_emit::order_typed_call_args_from_semantics( args.clone(), func.clone(), @@ -27054,10 +27402,8 @@ pub fn emit_typed_call( let a = pair.1.clone(); let arg_emit_info = match application_plan .clone() - .iter() + .get((idx.clone()) as usize) .cloned() - .skip(idx.clone() as usize) - .next() { Some(application) => { crate::v1_compiler_infer_emit_info::emit_info_with_expected_type( @@ -27511,13 +27857,7 @@ pub fn emit_nested_rt_concat( ) }; { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = next_acc.clone(); let __tco_2 = shared_types; __tco_loop_remaining = __tco_0; @@ -27892,13 +28232,7 @@ pub fn lambda_scope_from_children( .fold(scope.clone(), |acc: Rc, pair: (i64, String)| { let idx = pair.0.clone(); let param_name = pair.1.clone(); - let param_type = match param_nodes - .clone() - .iter() - .cloned() - .skip(idx.clone() as usize) - .next() - { + let param_type = match param_nodes.clone().get((idx.clone()) as usize).cloned() { Some(pn) => match pn.inferred.clone().as_deref().cloned() { Some(InferredNode::Resolved { node: resolved_type, @@ -27947,13 +28281,7 @@ pub fn lambda_param_type_strs( let inferred_type = if (fold_acc_uses_fallback.clone() && (idx.clone() == 0)) { std::option::Option::None } else { - match param_nodes - .clone() - .iter() - .cloned() - .skip(idx.clone() as usize) - .next() - { + match param_nodes.clone().get((idx.clone()) as usize).cloned() { Some(pn) => match pn.inferred.clone().as_deref().cloned() { Some(InferredNode::Resolved { node: param_type, .. @@ -28008,16 +28336,11 @@ pub fn lambda_param_type_strs( std::option::Option::None => std::option::Option::None, } }; - let fallback_type = match fallback_types - .clone() - .iter() - .cloned() - .skip(idx.clone() as usize) - .next() - { - Some(ty) => ty.clone(), - std::option::Option::None => "_".to_string(), - }; + let fallback_type = + match fallback_types.clone().get((idx.clone()) as usize).cloned() { + Some(ty) => ty.clone(), + std::option::Option::None => "_".to_string(), + }; let spec = crate::v1_compiler_emit_core_support::language_spec(RenderTarget::Rust); let ident = crate::v1_compiler_emit::emit_ident(param_name.clone(), RenderTarget::Rust); @@ -28057,15 +28380,7 @@ pub fn emit_typed_collection_lambda( scope.type_env.clone().source_indices.clone(), ); let bd = crate::v1_std_core::lambda_body(lambda_expr.clone()); - let pn = Rc::new( - lambda_expr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let pn = Rc::new(v1_rt::list_skip(&lambda_expr.children.clone(), 1)); let param_strs = lambda_param_type_strs( ps.clone(), pn.clone(), @@ -28136,15 +28451,7 @@ pub fn emit_typed_fold_lambda( scope.type_env.clone().source_indices.clone(), ); let bd = crate::v1_std_core::lambda_body(lambda_expr.clone()); - let pn = Rc::new( - lambda_expr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let pn = Rc::new(v1_rt::list_skip(&lambda_expr.children.clone(), 1)); let safe_acc_type = rust_fold_safe_acc_type(acc_type_str.clone()); let fallback_types = Rc::new({ let mut __result = Vec::new(); @@ -28406,7 +28713,7 @@ pub fn emit_rust_fold_method_call( scope.type_env.clone().source_indices.clone(), acc_type_node.clone(), ); - let fold_lambda_node = match args.clone().iter().cloned().skip(1 as usize).next() { + let fold_lambda_node = match args.clone().get((1) as usize).cloned() { Some(a) => crate::v1_std_core::arg_value(a.clone()), std::option::Option::None => type_variable_node("".to_string()), }; @@ -28700,7 +29007,7 @@ pub fn emit_rust_fold_method_call( let sharing = crate::v1_compiler_emit_core_support::language_spec(RenderTarget::Rust) .sharing .clone(); - let elem_unused = match args.clone().iter().cloned().skip(1 as usize).next() { + let elem_unused = match args.clone().get((1) as usize).cloned() { Some(a) => fold_lambda_element_unused( crate::v1_std_core::arg_value(a.clone()), scope.type_env.clone().source_indices.clone(), @@ -28720,7 +29027,7 @@ pub fn emit_rust_fold_method_call( iter_template.clone(), recv_str.clone(), ); - let step_awaits = match args.clone().iter().cloned().skip(1 as usize).next() { + let step_awaits = match args.clone().get((1) as usize).cloned() { Some(a) => rust_expr_reaches_awaited_call( crate::v1_std_core::arg_value(a.clone()), registry.clone(), @@ -28742,7 +29049,7 @@ pub fn emit_rust_fold_method_call( ) } else { { - let fold_fn = match args.clone().iter().cloned().skip(1 as usize).next() { + let fold_fn = match args.clone().get((1) as usize).cloned() { Some(a) => emit_typed_fold_lambda( crate::v1_std_core::arg_value(a.clone()), lambda_acc_type_str.clone(), @@ -28791,13 +29098,13 @@ pub fn emit_rust_effectful_fold_loop( ExprData::ExprLambda => { let si = scope.type_env.clone().source_indices.clone(); let ps = crate::v1_std_core::lambda_param_names_at(lambda_expr.clone(), si.clone()); -let pn = Rc::new(lambda_expr.children.clone().iter().cloned().skip(1 as usize).collect::>()); +let pn = Rc::new(v1_rt::list_skip(&lambda_expr.children.clone(), 1)); let acc_param_type = rust_fold_safe_acc_type(acc_type_str.clone()); let acc_name = match ps.clone().first().cloned() { Some(n) => n.clone(), std::option::Option::None => "_".to_string(), }; -let elem_name = match ps.clone().iter().cloned().skip(1 as usize).next() { +let elem_name = match ps.clone().get((1) as usize).cloned() { Some(n) => n.clone(), std::option::Option::None => "_".to_string(), }; @@ -28898,7 +29205,7 @@ pub fn fold_lambda_element_unused( lambda_expr.clone(), source_indices.clone(), ); - match ps.clone().iter().cloned().skip(1 as usize).next() { + match ps.clone().get((1) as usize).cloned() { Some(elem_name) => (elem_name.clone() == "_".to_string()), std::option::Option::None => false, } @@ -29111,15 +29418,10 @@ pub fn emit_rust_map_method_call( let lambda_scope = lambda_scope_from_children( scope.clone(), ps.clone(), - Rc::new( - crate::v1_std_core::arg_value(a.clone()) - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip( + &crate::v1_std_core::arg_value(a.clone()).children.clone(), + 1, + )), ); let body_str = emit_typed_expr( bd.clone(), @@ -29210,15 +29512,10 @@ pub fn emit_rust_map_method_call( let lambda_scope = lambda_scope_from_children( scope.clone(), ps.clone(), - Rc::new( - crate::v1_std_core::arg_value(a.clone()) - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip( + &crate::v1_std_core::arg_value(a.clone()).children.clone(), + 1, + )), ); let body_str = emit_typed_expr( bd.clone(), @@ -29334,15 +29631,10 @@ pub fn emit_rust_higher_order_method( let lambda_scope = lambda_scope_from_children( scope.clone(), ps.clone(), - Rc::new( - crate::v1_std_core::arg_value(a.clone()) - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip( + &crate::v1_std_core::arg_value(a.clone()).children.clone(), + 1, + )), ); let body_str = emit_typed_expr( bd.clone(), @@ -29654,10 +29946,10 @@ pub fn emit_rust_first_method_call( ); v1_rt::concat( v1_rt::concat( - v1_rt::concat(recv_str.clone(), ".iter().cloned().skip(".to_string()), + v1_rt::concat(recv_str.clone(), ".get((".to_string()), n_str.clone(), ), - " as usize).next()".to_string(), + ") as usize).cloned()".to_string(), ) } } else { @@ -30044,7 +30336,7 @@ pub fn emit_typed_method_call( method_def.clone(), ); let method_name = if (method_name_raw.clone() == "length".to_string()) { - if is_string_typed_expr( + if is_host_text_typed_expr( receiver.clone(), scope.type_env.clone().source_indices.clone(), ) { @@ -30438,7 +30730,7 @@ pub fn freemonoid_match_arm_for(arms: Rc>>, variant: String) -> Opt pub fn arm_resolved_parent_enum( arm: Rc, scrut_type: String, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Option { match (*crate::v1_std_core::arm_pattern(arm.clone())).clone() { MatchPattern::VariantPattern { @@ -30485,7 +30777,7 @@ pub fn freemonoid_catchall_arm(arms: Rc>>) -> Option> { pub fn arms_are_freemonoid_coproduct( arms: Rc>>, scrut_type: String, - type_summaries: Rc>>, + type_summaries: Rc, ) -> bool { { let has_empty = match freemonoid_match_arm_for(arms.clone(), "Empty".to_string()) { @@ -33088,11 +33380,16 @@ pub fn emit_field_value_with_context( }; let corrected_parent = match expected_type.clone() { Some(et) => { - if crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + if match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), variant_name.clone(), et.clone(), - ) { + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + } { Some(et.clone()) } else { pe.clone() @@ -33202,7 +33499,7 @@ pub fn record_field_expected_type( pub fn struct_candidates_by_field_names( field_names: Rc>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc>> { { let n_fields = (field_names.clone().len() as i64); @@ -33211,7 +33508,11 @@ pub fn struct_candidates_by_field_names( } Rc::new({ let mut __result = Vec::new(); - for summary in Rc::new(v1_rt::map_values(&type_summaries)).iter().cloned() { + for summary in + crate::v1_compiler_infer_emit_info::type_summary_values(type_summaries.clone()) + .iter() + .cloned() + { if match (*summary.repr.clone()).clone() { TypeRepr::StructRepr => { ({ @@ -33317,7 +33618,7 @@ pub fn effectful_self_recursion_diagnostics( pub fn anonymous_record_struct_candidates( field_names: Rc>, field_type_hints: Rc>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc>> { { let candidates = @@ -33368,7 +33669,7 @@ pub fn anonymous_record_struct_candidates( pub fn ambiguous_anonymous_record_literal_diagnostics( n: Rc, - type_summaries: Rc>>, + type_summaries: Rc, source_indices: Rc>>, module_name: String, ) -> Rc>> { @@ -33551,7 +33852,7 @@ pub fn ambiguous_anonymous_record_literal_diagnostics( pub fn find_struct_name_by_fields( field_names: Rc>, field_type_hints: Rc>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Option { { let candidates = anonymous_record_struct_candidates( @@ -33572,7 +33873,7 @@ pub fn find_struct_name_by_fields( pub fn find_unique_struct_name_by_fields( field_names: Rc>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Option { { let candidates = @@ -33614,7 +33915,10 @@ pub fn emit_typed_record_lit( resolved_type.clone(), ); if ((resolved_type.ident_span.clone() != std::option::Option::None) - && v1_rt::map_contains_key(&emit_info.type_summaries.clone(), rt_name.clone())) + && (crate::v1_compiler_infer_emit_info::type_summary_decided( + emit_info.type_summaries.clone(), + rt_name.clone(), + ) != std::option::Option::None)) { Some(rt_name.clone()) } else { @@ -33895,8 +34199,10 @@ pub fn emit_typed_record_lit( ) { return "panic!(\"record-shaped carrier has no realization in the selected Rust target inhabitant\")".to_string(); } - let tn_is_known_struct = - v1_rt::map_contains_key(&emit_info.type_summaries.clone(), tn.clone()); + let tn_is_known_struct = (crate::v1_compiler_infer_emit_info::type_summary_decided( + emit_info.type_summaries.clone(), + tn.clone(), + ) != std::option::Option::None); let ctor_name = if tn_is_known_struct.clone() { tn.clone() } else { @@ -33963,11 +34269,16 @@ pub fn emit_typed_record_lit( && (rt_name.clone() != variant_surface_name.clone())) && !rt_is_type_var.clone()) && (rt_name.clone() != "Error".to_string())) - && crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( + && match (*crate::v1_compiler_infer_emit_info::variant_belongs_to_enum( emit_info.type_summaries.clone(), variant_surface_name.clone(), rt_name.clone(), )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + }) { Some(rt_name.clone()) } else { @@ -35283,6 +35594,24 @@ pub fn is_string_typed_expr( } } +pub fn is_host_text_typed_expr( + e: Rc, + source_indices: Rc>>, +) -> bool { + match e.inferred.clone().as_deref().cloned() { + Some(InferredNode::Resolved { node: rt, .. }) => { + let is_optional = (rt.return_cardinality.clone() == Cardinality::CardOptional); + let inner = if is_optional.clone() { + crate::v1_std_core::with_required_cardinality(rt.clone()) + } else { + rt.clone() + }; + is_host_text_carrier_type(inner.clone(), source_indices.clone()) + } + _ => false, + } +} + pub fn emit_typed_string_interp( parts: Rc>>, registry: Rc>>, @@ -35487,14 +35816,7 @@ pub fn emit_tco_init_block_stmts( }); } Some(stmt) => { - let rest = Rc::new( - remaining - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&remaining.clone(), 1)); match rest.clone().first().cloned() { std::option::Option::None => { break Rc::new(BlockEmitState { @@ -36679,13 +37001,7 @@ pub fn emit_typed_tco_reassign( let mut __result = Vec::new(); for pair in pairs.iter().cloned() { __result.push({ - let av = match arg_values - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + let av = match arg_values.clone().get((pair.0.clone()) as usize).cloned() { Some(v) => v.clone(), std::option::Option::None => pair.1.clone(), }; @@ -37468,6 +37784,7 @@ pub fn emit_transport_call( rsf.clone(), op_node.clone(), source_indices.clone(), + env.clone(), ), BoundOperation::FileBound { verb: v, @@ -38823,7 +39140,7 @@ pub fn emit_exit_code_handling( v1_rt::concat( v1_rt::concat( "\n _ => { ".to_string(), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38885,7 +39202,7 @@ pub fn emit_exit_arm( v1_rt::concat(" ".to_string(), pattern.clone()), " => { ".to_string(), ), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38905,6 +39222,7 @@ pub fn emit_shell_call( result_fields: Rc>>, op_node: Rc, source_indices: Rc>>, + env: Rc, ) -> String { { let argv = transport.children.clone(); @@ -38959,7 +39277,8 @@ pub fn emit_shell_call( Some(_) => true, std::option::Option::None => false, }; - let let_kw = if has_stdin.clone() { + let needs_capture = shell_needs_capture_accounting(result_fields.clone()); + let let_kw = if (has_stdin.clone() || needs_capture.clone()) { "let mut output".to_string() } else { "let output".to_string() @@ -39003,16 +39322,7 @@ pub fn emit_shell_call( let arg_lines = if ((argv.clone().len() as i64) > 1) { Rc::new({ let mut __result = Vec::new(); - for arg in Rc::new( - argv.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - { + for arg in Rc::new(v1_rt::list_skip(&argv.clone(), 1)).iter().cloned() { __result.push( if shell_argv_element_is_word_list( arg.clone(), @@ -39078,79 +39388,136 @@ pub fn emit_shell_call( __result }); let wd_line = " .current_dir(self.working_dir.as_deref().unwrap_or(\".\"))".to_string(); - if has_stdin.clone() { + let return_line = emit_exit_code_handling( + op_node.clone(), + result_fields.clone(), + source_indices.clone(), + ); + if needs_capture.clone() { { - let stdin_expr = - crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) + let policy_bind = emit_shell_stderr_policy_binding(op_node.clone(), env.clone()); + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let stdin_thread = if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) .clone() .unwrap(); - let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { - ExprData::ExprVar { - binding_kind: _, .. - } => crate::v1_compiler_emit::emit_ident( - crate::v1_std_core::expr_var_name_at( - stdin_expr.clone(), - source_indices.clone(), - ), - RenderTarget::Rust, - ), - _ => crate::v1_compiler_emit::emit_simple_expr( - stdin_expr.clone(), - RenderTarget::Rust, - source_indices.clone(), - ), + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + v1_rt::concat(v1_rt::concat("let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n Ok(())\n});\n".to_string()) + } + } else { + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string() }; - let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); - let spawn_exec = " .spawn()?;".to_string(); - let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); - let wait_line = "let output = output.wait_with_output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), + let capture_lines = v1_rt::concat( + v1_rt::concat(shell_capture_drain_start(), stdin_thread.clone()), + shell_capture_join_project(), ); - let all_lines = v1_rt::concat( + v1_rt::concat( v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + v1_rt::concat( + Rc::new(vec![policy_bind.clone(), cmd_line.clone()]), + arg_lines.clone(), + ), env_lines.clone(), ), Rc::new(vec![ wd_line.clone(), spawn_line.clone(), spawn_exec.clone(), - write_block.clone(), - wait_line.clone(), - check_line.clone(), + capture_lines.clone(), return_line.clone(), ]), - ); - all_lines.clone().join(&"\n".to_string()) + ) + .join(&"\n".to_string()) } } else { - { - let output_line = " .output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), - ); - let all_lines = v1_rt::concat( + if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) + .clone() + .unwrap(); + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); + let wait_line = "let output = output.wait_with_output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), - env_lines.clone(), - ), - Rc::new(vec![ - wd_line.clone(), - output_line.clone(), - check_line.clone(), - return_line.clone(), - ]), - ); - all_lines.clone().join(&"\n".to_string()) + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + spawn_line.clone(), + spawn_exec.clone(), + write_block.clone(), + wait_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } + } else { + { + let output_line = " .output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + output_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } } } } @@ -39334,6 +39701,66 @@ pub fn shell_stderr_binding_line() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { + { + let mut __found = false; + for f in result_fields.iter().cloned() { + if crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone()) { + __found = true; + break; + } + } + __found + } +} + +pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { + v1_rt::concat(shell_stderr_binding_line(), " ".to_string()) + } +} + +pub fn emit_shell_stderr_policy_binding(op_node: Rc, env: Rc) -> String { + if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( + op_node.clone(), + env.clone(), + ) { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n".to_string(), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n".to_string()), " match v1_rt::read_host_budget_bytes() {\n".to_string()), " (Some(n), source) => (Some(n as usize), source),\n".to_string()), " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n".to_string()), " }\n".to_string()), " }\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n".to_string()), " let n = crate::std_measure::byte_size_count(bytes.clone());\n".to_string()), " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n".to_string()), " (None, String::new())\n".to_string()), " }\n".to_string()), "};\n".to_string()), "let __stderr_tail_bytes: usize = match &*stderr_capture {\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n".to_string()), "};\n".to_string()) + } else { + v1_rt::concat( + v1_rt::concat( + "return Err(\"".to_string(), + crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: "stderr_truncated".to_string(), + }, + )), + ), + "\".into());\n".to_string(), + ) + } +} + +pub fn shell_capture_drain_start() -> String { + thread_local! { + static CACHED: String = { + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn shell_capture_join_project() -> String { + thread_local! { + static CACHED: String = { + "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn emit_shell_return(result_fields: Rc>>) -> String { v1_rt::concat( v1_rt::concat( @@ -39383,24 +39810,28 @@ pub fn shell_projection_value(result_fields: Rc>>) -> S } pub fn shell_stderr_prelude(result_fields: Rc>>) -> String { - { - let needs_stderr = { - let mut __found = false; - for f in result_fields.iter().cloned() { - if match f.channel.clone() { - ShellResultChannel::ShellChanStderr => true, - _ => false, - } { - __found = true; - break; + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { + { + let needs_stderr = { + let mut __found = false; + for f in result_fields.iter().cloned() { + if match f.channel.clone() { + ShellResultChannel::ShellChanStderr => true, + _ => false, + } { + __found = true; + break; + } } + __found + }; + if needs_stderr.clone() { + v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) + } else { + "".to_string() } - __found - }; - if needs_stderr.clone() { - v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) - } else { - "".to_string() } } } @@ -39418,15 +39849,9 @@ pub fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: bool) - "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()" .to_string(), ), - ShellResultChannel::ShellChanStderrTruncated => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrTotalBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrRetainedBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } + ShellResultChannel::ShellChanStderrTruncated => "stderr_truncated".to_string(), + ShellResultChannel::ShellChanStderrTotalBytes => "stderr_total_bytes".to_string(), + ShellResultChannel::ShellChanStderrRetainedBytes => "stderr_retained_bytes".to_string(), }; if is_optional.clone() { v1_rt::concat( @@ -40085,18 +40510,21 @@ pub fn emit_data_def( annotation_type_node.clone(), scope.type_env.clone().source_indices.clone(), ) { - crate::v1_compiler_coercion::coerce_primitive_type( - crate::v1_compiler_coercion::type_reference_realization( - annotation_type_node.clone(), + rust_carrier_optional_wrap( + annotation_type_node.clone(), + crate::v1_compiler_coercion::coerce_primitive_type( + crate::v1_compiler_coercion::type_reference_realization( + annotation_type_node.clone(), + crate::v1_std_core::authored_name_at( + scope.type_env.clone().source_indices.clone(), + annotation_type_node.clone(), + ), + RenderTarget::Rust, + ), crate::v1_std_core::authored_name_at( scope.type_env.clone().source_indices.clone(), annotation_type_node.clone(), ), - RenderTarget::Rust, - ), - crate::v1_std_core::authored_name_at( - scope.type_env.clone().source_indices.clone(), - annotation_type_node.clone(), ), ) } else { @@ -40251,6 +40679,10 @@ pub fn data_row_type_forbids_deserialize( ) } +pub fn data_row_declares_optional(type_node: Rc) -> bool { + (type_node.return_cardinality.clone() == Cardinality::CardOptional) +} + pub fn emit_data_def_body( type_node: Rc, value: Rc, @@ -40437,10 +40869,29 @@ pub fn emit_data_def_body( scope.type_env.clone().source_indices.clone(), )) { - match (*crate::v1_compiler_emit::emit_data_value_json(value.clone(), scope.type_env.clone().source_indices.clone(), emit_info.data_variant_wire_spellings.clone())).clone() { + { + let row_json = if data_row_declares_optional(type_node.clone()) { + crate::v1_compiler_emit::emit_declared_optional_row_json( + value.clone(), + crate::v1_std_core::authored_name_at( + scope.type_env.clone().source_indices.clone(), + type_node.clone(), + ), + scope.type_env.clone().source_indices.clone(), + emit_info.data_variant_wire_spellings.clone(), + ) + } else { + crate::v1_compiler_emit::emit_data_value_json( + value.clone(), + scope.type_env.clone().source_indices.clone(), + emit_info.data_variant_wire_spellings.clone(), + ) + }; + match (*row_json.clone()).clone() { EmitterOutcome::Refused { reason: r, .. } => v1_rt::concat(v1_rt::concat(" compile_error!(\"".to_string(), crate::v1_compiler_emit_core_support::escape_string_literal_body(r.clone())), "\")".to_string()), EmitterOutcome::Emitted { json: json_str, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(" serde_json::from_str(\"".to_string(), crate::v1_compiler_emit_core_support::escape_string_literal_body(json_str.clone())), "\")\n".to_string()), " .expect(\"valid data definition\")".to_string()), } + } } else { { let is_map = crate::v1_compiler_infer_types::node_is_keyed_collection( @@ -41547,11 +41998,417 @@ pub fn emit_gunbc_cli_dispatch_generated(crate_name: String) -> Rc { ) } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NativeEntryEffects { + pub entry_module: String, + pub entry_function: String, + pub admissions: Rc>>, +} + +pub fn native_entry_function_name(modules: Rc>>) -> Option { + if compiler_pipeline_entry_is_native_serve(modules.clone()) { + Some(native_serve_entry_function()) + } else { + if compiler_pipeline_entry_is_native_cli(modules.clone()) { + Some(native_cli_entry_function()) + } else { + if compiler_pipeline_entry_is_native_claim(modules.clone()) { + Some(native_claim_entry_function()) + } else { + std::option::Option::None + } + } + } +} + +pub fn native_serve_entry_function() -> String { + thread_local! { + static CACHED: String = { + "native_serve_handle".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn native_cli_entry_function() -> String { + thread_local! { + static CACHED: String = { + "v2_cli_main".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn native_claim_entry_function() -> String { + thread_local! { + static CACHED: String = { + "native_claim_report".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn native_effect_decl_sites( + modules: Rc>>, + kind: ParsedModuleItemKind, + name: String, +) -> Rc>> { + Rc::new({ + let mut __result = Vec::new(); + for tm in modules.iter().cloned() { + __result.extend( + (*{ + let si = tm.type_env.clone().source_indices.clone(); + let module_name = + crate::v1_std_core::authored_name_at(si.clone(), tm.module.clone()); + Rc::new({ + let mut __result = Vec::new(); + for item in Rc::new({ + let mut __result = Vec::new(); + for item in tm.items.clone().iter().cloned() { + if ((item.module_item_kind.clone() == kind.clone()) + && (crate::v1_std_core::authored_name_at( + si.clone(), + item.clone(), + ) == name.clone())) + { + __result.push(item); + } + } + __result + }) + .iter() + .cloned() + { + __result.push(Rc::new(NativeEffectDeclSite { + item: item.clone(), + module_name: module_name.clone(), + })); + } + __result + }) + }) + .iter() + .cloned(), + ); + } + __result + }) +} + +pub fn native_entry_denominator(modules: Rc>>) -> Rc { + if compiler_pipeline_entry_is_native_serve(modules.clone()) { + native_service_route_denominator() + } else { + if compiler_pipeline_entry_is_native_claim(modules.clone()) { + native_claim_route_denominator() + } else { + native_compiler_route_denominator() + } + } +} + +pub fn native_entry_effects( + modules: Rc>>, + registry: Rc>>, +) -> Option> { + match native_entry_function_name(modules.clone()) { + std::option::Option::None => std::option::Option::None, + Some(entry_fn) => { + let entry_module = compiler_pipeline_entry_module_name(modules.clone()); + let denominator = native_entry_denominator(modules.clone()); + let entry_si = Rc::new({ + let mut __result = Vec::new(); + for tm in Rc::new({ + let mut __result = Vec::new(); + for tm in modules.iter().cloned() { + if (crate::v1_std_core::authored_name_at( + tm.type_env.clone().source_indices.clone(), + tm.module.clone(), + ) == entry_module.clone()) + { + __result.push(tm); + } + } + __result + }) + .iter() + .cloned() + { + __result.push(tm.type_env.clone().source_indices.clone()); + } + __result + }) + .first() + .cloned(); + let si = match entry_si.clone() { + Some(x) => x.clone(), + std::option::Option::None => v1_rt::rc_empty_map::>(), + }; + let info = crate::v1_compiler_emit::lookup_item_by_identity( + registry.clone(), + Rc::new(DeclaredCallableIdentity { + owner_module_path: entry_module.clone(), + decl_name: entry_fn.clone(), + }), + ); + let resources = match info.clone() { + Some(i) => i.resource_requirements.clone(), + std::option::Option::None => Rc::new(vec![]), + }; + let services = match info.clone() { + Some(i) => i.service_names.clone(), + std::option::Option::None => Rc::new(vec![]), + }; + let resource_admissions = Rc::new({ + let mut __result = Vec::new(); + for r in resources.iter().cloned() { + __result.push({ + let rname = type_leaf_name_for_collapse(r.resource.clone(), si.clone()); + crate::v1_compiler_emit::admit_native_resource_effect( + r.binding_name.clone(), + rname.clone(), + crate::v1_compiler_emit::native_effect_resolution( + native_effect_decl_sites( + modules.clone(), + ParsedModuleItemKind::ModuleItemResource, + rname.clone(), + ), + ), + denominator.clone(), + ) + }); + } + __result + }); + let service_admissions = Rc::new({ + let mut __result = Vec::new(); + for sn in services.iter().cloned() { + __result.push(crate::v1_compiler_emit::admit_native_service_effect( + sn.clone(), + crate::v1_compiler_emit::native_effect_resolution( + native_effect_decl_sites( + modules.clone(), + ParsedModuleItemKind::ModuleItemService, + sn.clone(), + ), + ), + denominator.clone(), + si.clone(), + )); + } + __result + }); + Some(Rc::new(NativeEntryEffects { + entry_module: entry_module.clone(), + entry_function: entry_fn.clone(), + admissions: v1_rt::concat(resource_admissions.clone(), service_admissions.clone()), + })) + } + } +} + +pub fn native_entry_effect_diagnostics( + effects: Option>, + modules: Rc>>, +) -> Rc>> { + match effects.clone() { + std::option::Option::None => Rc::new(vec![]), + Some(e) => { + let refusals = Rc::new({ + let mut __result = Vec::new(); + for a in e.admissions.clone().iter().cloned() { + __result.extend( + (*match crate::v1_compiler_emit::native_effect_admission_refusal(a.clone()) + { + Some(r) => Rc::new(vec![r.clone()]), + std::option::Option::None => Rc::new(vec![]), + }) + .iter() + .cloned(), + ); + } + __result + }); + if ((refusals.clone().len() as i64) == 0) { + Rc::new(vec![]) + } else { + { + let span = Rc::new({ + let mut __result = Vec::new(); + for tm in Rc::new({ + let mut __result = Vec::new(); + for tm in modules.iter().cloned() { + if (crate::v1_std_core::authored_name_at( + tm.type_env.clone().source_indices.clone(), + tm.module.clone(), + ) == e.entry_module.clone()) + { + __result.push(tm); + } + } + __result + }) + .iter() + .cloned() + { + __result.push(tm.module.clone().span.clone()); + } + __result + }) + .first() + .cloned(); + match span.clone() { + Some(sp) => Rc::new(vec![crate::v1_std_core::make_error_node( + Rc::new(CompilerDiagnostic::NativeEffectRealizationRefused { + entry: v1_rt::concat( + e.entry_module.clone(), + v1_rt::concat(".".to_string(), e.entry_function.clone()), + ), + refusals: refusals.clone(), + span: sp.clone(), + }), + e.entry_module.clone(), + )]), + std::option::Option::None => { + Rc::new(vec![crate::v1_std_core::make_error_node( + Rc::new(CompilerDiagnostic::NativeEffectRealizationRefused { + entry: v1_rt::concat( + e.entry_module.clone(), + v1_rt::concat(".".to_string(), e.entry_function.clone()), + ), + refusals: v1_rt::concat( + refusals.clone(), + Rc::new(vec![ + "the entry module is not in the closure".to_string() + ]), + ), + span: crate::v1_std_core::no_span(), + }), + e.entry_module.clone(), + )]) + } + } + } + } + } + } +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct NativeEffectBindingText { + pub var: String, + pub type_path: String, + pub binding_line: String, + pub argument: String, +} + +pub fn native_effect_binding_of( + var: String, + type_path: String, + constructor: String, +) -> Rc { + Rc::new(NativeEffectBindingText { + var: var.clone(), + type_path: type_path.clone(), + binding_line: v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(" let ".to_string(), var.clone()), + " = ".to_string(), + ), + constructor.clone(), + ), + ";\n".to_string(), + ), + argument: v1_rt::concat("&".to_string(), var.clone()), + }) +} + +pub fn native_effect_binding_text( + a: Rc, + crate_name: String, +) -> Option> { + match (*a.clone()).clone() { + NativeEffectRealizationAdmission::NativeEffectHandlerBound { + effect_contract: c, .. + } => match (*c.clone()).clone() { + NativeEffectContract::NativeServiceEffect { declaration: d, .. } => { + let sn = d.decl_name.clone(); + let path = v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(crate_name.clone(), "::".to_string()), + crate::gunbc_rust_emitted_edge::module_to_filename( + d.module_path.clone(), + ), + ), + "::".to_string(), + ), + crate::v1_compiler_emit_core_support::sanitize_service_name(sn.clone()), + ); + Some(native_effect_binding_of( + crate::v1_compiler_emit_core_support::service_var_name(sn.clone()), + path.clone(), + v1_rt::concat(path.clone(), "::new(Default::default())".to_string()), + )) + } + NativeEffectContract::NativeResourceEffect { + binding: b, + declaration: d, + .. + } => { + let path = v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(crate_name.clone(), "::".to_string()), + crate::gunbc_rust_emitted_edge::module_to_filename( + d.module_path.clone(), + ), + ), + "::".to_string(), + ), + d.decl_name.clone(), + ); + Some(native_effect_binding_of( + crate::v1_compiler_emit::emit_ident(b.clone(), RenderTarget::Rust), + path.clone(), + path.clone(), + )) + } + }, + _ => std::option::Option::None, + } +} + +pub fn native_effect_bindings( + effects: Option>, + crate_name: String, +) -> Rc>> { + match effects.clone() { + std::option::Option::None => Rc::new(vec![]), + Some(e) => Rc::new({ + let mut __result = Vec::new(); + for a in e.admissions.clone().iter().cloned() { + __result.extend( + (*match native_effect_binding_text(a.clone(), crate_name.clone()) { + Some(t) => Rc::new(vec![t.clone()]), + std::option::Option::None => Rc::new(vec![]), + }) + .iter() + .cloned(), + ); + } + __result + }), + } +} + pub fn emit_main_rs( modules: Rc>>, has_services: bool, crate_name: String, - registry: Rc>>, + effects: Option>, ) -> Rc { { let retained_host_pipeline = compiler_pipeline_entry_is_retained_host(modules.clone()); @@ -41581,22 +42438,21 @@ pub fn emit_main_rs( return emit_native_cli_driver_main_rs( crate_name.clone(), pipeline_module.clone(), + native_effect_bindings(effects.clone(), crate_name.clone()), ); } if compiler_pipeline_entry_is_native_claim(modules.clone()) { return emit_native_claim_driver_main_rs( crate_name.clone(), pipeline_module.clone(), - native_claim_report_effect_demand( - registry.clone(), - pipeline_module.clone(), - ), + native_effect_bindings(effects.clone(), crate_name.clone()), ); } if compiler_pipeline_entry_is_native_serve(modules.clone()) { return emit_native_serve_driver_main_rs( crate_name.clone(), pipeline_module.clone(), + native_effect_bindings(effects.clone(), crate_name.clone()), ); } if compiler_pipeline_entry_is_direct_ingest(modules.clone()) { @@ -41677,104 +42533,23 @@ pub fn emit_host_source_root_read_rs(crate_name: String) -> String { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("fn read_ingest(source_roots: &[String]) -> Vec> {\n".to_string(), " let filesystem = ".to_string()), crate_name.clone()), "::extdeps_filesystem_filesystem_io::Filesystem::new(Default::default());\n".to_string()), " let runtime = match tokio::runtime::Builder::new_current_thread().build() { Ok(runtime) => runtime, Err(cause) => { eprintln!(\"REFUSED: native filesystem executor: {}\", cause); std::process::exit(2); } };\n".to_string()), " let observed = runtime.block_on(".to_string()), crate_name.clone()), "::gunbc_source_root_read::source_root_read(Rc::new(source_roots.iter().cloned().collect()), &filesystem));\n".to_string()), " match observed {\n".to_string()), " Ok(files) => match &*files {\n".to_string()), " ".to_string()), crate_name.clone()), "::gunbc_source_root_read::SourceRootFiles::SourceRootFilesRead { files } =>\n".to_string()), " ".to_string()), crate_name.clone()), "::v2_compiler_source_authority::source_root_ingest_from_files(files.clone()).iter().cloned().collect(),\n".to_string()), " ".to_string()), crate_name.clone()), "::gunbc_source_root_read::SourceRootFiles::SourceRootFilesRefused { path, kind, cause } => {\n".to_string()), " eprintln!(\"REFUSED: could not read source root at {} [{}]: {}\", path, kind, cause); std::process::exit(2);\n".to_string()), " }\n".to_string()), " },\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: native filesystem transport: {}\", cause); std::process::exit(2); }\n".to_string()), " }\n".to_string()), "}\n".to_string()) } -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(tag = "_variant")] -pub enum NativeClaimEffectDemand { - NativeClaimPure, - NativeClaimFilesystem, - NativeClaimUndeclaredEffect { detail: String }, -} -impl NativeClaimEffectDemand { - pub fn detail(&self) -> String { - match self { - NativeClaimEffectDemand::NativeClaimPure => panic!("no detail on unit variant"), - NativeClaimEffectDemand::NativeClaimFilesystem => panic!("no detail on unit variant"), - NativeClaimEffectDemand::NativeClaimUndeclaredEffect { detail: __val, .. } => { - __val.clone() - } - } - } -} - -pub fn native_claim_report_effect_demand( - registry: Rc>>, - pipeline_module: String, -) -> Rc { - match crate::v1_compiler_emit::lookup_item_by_identity( - registry.clone(), - Rc::new(DeclaredCallableIdentity { - owner_module_path: pipeline_module.clone(), - decl_name: "native_claim_report".to_string(), - }), - ) { - Some(info) => { - if !crate::v1_compiler_infer_items::item_is_effectful_callee(info.clone()) { - Rc::new(NativeClaimEffectDemand::NativeClaimPure) - } else { - if ((((crate::v1_compiler_infer_items::item_resource_names(info.clone()).len() - as i64) - == 0) - && ((info.service_names.clone().len() as i64) == 1)) - && { - let mut __all = true; - for sn in info.service_names.clone().iter().cloned() { - if !(crate::v1_compiler_emit_core_support::service_var_name(sn.clone()) - == "filesystem".to_string()) - { - __all = false; - break; - } - } - __all - }) - { - Rc::new(NativeClaimEffectDemand::NativeClaimFilesystem) - } else { - Rc::new(NativeClaimEffectDemand::NativeClaimUndeclaredEffect { - detail: v1_rt::concat( - "services [".to_string(), - v1_rt::concat( - info.service_names.clone().join(&", ".to_string()), - v1_rt::concat( - "] resources [".to_string(), - v1_rt::concat( - crate::v1_compiler_infer_items::item_resource_names( - info.clone(), - ) - .join(&", ".to_string()), - "]".to_string(), - ), - ), - ), - ), - }) - } - } - } - std::option::Option::None => { - Rc::new(NativeClaimEffectDemand::NativeClaimUndeclaredEffect { - detail: "native_claim_report has no registry row".to_string(), - }) - } - } -} - pub fn emit_native_claim_driver_main_rs( crate_name: String, pipeline_module: String, - demand: Rc, + bindings: Rc>>, ) -> Rc { { let pipeline_mod = crate::gunbc_rust_emitted_edge::module_to_filename(pipeline_module.clone()); - match (*demand.clone()).clone() { - NativeClaimEffectDemand::NativeClaimPure => emit_native_claim_driver_pure_main_rs(crate_name.clone(), pipeline_mod.clone()), - NativeClaimEffectDemand::NativeClaimFilesystem => emit_native_claim_driver_filesystem_main_rs(crate_name.clone(), pipeline_mod.clone()), - NativeClaimEffectDemand::NativeClaimUndeclaredEffect { detail: detail, .. } => Rc::new(TextFile { - path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), - content: v1_rt::concat("// Generated by v1 compiler -- do not edit.\n\ncompile_error!(\"REFUSED: a NativeClaimDriver report may demand only the Filesystem service; ".to_string(), v1_rt::concat(crate::v1_compiler_emit_core_support::escape_string_literal_body(detail.clone()), "\");\n".to_string())), -}), -} + if ((bindings.clone().len() as i64) == 0) { + emit_native_claim_driver_pure_main_rs(crate_name.clone(), pipeline_mod.clone()) + } else { + emit_native_claim_driver_effect_main_rs( + crate_name.clone(), + pipeline_mod.clone(), + bindings.clone(), + ) + } } } @@ -41782,14 +42557,33 @@ pub fn native_claim_terminal_exit_lines() -> String { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" print!(\"{}\", report.stdout);".to_string(), "\n".to_string()), " match &*report.terminal {".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimHeld => std::process::exit(0),".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNotHeld { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NOT HELD: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNoObservation { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NO OBSERVATION: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()) } -pub fn emit_native_claim_driver_filesystem_main_rs( +pub fn emit_native_claim_driver_effect_main_rs( crate_name: String, pipeline_mod: String, + bindings: Rc>>, ) -> Rc { - Rc::new(TextFile { + { + let binding_lines = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(b.binding_line.clone()); + } + __result + }) + .join(&"".to_string()); + let effect_args = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(b.argument.clone()); + } + __result + }) + .join(&", ".to_string()); + Rc::new(TextFile { path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), - content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::NativeClaimTerminal;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::native_claim_report;".to_string()), "\n".to_string()), "\n".to_string()), "#[tokio::main(flavor = \"current_thread\")]".to_string()), "\n".to_string()), "async fn main() {".to_string()), "\n".to_string()), " let filesystem = ".to_string()), crate_name.clone()), "::extdeps_filesystem_filesystem_io::Filesystem::new(Default::default());".to_string()), "\n".to_string()), " let report = match native_claim_report(&filesystem).await {".to_string()), "\n".to_string()), " Ok(report) => report,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"NO OBSERVATION: native filesystem transport: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), native_claim_terminal_exit_lines()), "}".to_string()), "\n".to_string()), + content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::NativeClaimTerminal;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::native_claim_report;".to_string()), "\n".to_string()), "\n".to_string()), "#[tokio::main(flavor = \"current_thread\")]".to_string()), "\n".to_string()), "async fn main() {".to_string()), "\n".to_string()), binding_lines.clone()), " let report = match native_claim_report(".to_string()), effect_args.clone()), ").await {".to_string()), "\n".to_string()), " Ok(report) => report,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"NO OBSERVATION: native effect transport: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), native_claim_terminal_exit_lines()), "}".to_string()), "\n".to_string()), }) + } } pub fn emit_native_claim_driver_pure_main_rs( @@ -41805,24 +42599,77 @@ pub fn emit_native_claim_driver_pure_main_rs( pub fn emit_native_serve_driver_main_rs( crate_name: String, pipeline_module: String, + bindings: Rc>>, ) -> Rc { { let pipeline_mod = crate::gunbc_rust_emitted_edge::module_to_filename(pipeline_module.clone()); + let effectful = ((bindings.clone().len() as i64) > 0); + let binding_lines = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(b.binding_line.clone()); + } + __result + }) + .join(&"".to_string()); + let handler_effect_args = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(v1_rt::concat(", ".to_string(), b.argument.clone())); + } + __result + }) + .join(&"".to_string()); + let effect_failed_import = if effectful.clone() { + " native_serve_effect_failed,".to_string() + } else { + "".to_string() + }; + let worker_realize = if effectful.clone() { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {".to_string(), "\n".to_string()), " Ok(runtime) => runtime,".to_string()), "\n".to_string()), " Err(cause) => { let _ = ready.send(Err(format!(\"native effect executor: {}\", cause))); return; }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), binding_lines.clone()) + } else { + "".to_string() + }; + let worker_call = if effectful.clone() { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" let response = match runtime.block_on(native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision".to_string(), handler_effect_args.clone()), ")) {".to_string()), "\n".to_string()), " Ok(response) => response,".to_string()), "\n".to_string()), " Err(cause) => native_serve_effect_failed(cause.to_string()),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()) + } else { + " let response = native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision);\n".to_string() + }; Rc::new(TextFile { path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), - content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::io::{BufRead, Read, Write};".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::extdeps_http_server::ServeWireResponse;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_measure::{byte_size_count, millisecond_count};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_native_serve::{".to_string()), "\n".to_string()), " native_serve_announcement, native_serve_frame, native_serve_handler_name, native_serve_read_refused,".to_string()), "\n".to_string()), " native_serve_admission, native_serve_deadline_refused, native_serve_handler_failed, native_serve_outgoing, native_serve_route,".to_string()), "\n".to_string()), " native_serve_start, NativeServeAdmission, NativeServeContract, NativeServeFrame, NativeServeOutgoing,".to_string()), "\n".to_string()), " NativeServeRoute, NativeServeStart,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::native_serve_handle;".to_string()), "\n".to_string()), "\n".to_string()), "// What a worker hands back across the thread boundary: owned plain values, because the compiled".to_string()), "\n".to_string()), "// handler's values are reference-counted and stay on the worker's thread.".to_string()), "\n".to_string()), "struct WireParts {".to_string()), "\n".to_string()), " status: i64,".to_string()), "\n".to_string()), " content_type_label: String,".to_string()), "\n".to_string()), " header_lines: Vec,".to_string()), "\n".to_string()), " body: String,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn wire_parts(response: &NativeServeOutgoing) -> WireParts {".to_string()), "\n".to_string()), " WireParts {".to_string()), "\n".to_string()), " status: response.status,".to_string()), "\n".to_string()), " content_type_label: response.content_type_label.clone(),".to_string()), "\n".to_string()), " header_lines: response.header_lines.iter().cloned().collect(),".to_string()), "\n".to_string()), " body: response.body.clone(),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn write_parts(stream: &mut std::net::TcpStream, response: &WireParts) {".to_string()), "\n".to_string()), " let mut head = format!(".to_string()), "\n".to_string()), " \"HTTP/1.1 {} \\r\\nContent-Type: {}\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\",".to_string()), "\n".to_string()), " response.status, response.content_type_label, response.body.len()".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " for line in response.header_lines.iter() { head.push_str(line); head.push_str(\"\\r\\n\"); }".to_string()), "\n".to_string()), " head.push_str(\"\\r\\n\");".to_string()), "\n".to_string()), " if let Err(cause) = stream.write_all(head.as_bytes()).and_then(|_| stream.write_all(response.body.as_bytes())) {".to_string()), "\n".to_string()), " eprintln!(\"native-serve: write error: {}\", cause);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn write_response(stream: &mut std::net::TcpStream, response: &NativeServeOutgoing) {".to_string()), "\n".to_string()), " write_parts(stream, &wire_parts(response));".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// A worker's state, shared with the loop: whether the loop stopped waiting for it. The loop counts a".to_string()), "\n".to_string()), "// worker as stuck only when it abandons it, and the worker uncounts itself when it finally returns,".to_string()), "\n".to_string()), "// under one lock, so the count is exact whichever side gets there first.".to_string()), "\n".to_string()), "const WORKER_WAITING: u8 = 0;".to_string()), "\n".to_string()), "const WORKER_ABANDONED: u8 = 1;".to_string()), "\n".to_string()), "const WORKER_DONE: u8 = 2;".to_string()), "\n".to_string()), "\n".to_string()), "fn lock_state(m: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> {".to_string()), "\n".to_string()), " m.lock().unwrap_or_else(|e| e.into_inner())".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// The worker's cleanup, run when its guard drops: on return AND during a panic's unwind. A worker the".to_string()), "\n".to_string()), "// loop abandoned uncounts itself here, exactly once; every worker ends DONE.".to_string()), "\n".to_string()), "struct WorkerGuard {".to_string()), "\n".to_string()), " state: std::sync::Arc>,".to_string()), "\n".to_string()), " stuck: std::sync::Arc>,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "impl Drop for WorkerGuard {".to_string()), "\n".to_string()), " fn drop(&mut self) {".to_string()), "\n".to_string()), " let mut s = lock_state(&self.state);".to_string()), "\n".to_string()), " if *s == WORKER_ABANDONED {".to_string()), "\n".to_string()), " let mut n = lock_state(&self.stuck);".to_string()), "\n".to_string()), " *n = n.saturating_sub(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " *s = WORKER_DONE;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "struct HandlerCall {".to_string()), "\n".to_string()), " method: String,".to_string()), "\n".to_string()), " path: String,".to_string()), "\n".to_string()), " body: String,".to_string()), "\n".to_string()), " tailscale_identity: String,".to_string()), "\n".to_string()), " release_revision: String,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn read_line(reader: &mut impl BufRead, budget: &mut usize) -> Result, String> {".to_string()), "\n".to_string()), " let mut bytes = Vec::new();".to_string()), "\n".to_string()), " match reader.read_until(b'\\n', &mut bytes) {".to_string()), "\n".to_string()), " Ok(0) => return Ok(None),".to_string()), "\n".to_string()), " Ok(n) if n > *budget => return Err(\"the request head exceeds the serve head limit\".to_string()),".to_string()), "\n".to_string()), " Ok(n) => *budget -= n,".to_string()), "\n".to_string()), " Err(cause) if bytes.is_empty() && matches!(cause.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => return Ok(None),".to_string()), "\n".to_string()), " Err(cause) => return Err(format!(\"read: {}\", cause)),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " if bytes.ends_with(b\"\\n\") { bytes.pop(); }".to_string()), "\n".to_string()), " if bytes.ends_with(b\"\\r\") { bytes.pop(); }".to_string()), "\n".to_string()), " String::from_utf8(bytes).map(Some).map_err(|_| \"the request head is not utf-8\".to_string())".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "enum Exchange {".to_string()), "\n".to_string()), " Idle,".to_string()), "\n".to_string()), " Answer(Rc),".to_string()), "\n".to_string()), " Handle(HandlerCall),".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn exchange(stream: &std::net::TcpStream, contract: &Rc, bound_host: &str, bound_port: i64) -> Exchange {".to_string()), "\n".to_string()), " let limit = (byte_size_count(contract.head_limit.clone()) + byte_size_count(contract.body_limit.clone())) as u64;".to_string()), "\n".to_string()), " let mut reader = std::io::BufReader::new(stream.take(limit));".to_string()), "\n".to_string()), " let mut budget = byte_size_count(contract.head_limit.clone()) as usize;".to_string()), "\n".to_string()), " let request_line = match read_line(&mut reader, &mut budget) {".to_string()), "\n".to_string()), " Ok(Some(line)) => line,".to_string()), "\n".to_string()), " Ok(None) => return Exchange::Idle,".to_string()), "\n".to_string()), " Err(reason) => return Exchange::Answer(native_serve_read_refused(reason)),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut header_lines = Vec::new();".to_string()), "\n".to_string()), " loop {".to_string()), "\n".to_string()), " match read_line(&mut reader, &mut budget) {".to_string()), "\n".to_string()), " Ok(Some(line)) if line.is_empty() => break,".to_string()), "\n".to_string()), " Ok(Some(line)) => header_lines.push(line),".to_string()), "\n".to_string()), " Ok(None) => return Exchange::Answer(native_serve_read_refused(\"connection closed before end of headers\".to_string())),".to_string()), "\n".to_string()), " Err(reason) => return Exchange::Answer(native_serve_read_refused(reason)),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let head = match &*native_serve_frame(contract.clone(), request_line, Rc::new(header_lines.into())) {".to_string()), "\n".to_string()), " NativeServeFrame::NativeServeFrameRefused { response } => return Exchange::Answer(response.clone()),".to_string()), "\n".to_string()), " NativeServeFrame::NativeServeFramed { head, body_length } => {".to_string()), "\n".to_string()), " let mut body = vec![0u8; *body_length as usize];".to_string()), "\n".to_string()), " if let Err(cause) = reader.read_exact(&mut body) {".to_string()), "\n".to_string()), " return Exchange::Answer(native_serve_read_refused(format!(\"read body: {}\", cause)));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match String::from_utf8(body) {".to_string()), "\n".to_string()), " Ok(text) => (head.clone(), text),".to_string()), "\n".to_string()), " Err(_) => return Exchange::Answer(native_serve_read_refused(\"the body is not utf-8\".to_string())),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let (head, body) = head;".to_string()), "\n".to_string()), " match &*native_serve_route(contract.clone(), head.clone(), native_serve_handler_name(), bound_host.to_string(), bound_port) {".to_string()), "\n".to_string()), " NativeServeRoute::NativeServeAnswered { response } => Exchange::Answer(response.clone()),".to_string()), "\n".to_string()), " NativeServeRoute::NativeServeToHandler => Exchange::Handle(HandlerCall {".to_string()), "\n".to_string()), " method: head.method.clone(),".to_string()), "\n".to_string()), " path: head.target.clone(),".to_string()), "\n".to_string()), " body,".to_string()), "\n".to_string()), " tailscale_identity: head.tailscale_identity.clone(),".to_string()), "\n".to_string()), " release_revision: contract.release_revision.clone(),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), " let contract = match &*native_serve_start(Rc::new(argv.into())) {".to_string()), "\n".to_string()), " NativeServeStart::NativeServeListen { contract } => contract.clone(),".to_string()), "\n".to_string()), " NativeServeStart::NativeServeStartRefused { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {}\", reason);".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let listener = match std::net::TcpListener::bind((contract.host.as_str(), contract.port as u16)) {".to_string()), "\n".to_string()), " Ok(listener) => listener,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: failed to bind {}:{}: {}\", contract.host, contract.port, cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let bound = match listener.local_addr() {".to_string()), "\n".to_string()), " Ok(addr) => addr,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: bound but could not read the bound address: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let bound_host = bound.ip().to_string();".to_string()), "\n".to_string()), " let bound_port = bound.port() as i64;".to_string()), "\n".to_string()), " eprintln!(\"{}\", native_serve_announcement(contract.clone(), bound_host.clone(), bound_port));".to_string()), "\n".to_string()), " let timeout = std::time::Duration::from_millis(millisecond_count(contract.read_timeout.clone()) as u64);".to_string()), "\n".to_string()), " let deadline = std::time::Duration::from_millis(millisecond_count(contract.request_deadline.clone()) as u64);".to_string()), "\n".to_string()), " let stuck = std::sync::Arc::new(std::sync::Mutex::new(0usize));".to_string()), "\n".to_string()), " for accepted in listener.incoming() {".to_string()), "\n".to_string()), " let mut stream = match accepted {".to_string()), "\n".to_string()), " Ok(stream) => stream,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"native-serve: accept error: {}\", cause); continue; }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " if let Err(cause) = stream.set_read_timeout(Some(timeout)) {".to_string()), "\n".to_string()), " eprintln!(\"native-serve: set_read_timeout: {}\", cause);".to_string()), "\n".to_string()), " continue;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match exchange(&stream, &contract, &bound_host, bound_port) {".to_string()), "\n".to_string()), " Exchange::Idle => {}".to_string()), "\n".to_string()), " Exchange::Answer(response) => write_response(&mut stream, &native_serve_outgoing(response)),".to_string()), "\n".to_string()), " Exchange::Handle(call) => {".to_string()), "\n".to_string()), " let stuck_now = *lock_state(&stuck) as i64;".to_string()), "\n".to_string()), " match &*native_serve_admission(contract.clone(), stuck_now) {".to_string()), "\n".to_string()), " NativeServeAdmission::NativeServeRefuseStuck { response, exit_code } => {".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(response.clone()));".to_string()), "\n".to_string()), " std::process::exit(*exit_code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeServeAdmission::NativeServeAdmitWorker => {".to_string()), "\n".to_string()), " let (sender, receiver) = std::sync::mpsc::channel::();".to_string()), "\n".to_string()), " let state = std::sync::Arc::new(std::sync::Mutex::new(WORKER_WAITING));".to_string()), "\n".to_string()), " let guard = WorkerGuard { state: state.clone(), stuck: stuck.clone() };".to_string()), "\n".to_string()), " std::thread::spawn(move || {".to_string()), "\n".to_string()), " let _guard = guard;".to_string()), "\n".to_string()), " let response = native_serve_handle(call.method, call.path, call.body, call.tailscale_identity, call.release_revision);".to_string()), "\n".to_string()), " let _ = sender.send(wire_parts(&native_serve_outgoing(response)));".to_string()), "\n".to_string()), " });".to_string()), "\n".to_string()), " match receiver.recv_timeout(deadline) {".to_string()), "\n".to_string()), " Ok(parts) => write_parts(&mut stream, &parts),".to_string()), "\n".to_string()), " Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {".to_string()), "\n".to_string()), " {".to_string()), "\n".to_string()), " let mut s = lock_state(&state);".to_string()), "\n".to_string()), " if *s != WORKER_DONE {".to_string()), "\n".to_string()), " *s = WORKER_ABANDONED;".to_string()), "\n".to_string()), " *lock_state(&stuck) += 1;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(native_serve_deadline_refused(contract.clone())));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), + content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::io::{BufRead, Read, Write};".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::extdeps_http_server::ServeWireResponse;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_measure::{byte_size_count, millisecond_count};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_native_serve::{".to_string()), "\n".to_string()), " native_serve_announcement,".to_string()), effect_failed_import.clone()), " native_serve_frame, native_serve_handler_name, native_serve_read_refused,".to_string()), "\n".to_string()), " native_serve_admission, native_serve_deadline_refused, native_serve_handler_failed, native_serve_outgoing, native_serve_route,".to_string()), "\n".to_string()), " native_serve_worker_replacement_exit_code, native_serve_worker_unrealized,".to_string()), "\n".to_string()), " native_serve_start, NativeServeAdmission, NativeServeContract, NativeServeFrame, NativeServeOutgoing,".to_string()), "\n".to_string()), " NativeServeRoute, NativeServeStart,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::native_serve_handle;".to_string()), "\n".to_string()), "\n".to_string()), "// What a worker hands back across the thread boundary: owned plain values, because the compiled".to_string()), "\n".to_string()), "// handler's values are reference-counted and stay on the worker's thread.".to_string()), "\n".to_string()), "struct WireParts {".to_string()), "\n".to_string()), " status: i64,".to_string()), "\n".to_string()), " content_type_label: String,".to_string()), "\n".to_string()), " header_lines: Vec,".to_string()), "\n".to_string()), " body: String,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn wire_parts(response: &NativeServeOutgoing) -> WireParts {".to_string()), "\n".to_string()), " WireParts {".to_string()), "\n".to_string()), " status: response.status,".to_string()), "\n".to_string()), " content_type_label: response.content_type_label.clone(),".to_string()), "\n".to_string()), " header_lines: response.header_lines.iter().cloned().collect(),".to_string()), "\n".to_string()), " body: response.body.clone(),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn write_parts(stream: &mut std::net::TcpStream, response: &WireParts) {".to_string()), "\n".to_string()), " let mut head = format!(".to_string()), "\n".to_string()), " \"HTTP/1.1 {} \\r\\nContent-Type: {}\\r\\nContent-Length: {}\\r\\nConnection: close\\r\\n\",".to_string()), "\n".to_string()), " response.status, response.content_type_label, response.body.len()".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " for line in response.header_lines.iter() { head.push_str(line); head.push_str(\"\\r\\n\"); }".to_string()), "\n".to_string()), " head.push_str(\"\\r\\n\");".to_string()), "\n".to_string()), " if let Err(cause) = stream.write_all(head.as_bytes()).and_then(|_| stream.write_all(response.body.as_bytes())) {".to_string()), "\n".to_string()), " eprintln!(\"native-serve: write error: {}\", cause);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn write_response(stream: &mut std::net::TcpStream, response: &NativeServeOutgoing) {".to_string()), "\n".to_string()), " write_parts(stream, &wire_parts(response));".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// A worker's state, shared with the loop: whether the loop stopped waiting for it. The loop counts a".to_string()), "\n".to_string()), "// worker as stuck only when it abandons it, and the worker uncounts itself when it finally returns,".to_string()), "\n".to_string()), "// under one lock, so the count is exact whichever side gets there first.".to_string()), "\n".to_string()), "const WORKER_WAITING: u8 = 0;".to_string()), "\n".to_string()), "const WORKER_ABANDONED: u8 = 1;".to_string()), "\n".to_string()), "const WORKER_DONE: u8 = 2;".to_string()), "\n".to_string()), "\n".to_string()), "fn lock_state(m: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> {".to_string()), "\n".to_string()), " m.lock().unwrap_or_else(|e| e.into_inner())".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// The worker's cleanup, run when its guard drops: on return AND during a panic's unwind. A worker the".to_string()), "\n".to_string()), "// loop abandoned uncounts itself here, exactly once; every worker ends DONE.".to_string()), "\n".to_string()), "struct WorkerGuard {".to_string()), "\n".to_string()), " state: std::sync::Arc>,".to_string()), "\n".to_string()), " stuck: std::sync::Arc>,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "impl Drop for WorkerGuard {".to_string()), "\n".to_string()), " fn drop(&mut self) {".to_string()), "\n".to_string()), " let mut s = lock_state(&self.state);".to_string()), "\n".to_string()), " if *s == WORKER_ABANDONED {".to_string()), "\n".to_string()), " let mut n = lock_state(&self.stuck);".to_string()), "\n".to_string()), " *n = n.saturating_sub(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " *s = WORKER_DONE;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "struct HandlerCall {".to_string()), "\n".to_string()), " method: String,".to_string()), "\n".to_string()), " path: String,".to_string()), "\n".to_string()), " body: String,".to_string()), "\n".to_string()), " tailscale_identity: String,".to_string()), "\n".to_string()), " release_revision: String,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// One worker generation: the thread that owns the effect executor and every admitted handle and runs".to_string()), "\n".to_string()), "// handler calls one at a time. Its state is the WorkerGuard protocol above; an abandoned generation".to_string()), "\n".to_string()), "// finishes the call in hand (if it ever does) and ends, uncounting itself.".to_string()), "\n".to_string()), "struct Generation {".to_string()), "\n".to_string()), " calls: std::sync::mpsc::Sender<(HandlerCall, std::sync::mpsc::Sender)>,".to_string()), "\n".to_string()), " state: std::sync::Arc>,".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn realize_generation(stuck: &std::sync::Arc>, ready_within: std::time::Duration) -> Result {".to_string()), "\n".to_string()), " let (calls, inbox) = std::sync::mpsc::channel::<(HandlerCall, std::sync::mpsc::Sender)>();".to_string()), "\n".to_string()), " let (ready, readiness) = std::sync::mpsc::channel::>();".to_string()), "\n".to_string()), " let state = std::sync::Arc::new(std::sync::Mutex::new(WORKER_WAITING));".to_string()), "\n".to_string()), " let guard = WorkerGuard { state: state.clone(), stuck: stuck.clone() };".to_string()), "\n".to_string()), " let worker_state = state.clone();".to_string()), "\n".to_string()), " std::thread::spawn(move || {".to_string()), "\n".to_string()), " let _guard = guard;".to_string()), "\n".to_string()), worker_realize.clone()), " let _ = ready.send(Ok(()));".to_string()), "\n".to_string()), " while let Ok((call, reply)) = inbox.recv() {".to_string()), "\n".to_string()), worker_call.clone()), " let _ = reply.send(wire_parts(&native_serve_outgoing(response)));".to_string()), "\n".to_string()), " if *lock_state(&worker_state) == WORKER_ABANDONED {".to_string()), "\n".to_string()), " return;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " });".to_string()), "\n".to_string()), " match readiness.recv_timeout(ready_within) {".to_string()), "\n".to_string()), " Ok(Ok(())) => Ok(Generation { calls, state }),".to_string()), "\n".to_string()), " Ok(Err(cause)) => Err(cause),".to_string()), "\n".to_string()), " Err(_) => Err(\"the worker ended or did not report ready within the request deadline\".to_string()),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn read_line(reader: &mut impl BufRead, budget: &mut usize) -> Result, String> {".to_string()), "\n".to_string()), " let mut bytes = Vec::new();".to_string()), "\n".to_string()), " match reader.read_until(b'\\n', &mut bytes) {".to_string()), "\n".to_string()), " Ok(0) => return Ok(None),".to_string()), "\n".to_string()), " Ok(n) if n > *budget => return Err(\"the request head exceeds the serve head limit\".to_string()),".to_string()), "\n".to_string()), " Ok(n) => *budget -= n,".to_string()), "\n".to_string()), " Err(cause) if bytes.is_empty() && matches!(cause.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => return Ok(None),".to_string()), "\n".to_string()), " Err(cause) => return Err(format!(\"read: {}\", cause)),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " if bytes.ends_with(b\"\\n\") { bytes.pop(); }".to_string()), "\n".to_string()), " if bytes.ends_with(b\"\\r\") { bytes.pop(); }".to_string()), "\n".to_string()), " String::from_utf8(bytes).map(Some).map_err(|_| \"the request head is not utf-8\".to_string())".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "enum Exchange {".to_string()), "\n".to_string()), " Idle,".to_string()), "\n".to_string()), " Answer(Rc),".to_string()), "\n".to_string()), " Handle(HandlerCall),".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn exchange(stream: &std::net::TcpStream, contract: &Rc, bound_host: &str, bound_port: i64) -> Exchange {".to_string()), "\n".to_string()), " let limit = (byte_size_count(contract.head_limit.clone()) + byte_size_count(contract.body_limit.clone())) as u64;".to_string()), "\n".to_string()), " let mut reader = std::io::BufReader::new(stream.take(limit));".to_string()), "\n".to_string()), " let mut budget = byte_size_count(contract.head_limit.clone()) as usize;".to_string()), "\n".to_string()), " let request_line = match read_line(&mut reader, &mut budget) {".to_string()), "\n".to_string()), " Ok(Some(line)) => line,".to_string()), "\n".to_string()), " Ok(None) => return Exchange::Idle,".to_string()), "\n".to_string()), " Err(reason) => return Exchange::Answer(native_serve_read_refused(reason)),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut header_lines = Vec::new();".to_string()), "\n".to_string()), " loop {".to_string()), "\n".to_string()), " match read_line(&mut reader, &mut budget) {".to_string()), "\n".to_string()), " Ok(Some(line)) if line.is_empty() => break,".to_string()), "\n".to_string()), " Ok(Some(line)) => header_lines.push(line),".to_string()), "\n".to_string()), " Ok(None) => return Exchange::Answer(native_serve_read_refused(\"connection closed before end of headers\".to_string())),".to_string()), "\n".to_string()), " Err(reason) => return Exchange::Answer(native_serve_read_refused(reason)),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let head = match &*native_serve_frame(contract.clone(), request_line, Rc::new(header_lines.into())) {".to_string()), "\n".to_string()), " NativeServeFrame::NativeServeFrameRefused { response } => return Exchange::Answer(response.clone()),".to_string()), "\n".to_string()), " NativeServeFrame::NativeServeFramed { head, body_length } => {".to_string()), "\n".to_string()), " let mut body = vec![0u8; *body_length as usize];".to_string()), "\n".to_string()), " if let Err(cause) = reader.read_exact(&mut body) {".to_string()), "\n".to_string()), " return Exchange::Answer(native_serve_read_refused(format!(\"read body: {}\", cause)));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match String::from_utf8(body) {".to_string()), "\n".to_string()), " Ok(text) => (head.clone(), text),".to_string()), "\n".to_string()), " Err(_) => return Exchange::Answer(native_serve_read_refused(\"the body is not utf-8\".to_string())),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let (head, body) = head;".to_string()), "\n".to_string()), " match &*native_serve_route(contract.clone(), head.clone(), native_serve_handler_name(), bound_host.to_string(), bound_port) {".to_string()), "\n".to_string()), " NativeServeRoute::NativeServeAnswered { response } => Exchange::Answer(response.clone()),".to_string()), "\n".to_string()), " NativeServeRoute::NativeServeToHandler => Exchange::Handle(HandlerCall {".to_string()), "\n".to_string()), " method: head.method.clone(),".to_string()), "\n".to_string()), " path: head.target.clone(),".to_string()), "\n".to_string()), " body,".to_string()), "\n".to_string()), " tailscale_identity: head.tailscale_identity.clone(),".to_string()), "\n".to_string()), " release_revision: contract.release_revision.clone(),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), " let contract = match &*native_serve_start(Rc::new(argv.into())) {".to_string()), "\n".to_string()), " NativeServeStart::NativeServeListen { contract } => contract.clone(),".to_string()), "\n".to_string()), " NativeServeStart::NativeServeStartRefused { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {}\", reason);".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let listener = match std::net::TcpListener::bind((contract.host.as_str(), contract.port as u16)) {".to_string()), "\n".to_string()), " Ok(listener) => listener,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: failed to bind {}:{}: {}\", contract.host, contract.port, cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let bound = match listener.local_addr() {".to_string()), "\n".to_string()), " Ok(addr) => addr,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: bound but could not read the bound address: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let bound_host = bound.ip().to_string();".to_string()), "\n".to_string()), " let bound_port = bound.port() as i64;".to_string()), "\n".to_string()), " let deadline = std::time::Duration::from_millis(millisecond_count(contract.request_deadline.clone()) as u64);".to_string()), "\n".to_string()), " let stuck = std::sync::Arc::new(std::sync::Mutex::new(0usize));".to_string()), "\n".to_string()), " let mut generation = match realize_generation(&stuck, deadline) {".to_string()), "\n".to_string()), " Ok(generation) => generation,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {}\", native_serve_worker_unrealized(cause));".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " eprintln!(\"{}\", native_serve_announcement(contract.clone(), bound_host.clone(), bound_port));".to_string()), "\n".to_string()), " let timeout = std::time::Duration::from_millis(millisecond_count(contract.read_timeout.clone()) as u64);".to_string()), "\n".to_string()), " for accepted in listener.incoming() {".to_string()), "\n".to_string()), " let mut stream = match accepted {".to_string()), "\n".to_string()), " Ok(stream) => stream,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"native-serve: accept error: {}\", cause); continue; }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " if let Err(cause) = stream.set_read_timeout(Some(timeout)) {".to_string()), "\n".to_string()), " eprintln!(\"native-serve: set_read_timeout: {}\", cause);".to_string()), "\n".to_string()), " continue;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match exchange(&stream, &contract, &bound_host, bound_port) {".to_string()), "\n".to_string()), " Exchange::Idle => {}".to_string()), "\n".to_string()), " Exchange::Answer(response) => write_response(&mut stream, &native_serve_outgoing(response)),".to_string()), "\n".to_string()), " Exchange::Handle(call) => {".to_string()), "\n".to_string()), " let stuck_now = *lock_state(&stuck) as i64;".to_string()), "\n".to_string()), " match &*native_serve_admission(contract.clone(), stuck_now) {".to_string()), "\n".to_string()), " NativeServeAdmission::NativeServeRefuseStuck { response, exit_code } => {".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(response.clone()));".to_string()), "\n".to_string()), " std::process::exit(*exit_code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeServeAdmission::NativeServeAdmitWorker => {".to_string()), "\n".to_string()), " let (reply, answer) = std::sync::mpsc::channel::();".to_string()), "\n".to_string()), " let replace = if generation.calls.send((call, reply)).is_err() {".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));".to_string()), "\n".to_string()), " true".to_string()), "\n".to_string()), " } else {".to_string()), "\n".to_string()), " match answer.recv_timeout(deadline) {".to_string()), "\n".to_string()), " Ok(parts) => {".to_string()), "\n".to_string()), " write_parts(&mut stream, &parts);".to_string()), "\n".to_string()), " false".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(native_serve_handler_failed()));".to_string()), "\n".to_string()), " true".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {".to_string()), "\n".to_string()), " {".to_string()), "\n".to_string()), " let mut s = lock_state(&generation.state);".to_string()), "\n".to_string()), " if *s != WORKER_DONE {".to_string()), "\n".to_string()), " *s = WORKER_ABANDONED;".to_string()), "\n".to_string()), " *lock_state(&stuck) += 1;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " write_response(&mut stream, &native_serve_outgoing(native_serve_deadline_refused(contract.clone())));".to_string()), "\n".to_string()), " true".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " if replace {".to_string()), "\n".to_string()), " generation = match realize_generation(&stuck, deadline) {".to_string()), "\n".to_string()), " Ok(generation) => generation,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {}\", native_serve_worker_unrealized(cause));".to_string()), "\n".to_string()), " std::process::exit(native_serve_worker_replacement_exit_code() as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), }) } } -pub fn emit_native_cli_driver_main_rs(crate_name: String, pipeline_module: String) -> Rc { +pub fn emit_native_cli_driver_main_rs( + crate_name: String, + pipeline_module: String, + bindings: Rc>>, +) -> Rc { { let pipeline_mod = crate::gunbc_rust_emitted_edge::module_to_filename(pipeline_module.clone()); + let binding_lines = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(b.binding_line.clone()); + } + __result + }) + .join(&"".to_string()); + let effect_args = Rc::new({ + let mut __result = Vec::new(); + for b in bindings.iter().cloned() { + __result.push(v1_rt::concat(", ".to_string(), b.argument.clone())); + } + __result + }) + .join(&"".to_string()); Rc::new(TextFile { path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), - content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_process::ProcessExit;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::{".to_string()), "\n".to_string()), " v2_cli_exit, v2_cli_outcome_text, v2_cli_main,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "\n".to_string()), "#[tokio::main(flavor = \"current_thread\")]".to_string()), "\n".to_string()), "async fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), " let filesystem = ".to_string()), crate_name.clone()), "::extdeps_filesystem_filesystem_io::Filesystem::new(Default::default());".to_string()), "\n".to_string()), " let outcome = match v2_cli_main(Rc::new(argv.into()), &filesystem).await {".to_string()), "\n".to_string()), " Ok(outcome) => outcome,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: native filesystem transport: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let text = v2_cli_outcome_text(outcome.clone());".to_string()), "\n".to_string()), " print!(\"{text}\");".to_string()), "\n".to_string()), " match &*v2_cli_exit(outcome) {".to_string()), "\n".to_string()), " ProcessExit::ExitSuccess => {".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " ProcessExit::ExitFailure { code, reason } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(*code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), + content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_process::ProcessExit;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::{".to_string()), "\n".to_string()), " v2_cli_exit, v2_cli_outcome_text, v2_cli_main,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "\n".to_string()), "#[tokio::main(flavor = \"current_thread\")]".to_string()), "\n".to_string()), "async fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), binding_lines.clone()), " let outcome = match v2_cli_main(Rc::new(argv.into())".to_string()), effect_args.clone()), ").await {".to_string()), "\n".to_string()), " Ok(outcome) => outcome,".to_string()), "\n".to_string()), " Err(cause) => { eprintln!(\"REFUSED: native effect transport: {}\", cause); std::process::exit(2); }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let text = v2_cli_outcome_text(outcome.clone());".to_string()), "\n".to_string()), " print!(\"{text}\");".to_string()), "\n".to_string()), " match &*v2_cli_exit(outcome) {".to_string()), "\n".to_string()), " ProcessExit::ExitSuccess => {".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " ProcessExit::ExitFailure { code, reason } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(*code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), }) } } @@ -41836,7 +42683,7 @@ pub fn emit_source_root_eval_driver_main_rs( crate::gunbc_rust_emitted_edge::module_to_filename(pipeline_module.clone()); Rc::new(TextFile { path: v1_rt::concat(v1_rt::concat(rust_source_root(), "main".to_string()), rust_source_ext()), - content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "use std::time::Instant;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_witness_v2_native_route::{NativeRouteEmittedBuild, NativeRouteMalformedControl, NativeRouteOldRouteControl};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::{".to_string()), "\n".to_string()), " native_census_module_completeness, native_census_module_resolution, native_census_modules,".to_string()), "\n".to_string()), " native_census_module_inference, native_census_infer_type_contribution,".to_string()), "\n".to_string()), " native_census_residual_rows,".to_string()), "\n".to_string()), " native_census_cause_groups_add_module, native_census_cause_groups_from_file_refusals,".to_string()), "\n".to_string()), " native_census_cause_group_rows, native_census_closure_index, native_census_roots_ranked,".to_string()), "\n".to_string()), " native_demand_census_output,".to_string()), "\n".to_string()), " native_lane_controls, native_lane_file_refusal_index,".to_string()), "\n".to_string()), " native_lane_closure_ingest, native_lane_closure_modules, native_lane_closure_round_budget,".to_string()), "\n".to_string()), " native_lane_ingest_matches_closure, native_lane_ingest_receipt, native_lane_receipt,".to_string()), "\n".to_string()), " native_lane_source_facts, native_lane_universe_of_facts, native_test_context_absorb,".to_string()), "\n".to_string()), " native_test_context_finish, native_test_context_from_ingest, native_test_context_state_empty,".to_string()), "\n".to_string()), " native_test_front_end_prepare,".to_string()), "\n".to_string()), " native_driver_parse, native_driver_plan_exit, native_driver_plan_source_roots,".to_string()), "\n".to_string()), " NativeDriverPlan, NativeDriverVerb,".to_string()), "\n".to_string()), " NativeCensusModuleOutcome,".to_string()), "\n".to_string()), " NativeLaneHostFacts,".to_string()), "\n".to_string()), " NativeRouteMemberRow,".to_string()), "\n".to_string()), " native_demand_eval_nanos, native_demand_eval_samples,".to_string()), "\n".to_string()), " native_demand_prepare_nanos, native_demand_prepare_samples,".to_string()), "\n".to_string()), " native_demand_prepare_ok, native_demand_prepare_refused,".to_string()), "\n".to_string()), " native_demand_scheduling_nanos,".to_string()), "\n".to_string()), " native_demand_occurrence_census_lines, native_demand_occurrence_census_nanos,".to_string()), "\n".to_string()), " native_demand_run_demand_count, native_demand_run_readiness_derivations,".to_string()), "\n".to_string()), " native_demand_run_receipt_lines, native_demand_run_rows,".to_string()), "\n".to_string()), " native_demand_observation_progressed,".to_string()), "\n".to_string()), " native_demand_run_transition_count, native_demand_schedule_universe,".to_string()), "\n".to_string()), " native_demand_tested_tree_input,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_program_assembly::{".to_string()), "\n".to_string()), " program_assembly_measured_outcome, program_assembly_memo_hits,".to_string()), "\n".to_string()), " program_assembly_memo_lookup_calls, program_assembly_memo_misses,".to_string()), "\n".to_string()), " program_assembly_allocator_next, program_assembly_measured_alloc,".to_string()), "\n".to_string()), " program_assembly_phase_normalize_seeded, program_assembly_normalized_outcome,".to_string()), "\n".to_string()), " program_assembly_normalized_alloc, program_assembly_normalized_spans, program_assembly_ingest_scope_outcome,".to_string()), "\n".to_string()), " program_assembly_phase_parse_measured_seeded,".to_string()), "\n".to_string()), " program_assembly_phase_token_count, program_assembly_phase_tokenize,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_witness_v2_native_route::{".to_string()), "\n".to_string()), " native_route_admission, native_route_admission_summary, native_route_admitted,".to_string()), "\n".to_string()), " native_route_file_refusal_tally, native_route_member_row_text,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_native_frontier_ratchet::{".to_string()), "\n".to_string()), " native_frontier_proposal_file_lines, native_frontier_report_lines, native_frontier_verdict,".to_string()), "\n".to_string()), " native_frontier_verdict_word,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_native_test_vocabulary::NativeTestVerdict;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_source_authority::DagSourceReadWitness;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_std_diagnostic::Outcome;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_process::ProcessExit;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::extdeps_bazel_target_pattern::TargetPattern;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::{".to_string()), "\n".to_string()), " native_driver_cost_account, native_driver_cost_remainder_tolerance_nanos,".to_string()), "\n".to_string()), " native_driver_exclusive_sum, NativeDriverCostAccounting,".to_string()), "\n".to_string()), " native_driver_exclusive_rows, native_driver_exclusive_row_name,".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_measure::{nanosecond, nanosecond_count};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v1_rt::observed_thread_cpu_nanos;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_instruments_type_declaration_use_census::{type_census_receipt, type_census_run_add, type_census_run_empty};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::NativeClaimTerminal;".to_string()), "\n".to_string()), "\n".to_string()), emit_host_source_root_read_rs(crate_name.clone())), "fn host_fact(facts: &std::collections::HashMap, key: &str) -> String {".to_string()), "\n".to_string()), " match facts.get(key) {".to_string()), "\n".to_string()), " Some(value) => value.clone(),".to_string()), "\n".to_string()), " None => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: host facts file carries no {key} row\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn host_fact_int(facts: &std::collections::HashMap, key: &str) -> i64 {".to_string()), "\n".to_string()), " let value = host_fact(facts, key);".to_string()), "\n".to_string()), " match value.parse::() {".to_string()), "\n".to_string()), " Ok(parsed) => parsed,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: host fact {key} is not an integer: {value:?} ({cause})\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn read_host_facts(path: &str) -> NativeLaneHostFacts {".to_string()), "\n".to_string()), " let text = match std::fs::read_to_string(path) {".to_string()), "\n".to_string()), " Ok(text) => text,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: could not read host facts file {path}: {cause}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut facts: std::collections::HashMap = std::collections::HashMap::new();".to_string()), "\n".to_string()), " for line in text.lines() {".to_string()), "\n".to_string()), " if line.is_empty() {".to_string()), "\n".to_string()), " continue;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match line.split_once('\\t') {".to_string()), "\n".to_string()), " Some((key, value)) if !key.is_empty() => {".to_string()), "\n".to_string()), " if facts.insert(key.to_string(), value.to_string()).is_some() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: duplicate host fact row: {key}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " _ => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: malformed host fact line (expected keyvalue): {line}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let malformed_path = host_fact(&facts, \"malformed_control_path\");".to_string()), "\n".to_string()), " let malformed_reason = host_fact(&facts, \"malformed_control_reason\");".to_string()), "\n".to_string()), " let malformed_control = if malformed_path.is_empty() {".to_string()), "\n".to_string()), " NativeRouteMalformedControl::MalformedSpecimenAccepted".to_string()), "\n".to_string()), " } else {".to_string()), "\n".to_string()), " NativeRouteMalformedControl::MalformedSpecimenRefused {".to_string()), "\n".to_string()), " path: malformed_path,".to_string()), "\n".to_string()), " reason: malformed_reason,".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let old_route_disposition = host_fact(&facts, \"old_route_disposition\");".to_string()), "\n".to_string()), " let old_route_executable = host_fact(&facts, \"old_route_executable\");".to_string()), "\n".to_string()), " let old_route_control = match old_route_disposition.as_str() {".to_string()), "\n".to_string()), " \"withdrawn\" => NativeRouteOldRouteControl::OldRouteWithdrawn {".to_string()), "\n".to_string()), " withdrawn_executable: old_route_executable,".to_string()), "\n".to_string()), " },".to_string()), "\n".to_string()), " \"not_present_at_window\" => NativeRouteOldRouteControl::OldRouteNotPresentAtWindow {".to_string()), "\n".to_string()), " expected_executable: old_route_executable,".to_string()), "\n".to_string()), " },".to_string()), "\n".to_string()), " other => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: old_route_disposition names no control this route can record: {other} -- expected withdrawn or not_present_at_window\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " // The emitted build is the host's observation of the cargo spawn this binary is the".to_string()), "\n".to_string()), " // product of: six rows, the argv as one row per word under a length row. A count that".to_string()), "\n".to_string()), " // does not parse, or an index row it promises that is missing, refuses like any other".to_string()), "\n".to_string()), " // missing fact; nothing is defaulted.".to_string()), "\n".to_string()), " let cargo_argv_len = host_fact_int(&facts, \"cargo_argv_len\");".to_string()), "\n".to_string()), " let mut cargo_argv: Vec = Vec::new();".to_string()), "\n".to_string()), " for index in 0..cargo_argv_len {".to_string()), "\n".to_string()), " cargo_argv.push(host_fact(&facts, &format!(\"cargo_argv_{index}\")));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let emitted_build = NativeRouteEmittedBuild {".to_string()), "\n".to_string()), " cargo_argv: Rc::new(cargo_argv.into()),".to_string()), "\n".to_string()), " rustflags: host_fact(&facts, \"rustflags\"),".to_string()), "\n".to_string()), " compiler_path: host_fact(&facts, \"compiler_path\"),".to_string()), "\n".to_string()), " rustc_identity: host_fact(&facts, \"rustc_identity\"),".to_string()), "\n".to_string()), " exit_status: host_fact_int(&facts, \"exit_status\"),".to_string()), "\n".to_string()), " warning_count: host_fact_int(&facts, \"warning_count\"),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " NativeLaneHostFacts {".to_string()), "\n".to_string()), " tested_tree: host_fact(&facts, \"tested_tree\"),".to_string()), "\n".to_string()), " preparation_seed_identity: host_fact(&facts, \"preparation_seed_identity\"),".to_string()), "\n".to_string()), " emitted_closure_identity: host_fact(&facts, \"emitted_closure_identity\"),".to_string()), "\n".to_string()), " executable_path: host_fact(&facts, \"executable_path\"),".to_string()), "\n".to_string()), " executable_identity: host_fact(&facts, \"executable_identity\"),".to_string()), "\n".to_string()), " old_route_control: Rc::new(old_route_control),".to_string()), "\n".to_string()), " malformed_control: Rc::new(malformed_control),".to_string()), "\n".to_string()), " emitted_build: Rc::new(emitted_build),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn run_census(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " for refusal in context.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // GROUPED BY THE FATAL REASON, BY THE FOLD THAT OWNS THE GROUPING".to_string()), "\n".to_string()), " // (v2.compiler.compile native_census_cause_groups_add); this main only prints the groups.".to_string()), "\n".to_string()), " // Each root carries its closure fan-out (v2.compiler.compile native_census_closure_index);".to_string()), "\n".to_string()), " // this mode does not resolve, so no root claims an observed resolve refusal.".to_string()), "\n".to_string()), " let closure = native_census_closure_index(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " native_lane_file_refusal_index(context.file_refusals.clone()),".to_string()), "\n".to_string()), " native_census_modules(ingest.clone()),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure, false);".to_string()), "\n".to_string()), " let cause_groups = native_census_cause_group_rows(groups.clone());".to_string()), "\n".to_string()), " for group in cause_groups.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"cause_group\": group }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let roots = native_census_roots_ranked(cause_groups.clone());".to_string()), "\n".to_string()), " for root in roots.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_root\": root }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"_terminal\": \"complete\", \"mode\": \"census\", \"file_refusals\": context.file_refusals.len(), \"advised_files\": context.accepted_file_advisories.len(), \"cause_groups\": cause_groups.len(), \"roots\": roots.len() }));".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// THE THIRD MODE: THE WHOLE TREE, RESOLVED PER MODULE.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// WHY IT IS NOT A FLAG ON `census`. That mode has a live consumer -- the malformed control,".to_string()), "\n".to_string()), "// which the host runner spawns over a ONE-FILE scratch root and reads one per-file refusal".to_string()), "\n".to_string()), "// from -- and teaching it to resolve would make that control pay a whole-tree resolve to".to_string()), "\n".to_string()), "// learn a fact the front end already answered. Two modes because they are two subjects: a".to_string()), "\n".to_string()), "// roster of front-end refusals, and a roster of resolve refusals over every ingested module.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// WHY IT IS NOT THE `adjudicate` LOOP WIDENED. That loop walks `universe.modules` -- the".to_string()), "\n".to_string()), "// v2.test.* modules discovery enrolled rows for -- over a context folded from the import".to_string()), "\n".to_string()), "// CLOSURE of those modules. Both narrowings are correct for what adjudication asks and wrong".to_string()), "\n".to_string()), "// for a census: a residual reported over the closure of the test corpus is not a residual over".to_string()), "\n".to_string()), "// the tree, and the absence of a refusal in a population that was never resolved establishes".to_string()), "\n".to_string()), "// nothing about the dependents outside it.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// THE RESOLVED TREE IS NOT KEPT. NativeCensusModuleOutcome carries the diagnostics of a refusal".to_string()), "\n".to_string()), "// and nothing on the accepted arm, so each module's tree dies with the call that made it. The".to_string()), "\n".to_string()), "// adjudicating loop drops its tree explicitly because it must hold one to infer from; this loop".to_string()), "\n".to_string()), "// never holds one, which is the property that makes the whole-tree grain affordable at all.".to_string()), "\n".to_string()), "fn run_census_resolve(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let load_started = Instant::now();".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let corpus_reads = reads.len();".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let load_nanos = span_nanos(load_started);".to_string()), "\n".to_string()), " let context_started = Instant::now();".to_string()), "\n".to_string()), " // MUTABLE BECAUSE IT IS THREADED: each module's resolution returns the context it ended".to_string()), "\n".to_string()), " // with, carrying the module namespace provider (v2.compiler.name_resolve ResolutionContext".to_string()), "\n".to_string()), " // namespaces), and the next module is resolved over that one -- a namespace is produced at".to_string()), "\n".to_string()), " // most once per ingest only for a loop that binds what it gets back.".to_string()), "\n".to_string()), " let mut context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let context_nanos = span_nanos(context_started);".to_string()), "\n".to_string()), " // The front-end refusals are reported in the same shape `census` reports them, because they".to_string()), "\n".to_string()), " // are the same fact read from the same context: a module whose file the context refused is".to_string()), "\n".to_string()), " // attributed there and is NOT re-reported below as a resolve refusal it never reached.".to_string()), "\n".to_string()), " for refusal in context.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE ADVISORY POPULATION IS THE SAME CONTEXT FACT `census` REPORTS, printed in the same".to_string()), "\n".to_string()), " // shape, so a saved whole-tree receipt carries it without a second pass. Its count is taken".to_string()), "\n".to_string()), " // here, from the context that produced the rows, not from the threaded one at the end.".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let advised_files = context.accepted_file_advisories.len();".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " let roster_started = Instant::now();".to_string()), "\n".to_string()), " let modules = native_census_modules(ingest.clone());".to_string()), "\n".to_string()), " let roster_nanos = span_nanos(roster_started);".to_string()), "\n".to_string()), " // THE CAUSE GROUPS START FROM THE FRONT-END REFUSALS -- each a root carrying its closure".to_string()), "\n".to_string()), " // fan-out -- and take each module as it is resolved: a cascade member once under its root,".to_string()), "\n".to_string()), " // any other refusal per chain (v2.compiler.compile native_census_cause_groups_add_module).".to_string()), "\n".to_string()), " let closure = native_census_closure_index(context.clone(), refusal_index.clone(), modules.clone());".to_string()), "\n".to_string()), " let mut groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure, true);".to_string()), "\n".to_string()), " let mut resolve_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut resolve_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " let mut resolved_ok: u64 = 0;".to_string()), "\n".to_string()), " let mut file_refused: u64 = 0;".to_string()), "\n".to_string()), " let mut resolve_refused: u64 = 0;".to_string()), "\n".to_string()), " let mut residual_rows: u64 = 0;".to_string()), "\n".to_string()), " // THE PER-MODULE LINE IS ALSO THE RUN'S PROGRESS: `index` of `of`, so a reader of a multi-hour".to_string()), "\n".to_string()), " // census can tell a slow run from a hung one while it runs.".to_string()), "\n".to_string()), " let census_modules_total = modules.len();".to_string()), "\n".to_string()), " for (census_module_index, entry) in modules.iter().enumerate() {".to_string()), "\n".to_string()), " let resolve_started = Instant::now();".to_string()), "\n".to_string()), " let step = native_census_module_resolution(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " refusal_index.clone(),".to_string()), "\n".to_string()), " entry.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " context = step.context.clone();".to_string()), "\n".to_string()), " let outcome = step.outcome.clone();".to_string()), "\n".to_string()), " let module_resolve_nanos = span_nanos(resolve_started);".to_string()), "\n".to_string()), " groups = native_census_cause_groups_add_module(groups.clone(), entry.clone(), step.clone());".to_string()), "\n".to_string()), " let outcome_label = match &*outcome {".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleFileRefused => {".to_string()), "\n".to_string()), " file_refused += 1;".to_string()), "\n".to_string()), " \"file_refused\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleResolved => {".to_string()), "\n".to_string()), " resolved_ok += 1;".to_string()), "\n".to_string()), " \"resolved\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleResolveRefused { first, rest, .. } => {".to_string()), "\n".to_string()), " resolve_refused += 1;".to_string()), "\n".to_string()), " // ONE ROW PER FAILURE CHAIN, WHICH IS ONE ROW PER REFUSED OCCURRENCE. The rows are".to_string()), "\n".to_string()), " // written as they are decided rather than accumulated: the census is over the".to_string()), "\n".to_string()), " // whole tree, and a population held to the end is a second corpus-sized".to_string()), "\n".to_string()), " // allocation beside the context that produced it.".to_string()), "\n".to_string()), " for row in native_census_residual_rows(entry.clone(), first.clone(), rest.clone()).iter() {".to_string()), "\n".to_string()), " residual_rows += 1;".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_residual\": row }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " \"resolve_refused\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " resolve_total_nanos += module_resolve_nanos;".to_string()), "\n".to_string()), " resolve_samples.push(module_resolve_nanos);".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-census-resolve] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"module\": entry.module.clone(),".to_string()), "\n".to_string()), " \"path\": entry.path.clone(),".to_string()), "\n".to_string()), " \"resolve_nanos\": module_resolve_nanos,".to_string()), "\n".to_string()), " \"outcome\": outcome_label,".to_string()), "\n".to_string()), " \"occurrence_completeness\": native_census_module_completeness(outcome.clone()),".to_string()), "\n".to_string()), " \"index\": census_module_index + 1,".to_string()), "\n".to_string()), " \"of\": census_modules_total,".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " resolve_samples.sort_unstable();".to_string()), "\n".to_string()), " let cause_groups = native_census_cause_group_rows(groups.clone());".to_string()), "\n".to_string()), " for group in cause_groups.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"cause_group\": group }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE ROOTS, RANKED BY CLOSURE FAN-OUT (v2.compiler.compile native_census_roots_ranked).".to_string()), "\n".to_string()), " let roots = native_census_roots_ranked(cause_groups.clone());".to_string()), "\n".to_string()), " for root in roots.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_root\": root }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE TERMINAL LINE CARRIES THE COST AND THE POPULATION TOGETHER, because the reading this".to_string()), "\n".to_string()), " // mode exists to support is whether a whole-tree resolve is affordable at all, and a residual".to_string()), "\n".to_string()), " // count without the wall and the peak beside it cannot answer that.".to_string()), "\n".to_string()), " println!(".to_string()), "\n".to_string()), " \"{}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"_terminal\": \"complete\",".to_string()), "\n".to_string()), " \"mode\": \"census-resolve\",".to_string()), "\n".to_string()), " \"corpus_reads\": corpus_reads,".to_string()), "\n".to_string()), " \"modules\": modules.len(),".to_string()), "\n".to_string()), " \"file_refusals\": context.file_refusals.len(),".to_string()), "\n".to_string()), " \"advised_files\": advised_files,".to_string()), "\n".to_string()), " \"resolved\": resolved_ok,".to_string()), "\n".to_string()), " \"file_refused\": file_refused,".to_string()), "\n".to_string()), " \"resolve_refused\": resolve_refused,".to_string()), "\n".to_string()), " \"residual_rows\": residual_rows,".to_string()), "\n".to_string()), " \"cause_groups\": cause_groups.len(),".to_string()), "\n".to_string()), " \"roots\": roots.len(),".to_string()), "\n".to_string()), " \"load_nanos\": load_nanos,".to_string()), "\n".to_string()), " \"context_nanos\": context_nanos,".to_string()), "\n".to_string()), " \"roster_nanos\": roster_nanos,".to_string()), "\n".to_string()), " \"resolve_total_nanos\": resolve_total_nanos,".to_string()), "\n".to_string()), " \"resolve_p50_nanos\": native_cost_quantile_nanos(&resolve_samples, 1, 2),".to_string()), "\n".to_string()), " \"resolve_p95_nanos\": native_cost_quantile_nanos(&resolve_samples, 19, 20),".to_string()), "\n".to_string()), " \"resolve_max_nanos\": resolve_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// THE FOURTH MODE: D13's DEPENDENCY-DEMAND CENSUS (v2.compiler.compile native_demand_census_output).".to_string()), "\n".to_string()), "// The fold decides every line and the exit -- 0 held, 1 a census whose rows do not account for".to_string()), "\n".to_string()), "// every uses fn, 2 no observation -- and this function only reads the ingest, prints and exits.".to_string()), "\n".to_string()), "fn run_demand_census(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let output = native_demand_census_output(ingest);".to_string()), "\n".to_string()), " for line in output.lines.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " std::process::exit(output.exit as i32);".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// THE FIFTH MODE: THE WHOLE TREE, RESOLVED AND INFERRED PER MODULE (`census-infer`).".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// Every decision is a .dag fold: the per-module step (v2.compiler.compile".to_string()), "\n".to_string()), "// native_census_module_inference), the type-declaration census's contribution from it".to_string()), "\n".to_string()), "// (native_census_infer_type_contribution), the run it accumulates into and the receipt and".to_string()), "\n".to_string()), "// terminal that run decides (gunbc.instruments.type_declaration_use_census). This loop reads the".to_string()), "\n".to_string()), "// roots, threads the context, prints and sets the status -- the operations no fold can. A module's".to_string()), "\n".to_string()), "// trees live for one step; what is kept is the census's rows, not the trees.".to_string()), "\n".to_string()), "fn run_census_infer(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let mut context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " let modules = native_census_modules(ingest.clone());".to_string()), "\n".to_string()), " let mut run = type_census_run_empty();".to_string()), "\n".to_string()), " for entry in modules.iter() {".to_string()), "\n".to_string()), " let started = Instant::now();".to_string()), "\n".to_string()), " let step = native_census_module_inference(context.clone(), refusal_index.clone(), entry.clone());".to_string()), "\n".to_string()), " context = step.context.clone();".to_string()), "\n".to_string()), " let contribution = native_census_infer_type_contribution(context.clone(), entry.clone(), step.outcome.clone());".to_string()), "\n".to_string()), " run = type_census_run_add(run, contribution);".to_string()), "\n".to_string()), " eprintln!(\"[native-census-infer] {}\", serde_json::json!({ \"module\": entry.module.clone(), \"nanos\": span_nanos(started) }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let receipt = type_census_receipt(run);".to_string()), "\n".to_string()), " for line in receipt.lines.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match &*receipt.terminal {".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimHeld => {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"_terminal\": \"complete\", \"mode\": \"census-infer\", \"modules\": modules.len(), \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024) }));".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNotHeld { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NOT HELD: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNoObservation { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NO OBSERVATION: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn native_cost_quantile_nanos(sorted: &[u128], numer: usize, denom: usize) -> u128 {".to_string()), "\n".to_string()), " if sorted.is_empty() { return 0; }".to_string()), "\n".to_string()), " let idx = (sorted.len() - 1) * numer / denom;".to_string()), "\n".to_string()), " sorted[idx]".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// A SPAN IS READ ONCE, THROUGH A READER THAT REFUSES A CLOCK THAT WENT BACKWARDS.".to_string()), "\n".to_string()), "// Instant::elapsed is Instant::now().duration_since(earlier), and duration_since SATURATES:".to_string()), "\n".to_string()), "// a monotonic clock that is not actually monotonic -- which happens on some virtualised and".to_string()), "\n".to_string()), "// migrated hosts -- yields Duration::ZERO rather than an error. That is the fabricated zero".to_string()), "\n".to_string()), "// this receipt forbids, arriving by a quieter route than the saturating_sub it replaced: a".to_string()), "\n".to_string()), "// phase would report as having taken no time and the partition would balance for the wrong".to_string()), "\n".to_string()), "// reason. checked_duration_since returns None instead, so the impossible reading is".to_string()), "\n".to_string()), "// REPRESENTABLE and can be refused.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// AN EARLIER REVISION OF THIS FILE CLAIMED THE CLASS WAS STRUCTURALLY IMPOSSIBLE ONCE THE".to_string()), "\n".to_string()), "// subtraction was gone. That was wrong and is corrected rather than softened: deleting the".to_string()), "\n".to_string()), "// explicit subtraction removed the visible clamp, not the saturation underneath it. The".to_string()), "\n".to_string()), "// honest rung is MITIGATED BY A TYPED REFUSAL at a declared boundary -- the host clock is".to_string()), "\n".to_string()), "// external reality, which DESIGN section 4b keeps off the ladder rather than on its top rung.".to_string()), "\n".to_string()), "fn span_nanos(started: Instant) -> u128 {".to_string()), "\n".to_string()), " match Instant::now().checked_duration_since(started) {".to_string()), "\n".to_string()), " Some(d) => d.as_nanos(),".to_string()), "\n".to_string()), " None => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: the monotonic clock went backwards between two reads, so no span is measurable\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// THE PARTITION IS JUDGED ON THREAD CPU, AND THIS IS ITS SPAN (std.realization_measurement".to_string()), "\n".to_string()), "// ObserveThreadCpuAtSubject; extdeps.posix.clock_gettime ClockThreadCputimeId). The law".to_string()), "\n".to_string()), "// native_driver_cost_account enforces is that the phases partition the DRIVER'S OWN WORK. A".to_string()), "\n".to_string()), "// wall span also holds every interval the host descheduled this thread -- another tenant's".to_string()), "\n".to_string()), "// build, page-fault service, a lock wait -- none of which any phase performed, so on a wall".to_string()), "\n".to_string()), "// clock that time fell into the residual and the verdict tracked host load -- the same subject".to_string()), "\n".to_string()), "// reconciled on a quiet runner and refused on a contended one. The thread".to_string()), "\n".to_string()), "// CPU clock advances only while this thread runs, so the residual is uninstrumented driver".to_string()), "\n".to_string()), "// work and nothing else. The wall is still read once, around the whole run, and printed as an".to_string()), "\n".to_string()), "// observation; it is never an input to the verdict. The driver is single-threaded, so the".to_string()), "\n".to_string()), "// thread clock is the driver's whole CPU. An unsupported clock refuses inside the runtime".to_string()), "\n".to_string()), "// read; there is no wall fallback.".to_string()), "\n".to_string()), "#[derive(Clone, Copy)]".to_string()), "\n".to_string()), "struct CpuMark(u128);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn cpu_mark() -> CpuMark {".to_string()), "\n".to_string()), " CpuMark(observed_thread_cpu_nanos(String::new()) as u128)".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn cpu_span_nanos(started: CpuMark) -> u128 {".to_string()), "\n".to_string()), " let finished = cpu_mark();".to_string()), "\n".to_string()), " if finished.0 < started.0 {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: the thread CPU clock went backwards between two reads, so no span is measurable\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " finished.0 - started.0".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// A SPAN THAT DOES NOT FIT REFUSES RATHER THAN SATURATING (review 64474 control). This was".to_string()), "\n".to_string()), "// i64::try_from(n).unwrap_or(i64::MAX), which turns a failed conversion into a FIGURE -- and a".to_string()), "\n".to_string()), "// fabricated figure on a receipt is the state DESIGN section 5 forbids outright, whether the".to_string()), "\n".to_string()), "// invented number is a zero that reports a phase as instant or a maximum that reports it as".to_string()), "\n".to_string()), "// three centuries. The saturating arm also chose the direction that hides best: i64::MAX would".to_string()), "\n".to_string()), "// push the exclusive sum past the parent and surface as NativeDriverCostOverAttributed, a cost".to_string()), "\n".to_string()), "// verdict naming the wrong cause.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// ON REACHABILITY, STATED RATHER THAN LEFT AS AN IMPLIED CLAIM: i64 nanoseconds is about 292".to_string()), "\n".to_string()), "// years, so no span this driver measures reaches it, and the refusal below is not expected to".to_string()), "\n".to_string()), "// fire. It is written because the alternative is not silence but a LIE -- the arm has to answer".to_string()), "\n".to_string()), "// something, and the only honest answers are a real number or a refusal.".to_string()), "\n".to_string()), "fn native_cost_i64(n: u128) -> i64 {".to_string()), "\n".to_string()), " match i64::try_from(n) {".to_string()), "\n".to_string()), " Ok(v) => v,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: a measured span does not fit the cost carrier, so no partition can be reported: {n} ({cause})\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// AN UNREADABLE /proc IS ABSENT, NOT ZERO (review 64181). A zero here was printed as".to_string()), "\n".to_string()), "// peak_rss_bytes / rss_bytes and read as a measurement: a run on a host without /proc, or with".to_string()), "\n".to_string()), "// the key renamed, reported a process using no memory. Absence is carried as None and rendered".to_string()), "\n".to_string()), "// as JSON null, so the receipt says it does not know instead of inventing a figure. This is".to_string()), "\n".to_string()), "// telemetry on no receipt field, so it is the one place an Absent is carried rather than".to_string()), "\n".to_string()), "// refused: the run is still a valid observation of everything else.".to_string()), "\n".to_string()), "fn proc_status_kib(key: &str) -> Option {".to_string()), "\n".to_string()), " let text = std::fs::read_to_string(\"/proc/self/status\").ok()?;".to_string()), "\n".to_string()), " for line in text.lines() {".to_string()), "\n".to_string()), " if let Some(rest) = line.strip_prefix(key) {".to_string()), "\n".to_string()), " return rest".to_string()), "\n".to_string()), " .trim_start_matches(':')".to_string()), "\n".to_string()), " .trim()".to_string()), "\n".to_string()), " .split_whitespace()".to_string()), "\n".to_string()), " .next()".to_string()), "\n".to_string()), " .and_then(|n| n.parse::().ok());".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " None".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// ONE CLOCK FOR THE VERDICT, AND IT IS THE THREAD CPU ONE (see cpu_span_nanos). Every exclusive".to_string()), "\n".to_string()), "// phase span and the parent span are read from it, so the partition compares readings from a".to_string()), "\n".to_string()), "// single source; the wall Instant around the run is an observation only. The history below is".to_string()), "\n".to_string()), "// why one source matters (review 64474), and it applied to the monotonic clock first.".to_string()), "\n".to_string()), "// They were split before this: the parent came from Instant while the phase spans came from".to_string()), "\n".to_string()), "// SystemTime, which is the wall clock and is free to step backwards under NTP or an operator".to_string()), "\n".to_string()), "// adjustment. A step between two phase reads could then push the exclusive sum past a parent".to_string()), "\n".to_string()), "// span that never moved, and the driver would refuse with NativeDriverCostOverAttributed --".to_string()), "\n".to_string()), "// a cost refusal whose real cause was the clock. That is a fabricated attribution: the numbers".to_string()), "\n".to_string()), "// would be wrong while the receipt read as a measurement.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// THE SUBTRACTION IS GONE RATHER THAN GUARDED, which is the point. A span used to be an".to_string()), "\n".to_string()), "// explicit saturating_sub of two readings, so a backwards step clamped the difference to zero".to_string()), "\n".to_string()), "// and reported a phase as having taken NO TIME -- the exact state the annotation beside it".to_string()), "\n".to_string()), "// forbade, reached by the arm meant to prevent it. Instant::elapsed cannot return a negative".to_string()), "\n".to_string()), "// duration, so there is no difference to clamp and no clamp to review: DESIGN section 5".to_string()), "\n".to_string()), "// construction over validation, and the class climbs from mitigated to structurally".to_string()), "\n".to_string()), "// impossible. The pre-epoch refusal that guarded the old wall-clock read is DELETED with it,".to_string()), "\n".to_string()), "// because the hazard it named cannot arise on this clock -- production handling dissolves on".to_string()), "\n".to_string()), "// the climb (DESIGN section 4b(4)); no evidence is retired here, since that arm had none.".to_string()), "\n".to_string()), "fn run_adjudication(facts_path: &str, pattern: Rc, source_roots: &[String]) {".to_string()), "\n".to_string()), " let driver_start = cpu_mark();".to_string()), "\n".to_string()), " let driver_wall_start = Instant::now();".to_string()), "\n".to_string()), " // A MISSING GITHUB_SHA IS ABSENT, NOT \"local\" (review 64181). The receipt line carries this".to_string()), "\n".to_string()), " // as the tree the measurement describes; the literal \"local\" is a plausible identity that".to_string()), "\n".to_string()), " // no reader can distinguish from a tree actually named local.".to_string()), "\n".to_string()), " let head: Option = std::env::var(\"GITHUB_SHA\").ok();".to_string()), "\n".to_string()), " let facts = read_host_facts(facts_path);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the load row: reading the declared roots off disk. Its own exclusive row rather than a".to_string()), "\n".to_string()), " // field on another phase, so the IO is never mixed into a fold's number.".to_string()), "\n".to_string()), " let load_started = cpu_mark();".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let corpus_reads = reads.len();".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let load_nanos = cpu_span_nanos(load_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // universe_derivation: the floor's own per-file discovery fold plus the module-header and".to_string()), "\n".to_string()), " // import scans, over EVERY read -- discovery is a corpus-wide question -- and then the".to_string()), "\n".to_string()), " // import closure of the universe's modules, which is all the front end is given.".to_string()), "\n".to_string()), " let universe_started = cpu_mark();".to_string()), "\n".to_string()), " let source_facts = native_lane_source_facts(ingest.clone());".to_string()), "\n".to_string()), " // The universe is derived FROM THE FACTS ALREADY BOUND, not from the ingest again:".to_string()), "\n".to_string()), " // the ingest-taking entry would re-run the whole-corpus discovery fold the line above".to_string()), "\n".to_string()), " // just paid for, and that fold is the phase the pattern does NOT narrow. See".to_string()), "\n".to_string()), " // v2.compiler.compile native_lane_universe_of_facts -- the pattern is threaded, so this".to_string()), "\n".to_string()), " // is the same entry a narrower pattern reaches, never a filter over a default universe.".to_string()), "\n".to_string()), " let universe = match &*native_lane_universe_of_facts(source_facts.clone(), pattern.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"DERIVATION-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " // The walk's budget exhausting is a refusal of the whole derivation, never a truncated".to_string()), "\n".to_string()), " // closure: a narrowed ingest would surface only as unexplained resolve refusals later.".to_string()), "\n".to_string()), " let closure = match &*native_lane_closure_modules(".to_string()), "\n".to_string()), " source_facts.clone(),".to_string()), "\n".to_string()), " universe.clone(),".to_string()), "\n".to_string()), " native_lane_closure_round_budget(),".to_string()), "\n".to_string()), " ) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CLOSURE-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let closure_ingest = native_lane_closure_ingest(ingest.clone(), source_facts.clone(), closure.clone());".to_string()), "\n".to_string()), " let closure_reads = closure_ingest.len();".to_string()), "\n".to_string()), " // The ingest receipt's identity join, asked by the route: every ingested source declares a".to_string()), "\n".to_string()), " // closure module and every closure module some source declares was ingested. A miss is the".to_string()), "\n".to_string()), " // hidden prepass (or a hidden narrowing) this receipt exists to refuse.".to_string()), "\n".to_string()), " let ingest_receipt = native_lane_ingest_receipt(source_facts.clone(), closure.clone(), closure_ingest.clone());".to_string()), "\n".to_string()), " // The join answers a typed refusal naming the offending identity, so the counts below are".to_string()), "\n".to_string()), " // context for it rather than the whole diagnostic: a reader gets the module and the path,".to_string()), "\n".to_string()), " // not a pair of totals to re-derive the offender from.".to_string()), "\n".to_string()), " match &*native_lane_ingest_matches_closure(source_facts.clone(), ingest_receipt.clone(), closure_ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { .. } => {}".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"INGEST-CLOSURE-REFUSED: {:#?} (scanned={} ingested={} closure_modules={})\",".to_string()), "\n".to_string()), " diagnostics,".to_string()), "\n".to_string()), " ingest_receipt.scanned_paths.len(),".to_string()), "\n".to_string()), " ingest_receipt.ingested_paths.len(),".to_string()), "\n".to_string()), " ingest_receipt.closure_modules.len(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let universe_nanos = cpu_span_nanos(universe_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the context row: the front-end fold over the CLOSURE only, entered ONE PHASE AT A TIME.".to_string()), "\n".to_string()), " // The driver owns the clock, so it is the only place a phase span can be read; the phases".to_string()), "\n".to_string()), " // themselves are v2.compiler.program_assembly declarations and each binds the previous".to_string()), "\n".to_string()), " // phase's Outcome, so each phase BODY is single-authored and only the ORDER and the".to_string()), "\n".to_string()), " // inter-phase diagnostic merge exist twice -- here and in the modeled composition".to_string()), "\n".to_string()), " // program_assembly_read_to_normalized_root_seeded. (It named the UNSEEDED composition".to_string()), "\n".to_string()), " // until that one was deleted for having no caller; the seeded producer is now the single".to_string()), "\n".to_string()), " // modeled per-file front end, and this loop is still its second spelling.)".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // THAT SECOND SPELLING IS ADMITTED DEBT AND IS FILED, NOT ASSERTED AWAY (review 65068):".to_string()), "\n".to_string()), " // gunbc.recurring_failure_mode.realization_respells_a_modeled_folds_sequencing_to_instrument_it.".to_string()), "\n".to_string()), " // An earlier revision of this comment claimed the two are the same computation. Nothing".to_string()), "\n".to_string()), " // executes that claim, and a .dag witness comparing the composition against its own phases".to_string()), "\n".to_string()), " // CANNOT go red -- the composition is defined as the phase chain -- so the honest answer is".to_string()), "\n".to_string()), " // the row and its trigger (a realization seam that yields per-phase spans FROM a modeled".to_string()), "\n".to_string()), " // fold), not a green check. Reordering these calls or dropping the merge that".to_string()), "\n".to_string()), " // program_assembly_phase_parse_measured_seeded performs would change the context with".to_string()), "\n".to_string()), " // nothing red.".to_string()), "\n".to_string()), " let context_started = cpu_mark();".to_string()), "\n".to_string()), " // THE ONCE-PER-FOLD INVARIANT IS PAID ONCE, HERE, AND CARRIED INTO EVERY FILE. A second".to_string()), "\n".to_string()), " // dag_language_model() or prepare_grammar() call inside the loop would re-introduce at the".to_string()), "\n".to_string()), " // realization layer exactly the per-file recompute the fold hoisted out.".to_string()), "\n".to_string()), " let front_end = match &*native_test_front_end_prepare() {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let front_end_nanos = cpu_span_nanos(context_started);".to_string()), "\n".to_string()), " // THE INGEST'S OCCURRENCE SCOPE, computed once: every projected occurrence the loop's".to_string()), "\n".to_string()), " // normalize allocates states its cause in it (v2.compiler.program_assembly".to_string()), "\n".to_string()), " // program_assembly_ingest_scope).".to_string()), "\n".to_string()), " let ingest_scope = match &*program_assembly_ingest_scope_outcome(closure_ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut context_state = native_test_context_state_empty();".to_string()), "\n".to_string()), " let mut tokenize_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut parse_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut normalize_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut absorb_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut phase_calls: u64 = 0;".to_string()), "\n".to_string()), " let mut token_total: u64 = 0;".to_string()), "\n".to_string()), " let mut memo_hits_total: u64 = 0;".to_string()), "\n".to_string()), " let mut memo_misses_total: u64 = 0;".to_string()), "\n".to_string()), " let mut memo_lookups_total: u64 = 0;".to_string()), "\n".to_string()), " let mut byte_total: u64 = 0;".to_string()), "\n".to_string()), " let mut context_file_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " for read in closure_ingest.iter() {".to_string()), "\n".to_string()), " let file_started = cpu_mark();".to_string()), "\n".to_string()), " let tokenize_started = cpu_mark();".to_string()), "\n".to_string()), " let tokens = program_assembly_phase_tokenize(read.clone(), front_end.lex.clone());".to_string()), "\n".to_string()), " let file_tokenize_nanos = cpu_span_nanos(tokenize_started);".to_string()), "\n".to_string()), " // The COUNT the time is read against: a span alone cannot separate a large file from a".to_string()), "\n".to_string()), " // per-file constant. Counted outside every phase span so it prices none of them.".to_string()), "\n".to_string()), " let file_tokens = program_assembly_phase_token_count(tokens.clone());".to_string()), "\n".to_string()), " let parse_started = cpu_mark();".to_string()), "\n".to_string()), " // SEEDED FROM THE MARK THE CONTEXT HAS REACHED, because this loop IS a source graph:".to_string()), "\n".to_string()), " // std.occurrence_identity occurrence_identity_scope_law requires one allocator threaded".to_string()), "\n".to_string()), " // across every parsed source, and a per-file reset makes two files' occurrence #N the".to_string()), "\n".to_string()), " // same value with different meanings. The modeled fold threads it internally; this".to_string()), "\n".to_string()), " // respelling has to thread it explicitly or it keeps the reset the fold no longer has.".to_string()), "\n".to_string()), " let file_seed = context_state.alloc.clone();".to_string()), "\n".to_string()), " let file_seed_reported = file_seed.clone();".to_string()), "\n".to_string()), " let measured = program_assembly_phase_parse_measured_seeded(".to_string()), "\n".to_string()), " tokens.clone(),".to_string()), "\n".to_string()), " front_end.prepared.clone(),".to_string()), "\n".to_string()), " // The residue field is an Optional whose inner type rides a recursion cycle, so the".to_string()), "\n".to_string()), " // emitted record BOXES it while the phase takes the option by value. Deref the box".to_string()), "\n".to_string()), " // rather than clone it; the sibling fields above are Rc and clone correctly.".to_string()), "\n".to_string()), " (*front_end.residue).clone(),".to_string()), "\n".to_string()), " file_seed.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let file_parse_nanos = cpu_span_nanos(parse_started);".to_string()), "\n".to_string()), " // The memo accounting rides BESIDE the outcome, so it reports on a refusal too --".to_string()), "\n".to_string()), " // which is the expensive case this instrument exists to read. Counted outside every".to_string()), "\n".to_string()), " // phase span so it prices none of them.".to_string()), "\n".to_string()), " let file_memo_hits = program_assembly_memo_hits(measured.clone());".to_string()), "\n".to_string()), " let file_memo_misses = program_assembly_memo_misses(measured.clone());".to_string()), "\n".to_string()), " let file_memo_lookups = program_assembly_memo_lookup_calls(measured.clone());".to_string()), "\n".to_string()), " // Read BEFORE program_assembly_measured_outcome consumes `measured`. On a refused parse".to_string()), "\n".to_string()), " // this returns the seed unchanged. Not because nothing was minted -- a refused parse".to_string()), "\n".to_string()), " // HAS minted ids -- but because ParseExprRejected carries no provenance state, so the".to_string()), "\n".to_string()), " // mark the attempt reached is not recoverable. See the precondition on".to_string()), "\n".to_string()), " // v2.compiler.compile native_test_context_absorb.".to_string()), "\n".to_string()), " let file_alloc = program_assembly_measured_alloc(measured.clone(), file_seed);".to_string()), "\n".to_string()), " let parsed = program_assembly_measured_outcome(measured);".to_string()), "\n".to_string()), " let normalize_started = cpu_mark();".to_string()), "\n".to_string()), " // NORMALIZE ADVANCES THE ALLOCATOR: lowering allocates every image member an id, so the".to_string()), "\n".to_string()), " // next file seeds from the mark normalize reached, not the one parse reached.".to_string()), "\n".to_string()), " let normalized = program_assembly_phase_normalize_seeded(parsed.clone(), file_alloc.clone(), ingest_scope.clone());".to_string()), "\n".to_string()), " let root = program_assembly_normalized_outcome(normalized.clone());".to_string()), "\n".to_string()), " let file_spans = program_assembly_normalized_spans(normalized.clone());".to_string()), "\n".to_string()), " let file_alloc = program_assembly_normalized_alloc(normalized);".to_string()), "\n".to_string()), " let file_alloc_reported = file_alloc.clone();".to_string()), "\n".to_string()), " let file_normalize_nanos = cpu_span_nanos(normalize_started);".to_string()), "\n".to_string()), " // qualified-name read plus source-root index insertion: the half of the per-file step".to_string()), "\n".to_string()), " // that is not the front end, timed separately because a quadratic accumulator would".to_string()), "\n".to_string()), " // live here and nowhere else in this loop.".to_string()), "\n".to_string()), " let absorb_started = cpu_mark();".to_string()), "\n".to_string()), " context_state = native_test_context_absorb(context_state.clone(), read.clone(), root.clone(), file_alloc, file_spans);".to_string()), "\n".to_string()), " let file_absorb_nanos = cpu_span_nanos(absorb_started);".to_string()), "\n".to_string()), " let file_nanos = cpu_span_nanos(file_started);".to_string()), "\n".to_string()), " tokenize_nanos += file_tokenize_nanos;".to_string()), "\n".to_string()), " parse_nanos += file_parse_nanos;".to_string()), "\n".to_string()), " normalize_nanos += file_normalize_nanos;".to_string()), "\n".to_string()), " absorb_nanos += file_absorb_nanos;".to_string()), "\n".to_string()), " phase_calls += 1;".to_string()), "\n".to_string()), " token_total += file_tokens.max(0) as u64;".to_string()), "\n".to_string()), " memo_hits_total += file_memo_hits.max(0) as u64;".to_string()), "\n".to_string()), " memo_misses_total += file_memo_misses.max(0) as u64;".to_string()), "\n".to_string()), " memo_lookups_total += file_memo_lookups.max(0) as u64;".to_string()), "\n".to_string()), " let file_bytes = read.source.carried.len() as u64;".to_string()), "\n".to_string()), " byte_total += file_bytes;".to_string()), "\n".to_string()), " context_file_samples.push(file_nanos);".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-context-split] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"path\": read.compilation_unit.clone(),".to_string()), "\n".to_string()), " \"bytes\": file_bytes,".to_string()), "\n".to_string()), " \"tokens\": file_tokens,".to_string()), "\n".to_string()), " \"tokenize_nanos\": file_tokenize_nanos,".to_string()), "\n".to_string()), " \"parse_nanos\": file_parse_nanos,".to_string()), "\n".to_string()), " \"normalize_nanos\": file_normalize_nanos,".to_string()), "\n".to_string()), " \"absorb_nanos\": file_absorb_nanos,".to_string()), "\n".to_string()), " \"file_nanos\": file_nanos,".to_string()), "\n".to_string()), " \"memo_hits\": file_memo_hits,".to_string()), "\n".to_string()), " \"memo_misses\": file_memo_misses,".to_string()), "\n".to_string()), " \"memo_lookup_calls\": file_memo_lookups,".to_string()), "\n".to_string()), " // THE THREADING, MADE OBSERVABLE. occurrence_seed is the mark this file was".to_string()), "\n".to_string()), " // handed and occurrence_next the mark it reached, so a multi-file run shows".to_string()), "\n".to_string()), " // one sequence continuing across files instead of each file restarting at 0.".to_string()), "\n".to_string()), " // A REFUSED file reports next == seed. That is the mark it was HANDED, not a".to_string()), "\n".to_string()), " // claim that nothing was minted: a refused parse mints ids that the refusal".to_string()), "\n".to_string()), " // discards unrecoverably. The next file continues from the seed rather than".to_string()), "\n".to_string()), " // rewinding to zero.".to_string()), "\n".to_string()), " \"occurrence_seed\": program_assembly_allocator_next(file_seed_reported),".to_string()), "\n".to_string()), " \"occurrence_next\": program_assembly_allocator_next(file_alloc_reported),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let context = native_test_context_finish(context_state, front_end.clone(), closure_ingest.clone());".to_string()), "\n".to_string()), " let context_nanos = cpu_span_nanos(context_started);".to_string()), "\n".to_string()), " context_file_samples.sort_unstable();".to_string()), "\n".to_string()), " let context_phase_sum = front_end_nanos + tokenize_nanos + parse_nanos + normalize_nanos + absorb_nanos;".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-context-partition] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"basis\": \"context_span\",".to_string()), "\n".to_string()), " \"basis_note\": \"exclusive phase rows partition context_nanos; residual is loop overhead and the eprintln per file\",".to_string()), "\n".to_string()), " \"context_nanos\": context_nanos,".to_string()), "\n".to_string()), " \"front_end_prepare_nanos\": front_end_nanos,".to_string()), "\n".to_string()), " \"tokenize_nanos\": tokenize_nanos,".to_string()), "\n".to_string()), " \"parse_nanos\": parse_nanos,".to_string()), "\n".to_string()), " \"normalize_nanos\": normalize_nanos,".to_string()), "\n".to_string()), " \"absorb_nanos\": absorb_nanos,".to_string()), "\n".to_string()), " \"sum_phase_nanos\": context_phase_sum,".to_string()), "\n".to_string()), " \"residual_nanos\": context_nanos.saturating_sub(context_phase_sum),".to_string()), "\n".to_string()), " \"files\": phase_calls,".to_string()), "\n".to_string()), " \"tokenize_calls\": phase_calls,".to_string()), "\n".to_string()), " \"parse_calls\": phase_calls,".to_string()), "\n".to_string()), " \"normalize_calls\": phase_calls,".to_string()), "\n".to_string()), " \"absorb_calls\": phase_calls,".to_string()), "\n".to_string()), " \"front_end_prepare_calls\": 1,".to_string()), "\n".to_string()), " \"tokens\": token_total,".to_string()), "\n".to_string()), " \"memo_hits\": memo_hits_total,".to_string()), "\n".to_string()), " \"memo_misses\": memo_misses_total,".to_string()), "\n".to_string()), " \"memo_lookup_calls\": memo_lookups_total,".to_string()), "\n".to_string()), " \"bytes\": byte_total,".to_string()), "\n".to_string()), " \"file_refusals\": context.file_refusals.len(),".to_string()), "\n".to_string()), " \"file_p50_nanos\": native_cost_quantile_nanos(&context_file_samples, 1, 2),".to_string()), "\n".to_string()), " \"file_p95_nanos\": native_cost_quantile_nanos(&context_file_samples, 19, 20),".to_string()), "\n".to_string()), " \"file_max_nanos\": context_file_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the prepare and eval rows, entered separately per module so the number the profile".to_string()), "\n".to_string()), " // lane prices -- the eval row -- is the sum of native_lane_identity_row".to_string()), "\n".to_string()), " // and nothing else: not ingest, not resolve, not infer.".to_string()), "\n".to_string()), " let mut population: Vec> = Vec::new();".to_string()), "\n".to_string()), " let mut prepare_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut evaluate_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut prepare_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " let mut eval_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " // ExclusiveModuleRelease IS NOW STRUCTURALLY ZERO, AND THAT IS A REAL CHANGE RATHER".to_string()), "\n".to_string()), " // THAN A TIDY-UP. The displaced loop dropped each module's resolved tree inside a span,".to_string()), "\n".to_string()), " // because dropping at iteration end paid time no exclusive row covered. The engine holds".to_string()), "\n".to_string()), " // every settled demand's value in its entries for the whole run, so there is no per-module".to_string()), "\n".to_string()), " // release point to time -- resolved trees now accumulate where they used to be released.".to_string()), "\n".to_string()), " // The row stays in the partition reporting zero rather than being deleted: a key removed".to_string()), "\n".to_string()), " // here vanishes from the sum and the serialized line at once, silently, and the residual".to_string()), "\n".to_string()), " // it used to carry would be absorbed into the tolerance rather than reported. Retention is".to_string()), "\n".to_string()), " // std.materialization_ladder's question and the cut that answers it owns this row.".to_string()), "\n".to_string()), " let module_release_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut relay_emit_nanos: u128 = 0;".to_string()), "\n".to_string()), " // The per-path refusal index is built ONCE here rather than per module: this loop is the".to_string()), "\n".to_string()), " // only place that knows a loop is about to happen, and the join it feeds was O(modules x".to_string()), "\n".to_string()), " // refusals) when each module scanned the refusal list for itself.".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " // ═══════════════════════════════════════════════════════════════════════════════════════".to_string()), "\n".to_string()), " // THE SCHEDULE IS THE ENGINE'S. THIS LOOP HOLDS THE CLOCK AND NOTHING ELSE.".to_string()), "\n".to_string()), " // ═══════════════════════════════════════════════════════════════════════════════════════".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // What stood here was `for entry in universe.modules` with `for declaration in".to_string()), "\n".to_string()), " // entry.declarations` nested inside it -- a dependency structure written as control flow, and".to_string()), "\n".to_string()), " // the last place in the native route that decided WHAT RUNS NEXT. It is deleted. The order now".to_string()), "\n".to_string()), " // comes from v2.std.demand_engine's one ready queue, over a graph".to_string()), "\n".to_string()), " // v2.compiler.native_demand_schedule DERIVES from the same universe roster this loop walked.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // WHY A LOOP REMAINS AT ALL, and why it is not the old one wearing a new name: a .dag fold".to_string()), "\n".to_string()), " // cannot read a clock, and the native cost partition is an EXHAUSTIVE exclusive sum over".to_string()), "\n".to_string()), " // parent_span with a 50ms tolerance arm. Replacing the walk with one opaque call would have".to_string()), "\n".to_string()), " // dissolved the prepare and eval rows into an unattributed remainder and made that arm fire --".to_string()), "\n".to_string()), " // so the driver asks the engine for the next admissible demand and spans the step. It supplies".to_string()), "\n".to_string()), " // no ordering, no module iteration and no declaration iteration; remove the spans and the body".to_string()), "\n".to_string()), " // is a drain.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // THE SEAT COUNT IS THE CALLER'S. One seat here, which is what makes this cut a pure authority".to_string()), "\n".to_string()), " // move: same demands, same order, same single execution at a time. Multi-seat realization binds".to_string()), "\n".to_string()), " // a different offer to the same drain and changes nothing else.".to_string()), "\n".to_string()), " // THE DRIVER'S OWN INPUT CONSTRUCTION, charged to prepare as it was before the engine landed:".to_string()), "\n".to_string()), " // it runs before any demand and is not the scheduler's bookkeeping, which the engine measures".to_string()), "\n".to_string()), " // for itself.".to_string()), "\n".to_string()), " let tested_tree_started = cpu_mark();".to_string()), "\n".to_string()), " let tested_tree_input = native_demand_tested_tree_input(head.clone().unwrap_or_default());".to_string()), "\n".to_string()), " prepare_total_nanos += cpu_span_nanos(tested_tree_started);".to_string()), "\n".to_string()), " // ONE CALL. The driver does not drain the graph, does not decide readiness and does not".to_string()), "\n".to_string()), " // classify a demand's stage -- those are the engine's and the plan's decisions, and a".to_string()), "\n".to_string()), " // rendered-main loop that made them was a second consumer of the engine's admission".to_string()), "\n".to_string()), " // vocabulary living in generated Rust, where no .dag check reaches.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // IT USED TO HOLD A LOOP FOR THE CLOCK, AND THAT REASON WAS WRONG. Each step now takes".to_string()), "\n".to_string()), " // its own span through observed_thread_cpu_nanos -- an ordinary builtin with no resource".to_string()), "\n".to_string()), " // requirement, modelled by std.realization_measurement as ObserveThreadCpuAtSubject's".to_string()), "\n".to_string()), " // realization seam -- so the exclusive prepare and eval rows survive with the spans taken".to_string()), "\n".to_string()), " // where the work is instead of around it.".to_string()), "\n".to_string()), " // THE ENGINE CALL IS ONE MEASURED SPAN (std.compiler_entry ExclusiveDemandScheduling). Its".to_string()), "\n".to_string()), " // demand windows are the prepare and eval rows; everything else the call spends is the".to_string()), "\n".to_string()), " // scheduler's, including the work between the engine's own bookkeeping spans that no row".to_string()), "\n".to_string()), " // timed when this row was the sum of those spans.".to_string()), "\n".to_string()), " let schedule_started = cpu_mark();".to_string()), "\n".to_string()), " let run = native_demand_schedule_universe(".to_string()), "\n".to_string()), " universe.modules.clone(),".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " refusal_index.clone(),".to_string()), "\n".to_string()), " tested_tree_input.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let schedule_span_nanos = cpu_span_nanos(schedule_started);".to_string()), "\n".to_string()), " // driver_collection: the driver's own work after the engine returned -- rows into the".to_string()), "\n".to_string()), " // population, the samples, the counters. Named for what it is, not folded into scheduling.".to_string()), "\n".to_string()), " let collection_started = cpu_mark();".to_string()), "\n".to_string()), " for row in native_demand_run_rows(run.clone()).iter() {".to_string()), "\n".to_string()), " population.push(row.clone());".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let engine_prepare_nanos = native_demand_prepare_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " let engine_eval_nanos = native_demand_eval_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " // OBSERVATION ONLY (v2.compiler.compile native_demand_occurrence_census_lines): the census is its".to_string()), "\n".to_string()), " // own demand kind, so its cost is the engine-reported ExclusiveOccurrenceCensus row, not a host span.".to_string()), "\n".to_string()), " let occurrence_census_nanos = native_demand_occurrence_census_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " prepare_total_nanos += engine_prepare_nanos;".to_string()), "\n".to_string()), " evaluate_total_nanos += engine_eval_nanos;".to_string()), "\n".to_string()), " // The engine's own bookkeeping sum stays an observation on the schedule line; it is not the row.".to_string()), "\n".to_string()), " let engine_bookkeeping_nanos = native_demand_scheduling_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " // Every demand window the engine attributes -- prepare, eval and the occurrence census -- is a row".to_string()), "\n".to_string()), " // of its own, so the scheduling row is the call span minus all three. Leaving the census out would".to_string()), "\n".to_string()), " // count its windows twice. An engine that attributed more than its call took is incoherent; refuse".to_string()), "\n".to_string()), " // rather than clamp the scheduling row to zero.".to_string()), "\n".to_string()), " let attributed_in_call = engine_prepare_nanos + engine_eval_nanos + occurrence_census_nanos;".to_string()), "\n".to_string()), " if attributed_in_call > schedule_span_nanos {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: native driver cost OverAttributed inside the engine call {{ attributed_nanos: {}, schedule_span_nanos: {} }}\", attributed_in_call, schedule_span_nanos);".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let demand_scheduling_nanos = schedule_span_nanos - attributed_in_call;".to_string()), "\n".to_string()), " for line in native_demand_occurrence_census_lines(run.clone(), universe.modules.len() as i64).iter() {".to_string()), "\n".to_string()), " eprintln!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for n in native_demand_prepare_samples(run.clone()).iter() {".to_string()), "\n".to_string()), " prepare_samples.push((*n).max(0) as u128);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for n in native_demand_eval_samples(run.clone()).iter() {".to_string()), "\n".to_string()), " eval_samples.push((*n).max(0) as u128);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let prepare_ok: u64 = native_demand_prepare_ok(run.clone()).max(0) as u64;".to_string()), "\n".to_string()), " let prepare_refused: u64 = native_demand_prepare_refused(run.clone()).max(0) as u64;".to_string()), "\n".to_string()), " let driver_collection_nanos = cpu_span_nanos(collection_started);".to_string()), "\n".to_string()), " // THE DRIVER REFUSES A RUN WHOSE CLOCK NEVER ADVANCED rather than reporting its zeros. A".to_string()), "\n".to_string()), " // realization whose body returns a constant zero leaves every transition CLASSIFIED and".to_string()), "\n".to_string()), " // VALIDLY OBSERVED, so completeness answers true over totals that are all zero, and the".to_string()), "\n".to_string()), " // emitted-body membership check cannot see it either -- that check establishes a bridge HAS".to_string()), "\n".to_string()), " // a body, never that the body measures. Reporting those zeros would be a fabricated".to_string()), "\n".to_string()), " // measurement presented as an observation, so the line stops here with a located cause.".to_string()), "\n".to_string()), " if !native_demand_observation_progressed(run.clone()) {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-demand-schedule] REFUSED: transitions={} executed and the elapsed observation accumulated no span; the emitted monotonic clock is not advancing\",".to_string()), "\n".to_string()), " native_demand_run_transition_count(run.clone()),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE EVENT TRACE, SEPARATE FROM THE RESULT. One line per admission in admission order, each".to_string()), "\n".to_string()), " // naming the demand key and the state it settled into. It is the schedule's receipt and it is".to_string()), "\n".to_string()), " // deliberately not the population: two runs agreeing on rows while disagreeing on order is a".to_string()), "\n".to_string()), " // real difference, and a receipt that carried only rows could not report it.".to_string()), "\n".to_string()), " let relay_started = cpu_mark();".to_string()), "\n".to_string()), " for line in native_demand_run_receipt_lines(run.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-demand-transition] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-demand-schedule] demands={} admitted={} readiness_derivations={} schedule_span_nanos={} engine_bookkeeping_nanos={}\",".to_string()), "\n".to_string()), " native_demand_run_demand_count(run.clone()),".to_string()), "\n".to_string()), " native_demand_run_transition_count(run.clone()),".to_string()), "\n".to_string()), " native_demand_run_readiness_derivations(run.clone()),".to_string()), "\n".to_string()), " schedule_span_nanos,".to_string()), "\n".to_string()), " engine_bookkeeping_nanos,".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " relay_emit_nanos += cpu_span_nanos(relay_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // receipt_admission: minting the receipt and evaluating the authority's admission and summary".to_string()), "\n".to_string()), " // over it. The controls are executed here because they are receipt inputs, not universe members.".to_string()), "\n".to_string()), " let admission_started = cpu_mark();".to_string()), "\n".to_string()), " let receipt = native_lane_receipt(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " universe.clone(),".to_string()), "\n".to_string()), " Rc::new(population.clone().into()),".to_string()), "\n".to_string()), " native_lane_controls(context.clone()),".to_string()), "\n".to_string()), " Rc::new(facts),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let admission = native_route_admission(receipt.clone());".to_string()), "\n".to_string()), " let summary = native_route_admission_summary(admission.clone());".to_string()), "\n".to_string()), " let file_refusal_tally: Vec = native_route_file_refusal_tally(receipt.clone())".to_string()), "\n".to_string()), " .iter()".to_string()), "\n".to_string()), " .map(|row| serde_json::json!(row))".to_string()), "\n".to_string()), " .collect();".to_string()), "\n".to_string()), " let admitted = native_route_admitted(admission.clone());".to_string()), "\n".to_string()), " // THE FRONTIER RATCHET, OVER THE SAME RECEIPT AND INSIDE THE SAME SPAN: the verdict is".to_string()), "\n".to_string()), " // gunbc.native_frontier_ratchet's, and this main only carries its word to the terminal marker".to_string()), "\n".to_string()), " // and its lines to stderr. It is measured under receipt_admission because it is judgement of".to_string()), "\n".to_string()), " // the receipt, the row that span already names.".to_string()), "\n".to_string()), " let frontier = native_frontier_verdict(receipt.clone(), admission.clone(), pattern.clone());".to_string()), "\n".to_string()), " let frontier_word = native_frontier_verdict_word(frontier.clone());".to_string()), "\n".to_string()), " let admission_nanos = cpu_span_nanos(admission_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // row_serialization: SERIALIZING THE OBSERVATIONS AND WRITING THEM, MEASURED (review".to_string()), "\n".to_string()), " // 64181 named this span; #11105 made leaving it unattributed fatal). Every row the".to_string()), "\n".to_string()), " // population carries is turned into JSON here and written to stdout, which is real work".to_string()), "\n".to_string()), " // no other row covers -- eval is the identity fold and nothing else. It is TIMED, never".to_string()), "\n".to_string()), " // derived as parent minus the others: a row defined as the remainder would force the".to_string()), "\n".to_string()), " // residual to zero and make the tolerance arm unfireable by construction.".to_string()), "\n".to_string()), " let serialization_started = cpu_mark();".to_string()), "\n".to_string()), " for refusal in receipt.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for row in population.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::to_string(row).unwrap());".to_string()), "\n".to_string()), " // THE OPERATOR'S LINE, BESIDE THE PERSISTED ROW. The JSON above is the receipt; this is".to_string()), "\n".to_string()), " // the same value rendered by the authority for a reader of the command's output, so a".to_string()), "\n".to_string()), " // row that did not pass names its subject, stage, cause and position without anyone".to_string()), "\n".to_string()), " // re-deriving them from the bytes just written. Passing rows print nothing here: the".to_string()), "\n".to_string()), " // terminal marker carries their count. EXHAUSTIVE ON PURPOSE -- a verdict arm added to".to_string()), "\n".to_string()), " // the vocabulary must decide here whether it is operator-visible.".to_string()), "\n".to_string()), " match &*row.verdict {".to_string()), "\n".to_string()), " NativeTestVerdict::NativeTestPassed => {}".to_string()), "\n".to_string()), " NativeTestVerdict::NativeTestReturnedFalse".to_string()), "\n".to_string()), " | NativeTestVerdict::NativeTestReturnedOther".to_string()), "\n".to_string()), " | NativeTestVerdict::NativeTestRefused { .. } => {".to_string()), "\n".to_string()), " eprintln!(\"[native-verdict] {}\", native_route_member_row_text(row.clone()));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for line in native_frontier_report_lines(frontier.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-frontier] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for line in native_frontier_proposal_file_lines(receipt.clone(), frontier.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-frontier-roster] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let row_serialization_nanos = cpu_span_nanos(serialization_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // THE PARTITION, THROUGH THE ONE LAW. The rows are the row set std.compiler_entry owns and".to_string()), "\n".to_string()), " // verdict is native_driver_cost_account over them -- not a second reconcile. The two rows".to_string()), "\n".to_string()), " // this route pays that a driver handed a universe file does not (universe_derivation,".to_string()), "\n".to_string()), " // receipt_admission) are members of the same exclusive sum, so an over-attribution through".to_string()), "\n".to_string()), " // either is caught here rather than by a check of its own.".to_string()), "\n".to_string()), " prepare_samples.sort_unstable();".to_string()), "\n".to_string()), " eval_samples.sort_unstable();".to_string()), "\n".to_string()), " let parent_span_nanos = cpu_span_nanos(driver_start);".to_string()), "\n".to_string()), " let parent_wall_nanos = span_nanos(driver_wall_start);".to_string()), "\n".to_string()), " // THE DRIVER IS EXHAUSTIVE OVER THE KEY WITH NO WILDCARD, deliberately: a row added to".to_string()), "\n".to_string()), " // std.compiler_entry makes THIS match refuse to compile until the driver says what".to_string()), "\n".to_string()), " // measures it. A wildcard here would answer zero for a span nobody wired up, which is".to_string()), "\n".to_string()), " // the absorbing arm DESIGN section 5 forbids -- and it is what the old record literal".to_string()), "\n".to_string()), " // did silently, three folds running.".to_string()), "\n".to_string()), " let exclusive = native_driver_exclusive_rows(move |k| match k {".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveLoad => nanosecond(native_cost_i64(load_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveUniverseDerivation => nanosecond(native_cost_i64(universe_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveContext => nanosecond(native_cost_i64(context_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusivePrepare => nanosecond(native_cost_i64(prepare_total_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveEval => nanosecond(native_cost_i64(evaluate_total_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveReceiptAdmission => nanosecond(native_cost_i64(admission_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveRowSerialization => nanosecond(native_cost_i64(row_serialization_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveDemandScheduling => nanosecond(native_cost_i64(demand_scheduling_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveDriverCollection => nanosecond(native_cost_i64(driver_collection_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveOccurrenceCensus => nanosecond(native_cost_i64(occurrence_census_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveModuleRelease => nanosecond(native_cost_i64(module_release_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveRelayEmit => nanosecond(native_cost_i64(relay_emit_nanos)),".to_string()), "\n".to_string()), " });".to_string()), "\n".to_string()), " let exclusive_sum = nanosecond_count(native_driver_exclusive_sum(exclusive.clone())) as u128;".to_string()), "\n".to_string()), " let tolerance_nanos = nanosecond_count(native_driver_cost_remainder_tolerance_nanos()) as u128;".to_string()), "\n".to_string()), " let accounting = native_driver_cost_account(".to_string()), "\n".to_string()), " nanosecond(native_cost_i64(parent_span_nanos)),".to_string()), "\n".to_string()), " exclusive.clone(),".to_string()), "\n".to_string()), " native_driver_cost_remainder_tolerance_nanos(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " // The remainder is the residual the accounting itself returns, never parent minus sum: on".to_string()), "\n".to_string()), " // over-attributed arm there IS no remainder, and a clamped zero there would be a".to_string()), "\n".to_string()), " // plausible number standing where the measurement is incoherent.".to_string()), "\n".to_string()), " let (verdict, remainder_nanos) = match accounting.as_ref() {".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostOverAttributed { .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostOverAttributed\"#, serde_json::Value::Null),".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostRemainderExceedsTolerance { residual, .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostRemainderExceedsTolerance\"#, serde_json::json!(nanosecond_count(residual.clone()))),".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostReconciled { residual, .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostReconciled\"#, serde_json::json!(nanosecond_count(residual.clone()))),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let identities = universe.identities.len() as u64;".to_string()), "\n".to_string()), " let eval_mean = if eval_samples.is_empty() { 0 } else { evaluate_total_nanos / eval_samples.len() as u128 };".to_string()), "\n".to_string()), " let prepare_mean = if prepare_samples.is_empty() { 0 } else { prepare_total_nanos / prepare_samples.len() as u128 };".to_string()), "\n".to_string()), " // THE ROWS ARE PROJECTED FROM THE VALUE, NOT RE-LISTED (review 64181). The field names".to_string()), "\n".to_string()), " // were written once to build NativeDriverExclusiveRows and again to render it, which is the".to_string()), "\n".to_string()), " // second spelling this file warns about for prose, applied to the emitted object: a driver".to_string()), "\n".to_string()), " // gaining a phase would have updated the struct and left the receipt short. Serializing the".to_string()), "\n".to_string()), " // value carries the row set from the type, so the JSON cannot disagree with the fold. The".to_string()), "\n".to_string()), " // rows therefore render as std.measure Nanosecond objects rather than bare integers -- the".to_string()), "\n".to_string()), " // unit travels with the number, which is the point of the type.".to_string()), "\n".to_string()), " // A NON-RECONCILED ARM IS A PROCESS REFUSAL (#11105, review 63891). The receipt is".to_string()), "\n".to_string()), " // printed first so the numbers that caused the refusal are on the wire, and THEN the".to_string()), "\n".to_string()), " // line stops: a verdict string with exit 0 is the inert lens DESIGN section 6 names.".to_string()), "\n".to_string()), " // The full-N run of this route refused here on 158600186 ns against a 50000000 ns".to_string()), "\n".to_string()), " // tolerance before row_serialization was a measured row, which is what put that row on".to_string()), "\n".to_string()), " // the type rather than a wider tolerance on this arm.".to_string()), "\n".to_string()), " eprintln!(\"[native-cost-partition] {}\", serde_json::json!({".to_string()), "\n".to_string()), " \"basis\": \"native_driver_thread_cpu\",".to_string()), "\n".to_string()), " \"basis_note\": \"single-threaded SourceRootEvalDriver thread CPU (CLOCK_THREAD_CPUTIME_ID); exclusive rows partition parent_span_nanos; parent_wall_nanos is an observation, never judged\",".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"head\": head,".to_string()), "\n".to_string()), " \"mode\": \"adjudicate\",".to_string()), "\n".to_string()), " \"verdict\": verdict,".to_string()), "\n".to_string()), " \"tolerance_nanos\": tolerance_nanos,".to_string()), "\n".to_string()), " \"parent_span_nanos\": parent_span_nanos,".to_string()), "\n".to_string()), " \"parent_wall_nanos\": parent_wall_nanos,".to_string()), "\n".to_string()), " \"exclusive\": exclusive.rows.iter().map(|r| (".to_string()), "\n".to_string()), " native_driver_exclusive_row_name(r.key.clone()),".to_string()), "\n".to_string()), " serde_json::json!(r.nanos),".to_string()), "\n".to_string()), " )).collect::>(),".to_string()), "\n".to_string()), " \"sum_exclusive_nanos\": exclusive_sum,".to_string()), "\n".to_string()), " \"remainder_nanos\": remainder_nanos,".to_string()), "\n".to_string()), " \"corpus_reads\": corpus_reads,".to_string()), "\n".to_string()), " \"closure_reads\": closure_reads,".to_string()), "\n".to_string()), " \"scanned_paths\": ingest_receipt.scanned_paths.len(),".to_string()), "\n".to_string()), " \"ingested_paths\": ingest_receipt.ingested_paths.len(),".to_string()), "\n".to_string()), " \"closure_modules\": closure.len(),".to_string()), "\n".to_string()), " \"identities\": identities,".to_string()), "\n".to_string()), " \"unique_modules\": universe.modules.len(),".to_string()), "\n".to_string()), " \"file_refusals\": receipt.file_refusals.len(),".to_string()), "\n".to_string()), " \"prepare_ok\": prepare_ok,".to_string()), "\n".to_string()), " \"prepare_refused\": prepare_refused,".to_string()), "\n".to_string()), " \"prepare_mean_nanos\": prepare_mean,".to_string()), "\n".to_string()), " \"prepare_p50_nanos\": native_cost_quantile_nanos(&prepare_samples, 1, 2),".to_string()), "\n".to_string()), " \"prepare_p95_nanos\": native_cost_quantile_nanos(&prepare_samples, 19, 20),".to_string()), "\n".to_string()), " \"prepare_max_nanos\": prepare_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"eval_mean_nanos\": eval_mean,".to_string()), "\n".to_string()), " \"eval_p50_nanos\": native_cost_quantile_nanos(&eval_samples, 1, 2),".to_string()), "\n".to_string()), " \"eval_p95_nanos\": native_cost_quantile_nanos(&eval_samples, 19, 20),".to_string()), "\n".to_string()), " \"eval_max_nanos\": eval_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " \"rss_bytes\": proc_status_kib(\"VmRSS\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " }));".to_string()), "\n".to_string()), " match accounting.as_ref() {".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostReconciled { .. } => {}".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostOverAttributed { sum_exclusive, parent_span } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"REFUSED: native driver cost OverAttributed {{ sum_exclusive_nanos: {}, parent_span_nanos: {} }}\",".to_string()), "\n".to_string()), " nanosecond_count(sum_exclusive.clone()) as u128,".to_string()), "\n".to_string()), " nanosecond_count(parent_span.clone()) as u128".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostRemainderExceedsTolerance { residual, tolerance } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"REFUSED: native driver cost RemainderExceedsTolerance {{ residual_nanos: {}, tolerance_nanos: {} }} -- unattributed driver work; name the span, do not widen the tolerance\",".to_string()), "\n".to_string()), " nanosecond_count(residual.clone()) as u128,".to_string()), "\n".to_string()), " nanosecond_count(tolerance.clone()) as u128".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " println!(".to_string()), "\n".to_string()), " \"{}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"_terminal\": \"complete\",".to_string()), "\n".to_string()), " \"mode\": \"adjudicate\",".to_string()), "\n".to_string()), " \"rows\": population.len(),".to_string()), "\n".to_string()), " \"universe\": universe.identities.len(),".to_string()), "\n".to_string()), " \"file_refusals\": receipt.file_refusals.len(),".to_string()), "\n".to_string()), " \"file_refusal_tally\": file_refusal_tally,".to_string()), "\n".to_string()), " \"advised_files\": context.accepted_file_advisories.len(),".to_string()), "\n".to_string()), " \"admitted\": admitted,".to_string()), "\n".to_string()), " \"summary\": summary,".to_string()), "\n".to_string()), " \"frontier\": frontier_word,".to_string()), "\n".to_string()), " })".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " if admitted {".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), " let plan = native_driver_parse(Rc::new(argv.into()));".to_string()), "\n".to_string()), " if let ProcessExit::ExitFailure { code, reason } = &*native_driver_plan_exit(plan.clone()) {".to_string()), "\n".to_string()), " eprintln!(\"{reason}\");".to_string()), "\n".to_string()), " std::process::exit(*code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let source_roots: Vec = native_driver_plan_source_roots(plan.clone()).iter().cloned().collect();".to_string()), "\n".to_string()), " match &*plan {".to_string()), "\n".to_string()), " NativeDriverPlan::NativeDriverPlanRefused { reason, detail } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: native driver plan refused ({reason:?}) but its exit was success -- {detail}\");".to_string()), "\n".to_string()), " std::process::exit(70);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeDriverPlan::NativeDriverPlanned { verb } => {".to_string()), "\n".to_string()), " let verb: &NativeDriverVerb = verb;".to_string()), "\n".to_string()), " match verb {".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensus { .. } => run_census(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensusResolve { .. } => run_census_resolve(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensusInfer { .. } => run_census_infer(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverDemandCensus { .. } => run_demand_census(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverAdjudicate { facts_path, pattern, .. } => run_adjudication(facts_path, Rc::new(TargetPattern::clone(pattern)), &source_roots),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), + content: v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("// Generated by v1 compiler -- do not edit.".to_string(), "\n".to_string()), "\n".to_string()), "#![allow(unused_parens, clippy::all, clippy::disallowed_macros)]".to_string()), "\n".to_string()), "\n".to_string()), "use std::rc::Rc;".to_string()), "\n".to_string()), "use std::time::Instant;".to_string()), "\n".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_witness_v2_native_route::{NativeRouteEmittedBuild, NativeRouteMalformedControl, NativeRouteOldRouteControl};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::".to_string()), pipeline_mod.clone()), "::{".to_string()), "\n".to_string()), " native_census_module_completeness, native_census_module_resolution, native_census_modules,".to_string()), "\n".to_string()), " native_census_module_inference, native_census_infer_type_contribution,".to_string()), "\n".to_string()), " native_census_residual_rows,".to_string()), "\n".to_string()), " native_census_cause_groups_add_module, native_census_cause_groups_from_file_refusals,".to_string()), "\n".to_string()), " native_census_cause_group_rows, native_census_closure_index, native_census_roots_ranked,".to_string()), "\n".to_string()), " native_demand_census_output,".to_string()), "\n".to_string()), " native_lane_controls, native_lane_file_refusal_index,".to_string()), "\n".to_string()), " native_lane_closure_ingest, native_lane_closure_modules, native_lane_closure_round_budget,".to_string()), "\n".to_string()), " native_lane_ingest_matches_closure, native_lane_ingest_receipt, native_lane_receipt,".to_string()), "\n".to_string()), " native_lane_source_facts, native_lane_universe_of_facts, native_test_context_absorb,".to_string()), "\n".to_string()), " native_test_context_finish, native_test_context_from_ingest, native_test_context_state_empty,".to_string()), "\n".to_string()), " native_test_front_end_prepare,".to_string()), "\n".to_string()), " native_driver_parse, native_driver_plan_exit, native_driver_plan_source_roots,".to_string()), "\n".to_string()), " NativeDriverPlan, NativeDriverVerb,".to_string()), "\n".to_string()), " NativeCensusModuleOutcome,".to_string()), "\n".to_string()), " NativeLaneHostFacts,".to_string()), "\n".to_string()), " NativeRouteMemberRow,".to_string()), "\n".to_string()), " native_demand_eval_nanos, native_demand_eval_samples,".to_string()), "\n".to_string()), " native_demand_prepare_nanos, native_demand_prepare_samples,".to_string()), "\n".to_string()), " native_demand_prepare_ok, native_demand_prepare_refused,".to_string()), "\n".to_string()), " native_demand_scheduling_nanos,".to_string()), "\n".to_string()), " native_demand_occurrence_census_lines, native_demand_occurrence_census_nanos,".to_string()), "\n".to_string()), " native_demand_run_demand_count, native_demand_run_readiness_derivations,".to_string()), "\n".to_string()), " native_demand_run_receipt_lines, native_demand_run_rows,".to_string()), "\n".to_string()), " native_demand_observation_progressed,".to_string()), "\n".to_string()), " native_demand_run_transition_count, native_demand_schedule_universe,".to_string()), "\n".to_string()), " native_demand_tested_tree_input,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_program_assembly::{".to_string()), "\n".to_string()), " program_assembly_allocator_next, program_assembly_parsed_alloc,".to_string()), "\n".to_string()), " program_assembly_phase_normalize_seeded, program_assembly_normalized_outcome,".to_string()), "\n".to_string()), " program_assembly_normalized_alloc, program_assembly_normalized_spans, program_assembly_ingest_scope_outcome,".to_string()), "\n".to_string()), " program_assembly_phase_parse_seeded,".to_string()), "\n".to_string()), " program_assembly_phase_token_count, program_assembly_phase_tokenize,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_witness_v2_native_route::{".to_string()), "\n".to_string()), " native_route_admission, native_route_admission_summary, native_route_admitted,".to_string()), "\n".to_string()), " native_route_file_refusal_tally, native_route_member_row_text,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_native_frontier_ratchet::{".to_string()), "\n".to_string()), " native_frontier_proposal_file_lines, native_frontier_report_lines, native_frontier_verdict,".to_string()), "\n".to_string()), " native_frontier_verdict_word,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_native_test_vocabulary::NativeTestVerdict;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_compiler_source_authority::DagSourceReadWitness;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v2_std_diagnostic::Outcome;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_process::ProcessExit;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::extdeps_bazel_target_pattern::TargetPattern;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::{".to_string()), "\n".to_string()), " native_driver_cost_account, native_driver_cost_remainder_tolerance_nanos,".to_string()), "\n".to_string()), " native_driver_exclusive_sum, NativeDriverCostAccounting,".to_string()), "\n".to_string()), " native_driver_exclusive_rows, native_driver_exclusive_row_name,".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey,".to_string()), "\n".to_string()), "};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_measure::{nanosecond, nanosecond_count};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::v1_rt::observed_thread_cpu_nanos;".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::gunbc_instruments_type_declaration_use_census::{type_census_receipt, type_census_run_add, type_census_run_empty};".to_string()), "\n".to_string()), "use ".to_string()), crate_name.clone()), "::std_compiler_entry::NativeClaimTerminal;".to_string()), "\n".to_string()), "\n".to_string()), emit_host_source_root_read_rs(crate_name.clone())), "fn host_fact(facts: &std::collections::HashMap, key: &str) -> String {".to_string()), "\n".to_string()), " match facts.get(key) {".to_string()), "\n".to_string()), " Some(value) => value.clone(),".to_string()), "\n".to_string()), " None => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: host facts file carries no {key} row\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn host_fact_int(facts: &std::collections::HashMap, key: &str) -> i64 {".to_string()), "\n".to_string()), " let value = host_fact(facts, key);".to_string()), "\n".to_string()), " match value.parse::() {".to_string()), "\n".to_string()), " Ok(parsed) => parsed,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: host fact {key} is not an integer: {value:?} ({cause})\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn read_host_facts(path: &str) -> NativeLaneHostFacts {".to_string()), "\n".to_string()), " let text = match std::fs::read_to_string(path) {".to_string()), "\n".to_string()), " Ok(text) => text,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: could not read host facts file {path}: {cause}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut facts: std::collections::HashMap = std::collections::HashMap::new();".to_string()), "\n".to_string()), " for line in text.lines() {".to_string()), "\n".to_string()), " if line.is_empty() {".to_string()), "\n".to_string()), " continue;".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match line.split_once('\\t') {".to_string()), "\n".to_string()), " Some((key, value)) if !key.is_empty() => {".to_string()), "\n".to_string()), " if facts.insert(key.to_string(), value.to_string()).is_some() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: duplicate host fact row: {key}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " _ => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: malformed host fact line (expected keyvalue): {line}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let malformed_path = host_fact(&facts, \"malformed_control_path\");".to_string()), "\n".to_string()), " let malformed_reason = host_fact(&facts, \"malformed_control_reason\");".to_string()), "\n".to_string()), " let malformed_control = if malformed_path.is_empty() {".to_string()), "\n".to_string()), " NativeRouteMalformedControl::MalformedSpecimenAccepted".to_string()), "\n".to_string()), " } else {".to_string()), "\n".to_string()), " NativeRouteMalformedControl::MalformedSpecimenRefused {".to_string()), "\n".to_string()), " path: malformed_path,".to_string()), "\n".to_string()), " reason: malformed_reason,".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let old_route_disposition = host_fact(&facts, \"old_route_disposition\");".to_string()), "\n".to_string()), " let old_route_executable = host_fact(&facts, \"old_route_executable\");".to_string()), "\n".to_string()), " let old_route_control = match old_route_disposition.as_str() {".to_string()), "\n".to_string()), " \"withdrawn\" => NativeRouteOldRouteControl::OldRouteWithdrawn {".to_string()), "\n".to_string()), " withdrawn_executable: old_route_executable,".to_string()), "\n".to_string()), " },".to_string()), "\n".to_string()), " \"not_present_at_window\" => NativeRouteOldRouteControl::OldRouteNotPresentAtWindow {".to_string()), "\n".to_string()), " expected_executable: old_route_executable,".to_string()), "\n".to_string()), " },".to_string()), "\n".to_string()), " other => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: old_route_disposition names no control this route can record: {other} -- expected withdrawn or not_present_at_window\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " // The emitted build is the host's observation of the cargo spawn this binary is the".to_string()), "\n".to_string()), " // product of: six rows, the argv as one row per word under a length row. A count that".to_string()), "\n".to_string()), " // does not parse, or an index row it promises that is missing, refuses like any other".to_string()), "\n".to_string()), " // missing fact; nothing is defaulted.".to_string()), "\n".to_string()), " let cargo_argv_len = host_fact_int(&facts, \"cargo_argv_len\");".to_string()), "\n".to_string()), " let mut cargo_argv: Vec = Vec::new();".to_string()), "\n".to_string()), " for index in 0..cargo_argv_len {".to_string()), "\n".to_string()), " cargo_argv.push(host_fact(&facts, &format!(\"cargo_argv_{index}\")));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let emitted_build = NativeRouteEmittedBuild {".to_string()), "\n".to_string()), " cargo_argv: Rc::new(cargo_argv.into()),".to_string()), "\n".to_string()), " rustflags: host_fact(&facts, \"rustflags\"),".to_string()), "\n".to_string()), " compiler_path: host_fact(&facts, \"compiler_path\"),".to_string()), "\n".to_string()), " rustc_identity: host_fact(&facts, \"rustc_identity\"),".to_string()), "\n".to_string()), " exit_status: host_fact_int(&facts, \"exit_status\"),".to_string()), "\n".to_string()), " warning_count: host_fact_int(&facts, \"warning_count\"),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " NativeLaneHostFacts {".to_string()), "\n".to_string()), " tested_tree: host_fact(&facts, \"tested_tree\"),".to_string()), "\n".to_string()), " preparation_seed_identity: host_fact(&facts, \"preparation_seed_identity\"),".to_string()), "\n".to_string()), " emitted_closure_identity: host_fact(&facts, \"emitted_closure_identity\"),".to_string()), "\n".to_string()), " executable_path: host_fact(&facts, \"executable_path\"),".to_string()), "\n".to_string()), " executable_identity: host_fact(&facts, \"executable_identity\"),".to_string()), "\n".to_string()), " old_route_control: Rc::new(old_route_control),".to_string()), "\n".to_string()), " malformed_control: Rc::new(malformed_control),".to_string()), "\n".to_string()), " emitted_build: Rc::new(emitted_build),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn run_census(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " for refusal in context.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // GROUPED BY THE FATAL REASON, BY THE FOLD THAT OWNS THE GROUPING".to_string()), "\n".to_string()), " // (v2.compiler.compile native_census_cause_groups_add); this main only prints the groups.".to_string()), "\n".to_string()), " // Each root carries its closure fan-out (v2.compiler.compile native_census_closure_index);".to_string()), "\n".to_string()), " // this mode does not resolve, so no root claims an observed resolve refusal.".to_string()), "\n".to_string()), " let closure = native_census_closure_index(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " native_lane_file_refusal_index(context.file_refusals.clone()),".to_string()), "\n".to_string()), " native_census_modules(ingest.clone()),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure, false);".to_string()), "\n".to_string()), " let cause_groups = native_census_cause_group_rows(groups.clone());".to_string()), "\n".to_string()), " for group in cause_groups.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"cause_group\": group }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let roots = native_census_roots_ranked(cause_groups.clone());".to_string()), "\n".to_string()), " for root in roots.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_root\": root }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"_terminal\": \"complete\", \"mode\": \"census\", \"file_refusals\": context.file_refusals.len(), \"advised_files\": context.accepted_file_advisories.len(), \"cause_groups\": cause_groups.len(), \"roots\": roots.len() }));".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// THE THIRD MODE: THE WHOLE TREE, RESOLVED PER MODULE.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// WHY IT IS NOT A FLAG ON `census`. That mode has a live consumer -- the malformed control,".to_string()), "\n".to_string()), "// which the host runner spawns over a ONE-FILE scratch root and reads one per-file refusal".to_string()), "\n".to_string()), "// from -- and teaching it to resolve would make that control pay a whole-tree resolve to".to_string()), "\n".to_string()), "// learn a fact the front end already answered. Two modes because they are two subjects: a".to_string()), "\n".to_string()), "// roster of front-end refusals, and a roster of resolve refusals over every ingested module.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// WHY IT IS NOT THE `adjudicate` LOOP WIDENED. That loop walks `universe.modules` -- the".to_string()), "\n".to_string()), "// v2.test.* modules discovery enrolled rows for -- over a context folded from the import".to_string()), "\n".to_string()), "// CLOSURE of those modules. Both narrowings are correct for what adjudication asks and wrong".to_string()), "\n".to_string()), "// for a census: a residual reported over the closure of the test corpus is not a residual over".to_string()), "\n".to_string()), "// the tree, and the absence of a refusal in a population that was never resolved establishes".to_string()), "\n".to_string()), "// nothing about the dependents outside it.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// THE RESOLVED TREE IS NOT KEPT. NativeCensusModuleOutcome carries the diagnostics of a refusal".to_string()), "\n".to_string()), "// and nothing on the accepted arm, so each module's tree dies with the call that made it. The".to_string()), "\n".to_string()), "// adjudicating loop drops its tree explicitly because it must hold one to infer from; this loop".to_string()), "\n".to_string()), "// never holds one, which is the property that makes the whole-tree grain affordable at all.".to_string()), "\n".to_string()), "fn run_census_resolve(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let load_started = Instant::now();".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let corpus_reads = reads.len();".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let load_nanos = span_nanos(load_started);".to_string()), "\n".to_string()), " let context_started = Instant::now();".to_string()), "\n".to_string()), " // MUTABLE BECAUSE IT IS THREADED: each module's resolution returns the context it ended".to_string()), "\n".to_string()), " // with, carrying the module namespace provider (v2.compiler.name_resolve ResolutionContext".to_string()), "\n".to_string()), " // namespaces), and the next module is resolved over that one -- a namespace is produced at".to_string()), "\n".to_string()), " // most once per ingest only for a loop that binds what it gets back.".to_string()), "\n".to_string()), " let mut context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let context_nanos = span_nanos(context_started);".to_string()), "\n".to_string()), " // The front-end refusals are reported in the same shape `census` reports them, because they".to_string()), "\n".to_string()), " // are the same fact read from the same context: a module whose file the context refused is".to_string()), "\n".to_string()), " // attributed there and is NOT re-reported below as a resolve refusal it never reached.".to_string()), "\n".to_string()), " for refusal in context.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE ADVISORY POPULATION IS THE SAME CONTEXT FACT `census` REPORTS, printed in the same".to_string()), "\n".to_string()), " // shape, so a saved whole-tree receipt carries it without a second pass. Its count is taken".to_string()), "\n".to_string()), " // here, from the context that produced the rows, not from the threaded one at the end.".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let advised_files = context.accepted_file_advisories.len();".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " let roster_started = Instant::now();".to_string()), "\n".to_string()), " let modules = native_census_modules(ingest.clone());".to_string()), "\n".to_string()), " let roster_nanos = span_nanos(roster_started);".to_string()), "\n".to_string()), " // THE CAUSE GROUPS START FROM THE FRONT-END REFUSALS -- each a root carrying its closure".to_string()), "\n".to_string()), " // fan-out -- and take each module as it is resolved: a cascade member once under its root,".to_string()), "\n".to_string()), " // any other refusal per chain (v2.compiler.compile native_census_cause_groups_add_module).".to_string()), "\n".to_string()), " let closure = native_census_closure_index(context.clone(), refusal_index.clone(), modules.clone());".to_string()), "\n".to_string()), " let mut groups = native_census_cause_groups_from_file_refusals(context.file_refusals.clone(), closure, true);".to_string()), "\n".to_string()), " let mut resolve_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut resolve_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " let mut resolved_ok: u64 = 0;".to_string()), "\n".to_string()), " let mut file_refused: u64 = 0;".to_string()), "\n".to_string()), " let mut resolve_refused: u64 = 0;".to_string()), "\n".to_string()), " let mut residual_rows: u64 = 0;".to_string()), "\n".to_string()), " // THE PER-MODULE LINE IS ALSO THE RUN'S PROGRESS: `index` of `of`, so a reader of a multi-hour".to_string()), "\n".to_string()), " // census can tell a slow run from a hung one while it runs.".to_string()), "\n".to_string()), " let census_modules_total = modules.len();".to_string()), "\n".to_string()), " for (census_module_index, entry) in modules.iter().enumerate() {".to_string()), "\n".to_string()), " let resolve_started = Instant::now();".to_string()), "\n".to_string()), " let step = native_census_module_resolution(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " refusal_index.clone(),".to_string()), "\n".to_string()), " entry.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " context = step.context.clone();".to_string()), "\n".to_string()), " let outcome = step.outcome.clone();".to_string()), "\n".to_string()), " let module_resolve_nanos = span_nanos(resolve_started);".to_string()), "\n".to_string()), " groups = native_census_cause_groups_add_module(groups.clone(), entry.clone(), step.clone());".to_string()), "\n".to_string()), " let outcome_label = match &*outcome {".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleFileRefused => {".to_string()), "\n".to_string()), " file_refused += 1;".to_string()), "\n".to_string()), " \"file_refused\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleResolved => {".to_string()), "\n".to_string()), " resolved_ok += 1;".to_string()), "\n".to_string()), " \"resolved\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeCensusModuleOutcome::NativeCensusModuleResolveRefused { first, rest, .. } => {".to_string()), "\n".to_string()), " resolve_refused += 1;".to_string()), "\n".to_string()), " // ONE ROW PER FAILURE CHAIN, WHICH IS ONE ROW PER REFUSED OCCURRENCE. The rows are".to_string()), "\n".to_string()), " // written as they are decided rather than accumulated: the census is over the".to_string()), "\n".to_string()), " // whole tree, and a population held to the end is a second corpus-sized".to_string()), "\n".to_string()), " // allocation beside the context that produced it.".to_string()), "\n".to_string()), " for row in native_census_residual_rows(entry.clone(), first.clone(), rest.clone()).iter() {".to_string()), "\n".to_string()), " residual_rows += 1;".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_residual\": row }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " \"resolve_refused\"".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " resolve_total_nanos += module_resolve_nanos;".to_string()), "\n".to_string()), " resolve_samples.push(module_resolve_nanos);".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-census-resolve] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"module\": entry.module.clone(),".to_string()), "\n".to_string()), " \"path\": entry.path.clone(),".to_string()), "\n".to_string()), " \"resolve_nanos\": module_resolve_nanos,".to_string()), "\n".to_string()), " \"outcome\": outcome_label,".to_string()), "\n".to_string()), " \"occurrence_completeness\": native_census_module_completeness(outcome.clone()),".to_string()), "\n".to_string()), " \"index\": census_module_index + 1,".to_string()), "\n".to_string()), " \"of\": census_modules_total,".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " resolve_samples.sort_unstable();".to_string()), "\n".to_string()), " let cause_groups = native_census_cause_group_rows(groups.clone());".to_string()), "\n".to_string()), " for group in cause_groups.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"cause_group\": group }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE ROOTS, RANKED BY CLOSURE FAN-OUT (v2.compiler.compile native_census_roots_ranked).".to_string()), "\n".to_string()), " let roots = native_census_roots_ranked(cause_groups.clone());".to_string()), "\n".to_string()), " for root in roots.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"census_root\": root }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE TERMINAL LINE CARRIES THE COST AND THE POPULATION TOGETHER, because the reading this".to_string()), "\n".to_string()), " // mode exists to support is whether a whole-tree resolve is affordable at all, and a residual".to_string()), "\n".to_string()), " // count without the wall and the peak beside it cannot answer that.".to_string()), "\n".to_string()), " println!(".to_string()), "\n".to_string()), " \"{}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"_terminal\": \"complete\",".to_string()), "\n".to_string()), " \"mode\": \"census-resolve\",".to_string()), "\n".to_string()), " \"corpus_reads\": corpus_reads,".to_string()), "\n".to_string()), " \"modules\": modules.len(),".to_string()), "\n".to_string()), " \"file_refusals\": context.file_refusals.len(),".to_string()), "\n".to_string()), " \"advised_files\": advised_files,".to_string()), "\n".to_string()), " \"resolved\": resolved_ok,".to_string()), "\n".to_string()), " \"file_refused\": file_refused,".to_string()), "\n".to_string()), " \"resolve_refused\": resolve_refused,".to_string()), "\n".to_string()), " \"residual_rows\": residual_rows,".to_string()), "\n".to_string()), " \"cause_groups\": cause_groups.len(),".to_string()), "\n".to_string()), " \"roots\": roots.len(),".to_string()), "\n".to_string()), " \"load_nanos\": load_nanos,".to_string()), "\n".to_string()), " \"context_nanos\": context_nanos,".to_string()), "\n".to_string()), " \"roster_nanos\": roster_nanos,".to_string()), "\n".to_string()), " \"resolve_total_nanos\": resolve_total_nanos,".to_string()), "\n".to_string()), " \"resolve_p50_nanos\": native_cost_quantile_nanos(&resolve_samples, 1, 2),".to_string()), "\n".to_string()), " \"resolve_p95_nanos\": native_cost_quantile_nanos(&resolve_samples, 19, 20),".to_string()), "\n".to_string()), " \"resolve_max_nanos\": resolve_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "// THE FOURTH MODE: D13's DEPENDENCY-DEMAND CENSUS (v2.compiler.compile native_demand_census_output).".to_string()), "\n".to_string()), "// The fold decides every line and the exit -- 0 held, 1 a census whose rows do not account for".to_string()), "\n".to_string()), "// every uses fn, 2 no observation -- and this function only reads the ingest, prints and exits.".to_string()), "\n".to_string()), "fn run_demand_census(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let output = native_demand_census_output(ingest);".to_string()), "\n".to_string()), " for line in output.lines.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " std::process::exit(output.exit as i32);".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// THE FIFTH MODE: THE WHOLE TREE, RESOLVED AND INFERRED PER MODULE (`census-infer`).".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// Every decision is a .dag fold: the per-module step (v2.compiler.compile".to_string()), "\n".to_string()), "// native_census_module_inference), the type-declaration census's contribution from it".to_string()), "\n".to_string()), "// (native_census_infer_type_contribution), the run it accumulates into and the receipt and".to_string()), "\n".to_string()), "// terminal that run decides (gunbc.instruments.type_declaration_use_census). This loop reads the".to_string()), "\n".to_string()), "// roots, threads the context, prints and sets the status -- the operations no fold can. A module's".to_string()), "\n".to_string()), "// trees live for one step; what is kept is the census's rows, not the trees.".to_string()), "\n".to_string()), "fn run_census_infer(source_roots: &[String]) {".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let mut context = match &*native_test_context_from_ingest(ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " let modules = native_census_modules(ingest.clone());".to_string()), "\n".to_string()), " let mut run = type_census_run_empty();".to_string()), "\n".to_string()), " for entry in modules.iter() {".to_string()), "\n".to_string()), " let started = Instant::now();".to_string()), "\n".to_string()), " let step = native_census_module_inference(context.clone(), refusal_index.clone(), entry.clone());".to_string()), "\n".to_string()), " context = step.context.clone();".to_string()), "\n".to_string()), " let contribution = native_census_infer_type_contribution(context.clone(), entry.clone(), step.outcome.clone());".to_string()), "\n".to_string()), " run = type_census_run_add(run, contribution);".to_string()), "\n".to_string()), " eprintln!(\"[native-census-infer] {}\", serde_json::json!({ \"module\": entry.module.clone(), \"nanos\": span_nanos(started) }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let receipt = type_census_receipt(run);".to_string()), "\n".to_string()), " for line in receipt.lines.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " match &*receipt.terminal {".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimHeld => {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"_terminal\": \"complete\", \"mode\": \"census-infer\", \"modules\": modules.len(), \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024) }));".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNotHeld { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NOT HELD: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeClaimTerminal::NativeClaimNoObservation { reason } => {".to_string()), "\n".to_string()), " eprintln!(\"NO OBSERVATION: {reason}\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), "fn native_cost_quantile_nanos(sorted: &[u128], numer: usize, denom: usize) -> u128 {".to_string()), "\n".to_string()), " if sorted.is_empty() { return 0; }".to_string()), "\n".to_string()), " let idx = (sorted.len() - 1) * numer / denom;".to_string()), "\n".to_string()), " sorted[idx]".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// A SPAN IS READ ONCE, THROUGH A READER THAT REFUSES A CLOCK THAT WENT BACKWARDS.".to_string()), "\n".to_string()), "// Instant::elapsed is Instant::now().duration_since(earlier), and duration_since SATURATES:".to_string()), "\n".to_string()), "// a monotonic clock that is not actually monotonic -- which happens on some virtualised and".to_string()), "\n".to_string()), "// migrated hosts -- yields Duration::ZERO rather than an error. That is the fabricated zero".to_string()), "\n".to_string()), "// this receipt forbids, arriving by a quieter route than the saturating_sub it replaced: a".to_string()), "\n".to_string()), "// phase would report as having taken no time and the partition would balance for the wrong".to_string()), "\n".to_string()), "// reason. checked_duration_since returns None instead, so the impossible reading is".to_string()), "\n".to_string()), "// REPRESENTABLE and can be refused.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// AN EARLIER REVISION OF THIS FILE CLAIMED THE CLASS WAS STRUCTURALLY IMPOSSIBLE ONCE THE".to_string()), "\n".to_string()), "// subtraction was gone. That was wrong and is corrected rather than softened: deleting the".to_string()), "\n".to_string()), "// explicit subtraction removed the visible clamp, not the saturation underneath it. The".to_string()), "\n".to_string()), "// honest rung is MITIGATED BY A TYPED REFUSAL at a declared boundary -- the host clock is".to_string()), "\n".to_string()), "// external reality, which DESIGN section 4b keeps off the ladder rather than on its top rung.".to_string()), "\n".to_string()), "fn span_nanos(started: Instant) -> u128 {".to_string()), "\n".to_string()), " match Instant::now().checked_duration_since(started) {".to_string()), "\n".to_string()), " Some(d) => d.as_nanos(),".to_string()), "\n".to_string()), " None => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: the monotonic clock went backwards between two reads, so no span is measurable\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// THE PARTITION IS JUDGED ON THREAD CPU, AND THIS IS ITS SPAN (std.realization_measurement".to_string()), "\n".to_string()), "// ObserveThreadCpuAtSubject; extdeps.posix.clock_gettime ClockThreadCputimeId). The law".to_string()), "\n".to_string()), "// native_driver_cost_account enforces is that the phases partition the DRIVER'S OWN WORK. A".to_string()), "\n".to_string()), "// wall span also holds every interval the host descheduled this thread -- another tenant's".to_string()), "\n".to_string()), "// build, page-fault service, a lock wait -- none of which any phase performed, so on a wall".to_string()), "\n".to_string()), "// clock that time fell into the residual and the verdict tracked host load -- the same subject".to_string()), "\n".to_string()), "// reconciled on a quiet runner and refused on a contended one. The thread".to_string()), "\n".to_string()), "// CPU clock advances only while this thread runs, so the residual is uninstrumented driver".to_string()), "\n".to_string()), "// work and nothing else. The wall is still read once, around the whole run, and printed as an".to_string()), "\n".to_string()), "// observation; it is never an input to the verdict. The driver is single-threaded, so the".to_string()), "\n".to_string()), "// thread clock is the driver's whole CPU. An unsupported clock refuses inside the runtime".to_string()), "\n".to_string()), "// read; there is no wall fallback.".to_string()), "\n".to_string()), "#[derive(Clone, Copy)]".to_string()), "\n".to_string()), "struct CpuMark(u128);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn cpu_mark() -> CpuMark {".to_string()), "\n".to_string()), " CpuMark(observed_thread_cpu_nanos(String::new()) as u128)".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn cpu_span_nanos(started: CpuMark) -> u128 {".to_string()), "\n".to_string()), " let finished = cpu_mark();".to_string()), "\n".to_string()), " if finished.0 < started.0 {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: the thread CPU clock went backwards between two reads, so no span is measurable\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " finished.0 - started.0".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// A SPAN THAT DOES NOT FIT REFUSES RATHER THAN SATURATING (review 64474 control). This was".to_string()), "\n".to_string()), "// i64::try_from(n).unwrap_or(i64::MAX), which turns a failed conversion into a FIGURE -- and a".to_string()), "\n".to_string()), "// fabricated figure on a receipt is the state DESIGN section 5 forbids outright, whether the".to_string()), "\n".to_string()), "// invented number is a zero that reports a phase as instant or a maximum that reports it as".to_string()), "\n".to_string()), "// three centuries. The saturating arm also chose the direction that hides best: i64::MAX would".to_string()), "\n".to_string()), "// push the exclusive sum past the parent and surface as NativeDriverCostOverAttributed, a cost".to_string()), "\n".to_string()), "// verdict naming the wrong cause.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// ON REACHABILITY, STATED RATHER THAN LEFT AS AN IMPLIED CLAIM: i64 nanoseconds is about 292".to_string()), "\n".to_string()), "// years, so no span this driver measures reaches it, and the refusal below is not expected to".to_string()), "\n".to_string()), "// fire. It is written because the alternative is not silence but a LIE -- the arm has to answer".to_string()), "\n".to_string()), "// something, and the only honest answers are a real number or a refusal.".to_string()), "\n".to_string()), "fn native_cost_i64(n: u128) -> i64 {".to_string()), "\n".to_string()), " match i64::try_from(n) {".to_string()), "\n".to_string()), " Ok(v) => v,".to_string()), "\n".to_string()), " Err(cause) => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: a measured span does not fit the cost carrier, so no partition can be reported: {n} ({cause})\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// AN UNREADABLE /proc IS ABSENT, NOT ZERO (review 64181). A zero here was printed as".to_string()), "\n".to_string()), "// peak_rss_bytes / rss_bytes and read as a measurement: a run on a host without /proc, or with".to_string()), "\n".to_string()), "// the key renamed, reported a process using no memory. Absence is carried as None and rendered".to_string()), "\n".to_string()), "// as JSON null, so the receipt says it does not know instead of inventing a figure. This is".to_string()), "\n".to_string()), "// telemetry on no receipt field, so it is the one place an Absent is carried rather than".to_string()), "\n".to_string()), "// refused: the run is still a valid observation of everything else.".to_string()), "\n".to_string()), "fn proc_status_kib(key: &str) -> Option {".to_string()), "\n".to_string()), " let text = std::fs::read_to_string(\"/proc/self/status\").ok()?;".to_string()), "\n".to_string()), " for line in text.lines() {".to_string()), "\n".to_string()), " if let Some(rest) = line.strip_prefix(key) {".to_string()), "\n".to_string()), " return rest".to_string()), "\n".to_string()), " .trim_start_matches(':')".to_string()), "\n".to_string()), " .trim()".to_string()), "\n".to_string()), " .split_whitespace()".to_string()), "\n".to_string()), " .next()".to_string()), "\n".to_string()), " .and_then(|n| n.parse::().ok());".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " None".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "// ONE CLOCK FOR THE VERDICT, AND IT IS THE THREAD CPU ONE (see cpu_span_nanos). Every exclusive".to_string()), "\n".to_string()), "// phase span and the parent span are read from it, so the partition compares readings from a".to_string()), "\n".to_string()), "// single source; the wall Instant around the run is an observation only. The history below is".to_string()), "\n".to_string()), "// why one source matters (review 64474), and it applied to the monotonic clock first.".to_string()), "\n".to_string()), "// They were split before this: the parent came from Instant while the phase spans came from".to_string()), "\n".to_string()), "// SystemTime, which is the wall clock and is free to step backwards under NTP or an operator".to_string()), "\n".to_string()), "// adjustment. A step between two phase reads could then push the exclusive sum past a parent".to_string()), "\n".to_string()), "// span that never moved, and the driver would refuse with NativeDriverCostOverAttributed --".to_string()), "\n".to_string()), "// a cost refusal whose real cause was the clock. That is a fabricated attribution: the numbers".to_string()), "\n".to_string()), "// would be wrong while the receipt read as a measurement.".to_string()), "\n".to_string()), "//".to_string()), "\n".to_string()), "// THE SUBTRACTION IS GONE RATHER THAN GUARDED, which is the point. A span used to be an".to_string()), "\n".to_string()), "// explicit saturating_sub of two readings, so a backwards step clamped the difference to zero".to_string()), "\n".to_string()), "// and reported a phase as having taken NO TIME -- the exact state the annotation beside it".to_string()), "\n".to_string()), "// forbade, reached by the arm meant to prevent it. Instant::elapsed cannot return a negative".to_string()), "\n".to_string()), "// duration, so there is no difference to clamp and no clamp to review: DESIGN section 5".to_string()), "\n".to_string()), "// construction over validation, and the class climbs from mitigated to structurally".to_string()), "\n".to_string()), "// impossible. The pre-epoch refusal that guarded the old wall-clock read is DELETED with it,".to_string()), "\n".to_string()), "// because the hazard it named cannot arise on this clock -- production handling dissolves on".to_string()), "\n".to_string()), "// the climb (DESIGN section 4b(4)); no evidence is retired here, since that arm had none.".to_string()), "\n".to_string()), "fn run_adjudication(facts_path: &str, pattern: Rc, source_roots: &[String]) {".to_string()), "\n".to_string()), " let driver_start = cpu_mark();".to_string()), "\n".to_string()), " let driver_wall_start = Instant::now();".to_string()), "\n".to_string()), " // A MISSING GITHUB_SHA IS ABSENT, NOT \"local\" (review 64181). The receipt line carries this".to_string()), "\n".to_string()), " // as the tree the measurement describes; the literal \"local\" is a plausible identity that".to_string()), "\n".to_string()), " // no reader can distinguish from a tree actually named local.".to_string()), "\n".to_string()), " let head: Option = std::env::var(\"GITHUB_SHA\").ok();".to_string()), "\n".to_string()), " let facts = read_host_facts(facts_path);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the load row: reading the declared roots off disk. Its own exclusive row rather than a".to_string()), "\n".to_string()), " // field on another phase, so the IO is never mixed into a fold's number.".to_string()), "\n".to_string()), " let load_started = cpu_mark();".to_string()), "\n".to_string()), " if source_roots.is_empty() {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: no source roots given\");".to_string()), "\n".to_string()), " std::process::exit(2);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let reads = read_ingest(source_roots);".to_string()), "\n".to_string()), " let corpus_reads = reads.len();".to_string()), "\n".to_string()), " let ingest = Rc::new(im::vector::Vector::from(reads));".to_string()), "\n".to_string()), " let load_nanos = cpu_span_nanos(load_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // universe_derivation: the floor's own per-file discovery fold plus the module-header and".to_string()), "\n".to_string()), " // import scans, over EVERY read -- discovery is a corpus-wide question -- and then the".to_string()), "\n".to_string()), " // import closure of the universe's modules, which is all the front end is given.".to_string()), "\n".to_string()), " let universe_started = cpu_mark();".to_string()), "\n".to_string()), " let source_facts = native_lane_source_facts(ingest.clone());".to_string()), "\n".to_string()), " // The universe is derived FROM THE FACTS ALREADY BOUND, not from the ingest again:".to_string()), "\n".to_string()), " // the ingest-taking entry would re-run the whole-corpus discovery fold the line above".to_string()), "\n".to_string()), " // just paid for, and that fold is the phase the pattern does NOT narrow. See".to_string()), "\n".to_string()), " // v2.compiler.compile native_lane_universe_of_facts -- the pattern is threaded, so this".to_string()), "\n".to_string()), " // is the same entry a narrower pattern reaches, never a filter over a default universe.".to_string()), "\n".to_string()), " let universe = match &*native_lane_universe_of_facts(source_facts.clone(), pattern.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"DERIVATION-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " // The walk's budget exhausting is a refusal of the whole derivation, never a truncated".to_string()), "\n".to_string()), " // closure: a narrowed ingest would surface only as unexplained resolve refusals later.".to_string()), "\n".to_string()), " let closure = match &*native_lane_closure_modules(".to_string()), "\n".to_string()), " source_facts.clone(),".to_string()), "\n".to_string()), " universe.clone(),".to_string()), "\n".to_string()), " native_lane_closure_round_budget(),".to_string()), "\n".to_string()), " ) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CLOSURE-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let closure_ingest = native_lane_closure_ingest(ingest.clone(), source_facts.clone(), closure.clone());".to_string()), "\n".to_string()), " let closure_reads = closure_ingest.len();".to_string()), "\n".to_string()), " // The ingest receipt's identity join, asked by the route: every ingested source declares a".to_string()), "\n".to_string()), " // closure module and every closure module some source declares was ingested. A miss is the".to_string()), "\n".to_string()), " // hidden prepass (or a hidden narrowing) this receipt exists to refuse.".to_string()), "\n".to_string()), " let ingest_receipt = native_lane_ingest_receipt(source_facts.clone(), closure.clone(), closure_ingest.clone());".to_string()), "\n".to_string()), " // The join answers a typed refusal naming the offending identity, so the counts below are".to_string()), "\n".to_string()), " // context for it rather than the whole diagnostic: a reader gets the module and the path,".to_string()), "\n".to_string()), " // not a pair of totals to re-derive the offender from.".to_string()), "\n".to_string()), " match &*native_lane_ingest_matches_closure(source_facts.clone(), ingest_receipt.clone(), closure_ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { .. } => {}".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"INGEST-CLOSURE-REFUSED: {:#?} (scanned={} ingested={} closure_modules={})\",".to_string()), "\n".to_string()), " diagnostics,".to_string()), "\n".to_string()), " ingest_receipt.scanned_paths.len(),".to_string()), "\n".to_string()), " ingest_receipt.ingested_paths.len(),".to_string()), "\n".to_string()), " ingest_receipt.closure_modules.len(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let universe_nanos = cpu_span_nanos(universe_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the context row: the front-end fold over the CLOSURE only, entered ONE PHASE AT A TIME.".to_string()), "\n".to_string()), " // The driver owns the clock, so it is the only place a phase span can be read; the phases".to_string()), "\n".to_string()), " // themselves are v2.compiler.program_assembly declarations and each binds the previous".to_string()), "\n".to_string()), " // phase's Outcome, so each phase BODY is single-authored and only the ORDER and the".to_string()), "\n".to_string()), " // inter-phase diagnostic merge exist twice -- here and in the modeled composition".to_string()), "\n".to_string()), " // program_assembly_read_to_normalized_root_seeded. (It named the UNSEEDED composition".to_string()), "\n".to_string()), " // until that one was deleted for having no caller; the seeded producer is now the single".to_string()), "\n".to_string()), " // modeled per-file front end, and this loop is still its second spelling.)".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // THAT SECOND SPELLING IS ADMITTED DEBT AND IS FILED, NOT ASSERTED AWAY (review 65068):".to_string()), "\n".to_string()), " // gunbc.recurring_failure_mode.realization_respells_a_modeled_folds_sequencing_to_instrument_it.".to_string()), "\n".to_string()), " // An earlier revision of this comment claimed the two are the same computation. Nothing".to_string()), "\n".to_string()), " // executes that claim, and a .dag witness comparing the composition against its own phases".to_string()), "\n".to_string()), " // CANNOT go red -- the composition is defined as the phase chain -- so the honest answer is".to_string()), "\n".to_string()), " // the row and its trigger (a realization seam that yields per-phase spans FROM a modeled".to_string()), "\n".to_string()), " // fold), not a green check. Reordering these calls or dropping the merge that".to_string()), "\n".to_string()), " // program_assembly_phase_parse_seeded performs would change the context with".to_string()), "\n".to_string()), " // nothing red.".to_string()), "\n".to_string()), " let context_started = cpu_mark();".to_string()), "\n".to_string()), " // THE ONCE-PER-FOLD INVARIANT IS PAID ONCE, HERE, AND CARRIED INTO EVERY FILE. A second".to_string()), "\n".to_string()), " // dag_language_model() or prepare_grammar() call inside the loop would re-introduce at the".to_string()), "\n".to_string()), " // realization layer exactly the per-file recompute the fold hoisted out.".to_string()), "\n".to_string()), " let front_end = match &*native_test_front_end_prepare() {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let front_end_nanos = cpu_span_nanos(context_started);".to_string()), "\n".to_string()), " // THE INGEST'S OCCURRENCE SCOPE, computed once: every projected occurrence the loop's".to_string()), "\n".to_string()), " // normalize allocates states its cause in it (v2.compiler.program_assembly".to_string()), "\n".to_string()), " // program_assembly_ingest_scope).".to_string()), "\n".to_string()), " let ingest_scope = match &*program_assembly_ingest_scope_outcome(closure_ingest.clone()) {".to_string()), "\n".to_string()), " Outcome::Accepted { value, .. } => value.clone(),".to_string()), "\n".to_string()), " Outcome::Rejected { diagnostics } => {".to_string()), "\n".to_string()), " eprintln!(\"CONTEXT-REFUSED: {diagnostics:#?}\");".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let mut context_state = native_test_context_state_empty();".to_string()), "\n".to_string()), " let mut tokenize_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut parse_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut normalize_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut absorb_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut phase_calls: u64 = 0;".to_string()), "\n".to_string()), " let mut token_total: u64 = 0;".to_string()), "\n".to_string()), " let mut byte_total: u64 = 0;".to_string()), "\n".to_string()), " let mut context_file_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " for read in closure_ingest.iter() {".to_string()), "\n".to_string()), " let file_started = cpu_mark();".to_string()), "\n".to_string()), " let tokenize_started = cpu_mark();".to_string()), "\n".to_string()), " let tokens = program_assembly_phase_tokenize(read.clone(), front_end.lex.clone());".to_string()), "\n".to_string()), " let file_tokenize_nanos = cpu_span_nanos(tokenize_started);".to_string()), "\n".to_string()), " // The COUNT the time is read against: a span alone cannot separate a large file from a".to_string()), "\n".to_string()), " // per-file constant. Counted outside every phase span so it prices none of them.".to_string()), "\n".to_string()), " let file_tokens = program_assembly_phase_token_count(tokens.clone());".to_string()), "\n".to_string()), " let parse_started = cpu_mark();".to_string()), "\n".to_string()), " // SEEDED FROM THE MARK THE CONTEXT HAS REACHED, because this loop IS a source graph:".to_string()), "\n".to_string()), " // std.occurrence_identity occurrence_identity_scope_law requires one allocator threaded".to_string()), "\n".to_string()), " // across every parsed source, and a per-file reset makes two files' occurrence #N the".to_string()), "\n".to_string()), " // same value with different meanings. The modeled fold threads it internally; this".to_string()), "\n".to_string()), " // respelling has to thread it explicitly or it keeps the reset the fold no longer has.".to_string()), "\n".to_string()), " let file_seed = context_state.alloc.clone();".to_string()), "\n".to_string()), " let file_seed_reported = file_seed.clone();".to_string()), "\n".to_string()), " let parsed = program_assembly_phase_parse_seeded(".to_string()), "\n".to_string()), " tokens.clone(),".to_string()), "\n".to_string()), " front_end.prepared.clone(),".to_string()), "\n".to_string()), " // The residue field is an Optional whose inner type rides a recursion cycle, so the".to_string()), "\n".to_string()), " // emitted record BOXES it while the phase takes the option by value. Deref the box".to_string()), "\n".to_string()), " // rather than clone it; the sibling fields above are Rc and clone correctly.".to_string()), "\n".to_string()), " (*front_end.residue).clone(),".to_string()), "\n".to_string()), " file_seed.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let file_parse_nanos = cpu_span_nanos(parse_started);".to_string()), "\n".to_string()), " // On a refused parse".to_string()), "\n".to_string()), " // this returns the seed unchanged. Not because nothing was minted -- a refused parse".to_string()), "\n".to_string()), " // HAS minted ids -- but because ParseExprRejected carries no provenance state, so the".to_string()), "\n".to_string()), " // mark the attempt reached is not recoverable. See the precondition on".to_string()), "\n".to_string()), " // v2.compiler.compile native_test_context_absorb.".to_string()), "\n".to_string()), " let file_alloc = program_assembly_parsed_alloc(parsed.clone(), file_seed);".to_string()), "\n".to_string()), " let normalize_started = cpu_mark();".to_string()), "\n".to_string()), " // NORMALIZE ADVANCES THE ALLOCATOR: lowering allocates every image member an id, so the".to_string()), "\n".to_string()), " // next file seeds from the mark normalize reached, not the one parse reached.".to_string()), "\n".to_string()), " let normalized = program_assembly_phase_normalize_seeded(parsed.clone(), file_alloc.clone(), ingest_scope.clone());".to_string()), "\n".to_string()), " let root = program_assembly_normalized_outcome(normalized.clone());".to_string()), "\n".to_string()), " let file_spans = program_assembly_normalized_spans(normalized.clone());".to_string()), "\n".to_string()), " let file_alloc = program_assembly_normalized_alloc(normalized);".to_string()), "\n".to_string()), " let file_alloc_reported = file_alloc.clone();".to_string()), "\n".to_string()), " let file_normalize_nanos = cpu_span_nanos(normalize_started);".to_string()), "\n".to_string()), " // qualified-name read plus source-root index insertion: the half of the per-file step".to_string()), "\n".to_string()), " // that is not the front end, timed separately because a quadratic accumulator would".to_string()), "\n".to_string()), " // live here and nowhere else in this loop.".to_string()), "\n".to_string()), " let absorb_started = cpu_mark();".to_string()), "\n".to_string()), " context_state = native_test_context_absorb(context_state.clone(), read.clone(), root.clone(), file_alloc, file_spans);".to_string()), "\n".to_string()), " let file_absorb_nanos = cpu_span_nanos(absorb_started);".to_string()), "\n".to_string()), " let file_nanos = cpu_span_nanos(file_started);".to_string()), "\n".to_string()), " tokenize_nanos += file_tokenize_nanos;".to_string()), "\n".to_string()), " parse_nanos += file_parse_nanos;".to_string()), "\n".to_string()), " normalize_nanos += file_normalize_nanos;".to_string()), "\n".to_string()), " absorb_nanos += file_absorb_nanos;".to_string()), "\n".to_string()), " phase_calls += 1;".to_string()), "\n".to_string()), " token_total += file_tokens.max(0) as u64;".to_string()), "\n".to_string()), " let file_bytes = read.source.carried.len() as u64;".to_string()), "\n".to_string()), " byte_total += file_bytes;".to_string()), "\n".to_string()), " context_file_samples.push(file_nanos);".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-context-split] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"path\": read.compilation_unit.clone(),".to_string()), "\n".to_string()), " \"bytes\": file_bytes,".to_string()), "\n".to_string()), " \"tokens\": file_tokens,".to_string()), "\n".to_string()), " \"tokenize_nanos\": file_tokenize_nanos,".to_string()), "\n".to_string()), " \"parse_nanos\": file_parse_nanos,".to_string()), "\n".to_string()), " \"normalize_nanos\": file_normalize_nanos,".to_string()), "\n".to_string()), " \"absorb_nanos\": file_absorb_nanos,".to_string()), "\n".to_string()), " \"file_nanos\": file_nanos,".to_string()), "\n".to_string()), " // THE THREADING, MADE OBSERVABLE. occurrence_seed is the mark this file was".to_string()), "\n".to_string()), " // handed and occurrence_next the mark it reached, so a multi-file run shows".to_string()), "\n".to_string()), " // one sequence continuing across files instead of each file restarting at 0.".to_string()), "\n".to_string()), " // A REFUSED file reports next == seed. That is the mark it was HANDED, not a".to_string()), "\n".to_string()), " // claim that nothing was minted: a refused parse mints ids that the refusal".to_string()), "\n".to_string()), " // discards unrecoverably. The next file continues from the seed rather than".to_string()), "\n".to_string()), " // rewinding to zero.".to_string()), "\n".to_string()), " \"occurrence_seed\": program_assembly_allocator_next(file_seed_reported),".to_string()), "\n".to_string()), " \"occurrence_next\": program_assembly_allocator_next(file_alloc_reported),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let context = native_test_context_finish(context_state, front_end.clone(), closure_ingest.clone());".to_string()), "\n".to_string()), " let context_nanos = cpu_span_nanos(context_started);".to_string()), "\n".to_string()), " context_file_samples.sort_unstable();".to_string()), "\n".to_string()), " let context_phase_sum = front_end_nanos + tokenize_nanos + parse_nanos + normalize_nanos + absorb_nanos;".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-context-partition] {}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"basis\": \"context_span\",".to_string()), "\n".to_string()), " \"basis_note\": \"exclusive phase rows partition context_nanos; residual is loop overhead and the eprintln per file\",".to_string()), "\n".to_string()), " \"context_nanos\": context_nanos,".to_string()), "\n".to_string()), " \"front_end_prepare_nanos\": front_end_nanos,".to_string()), "\n".to_string()), " \"tokenize_nanos\": tokenize_nanos,".to_string()), "\n".to_string()), " \"parse_nanos\": parse_nanos,".to_string()), "\n".to_string()), " \"normalize_nanos\": normalize_nanos,".to_string()), "\n".to_string()), " \"absorb_nanos\": absorb_nanos,".to_string()), "\n".to_string()), " \"sum_phase_nanos\": context_phase_sum,".to_string()), "\n".to_string()), " \"residual_nanos\": context_nanos.saturating_sub(context_phase_sum),".to_string()), "\n".to_string()), " \"files\": phase_calls,".to_string()), "\n".to_string()), " \"tokenize_calls\": phase_calls,".to_string()), "\n".to_string()), " \"parse_calls\": phase_calls,".to_string()), "\n".to_string()), " \"normalize_calls\": phase_calls,".to_string()), "\n".to_string()), " \"absorb_calls\": phase_calls,".to_string()), "\n".to_string()), " \"front_end_prepare_calls\": 1,".to_string()), "\n".to_string()), " \"tokens\": token_total,".to_string()), "\n".to_string()), " \"bytes\": byte_total,".to_string()), "\n".to_string()), " \"file_refusals\": context.file_refusals.len(),".to_string()), "\n".to_string()), " \"file_p50_nanos\": native_cost_quantile_nanos(&context_file_samples, 1, 2),".to_string()), "\n".to_string()), " \"file_p95_nanos\": native_cost_quantile_nanos(&context_file_samples, 19, 20),".to_string()), "\n".to_string()), " \"file_max_nanos\": context_file_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " }),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // the prepare and eval rows, entered separately per module so the number the profile".to_string()), "\n".to_string()), " // lane prices -- the eval row -- is the sum of native_lane_identity_row".to_string()), "\n".to_string()), " // and nothing else: not ingest, not resolve, not infer.".to_string()), "\n".to_string()), " let mut population: Vec> = Vec::new();".to_string()), "\n".to_string()), " let mut prepare_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut evaluate_total_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut prepare_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " let mut eval_samples: Vec = Vec::new();".to_string()), "\n".to_string()), " // ExclusiveModuleRelease IS NOW STRUCTURALLY ZERO, AND THAT IS A REAL CHANGE RATHER".to_string()), "\n".to_string()), " // THAN A TIDY-UP. The displaced loop dropped each module's resolved tree inside a span,".to_string()), "\n".to_string()), " // because dropping at iteration end paid time no exclusive row covered. The engine holds".to_string()), "\n".to_string()), " // every settled demand's value in its entries for the whole run, so there is no per-module".to_string()), "\n".to_string()), " // release point to time -- resolved trees now accumulate where they used to be released.".to_string()), "\n".to_string()), " // The row stays in the partition reporting zero rather than being deleted: a key removed".to_string()), "\n".to_string()), " // here vanishes from the sum and the serialized line at once, silently, and the residual".to_string()), "\n".to_string()), " // it used to carry would be absorbed into the tolerance rather than reported. Retention is".to_string()), "\n".to_string()), " // std.materialization_ladder's question and the cut that answers it owns this row.".to_string()), "\n".to_string()), " let module_release_nanos: u128 = 0;".to_string()), "\n".to_string()), " let mut relay_emit_nanos: u128 = 0;".to_string()), "\n".to_string()), " // The per-path refusal index is built ONCE here rather than per module: this loop is the".to_string()), "\n".to_string()), " // only place that knows a loop is about to happen, and the join it feeds was O(modules x".to_string()), "\n".to_string()), " // refusals) when each module scanned the refusal list for itself.".to_string()), "\n".to_string()), " let refusal_index = native_lane_file_refusal_index(context.file_refusals.clone());".to_string()), "\n".to_string()), " // ═══════════════════════════════════════════════════════════════════════════════════════".to_string()), "\n".to_string()), " // THE SCHEDULE IS THE ENGINE'S. THIS LOOP HOLDS THE CLOCK AND NOTHING ELSE.".to_string()), "\n".to_string()), " // ═══════════════════════════════════════════════════════════════════════════════════════".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // What stood here was `for entry in universe.modules` with `for declaration in".to_string()), "\n".to_string()), " // entry.declarations` nested inside it -- a dependency structure written as control flow, and".to_string()), "\n".to_string()), " // the last place in the native route that decided WHAT RUNS NEXT. It is deleted. The order now".to_string()), "\n".to_string()), " // comes from v2.std.demand_engine's one ready queue, over a graph".to_string()), "\n".to_string()), " // v2.compiler.native_demand_schedule DERIVES from the same universe roster this loop walked.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // WHY A LOOP REMAINS AT ALL, and why it is not the old one wearing a new name: a .dag fold".to_string()), "\n".to_string()), " // cannot read a clock, and the native cost partition is an EXHAUSTIVE exclusive sum over".to_string()), "\n".to_string()), " // parent_span with a 50ms tolerance arm. Replacing the walk with one opaque call would have".to_string()), "\n".to_string()), " // dissolved the prepare and eval rows into an unattributed remainder and made that arm fire --".to_string()), "\n".to_string()), " // so the driver asks the engine for the next admissible demand and spans the step. It supplies".to_string()), "\n".to_string()), " // no ordering, no module iteration and no declaration iteration; remove the spans and the body".to_string()), "\n".to_string()), " // is a drain.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // THE SEAT COUNT IS THE CALLER'S. One seat here, which is what makes this cut a pure authority".to_string()), "\n".to_string()), " // move: same demands, same order, same single execution at a time. Multi-seat realization binds".to_string()), "\n".to_string()), " // a different offer to the same drain and changes nothing else.".to_string()), "\n".to_string()), " // THE DRIVER'S OWN INPUT CONSTRUCTION, charged to prepare as it was before the engine landed:".to_string()), "\n".to_string()), " // it runs before any demand and is not the scheduler's bookkeeping, which the engine measures".to_string()), "\n".to_string()), " // for itself.".to_string()), "\n".to_string()), " let tested_tree_started = cpu_mark();".to_string()), "\n".to_string()), " let tested_tree_input = native_demand_tested_tree_input(head.clone().unwrap_or_default());".to_string()), "\n".to_string()), " prepare_total_nanos += cpu_span_nanos(tested_tree_started);".to_string()), "\n".to_string()), " // ONE CALL. The driver does not drain the graph, does not decide readiness and does not".to_string()), "\n".to_string()), " // classify a demand's stage -- those are the engine's and the plan's decisions, and a".to_string()), "\n".to_string()), " // rendered-main loop that made them was a second consumer of the engine's admission".to_string()), "\n".to_string()), " // vocabulary living in generated Rust, where no .dag check reaches.".to_string()), "\n".to_string()), " //".to_string()), "\n".to_string()), " // IT USED TO HOLD A LOOP FOR THE CLOCK, AND THAT REASON WAS WRONG. Each step now takes".to_string()), "\n".to_string()), " // its own span through observed_thread_cpu_nanos -- an ordinary builtin with no resource".to_string()), "\n".to_string()), " // requirement, modelled by std.realization_measurement as ObserveThreadCpuAtSubject's".to_string()), "\n".to_string()), " // realization seam -- so the exclusive prepare and eval rows survive with the spans taken".to_string()), "\n".to_string()), " // where the work is instead of around it.".to_string()), "\n".to_string()), " // THE ENGINE CALL IS ONE MEASURED SPAN (std.compiler_entry ExclusiveDemandScheduling). Its".to_string()), "\n".to_string()), " // demand windows are the prepare and eval rows; everything else the call spends is the".to_string()), "\n".to_string()), " // scheduler's, including the work between the engine's own bookkeeping spans that no row".to_string()), "\n".to_string()), " // timed when this row was the sum of those spans.".to_string()), "\n".to_string()), " let schedule_started = cpu_mark();".to_string()), "\n".to_string()), " let run = native_demand_schedule_universe(".to_string()), "\n".to_string()), " universe.modules.clone(),".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " refusal_index.clone(),".to_string()), "\n".to_string()), " tested_tree_input.clone(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let schedule_span_nanos = cpu_span_nanos(schedule_started);".to_string()), "\n".to_string()), " // driver_collection: the driver's own work after the engine returned -- rows into the".to_string()), "\n".to_string()), " // population, the samples, the counters. Named for what it is, not folded into scheduling.".to_string()), "\n".to_string()), " let collection_started = cpu_mark();".to_string()), "\n".to_string()), " for row in native_demand_run_rows(run.clone()).iter() {".to_string()), "\n".to_string()), " population.push(row.clone());".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let engine_prepare_nanos = native_demand_prepare_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " let engine_eval_nanos = native_demand_eval_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " // OBSERVATION ONLY (v2.compiler.compile native_demand_occurrence_census_lines): the census is its".to_string()), "\n".to_string()), " // own demand kind, so its cost is the engine-reported ExclusiveOccurrenceCensus row, not a host span.".to_string()), "\n".to_string()), " let occurrence_census_nanos = native_demand_occurrence_census_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " prepare_total_nanos += engine_prepare_nanos;".to_string()), "\n".to_string()), " evaluate_total_nanos += engine_eval_nanos;".to_string()), "\n".to_string()), " // The engine's own bookkeeping sum stays an observation on the schedule line; it is not the row.".to_string()), "\n".to_string()), " let engine_bookkeeping_nanos = native_demand_scheduling_nanos(run.clone()).max(0) as u128;".to_string()), "\n".to_string()), " // Every demand window the engine attributes -- prepare, eval and the occurrence census -- is a row".to_string()), "\n".to_string()), " // of its own, so the scheduling row is the call span minus all three. Leaving the census out would".to_string()), "\n".to_string()), " // count its windows twice. An engine that attributed more than its call took is incoherent; refuse".to_string()), "\n".to_string()), " // rather than clamp the scheduling row to zero.".to_string()), "\n".to_string()), " let attributed_in_call = engine_prepare_nanos + engine_eval_nanos + occurrence_census_nanos;".to_string()), "\n".to_string()), " if attributed_in_call > schedule_span_nanos {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: native driver cost OverAttributed inside the engine call {{ attributed_nanos: {}, schedule_span_nanos: {} }}\", attributed_in_call, schedule_span_nanos);".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let demand_scheduling_nanos = schedule_span_nanos - attributed_in_call;".to_string()), "\n".to_string()), " for line in native_demand_occurrence_census_lines(run.clone(), universe.modules.len() as i64).iter() {".to_string()), "\n".to_string()), " eprintln!(\"{}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for n in native_demand_prepare_samples(run.clone()).iter() {".to_string()), "\n".to_string()), " prepare_samples.push((*n).max(0) as u128);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for n in native_demand_eval_samples(run.clone()).iter() {".to_string()), "\n".to_string()), " eval_samples.push((*n).max(0) as u128);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let prepare_ok: u64 = native_demand_prepare_ok(run.clone()).max(0) as u64;".to_string()), "\n".to_string()), " let prepare_refused: u64 = native_demand_prepare_refused(run.clone()).max(0) as u64;".to_string()), "\n".to_string()), " let driver_collection_nanos = cpu_span_nanos(collection_started);".to_string()), "\n".to_string()), " // THE DRIVER REFUSES A RUN WHOSE CLOCK NEVER ADVANCED rather than reporting its zeros. A".to_string()), "\n".to_string()), " // realization whose body returns a constant zero leaves every transition CLASSIFIED and".to_string()), "\n".to_string()), " // VALIDLY OBSERVED, so completeness answers true over totals that are all zero, and the".to_string()), "\n".to_string()), " // emitted-body membership check cannot see it either -- that check establishes a bridge HAS".to_string()), "\n".to_string()), " // a body, never that the body measures. Reporting those zeros would be a fabricated".to_string()), "\n".to_string()), " // measurement presented as an observation, so the line stops here with a located cause.".to_string()), "\n".to_string()), " if !native_demand_observation_progressed(run.clone()) {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-demand-schedule] REFUSED: transitions={} executed and the elapsed observation accumulated no span; the emitted monotonic clock is not advancing\",".to_string()), "\n".to_string()), " native_demand_run_transition_count(run.clone()),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " // THE EVENT TRACE, SEPARATE FROM THE RESULT. One line per admission in admission order, each".to_string()), "\n".to_string()), " // naming the demand key and the state it settled into. It is the schedule's receipt and it is".to_string()), "\n".to_string()), " // deliberately not the population: two runs agreeing on rows while disagreeing on order is a".to_string()), "\n".to_string()), " // real difference, and a receipt that carried only rows could not report it.".to_string()), "\n".to_string()), " let relay_started = cpu_mark();".to_string()), "\n".to_string()), " for line in native_demand_run_receipt_lines(run.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-demand-transition] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"[native-demand-schedule] demands={} admitted={} readiness_derivations={} schedule_span_nanos={} engine_bookkeeping_nanos={}\",".to_string()), "\n".to_string()), " native_demand_run_demand_count(run.clone()),".to_string()), "\n".to_string()), " native_demand_run_transition_count(run.clone()),".to_string()), "\n".to_string()), " native_demand_run_readiness_derivations(run.clone()),".to_string()), "\n".to_string()), " schedule_span_nanos,".to_string()), "\n".to_string()), " engine_bookkeeping_nanos,".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " relay_emit_nanos += cpu_span_nanos(relay_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // receipt_admission: minting the receipt and evaluating the authority's admission and summary".to_string()), "\n".to_string()), " // over it. The controls are executed here because they are receipt inputs, not universe members.".to_string()), "\n".to_string()), " let admission_started = cpu_mark();".to_string()), "\n".to_string()), " let receipt = native_lane_receipt(".to_string()), "\n".to_string()), " context.clone(),".to_string()), "\n".to_string()), " universe.clone(),".to_string()), "\n".to_string()), " Rc::new(population.clone().into()),".to_string()), "\n".to_string()), " native_lane_controls(context.clone()),".to_string()), "\n".to_string()), " Rc::new(facts),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " let admission = native_route_admission(receipt.clone());".to_string()), "\n".to_string()), " let summary = native_route_admission_summary(admission.clone());".to_string()), "\n".to_string()), " let file_refusal_tally: Vec = native_route_file_refusal_tally(receipt.clone())".to_string()), "\n".to_string()), " .iter()".to_string()), "\n".to_string()), " .map(|row| serde_json::json!(row))".to_string()), "\n".to_string()), " .collect();".to_string()), "\n".to_string()), " let admitted = native_route_admitted(admission.clone());".to_string()), "\n".to_string()), " // THE FRONTIER RATCHET, OVER THE SAME RECEIPT AND INSIDE THE SAME SPAN: the verdict is".to_string()), "\n".to_string()), " // gunbc.native_frontier_ratchet's, and this main only carries its word to the terminal marker".to_string()), "\n".to_string()), " // and its lines to stderr. It is measured under receipt_admission because it is judgement of".to_string()), "\n".to_string()), " // the receipt, the row that span already names.".to_string()), "\n".to_string()), " let frontier = native_frontier_verdict(receipt.clone(), admission.clone(), pattern.clone());".to_string()), "\n".to_string()), " let frontier_word = native_frontier_verdict_word(frontier.clone());".to_string()), "\n".to_string()), " let admission_nanos = cpu_span_nanos(admission_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // row_serialization: SERIALIZING THE OBSERVATIONS AND WRITING THEM, MEASURED (review".to_string()), "\n".to_string()), " // 64181 named this span; #11105 made leaving it unattributed fatal). Every row the".to_string()), "\n".to_string()), " // population carries is turned into JSON here and written to stdout, which is real work".to_string()), "\n".to_string()), " // no other row covers -- eval is the identity fold and nothing else. It is TIMED, never".to_string()), "\n".to_string()), " // derived as parent minus the others: a row defined as the remainder would force the".to_string()), "\n".to_string()), " // residual to zero and make the tolerance arm unfireable by construction.".to_string()), "\n".to_string()), " let serialization_started = cpu_mark();".to_string()), "\n".to_string()), " for refusal in receipt.file_refusals.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"file_refusal\": refusal }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for advised in context.accepted_file_advisories.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::json!({ \"accepted_file_advisories\": advised }));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for row in population.iter() {".to_string()), "\n".to_string()), " println!(\"{}\", serde_json::to_string(row).unwrap());".to_string()), "\n".to_string()), " // THE OPERATOR'S LINE, BESIDE THE PERSISTED ROW. The JSON above is the receipt; this is".to_string()), "\n".to_string()), " // the same value rendered by the authority for a reader of the command's output, so a".to_string()), "\n".to_string()), " // row that did not pass names its subject, stage, cause and position without anyone".to_string()), "\n".to_string()), " // re-deriving them from the bytes just written. Passing rows print nothing here: the".to_string()), "\n".to_string()), " // terminal marker carries their count. EXHAUSTIVE ON PURPOSE -- a verdict arm added to".to_string()), "\n".to_string()), " // the vocabulary must decide here whether it is operator-visible.".to_string()), "\n".to_string()), " match &*row.verdict {".to_string()), "\n".to_string()), " NativeTestVerdict::NativeTestPassed => {}".to_string()), "\n".to_string()), " NativeTestVerdict::NativeTestReturnedFalse".to_string()), "\n".to_string()), " | NativeTestVerdict::NativeTestReturnedOther".to_string()), "\n".to_string()), " | NativeTestVerdict::NativeTestRefused { .. } => {".to_string()), "\n".to_string()), " eprintln!(\"[native-verdict] {}\", native_route_member_row_text(row.clone()));".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for line in native_frontier_report_lines(frontier.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-frontier] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " for line in native_frontier_proposal_file_lines(receipt.clone(), frontier.clone()).iter() {".to_string()), "\n".to_string()), " eprintln!(\"[native-frontier-roster] {}\", line);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let row_serialization_nanos = cpu_span_nanos(serialization_started);".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), " // THE PARTITION, THROUGH THE ONE LAW. The rows are the row set std.compiler_entry owns and".to_string()), "\n".to_string()), " // verdict is native_driver_cost_account over them -- not a second reconcile. The two rows".to_string()), "\n".to_string()), " // this route pays that a driver handed a universe file does not (universe_derivation,".to_string()), "\n".to_string()), " // receipt_admission) are members of the same exclusive sum, so an over-attribution through".to_string()), "\n".to_string()), " // either is caught here rather than by a check of its own.".to_string()), "\n".to_string()), " prepare_samples.sort_unstable();".to_string()), "\n".to_string()), " eval_samples.sort_unstable();".to_string()), "\n".to_string()), " let parent_span_nanos = cpu_span_nanos(driver_start);".to_string()), "\n".to_string()), " let parent_wall_nanos = span_nanos(driver_wall_start);".to_string()), "\n".to_string()), " // THE DRIVER IS EXHAUSTIVE OVER THE KEY WITH NO WILDCARD, deliberately: a row added to".to_string()), "\n".to_string()), " // std.compiler_entry makes THIS match refuse to compile until the driver says what".to_string()), "\n".to_string()), " // measures it. A wildcard here would answer zero for a span nobody wired up, which is".to_string()), "\n".to_string()), " // the absorbing arm DESIGN section 5 forbids -- and it is what the old record literal".to_string()), "\n".to_string()), " // did silently, three folds running.".to_string()), "\n".to_string()), " let exclusive = native_driver_exclusive_rows(move |k| match k {".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveLoad => nanosecond(native_cost_i64(load_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveUniverseDerivation => nanosecond(native_cost_i64(universe_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveContext => nanosecond(native_cost_i64(context_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusivePrepare => nanosecond(native_cost_i64(prepare_total_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveEval => nanosecond(native_cost_i64(evaluate_total_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveReceiptAdmission => nanosecond(native_cost_i64(admission_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveRowSerialization => nanosecond(native_cost_i64(row_serialization_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveDemandScheduling => nanosecond(native_cost_i64(demand_scheduling_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveDriverCollection => nanosecond(native_cost_i64(driver_collection_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveOccurrenceCensus => nanosecond(native_cost_i64(occurrence_census_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveModuleRelease => nanosecond(native_cost_i64(module_release_nanos)),".to_string()), "\n".to_string()), " NativeDriverExclusiveRowKey::ExclusiveRelayEmit => nanosecond(native_cost_i64(relay_emit_nanos)),".to_string()), "\n".to_string()), " });".to_string()), "\n".to_string()), " let exclusive_sum = nanosecond_count(native_driver_exclusive_sum(exclusive.clone())) as u128;".to_string()), "\n".to_string()), " let tolerance_nanos = nanosecond_count(native_driver_cost_remainder_tolerance_nanos()) as u128;".to_string()), "\n".to_string()), " let accounting = native_driver_cost_account(".to_string()), "\n".to_string()), " nanosecond(native_cost_i64(parent_span_nanos)),".to_string()), "\n".to_string()), " exclusive.clone(),".to_string()), "\n".to_string()), " native_driver_cost_remainder_tolerance_nanos(),".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " // The remainder is the residual the accounting itself returns, never parent minus sum: on".to_string()), "\n".to_string()), " // over-attributed arm there IS no remainder, and a clamped zero there would be a".to_string()), "\n".to_string()), " // plausible number standing where the measurement is incoherent.".to_string()), "\n".to_string()), " let (verdict, remainder_nanos) = match accounting.as_ref() {".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostOverAttributed { .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostOverAttributed\"#, serde_json::Value::Null),".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostRemainderExceedsTolerance { residual, .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostRemainderExceedsTolerance\"#, serde_json::json!(nanosecond_count(residual.clone()))),".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostReconciled { residual, .. } =>".to_string()), "\n".to_string()), " (r#\"NativeDriverCostReconciled\"#, serde_json::json!(nanosecond_count(residual.clone()))),".to_string()), "\n".to_string()), " };".to_string()), "\n".to_string()), " let identities = universe.identities.len() as u64;".to_string()), "\n".to_string()), " let eval_mean = if eval_samples.is_empty() { 0 } else { evaluate_total_nanos / eval_samples.len() as u128 };".to_string()), "\n".to_string()), " let prepare_mean = if prepare_samples.is_empty() { 0 } else { prepare_total_nanos / prepare_samples.len() as u128 };".to_string()), "\n".to_string()), " // THE ROWS ARE PROJECTED FROM THE VALUE, NOT RE-LISTED (review 64181). The field names".to_string()), "\n".to_string()), " // were written once to build NativeDriverExclusiveRows and again to render it, which is the".to_string()), "\n".to_string()), " // second spelling this file warns about for prose, applied to the emitted object: a driver".to_string()), "\n".to_string()), " // gaining a phase would have updated the struct and left the receipt short. Serializing the".to_string()), "\n".to_string()), " // value carries the row set from the type, so the JSON cannot disagree with the fold. The".to_string()), "\n".to_string()), " // rows therefore render as std.measure Nanosecond objects rather than bare integers -- the".to_string()), "\n".to_string()), " // unit travels with the number, which is the point of the type.".to_string()), "\n".to_string()), " // A NON-RECONCILED ARM IS A PROCESS REFUSAL (#11105, review 63891). The receipt is".to_string()), "\n".to_string()), " // printed first so the numbers that caused the refusal are on the wire, and THEN the".to_string()), "\n".to_string()), " // line stops: a verdict string with exit 0 is the inert lens DESIGN section 6 names.".to_string()), "\n".to_string()), " // The full-N run of this route refused here on 158600186 ns against a 50000000 ns".to_string()), "\n".to_string()), " // tolerance before row_serialization was a measured row, which is what put that row on".to_string()), "\n".to_string()), " // the type rather than a wider tolerance on this arm.".to_string()), "\n".to_string()), " eprintln!(\"[native-cost-partition] {}\", serde_json::json!({".to_string()), "\n".to_string()), " \"basis\": \"native_driver_thread_cpu\",".to_string()), "\n".to_string()), " \"basis_note\": \"single-threaded SourceRootEvalDriver thread CPU (CLOCK_THREAD_CPUTIME_ID); exclusive rows partition parent_span_nanos; parent_wall_nanos is an observation, never judged\",".to_string()), "\n".to_string()), " \"producer\": \"std.compiler_entry.SourceRootEvalDriver\",".to_string()), "\n".to_string()), " \"head\": head,".to_string()), "\n".to_string()), " \"mode\": \"adjudicate\",".to_string()), "\n".to_string()), " \"verdict\": verdict,".to_string()), "\n".to_string()), " \"tolerance_nanos\": tolerance_nanos,".to_string()), "\n".to_string()), " \"parent_span_nanos\": parent_span_nanos,".to_string()), "\n".to_string()), " \"parent_wall_nanos\": parent_wall_nanos,".to_string()), "\n".to_string()), " \"exclusive\": exclusive.rows.iter().map(|r| (".to_string()), "\n".to_string()), " native_driver_exclusive_row_name(r.key.clone()),".to_string()), "\n".to_string()), " serde_json::json!(r.nanos),".to_string()), "\n".to_string()), " )).collect::>(),".to_string()), "\n".to_string()), " \"sum_exclusive_nanos\": exclusive_sum,".to_string()), "\n".to_string()), " \"remainder_nanos\": remainder_nanos,".to_string()), "\n".to_string()), " \"corpus_reads\": corpus_reads,".to_string()), "\n".to_string()), " \"closure_reads\": closure_reads,".to_string()), "\n".to_string()), " \"scanned_paths\": ingest_receipt.scanned_paths.len(),".to_string()), "\n".to_string()), " \"ingested_paths\": ingest_receipt.ingested_paths.len(),".to_string()), "\n".to_string()), " \"closure_modules\": closure.len(),".to_string()), "\n".to_string()), " \"identities\": identities,".to_string()), "\n".to_string()), " \"unique_modules\": universe.modules.len(),".to_string()), "\n".to_string()), " \"file_refusals\": receipt.file_refusals.len(),".to_string()), "\n".to_string()), " \"prepare_ok\": prepare_ok,".to_string()), "\n".to_string()), " \"prepare_refused\": prepare_refused,".to_string()), "\n".to_string()), " \"prepare_mean_nanos\": prepare_mean,".to_string()), "\n".to_string()), " \"prepare_p50_nanos\": native_cost_quantile_nanos(&prepare_samples, 1, 2),".to_string()), "\n".to_string()), " \"prepare_p95_nanos\": native_cost_quantile_nanos(&prepare_samples, 19, 20),".to_string()), "\n".to_string()), " \"prepare_max_nanos\": prepare_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"eval_mean_nanos\": eval_mean,".to_string()), "\n".to_string()), " \"eval_p50_nanos\": native_cost_quantile_nanos(&eval_samples, 1, 2),".to_string()), "\n".to_string()), " \"eval_p95_nanos\": native_cost_quantile_nanos(&eval_samples, 19, 20),".to_string()), "\n".to_string()), " \"eval_max_nanos\": eval_samples.last().copied().unwrap_or(0),".to_string()), "\n".to_string()), " \"peak_rss_bytes\": proc_status_kib(\"VmHWM\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " \"rss_bytes\": proc_status_kib(\"VmRSS\").map(|kib| kib * 1024),".to_string()), "\n".to_string()), " }));".to_string()), "\n".to_string()), " match accounting.as_ref() {".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostReconciled { .. } => {}".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostOverAttributed { sum_exclusive, parent_span } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"REFUSED: native driver cost OverAttributed {{ sum_exclusive_nanos: {}, parent_span_nanos: {} }}\",".to_string()), "\n".to_string()), " nanosecond_count(sum_exclusive.clone()) as u128,".to_string()), "\n".to_string()), " nanosecond_count(parent_span.clone()) as u128".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeDriverCostAccounting::NativeDriverCostRemainderExceedsTolerance { residual, tolerance } => {".to_string()), "\n".to_string()), " eprintln!(".to_string()), "\n".to_string()), " \"REFUSED: native driver cost RemainderExceedsTolerance {{ residual_nanos: {}, tolerance_nanos: {} }} -- unattributed driver work; name the span, do not widen the tolerance\",".to_string()), "\n".to_string()), " nanosecond_count(residual.clone()) as u128,".to_string()), "\n".to_string()), " nanosecond_count(tolerance.clone()) as u128".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " println!(".to_string()), "\n".to_string()), " \"{}\",".to_string()), "\n".to_string()), " serde_json::json!({".to_string()), "\n".to_string()), " \"_terminal\": \"complete\",".to_string()), "\n".to_string()), " \"mode\": \"adjudicate\",".to_string()), "\n".to_string()), " \"rows\": population.len(),".to_string()), "\n".to_string()), " \"universe\": universe.identities.len(),".to_string()), "\n".to_string()), " \"file_refusals\": receipt.file_refusals.len(),".to_string()), "\n".to_string()), " \"file_refusal_tally\": file_refusal_tally,".to_string()), "\n".to_string()), " \"advised_files\": context.accepted_file_advisories.len(),".to_string()), "\n".to_string()), " \"admitted\": admitted,".to_string()), "\n".to_string()), " \"summary\": summary,".to_string()), "\n".to_string()), " \"frontier\": frontier_word,".to_string()), "\n".to_string()), " })".to_string()), "\n".to_string()), " );".to_string()), "\n".to_string()), " if admitted {".to_string()), "\n".to_string()), " std::process::exit(0);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " std::process::exit(1);".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "".to_string()), "\n".to_string()), "fn main() {".to_string()), "\n".to_string()), " let argv: Vec = std::env::args().skip(1).collect();".to_string()), "\n".to_string()), " let plan = native_driver_parse(Rc::new(argv.into()));".to_string()), "\n".to_string()), " if let ProcessExit::ExitFailure { code, reason } = &*native_driver_plan_exit(plan.clone()) {".to_string()), "\n".to_string()), " eprintln!(\"{reason}\");".to_string()), "\n".to_string()), " std::process::exit(*code as i32);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " let source_roots: Vec = native_driver_plan_source_roots(plan.clone()).iter().cloned().collect();".to_string()), "\n".to_string()), " match &*plan {".to_string()), "\n".to_string()), " NativeDriverPlan::NativeDriverPlanRefused { reason, detail } => {".to_string()), "\n".to_string()), " eprintln!(\"REFUSED: native driver plan refused ({reason:?}) but its exit was success -- {detail}\");".to_string()), "\n".to_string()), " std::process::exit(70);".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " NativeDriverPlan::NativeDriverPlanned { verb } => {".to_string()), "\n".to_string()), " let verb: &NativeDriverVerb = verb;".to_string()), "\n".to_string()), " match verb {".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensus { .. } => run_census(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensusResolve { .. } => run_census_resolve(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverCensusInfer { .. } => run_census_infer(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverDemandCensus { .. } => run_demand_census(&source_roots),".to_string()), "\n".to_string()), " NativeDriverVerb::NativeDriverAdjudicate { facts_path, pattern, .. } => run_adjudication(facts_path, Rc::new(TargetPattern::clone(pattern)), &source_roots),".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), " }".to_string()), "\n".to_string()), "}".to_string()), "\n".to_string()), "\n".to_string()), }) } } diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index 1b84f5114e5..5d272177451 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -121,13 +121,21 @@ pub use crate::v1_compiler_infer_cycle::detect_type_cycles_kahn; pub use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling; use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::*; use crate::v1_compiler_infer_emit_info::TypeRepr::{EnumRepr, StructRepr}; +use crate::v1_compiler_infer_emit_info::TypeSummaryLookup::{ + TypeSummaryFound, TypeSummaryLeafAmbiguous, TypeSummaryNotDeclared, +}; +use crate::v1_compiler_infer_emit_info::TypeSummaryQuestion::{ + QuestionDecided, QuestionNameAmbiguous, QuestionNotDeclared, +}; pub use crate::v1_compiler_infer_emit_info::{ add_emit_item_summary, build_enum_field_summaries, build_struct_field_summaries, close_fn_fields, derive_variant_to_enum, empty_emit_graph_info, empty_type_decl_index, - empty_type_env, lookup_emit_type_summary, type_decl_index_with_qualified_names, + empty_type_env, empty_type_summary_index, is_enum_in_summaries, lookup_emit_type_summary, + type_decl_index_with_qualified_names, type_summary_lookup, }; pub use crate::v1_compiler_infer_emit_info::{ - EmitGraphInfo, EmitInfoBuildState, TypeRepr, TypeSummary, + EmitGraphInfo, EmitInfoBuildState, TypeRepr, TypeSummary, TypeSummaryIndex, TypeSummaryLookup, + TypeSummaryQuestion, }; use crate::v1_compiler_infer_env::GlobalBareLookupState::{ GlobalBareAmbiguousBinding, GlobalBareUniqueBinding, @@ -143,11 +151,11 @@ pub use crate::v1_compiler_infer_env::{ is_recursive_type_by_name, listed_import_required_bare_call_blocked, lookup_binding_by_name, lookup_binding_on_chain, lookup_type, lookup_type_by_name, lookup_type_for, merge_inductive_fields, merge_type_env_cache, merge_type_env_cache_guarded, node_with_children, - node_with_inferred, put_inductive_field, put_inductive_field_cross, qualified_all_but_last, - qualify_borrowed_inferred, qualify_borrowed_type_names, qualify_decl_reference_positions, - str_bindings_from_bindings, symbol_index_insert, symbol_index_insert_decl, - symbol_index_insert_service, symbol_index_lookup, text_crossing_by_identity, - text_representation_by_identity, text_representation_is_text_arm, + node_with_inferred, overlay_skips_kernel_name, put_inductive_field, put_inductive_field_cross, + qualified_all_but_last, qualify_borrowed_inferred, qualify_borrowed_type_names, + qualify_decl_reference_positions, str_bindings_from_bindings, symbol_index_insert, + symbol_index_insert_decl, symbol_index_insert_service, symbol_index_lookup, + text_crossing_by_identity, text_representation_by_identity, text_representation_is_text_arm, text_representation_is_unidentified, text_representations_cross, type_reference_declaration, type_reference_declaration_ref, unit_variant_index_shadow_insert, }; @@ -481,13 +489,7 @@ pub fn infer_block_stmts( let next_scope = scope_after_stmt_node(stmt.clone(), stmt_rt.clone(), scope.clone()); { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = v1_rt::int_sub(remaining_count, 1); let __tco_2 = next_scope.clone(); let __tco_3 = v1_rt::rc_list_push(typed_stmts, stmt_typed.clone()); @@ -638,13 +640,7 @@ Rc::new(InferScopeComponents { } }); { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = parent_index; let __tco_2 = env; let __tco_3 = parent_result.svc_registry.clone(); @@ -658,13 +654,7 @@ Rc::new(InferScopeComponents { } } std::option::Option::None => { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = parent_index; let __tco_2 = env; let __tco_3 = svc_registry; @@ -737,10 +727,8 @@ pub fn classify_field_recursion( let value_type = match type_expr .children .clone() - .iter() + .get((value_index.clone()) as usize) .cloned() - .skip(value_index.clone() as usize) - .next() { Some(t) => { crate::v1_std_core::authored_name_at(source_indices.clone(), t.clone()) @@ -2692,6 +2680,44 @@ pub fn record_lit_expanded_from_expected( } } +pub fn variant_literal_expected_application( + owner: Rc, + expected: Option>, + scope: Rc, +) -> Option> { + { + if (((owner.params.clone().len() as i64) == 0) + || (owner.connective.clone() != Connective::Disj)) + { + return std::option::Option::None; + } + match expected.clone() { + Some(exp) => { + if ((exp.connective.clone() != Connective::NoConnective) + || ((exp.children.clone().len() as i64) == 0)) + { + std::option::Option::None + } else { + match crate::v1_compiler_infer_env::lookup_type_for( + scope.type_env.clone(), + exp.clone(), + ) { + Some(head) => { + if v1_rt::rc_ptr_eq(head.clone(), owner.clone()) { + Some(crate::v1_std_core::with_required_cardinality(exp.clone())) + } else { + std::option::Option::None + } + } + std::option::Option::None => std::option::Option::None, + } + } + } + std::option::Option::None => std::option::Option::None, + } + } +} + pub fn record_lit_expected_coproduct( expected: Option>, scope: Rc, @@ -2891,7 +2917,7 @@ pub fn record_lit_instantiated_fields( Some(template_fields) => { let subst = Rc::new(decl.params.clone().iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()).iter().cloned().fold(v1_rt::rc_empty_map::>(), |acc: Rc>>, pair: (i64, Rc)| { let slot = crate::v1_std_core::authored_name_at(si.clone(), pair.1.clone()); -match exp.children.clone().iter().cloned().skip(pair.0.clone() as usize).next() { +match exp.children.clone().get((pair.0.clone()) as usize).cloned() { Some(arg) => v1_rt::rc_map_insert(acc.clone(), slot.clone(), crate::v1_compiler_infer_types::child_type_node(arg.clone())), std::option::Option::None => acc.clone(), } @@ -4414,16 +4440,9 @@ pub fn list_literal_optional_member_diags( let head_type = crate::v1_compiler_infer_types::resolved_type(head.clone()); Rc::new({ let mut __result = Vec::new(); - for te in Rc::new( - typed_elements - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() + for te in Rc::new(v1_rt::list_skip(&typed_elements.clone(), 1)) + .iter() + .cloned() { __result.extend( (*optional_at_required_obligation_diags( @@ -6926,20 +6945,8 @@ pub fn coproduct_applied_type_arguments_conflict_scan( break true; } else { { - let __tco_0 = Rc::new( - declared_args - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); - let __tco_1 = Rc::new( - produced_args - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&declared_args, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&produced_args, 1)); let __tco_2 = scope; __tco_loop_declared_args = __tco_0; __tco_loop_produced_args = __tco_1; @@ -7079,20 +7086,8 @@ pub fn applied_type_arguments_conflict_scan( break true; } else { { - let __tco_0 = Rc::new( - declared_args - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); - let __tco_1 = Rc::new( - produced_args - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&declared_args, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&produced_args, 1)); let __tco_2 = scope; __tco_loop_declared_args = __tco_0; __tco_loop_produced_args = __tco_1; @@ -7970,14 +7965,14 @@ pub fn call_argument_formal_at_position( } __result }) - .iter() + .get( + (call_argument_positional_rank( + arguments.clone(), + argument_index.clone(), + source_indices.clone(), + )) as usize, + ) .cloned() - .skip(call_argument_positional_rank( - arguments.clone(), - argument_index.clone(), - source_indices.clone(), - ) as usize) - .next() { Some(positional) => Rc::new(CallArgumentFormalSelection::CallArgumentFormalSelected { formal_index: positional.0.clone(), @@ -8106,13 +8101,7 @@ pub fn build_call_application_plan( .cloned() { __result.extend( - (*match formals - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + (*match formals.clone().get((pair.0.clone()) as usize).cloned() { Some(carried) => Rc::new(vec![Rc::new(ResolvedCallFormal { formal_index: pair.0.clone(), formal: Rc::new(ResolvedFormal { @@ -8190,7 +8179,7 @@ pub fn direct_call_structured_application_mismatch_diags( let formal_subst = app.formal.clone().declared_type.clone(); let formal = app.formal.clone().declaration_bound_conformance.clone(); match app.matched_argument_index.clone() { - Some(arg_index) => match typed_args.clone().iter().cloned().skip(arg_index.clone() as usize).next() { + Some(arg_index) => match typed_args.clone().get((arg_index.clone()) as usize).cloned() { Some(ta) => { let actual_expr = crate::v1_std_core::arg_value(ta.clone()); let host_into_sequence = (!type_node_is_callable(formal.clone()) && crate::v1_compiler_infer_env::host_text_into_structural_sequence(formal.clone(), crate::v1_compiler_infer_resolve::peel_nominal_alias_identity(crate::v1_compiler_infer_types::resolved_type(actual_expr.clone()), type_env.clone(), module_name.clone()), crate::v1_compiler_infer_types::resolved_type(actual_expr.clone()), type_env.clone())); @@ -8274,7 +8263,7 @@ pub fn sibling_effect_order_diags( Some(x) => x.clone(), std::option::Option::None => "".to_string(), }; - let b = match effectful.clone().iter().cloned().skip(1 as usize).next() { + let b = match effectful.clone().get((1) as usize).cloned() { Some(x) => x.clone(), std::option::Option::None => "".to_string(), }; @@ -8338,10 +8327,8 @@ pub fn direct_call_argument_inhabitance_diags( (*match app.matched_argument_index.clone() { Some(arg_index) => match typed_args .clone() - .iter() + .get((arg_index.clone()) as usize) .cloned() - .skip(arg_index.clone() as usize) - .next() { Some(ta) => { if type_node_is_callable(app.formal.clone().declared_type.clone()) { @@ -8395,13 +8382,13 @@ pub fn direct_call_generic_type_argument_inhabitance_diags( let mut __result = Vec::new(); for app in plan.iter().cloned() { __result.extend((*match app.matched_argument_index.clone() { - Some(arg_index) => match typed_args.clone().iter().cloned().skip(arg_index.clone() as usize).next() { + Some(arg_index) => match typed_args.clone().get((arg_index.clone()) as usize).cloned() { Some(ta) => { let actual_expr = crate::v1_std_core::arg_value(ta.clone()); let actual = crate::v1_compiler_infer_types::resolved_type(actual_expr.clone()); if ((((exposure_is_application(exposure_view_for_node(app.formal.clone().declared_type.clone(), source_indices.clone())) && exposure_is_application(exposure_view_for_node(actual.clone(), source_indices.clone()))) && ((app.formal.clone().declared_type.clone().children.clone().len() as i64) > 0)) && ((app.formal.clone().declared_type.clone().children.clone().len() as i64) == (actual.children.clone().len() as i64))) && (crate::v1_std_core::authored_name_at(source_indices.clone(), app.formal.clone().declared_type.clone()) == crate::v1_std_core::authored_name_at(source_indices.clone(), actual.clone()))) { - Rc::new({ let mut __result = Vec::new(); for declared_pair in Rc::new(app.formal.clone().declared_type.clone().children.clone().iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()).iter().cloned() { __result.extend((*match app.formal.clone().substitution_basis.clone().children.clone().iter().cloned().skip(declared_pair.0.clone() as usize).next() { - Some(raw_declared_child) => match actual.children.clone().iter().cloned().skip(declared_pair.0.clone() as usize).next() { + Rc::new({ let mut __result = Vec::new(); for declared_pair in Rc::new(app.formal.clone().declared_type.clone().children.clone().iter().cloned().enumerate().map(|(i, v)| (i as i64, v)).collect::>()).iter().cloned() { __result.extend((*match app.formal.clone().substitution_basis.clone().children.clone().get((declared_pair.0.clone()) as usize).cloned() { + Some(raw_declared_child) => match actual.children.clone().get((declared_pair.0.clone()) as usize).cloned() { Some(produced_child) => if (applied_type_argument_identity_known(crate::v1_std_core::authored_name_at(source_indices.clone(), crate::v1_compiler_infer_types::child_type_node(raw_declared_child.clone())), scope.clone()) && applied_type_argument_identity_known(crate::v1_std_core::authored_name_at(source_indices.clone(), crate::v1_compiler_infer_types::child_type_node(produced_child.clone())), scope.clone())) { declared_type_obligation_diags(Rc::new(DeclaredTypeObligation { position: DeclaredTypePosition::PositionGenericTypeArgument, @@ -8615,10 +8602,8 @@ pub fn callable_signature_substitution( acc.clone(), |st: Rc>>, pair: (i64, Rc)| match a_params .clone() - .iter() + .get((pair.0.clone()) as usize) .cloned() - .skip(pair.0.clone() as usize) - .next() { Some(ap) => callable_leaf_substitution( callable_param_type(pair.1.clone()), @@ -8739,13 +8724,7 @@ pub fn callable_arrow_mismatch( .iter() .cloned() { - if match a_params - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + if match a_params.clone().get((pair.0.clone()) as usize).cloned() { Some(ap) => callable_component_ground_mismatch( callable_param_type(pair.1.clone()), callable_param_type(ap.clone()), @@ -8830,16 +8809,9 @@ pub fn expression_value_type(e: Rc) -> Rc { resolved_callable_type( Rc::new({ let mut __result = Vec::new(); - for lp in Rc::new( - e.children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() + for lp in Rc::new(v1_rt::list_skip(&e.children.clone(), 1)) + .iter() + .cloned() { __result.push(lambda_arrow_param(lp.clone())); } @@ -9068,12 +9040,9 @@ pub fn borrowed_callable_call_type( .cloned() { Some(named_ta) => Some(named_ta.clone()), - std::option::Option::None => typed_args - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next(), + std::option::Option::None => { + typed_args.clone().get((pair.0.clone()) as usize).cloned() + } }; match matched_arg.clone() { Some(ta) => unify_generics( @@ -9158,14 +9127,10 @@ pub fn call_arg_bound_param_at( source_indices: Rc>>, positionally_eligible_names: Rc>, ) -> Option { - Rc::new( - typed_args - .clone() - .iter() - .cloned() - .take(v1_rt::int_add(idx.clone(), 1) as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_take( + &typed_args.clone(), + v1_rt::int_add(idx.clone(), 1), + )) .iter() .cloned() .fold( @@ -9184,10 +9149,8 @@ pub fn call_arg_bound_param_at( std::option::Option::None => Rc::new(CallArgBoundParamAccum { bound: positionally_eligible_names .clone() - .iter() - .cloned() - .skip(acc.pos.clone() as usize) - .next(), + .get((acc.pos.clone()) as usize) + .cloned(), pos: v1_rt::int_add(acc.pos.clone(), 1), }), }, @@ -9406,10 +9369,8 @@ pub fn direct_call_shape_diags( if ((positional_args.clone().len() as i64) > positional_capacity.clone()) { match positional_args .clone() - .iter() + .get((positional_capacity.clone()) as usize) .cloned() - .skip(positional_capacity.clone() as usize) - .next() { Some(overflow) => Rc::new(vec![crate::v1_std_core::make_error_node( Rc::new(CompilerDiagnostic::CallPositionalSurplus { @@ -9613,10 +9574,8 @@ pub fn function_value_call_named_arg_diags( if (supplied.clone() > param_count.clone()) { match typed_args .clone() - .iter() + .get((param_count.clone()) as usize) .cloned() - .skip(param_count.clone() as usize) - .next() { Some(overflow) => Rc::new(vec![crate::v1_std_core::make_error_node( Rc::new(CompilerDiagnostic::CallPositionalSurplus { @@ -9715,7 +9674,7 @@ pub fn direct_call_arg_mismatch_diags( __result.extend((*{ let formal = app.formal.clone().declaration_bound_conformance.clone(); match app.matched_argument_index.clone() { - Some(arg_index) => match typed_args.clone().iter().cloned().skip(arg_index.clone() as usize).next() { + Some(arg_index) => match typed_args.clone().get((arg_index.clone()) as usize).cloned() { Some(ta) => { let actual_expr = crate::v1_std_core::arg_value(ta.clone()); if match (*actual_expr.expr_data.clone()).clone() { @@ -9802,7 +9761,7 @@ pub fn builtin_text_argument_crossing_diags( let arg = pair.1.clone(); let bound_name = match crate::v1_std_core::arg_name_at(arg.clone(), scope.type_env.clone().source_indices.clone()) { Some(label) => label.clone(), - std::option::Option::None => match formal_names.clone().iter().cloned().skip(pair.0.clone() as usize).next() { + std::option::Option::None => match formal_names.clone().get((pair.0.clone()) as usize).cloned() { Some(n) => n.clone(), std::option::Option::None => "".to_string(), }, @@ -10019,7 +9978,7 @@ pub fn infer_tier2b_builtin_with_kernel_diags( let operand_elem = if ((func_name.clone() == "set_insert".to_string()) || (func_name.clone() == "set_contains".to_string())) { - match typed_args.clone().iter().cloned().skip(1 as usize).next() { + match typed_args.clone().get((1) as usize).cloned() { Some(insert_arg) => { match crate::v1_std_core::arg_value(insert_arg.clone()) .inferred @@ -10036,7 +9995,7 @@ pub fn infer_tier2b_builtin_with_kernel_diags( std::option::Option::None => std::option::Option::None, } } else { - match typed_args.clone().iter().cloned().skip(1 as usize).next() { + match typed_args.clone().get((1) as usize).cloned() { Some(other_arg) => match crate::v1_std_core::arg_value(other_arg.clone()).inferred.clone().as_deref().cloned() { Some(InferredNode::Resolved { node: other_type, .. }) => match crate::v1_compiler_infer_lookup::set_element_type_in_env(other_type.clone(), scope.type_env.clone()) { Some(elem_slot) => Some(crate::v1_compiler_infer_types::child_type_node(elem_slot.clone())), @@ -10055,7 +10014,7 @@ pub fn infer_tier2b_builtin_with_kernel_diags( }; let union_other_bad = if (func_name.clone() == "set_union".to_string()) { - match typed_args.clone().iter().cloned().skip(1 as usize).next() { + match typed_args.clone().get((1) as usize).cloned() { Some(other_arg) => set_union_other_operand_is_resolved_non_set( other_arg.clone(), scope.clone(), @@ -10423,13 +10382,6 @@ pub struct UnresolvedMethodFrontierRow { pub fn unresolved_method_frontier() -> Rc>> { Rc::new(vec![Rc::new(UnresolvedMethodFrontierRow { - module_name: "extdeps.dns.domain_name".to_string(), - method: "list_push".to_string(), - occurrences: 1, - receiver_shape: "Primitive(ok)".to_string(), - cause: "receiver is a coproduct payload bound by pattern destructuring, which arrives typed as the VARIANT name rather than the field type.".to_string(), - dissolution: crate::std_dissolution::unbound_dissolution("coproduct payload binding typed as the field type, at which point the receiver is List and DECIDABLE".to_string()), -}), Rc::new(UnresolvedMethodFrontierRow { module_name: "v1.compiler.trace".to_string(), method: "map".to_string(), occurrences: 1, @@ -11743,7 +11695,7 @@ Rc::new(ArgInferResult { diagnostics: v1_rt::concat(ar.diagnostics.clone(), Rc::new(vec![contract_diag.clone()])), }) }, - DeclaredArgContract::ContractKnown { types: declared_types, .. } => match declared_types.clone().iter().cloned().skip(idx.clone() as usize).next() { + DeclaredArgContract::ContractKnown { types: declared_types, .. } => match declared_types.clone().get((idx.clone()) as usize).cloned() { Some(declared_type) => infer_arg_with_element_type(a.clone(), declared_type.clone(), nf_scope.clone()), std::option::Option::None => { let ar = infer_expr(nf_lam_value.clone(), nf_scope.clone(), std::option::Option::None); @@ -12006,10 +11958,8 @@ pub fn infer_call_arguments_generic_pass( formal_index, .. } => match formals .clone() - .iter() + .get((formal_index.clone()) as usize) .cloned() - .skip(formal_index.clone() as usize) - .next() { Some(carried) => carried.substitution_basis.clone(), std::option::Option::None => error_type(), @@ -12040,10 +11990,8 @@ pub fn infer_call_arguments_generic_pass( formal_index, .. } => match formals .clone() - .iter() + .get((formal_index.clone()) as usize) .cloned() - .skip(formal_index.clone() as usize) - .next() { Some(carried) => substitute_generics( carried.declaration_bound_conformance.clone(), @@ -12061,10 +12009,8 @@ pub fn infer_call_arguments_generic_pass( formal_index, .. } => match formals .clone() - .iter() + .get((formal_index.clone()) as usize) .cloned() - .skip(formal_index.clone() as usize) - .next() { Some(carried) => { formal_code_point_view(carried.clone(), scope.type_env.clone()) @@ -13304,14 +13250,7 @@ Rc::new(InferResult { } __found }; - let call_method_args = Rc::new( - call_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let call_method_args = Rc::new(v1_rt::list_skip(&call_args.clone(), 1)); let call_method_name = Some(func_name.clone()); let call_fold_info = extract_fold_init_info( call_method_name.clone(), @@ -13739,14 +13678,8 @@ Rc::new(InferResult { { { let receiver = method_receiver.clone(); - let remaining = Rc::new( - typed_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let remaining = + Rc::new(v1_rt::list_skip(&typed_args.clone(), 1)); let rest_result = rest_operation_result_type( method_resolution.clone(), texpr.span.clone(), @@ -14613,23 +14546,16 @@ crate::v1_compiler_infer_types::resolve_type_variables_from_template(t.clone(), }, }; let unified_arm_type = match arm_body_types.clone().first().cloned() { - Some(first_type) => Rc::new( - arm_body_types - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold(first_type.clone(), |acc: _, t: Rc| { - crate::v1_compiler_infer_types::prefer_specific_type( - acc, - t.clone(), - scope.type_env.clone().source_indices.clone(), - ) - }), + Some(first_type) => Rc::new(v1_rt::list_skip(&arm_body_types.clone(), 1)) + .iter() + .cloned() + .fold(first_type.clone(), |acc: _, t: Rc| { + crate::v1_compiler_infer_types::prefer_specific_type( + acc, + t.clone(), + scope.type_env.clone().source_indices.clone(), + ) + }), std::option::Option::None => scrut_rt.clone(), }; let arm_infer_results = if crate::v1_compiler_infer_types::is_fully_resolved( @@ -15411,15 +15337,7 @@ crate::v1_compiler_infer_types::resolve_type_variables_from_template(t.clone(), texpr.clone(), scope.type_env.clone().source_indices.clone(), ); - let lam_param_nodes = Rc::new( - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let lam_param_nodes = Rc::new(v1_rt::list_skip(&texpr.children.clone(), 1)); let elem_prov = match v1_rt::map_get(&scope.lambda_param_provenance.clone(), "elem".to_string()) { Some(p) => p.clone(), @@ -15450,14 +15368,7 @@ crate::v1_compiler_infer_types::resolve_type_variables_from_template(t.clone(), } else { Rc::new(SubValueRelation::SubValueUnknown) }; - match exp - .params - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + match exp.params.clone().get((pair.0.clone()) as usize).cloned() { Some(cp) => { let cpt = crate::v1_std_core::param_node_type_expr(cp.clone()); @@ -15615,16 +15526,11 @@ crate::v1_compiler_infer_types::resolve_type_variables_from_template(t.clone(), { __result.push({ let pn = pair.1.clone(); - let param_name = match lam_params - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { - Some(n) => n.clone(), - std::option::Option::None => "".to_string(), - }; + let param_name = + match lam_params.clone().get((pair.0.clone()) as usize).cloned() { + Some(n) => n.clone(), + std::option::Option::None => "".to_string(), + }; let param_type = match v1_rt::map_get(&lam_scope.locals.clone(), param_name.clone()) { Some(binding) => Some(Rc::new(InferredNode::Resolved { @@ -16529,13 +16435,7 @@ pub fn alias_chain_type_arg_subst( pair.1.clone(), env.source_indices.clone(), ); - match type_args - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + match type_args.clone().get((pair.0.clone()) as usize).cloned() { Some(arg) => v1_rt::rc_map_insert( acc.clone(), slot.clone(), @@ -17034,14 +16934,7 @@ pub fn infer_map_literal( scope: Rc, ) -> Rc { { - let value_expected = match map_node - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + let value_expected = match map_node.children.clone().get((1) as usize).cloned() { Some(v) => Some(v.clone()), std::option::Option::None => std::option::Option::None, }; @@ -18097,34 +17990,48 @@ Rc::new(FieldInferResult { let is_present_ctor = ((type_name.clone().unwrap() == "Present".to_string()) && (local_variant_parent.clone().as_deref() == expected_optional_parent.clone().as_deref())); - let resolved_node = if is_present_ctor.clone() { - { - let val_field = Rc::new({ - let mut __result = Vec::new(); - for fir in fi_infer_results.iter().cloned() { - if (crate::v1_std_core::field_init_node_name_at( - fir.typed_field.clone(), - scope.type_env.clone().source_indices.clone(), - ) == "value".to_string()) - { - __result.push(fir); + let applied_expected_owner = if is_present_ctor.clone() { + std::option::Option::None + } else { + variant_literal_expected_application( + raw_resolved.clone(), + expected.clone(), + scope.clone(), + ) + }; + let resolved_node = if (applied_expected_owner.clone() != std::option::Option::None) + { + applied_expected_owner.clone().unwrap() + } else { + if is_present_ctor.clone() { + { + let val_field = Rc::new({ + let mut __result = Vec::new(); + for fir in fi_infer_results.iter().cloned() { + if (crate::v1_std_core::field_init_node_name_at( + fir.typed_field.clone(), + scope.type_env.clone().source_indices.clone(), + ) == "value".to_string()) + { + __result.push(fir); + } } - } - __result - }) - .first() - .cloned(); - match val_field.clone() { - Some(val_fir) => crate::v1_std_core::with_optional_cardinality( - crate::v1_compiler_infer_types::resolved_type( - val_fir.infer_result.clone().typed.clone(), + __result + }) + .first() + .cloned(); + match val_field.clone() { + Some(val_fir) => crate::v1_std_core::with_optional_cardinality( + crate::v1_compiler_infer_types::resolved_type( + val_fir.infer_result.clone().typed.clone(), + ), ), - ), - std::option::Option::None => raw_resolved.clone(), + std::option::Option::None => raw_resolved.clone(), + } } + } else { + raw_resolved.clone() } - } else { - raw_resolved.clone() }; let type_diags = match effective_lookup.clone() { Some(_) => Rc::new(vec![]), @@ -18555,10 +18462,8 @@ pub fn maybe_insert_output_field_relation( let composed = match sig .output_provenance .clone() - .iter() + .get((idx.clone()) as usize) .cloned() - .skip(idx.clone() as usize) - .next() { Some(param_map) => { classify_call_via_provenance(val.clone(), param_map.clone(), ctx.clone()) @@ -19549,21 +19454,15 @@ pub fn classify_call_arg_provenance( pub fn merge_argument_relations(rels: Rc>>) -> Rc { match rels.clone().first().cloned() { std::option::Option::None => Rc::new(SubValueRelation::SubValueUnknown), - Some(first_rel) => Rc::new( - rels.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - first_rel.clone(), - |acc: Rc, rel: Rc| { - crate::std_induction::meet_sub_value(acc, rel.clone()) - }, - ), + Some(first_rel) => Rc::new(v1_rt::list_skip(&rels.clone(), 1)) + .iter() + .cloned() + .fold( + first_rel.clone(), + |acc: Rc, rel: Rc| { + crate::std_induction::meet_sub_value(acc, rel.clone()) + }, + ), } } @@ -20112,10 +20011,8 @@ pub fn build_call_evidence( } std::option::Option::None => match positional_args .clone() - .iter() + .get((def_idx.clone()) as usize) .cloned() - .skip(def_idx.clone() as usize) - .next() { Some(arg_val) => { read_arg_provenance(arg_val.clone(), pname.clone(), ctx.clone()) @@ -20273,7 +20170,7 @@ let p1 = match lparams.clone().first().cloned() { Some(n) => n.clone(), std::option::Option::None => "".to_string(), }; -let p2 = match lparams.clone().iter().cloned().skip(1 as usize).next() { +let p2 = match lparams.clone().get((1) as usize).cloned() { Some(n) => n.clone(), std::option::Option::None => "".to_string(), }; @@ -21027,10 +20924,8 @@ Rc::new(BlockAnnotateAcc { ); lparams .clone() - .iter() + .get((elem_position.clone().unwrap()) as usize) .cloned() - .skip(elem_position.clone().unwrap() as usize) - .next() } std::option::Option::None => std::option::Option::None, }; @@ -22118,12 +22013,7 @@ pub fn meet_per_field_results( let mut __result = Vec::new(); for r in results.iter().cloned() { __result.push( - match r - .clone() - .iter() - .cloned() - .skip(idx.clone() as usize) - .next() + match r.clone().get((idx.clone()) as usize).cloned() { Some(m) => m.clone(), std::option::Option::None => empty_prov_map(), @@ -22153,23 +22043,16 @@ pub fn meet_body_provenance_maps( ) -> Rc>> { match provs.clone().first().cloned() { std::option::Option::None => empty_prov_map(), - Some(first_prov) => Rc::new( - provs - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - first_prov.clone(), - |acc: Rc>>, - prov: Rc>>| { - intersect_prov_maps(acc, prov.clone()) - }, - ), + Some(first_prov) => Rc::new(v1_rt::list_skip(&provs.clone(), 1)) + .iter() + .cloned() + .fold( + first_prov.clone(), + |acc: Rc>>, + prov: Rc>>| { + intersect_prov_maps(acc, prov.clone()) + }, + ), } } @@ -22655,14 +22538,10 @@ if ((Rc::new(v1_rt::map_keys(&composed_field_map)).len() as i64) > 0) { acc.clone() } else { { - let final_lp = Rc::new( - stmts - .clone() - .iter() - .cloned() - .take(v1_rt::int_sub(stmt_count.clone(), 1) as usize) - .collect::>(), - ) + let final_lp = Rc::new(v1_rt::list_take( + &stmts.clone(), + v1_rt::int_sub(stmt_count.clone(), 1), + )) .iter() .cloned() .fold( @@ -22693,10 +22572,8 @@ if ((Rc::new(v1_rt::map_keys(&composed_field_map)).len() as i64) > 0) { ); match stmts .clone() - .iter() + .get((v1_rt::int_sub(stmt_count.clone(), 1)) as usize) .cloned() - .skip(v1_rt::int_sub(stmt_count.clone(), 1) as usize) - .next() { Some(last) => collect_variant_constructors( last.clone(), @@ -24007,15 +23884,7 @@ pub fn unify_callable_generics( pub fn lambda_callable_type(lambda: Rc) -> Rc { crate::v1_compiler_infer_types::make_callable_type( - Rc::new( - lambda - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&lambda.children.clone(), 1)), crate::v1_compiler_infer_types::resolved_type(lambda.clone()), ) } @@ -24041,10 +23910,8 @@ pub fn unify_callable_param_pairs( match formals.clone().first().cloned() { Some(f) => match actuals.clone().first().cloned() { Some(a) => { - let __tco_0 = - Rc::new(formals.iter().cloned().skip(1 as usize).collect::>()); - let __tco_1 = - Rc::new(actuals.iter().cloned().skip(1 as usize).collect::>()); + let __tco_0 = Rc::new(v1_rt::list_skip(&formals, 1)); + let __tco_1 = Rc::new(v1_rt::list_skip(&actuals, 1)); let __tco_2 = generic_names.clone(); let __tco_3 = source_indices.clone(); let __tco_4 = unify_generics( @@ -24548,30 +24415,23 @@ pub fn union_parent_type_env_caches( cache: crate::v1_compiler_infer_env::empty_type_env_cache(), conflicts: Rc::new(vec![]), }), - Some(head) => Rc::new( - parent_caches - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - Rc::new(GuardedTypeEnvCacheMerge { - cache: head.cache.clone(), - conflicts: Rc::new(vec![]), - }), - |acc: Rc, row: Rc| { - crate::v1_compiler_infer_env::merge_type_env_cache_guarded( - acc.cache.clone(), - row.cache.clone(), - row.import_path.clone(), - acc.conflicts.clone(), - ) - }, - ), + Some(head) => Rc::new(v1_rt::list_skip(&parent_caches.clone(), 1)) + .iter() + .cloned() + .fold( + Rc::new(GuardedTypeEnvCacheMerge { + cache: head.cache.clone(), + conflicts: Rc::new(vec![]), + }), + |acc: Rc, row: Rc| { + crate::v1_compiler_infer_env::merge_type_env_cache_guarded( + acc.cache.clone(), + row.cache.clone(), + row.import_path.clone(), + acc.conflicts.clone(), + ) + }, + ), } } } @@ -25478,32 +25338,84 @@ pub fn symbol_index_insert_unique_disj_variant_aliases( ) -> Rc { { let counts = disj_variant_name_counts(items.clone(), source_indices.clone()); - items.iter().cloned().fold(index.clone(), |acc: Rc, item: Rc| match item.connective.clone() { - Connective::Disj => { - let tname = crate::v1_std_core::authored_name_at(source_indices.clone(), item.clone()); -item.children.clone().iter().cloned().fold(acc.clone(), |a2: Rc, child: Rc| { - let vname = crate::v1_std_core::authored_name_at(source_indices.clone(), child.clone()); -let a2_qualified = crate::v1_compiler_infer_env::symbol_index_insert(a2, v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(module_path.clone(), ".".to_string()), tname.clone()), ".".to_string()), vname.clone()), item.clone()); -match v1_rt::map_get(&counts, vname.clone()) { - Some(1) => match v1_rt::map_get(&corpus_variant_counts, vname.clone()) { - Some(1) => if ((v1_rt::map_get(&corpus_item_counts, vname.clone()) != std::option::Option::None) || overlay_skips_kernel_name(vname.clone())) { - crate::v1_compiler_infer_env::symbol_index_insert(a2_qualified.clone(), v1_rt::concat(v1_rt::concat(module_path.clone(), ".".to_string()), vname.clone()), item.clone()) - } else { - crate::v1_compiler_infer_env::symbol_index_insert_decl(a2_qualified.clone(), module_path.clone(), Rc::new(TypeBinding { - name: vname.clone(), - resolved: item.clone(), - provenance: Rc::new(SubValueRelation::SubValueUnknown), - alias_rhs: std::option::Option::None, -})) - }, - _ => crate::v1_compiler_infer_env::symbol_index_insert(a2_qualified.clone(), v1_rt::concat(v1_rt::concat(module_path.clone(), ".".to_string()), vname.clone()), item.clone()), -}, - _ => a2_qualified.clone(), -} -}) -}, - _ => acc.clone(), -}) + items.iter().cloned().fold( + index.clone(), + |acc: Rc, item: Rc| match item.connective.clone() { + Connective::Disj => { + let tname = + crate::v1_std_core::authored_name_at(source_indices.clone(), item.clone()); + item.children.clone().iter().cloned().fold( + acc.clone(), + |a2: Rc, child: Rc| { + let vname = crate::v1_std_core::authored_name_at( + source_indices.clone(), + child.clone(), + ); + let a2_qualified = crate::v1_compiler_infer_env::symbol_index_insert( + a2, + v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(module_path.clone(), ".".to_string()), + tname.clone(), + ), + ".".to_string(), + ), + vname.clone(), + ), + item.clone(), + ); + match v1_rt::map_get(&counts, vname.clone()) { + Some(1) => match v1_rt::map_get( + &corpus_variant_counts, + vname.clone(), + ) { + Some(1) => if ((v1_rt::map_get( + &corpus_item_counts, + vname.clone(), + ) != std::option::Option::None) + || crate::v1_compiler_infer_env::overlay_skips_kernel_name( + vname.clone(), + )) { + crate::v1_compiler_infer_env::symbol_index_insert( + a2_qualified.clone(), + v1_rt::concat( + v1_rt::concat(module_path.clone(), ".".to_string()), + vname.clone(), + ), + item.clone(), + ) + } else { + crate::v1_compiler_infer_env::symbol_index_insert_decl( + a2_qualified.clone(), + module_path.clone(), + Rc::new(TypeBinding { + name: vname.clone(), + resolved: item.clone(), + provenance: Rc::new( + SubValueRelation::SubValueUnknown, + ), + alias_rhs: std::option::Option::None, + }), + ) + }, + _ => crate::v1_compiler_infer_env::symbol_index_insert( + a2_qualified.clone(), + v1_rt::concat( + v1_rt::concat(module_path.clone(), ".".to_string()), + vname.clone(), + ), + item.clone(), + ), + }, + _ => a2_qualified.clone(), + } + }, + ) + } + _ => acc.clone(), + }, + ) } } @@ -25965,6 +25877,38 @@ pub fn transparent_alias_identity_agrees( } } +pub fn file_named_imports_of( + module_node: Rc, + source_indices: Rc>>, +) -> Rc> { + Rc::new({ + let mut __result = Vec::new(); + for imp in crate::v1_std_core::module_imports(module_node.clone()) + .iter() + .cloned() + { + if !crate::v1_std_core::import_is_all(imp.clone()) { + __result.push(imp); + } + } + __result + }) + .iter() + .cloned() + .fold( + v1_rt::rc_empty_map::(), + |acc: Rc>, imp: Rc| { + let from_module = import_module_path_at(imp.clone(), source_indices.clone()); + crate::v1_std_core::import_specific_names_at(imp.clone(), source_indices.clone()) + .iter() + .cloned() + .fold(acc, |inner: Rc>, name: String| { + v1_rt::rc_map_insert(inner, name.clone(), from_module.clone()) + }) + }, + ) +} + pub fn build_symbol_index_census_raw_nodes( module_nodes: Rc>>, source_indices: Rc>>, @@ -26017,6 +25961,32 @@ pub fn build_symbol_index_census_raw_nodes( module_nodes.clone(), source_indices.clone(), ), + file_modules: module_nodes.iter().cloned().fold( + v1_rt::rc_empty_map::(), + |acc: Rc>, module_node: Rc| { + v1_rt::rc_map_insert( + acc, + module_node.span.clone().file.clone(), + crate::v1_std_core::authored_name_at( + source_indices.clone(), + module_node.clone(), + ), + ) + }, + ), + module_named_imports: module_nodes.iter().cloned().fold( + v1_rt::rc_empty_map::>>(), + |acc: Rc>>>, module_node: Rc| { + v1_rt::rc_map_insert( + acc, + crate::v1_std_core::authored_name_at( + source_indices.clone(), + module_node.clone(), + ), + file_named_imports_of(module_node.clone(), source_indices.clone()), + ) + }, + ), }) } } @@ -26581,6 +26551,8 @@ pub fn census_bare_fill_with_resolved_fn_sigs( services: services2.clone(), transparent_alias_rep: index.transparent_alias_rep.clone(), type_head_exposures: index.type_head_exposures.clone(), + file_modules: index.file_modules.clone(), + module_named_imports: index.module_named_imports.clone(), }) } } @@ -26625,6 +26597,8 @@ pub fn census_with_resolved_fn_sigs( services: fill.services.clone(), transparent_alias_rep: fill.transparent_alias_rep.clone(), type_head_exposures: fill.type_head_exposures.clone(), + file_modules: fill.file_modules.clone(), + module_named_imports: fill.module_named_imports.clone(), }) } } @@ -26668,6 +26642,14 @@ pub fn symbol_index_with_bare_fill( tree.type_head_exposures.clone(), closure.type_head_exposures.clone(), ), + file_modules: v1_rt::rc_map_merge( + tree.file_modules.clone(), + closure.file_modules.clone(), + ), + module_named_imports: v1_rt::rc_map_merge( + tree.module_named_imports.clone(), + closure.module_named_imports.clone(), + ), }) } } @@ -26753,6 +26735,14 @@ pub fn symbol_index_with_qualified_fill( fill.type_head_exposures.clone(), closure.type_head_exposures.clone(), ), + file_modules: v1_rt::rc_map_merge( + fill.file_modules.clone(), + closure.file_modules.clone(), + ), + module_named_imports: v1_rt::rc_map_merge( + fill.module_named_imports.clone(), + closure.module_named_imports.clone(), + ), }) } } @@ -26766,15 +26756,6 @@ pub fn direct_import_export_precedence_note() -> String { CACHED.with(|c: &String| c.clone()) } -pub fn overlay_skips_kernel_name(name: String) -> bool { - (((((crate::std_types::is_kernel_type(name.clone()) - || crate::std_types::is_container_type(name.clone())) - || (name.clone() == "Unit".to_string())) - || (name.clone() == "Optional".to_string())) - || (name.clone() == "Present".to_string())) - || (name.clone() == "Absent".to_string())) -} - pub fn overlay_direct_import_exports( ancestry_str_bindings: Rc>>, resolved_imports: Rc>>, @@ -26810,7 +26791,7 @@ pub fn overlay_direct_import_exports( selected.iter().cloned().fold( acc.clone(), |bacc: Rc>>, name: String| { - if overlay_skips_kernel_name(name.clone()) { + if crate::v1_compiler_infer_env::overlay_skips_kernel_name(name.clone()) { bacc.clone() } else { match v1_rt::map_get(&export_surface.str_bindings.clone(), name.clone()) @@ -27547,25 +27528,18 @@ pub fn build_type_env( source_indices.clone(), ); let parent_inductive_fields = match scope_parents.clone().first().cloned() { - Some(head_parent) => Rc::new( - scope_parents - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - head_parent.inductive_fields.clone(), - |acc: Rc>>>>, parent: Rc| { - crate::v1_compiler_infer_env::merge_inductive_fields( - acc, - parent.inductive_fields.clone(), - ) - }, - ), + Some(head_parent) => Rc::new(v1_rt::list_skip(&scope_parents.clone(), 1)) + .iter() + .cloned() + .fold( + head_parent.inductive_fields.clone(), + |acc: Rc>>>>, parent: Rc| { + crate::v1_compiler_infer_env::merge_inductive_fields( + acc, + parent.inductive_fields.clone(), + ) + }, + ), std::option::Option::None => { v1_rt::rc_empty_map::>>>() } @@ -28161,25 +28135,18 @@ pub fn build_type_env_unresolved( source_indices.clone(), ); let parent_inductive_fields = match scope_parents.clone().first().cloned() { - Some(head_parent) => Rc::new( - scope_parents - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() - .fold( - head_parent.inductive_fields.clone(), - |acc: Rc>>>>, parent: Rc| { - crate::v1_compiler_infer_env::merge_inductive_fields( - acc, - parent.inductive_fields.clone(), - ) - }, - ), + Some(head_parent) => Rc::new(v1_rt::list_skip(&scope_parents.clone(), 1)) + .iter() + .cloned() + .fold( + head_parent.inductive_fields.clone(), + |acc: Rc>>>>, parent: Rc| { + crate::v1_compiler_infer_env::merge_inductive_fields( + acc, + parent.inductive_fields.clone(), + ) + }, + ), std::option::Option::None => { v1_rt::rc_empty_map::>>>() } @@ -28511,13 +28478,7 @@ pub fn fold_module_contributions( std::option::Option::None => svc_locals.clone(), }; { - let __tco_0 = Rc::new( - remaining - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_0 = Rc::new(v1_rt::list_skip(&remaining, 1)); let __tco_1 = v1_rt::rc_list_push(resolved_items, contribution.resolved_item.clone()); let __tco_2 = next_func_sigs.clone(); @@ -31639,18 +31600,19 @@ pub fn enum_variant_shape_sets_for_item( acc: Rc, item: Rc, si: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc { { let item_name = crate::v1_std_core::authored_name_at(si.clone(), item.clone()); - let is_enum = match v1_rt::map_get(&type_summaries, item_name.clone()) { - Some(summary) => match (*summary.repr.clone()).clone() { - TypeRepr::EnumRepr { unit_only: _, .. } => true, - _ => false, - }, - std::option::Option::None => false, - }; - if is_enum.clone() { + if match (*crate::v1_compiler_infer_emit_info::is_enum_in_summaries( + type_summaries.clone(), + item_name.clone(), + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + } { item.children.clone().iter().cloned().fold( acc, |vacc: Rc, variant: Rc| { @@ -31692,7 +31654,7 @@ pub fn enum_variant_shape_sets_for_item( pub fn build_enum_variant_shape_sets( modules: Rc>>, - type_summaries: Rc>>, + type_summaries: Rc, ) -> Rc { modules.iter().cloned().fold( Rc::new(EnumVariantShapeSets { @@ -31817,7 +31779,7 @@ pub fn build_emit_graph_info( std::option::Option::None => crate::v1_compiler_infer_env::empty_symbol_index(), }; let init = Rc::new(EmitInfoBuildState { - type_summaries: v1_rt::rc_empty_map::>(), + type_summaries: crate::v1_compiler_infer_emit_info::empty_type_summary_index(), type_decl_items: crate::v1_compiler_infer_emit_info::type_decl_index_with_qualified_names( crate::v1_compiler_infer_emit_info::empty_type_decl_index(), diff --git a/src/v1/stage0/src/v1_compiler_infer_access.rs b/src/v1/stage0/src/v1_compiler_infer_access.rs index 135f7d12523..c22068e6c1a 100644 --- a/src/v1/stage0/src/v1_compiler_infer_access.rs +++ b/src/v1/stage0/src/v1_compiler_infer_access.rs @@ -86,7 +86,7 @@ pub fn keyed_collection_parts( && ((n.children.clone().len() as i64) >= 2)) { match n.children.clone().first().cloned() { - Some(key_child) => match n.children.clone().iter().cloned().skip(1 as usize).next() { + Some(key_child) => match n.children.clone().get((1) as usize).cloned() { Some(value_child) => Some(Rc::new(KeyedCollectionParts { key_type: crate::v1_compiler_infer_types::resolved_type(key_child.clone()), value_type: crate::v1_compiler_infer_types::resolved_type(value_child.clone()), diff --git a/src/v1/stage0/src/v1_compiler_infer_emit_info.rs b/src/v1/stage0/src/v1_compiler_infer_emit_info.rs index fc2047dca7a..f97716ea202 100644 --- a/src/v1/stage0/src/v1_compiler_infer_emit_info.rs +++ b/src/v1/stage0/src/v1_compiler_infer_emit_info.rs @@ -2,21 +2,36 @@ // Source module: v1.compiler.infer_emit_info use self::DataVariantWireSpelling::*; +use self::TypeDeclOccurrenceBinding::*; use self::TypeDeclReferenceRoute::*; use self::TypeDeclResolution::*; use self::TypeRepr::*; +use self::TypeSummaryLookup::*; +use self::TypeSummaryQuestion::*; +pub use crate::gunbc_structural_realization_bindings::kernel_mint_declaration_rows; use crate::std_decl_ref::DeclField::{NamedField, TypeParameter, WholeDeclaration}; pub use crate::std_decl_ref::{DeclField, DeclarationRef}; pub use crate::std_dissolution::DissolutionCondition; use crate::std_dissolution::DissolutionCondition::*; pub use crate::std_dissolution::{dissolution_description, unbound_dissolution}; +pub use crate::std_literal_elaboration::kernel_mint_declaration_for; +pub use crate::std_literal_elaboration::KernelMintDeclarationLookup; +use crate::std_literal_elaboration::KernelMintDeclarationLookup::{ + KernelMintDeclarationAbsent, KernelMintDeclarationAmbiguous, KernelMintDeclarationFound, +}; pub use crate::std_types::is_kernel_type; pub use crate::std_types::SourceSpan; pub use crate::v1_compiler_artifact::RenderTarget; use crate::v1_compiler_artifact::RenderTarget::Rust; pub use crate::v1_compiler_coercion::{declaration_realization, realized_checkpoint}; -pub use crate::v1_compiler_infer_env::{empty_symbol_index, empty_type_env, symbol_index_lookup}; -pub use crate::v1_compiler_infer_env::{SymbolIndex, TypeEnv}; +use crate::v1_compiler_infer_env::BareOccurrenceBinding::{ + BareOccurrenceDeclaredInItsFile, BareOccurrenceImportedByName, BareOccurrenceIsKernelName, + BareOccurrenceUndecided, +}; +pub use crate::v1_compiler_infer_env::{ + bare_occurrence_binding, empty_symbol_index, empty_type_env, symbol_index_lookup, +}; +pub use crate::v1_compiler_infer_env::{BareOccurrenceBinding, SymbolIndex, TypeEnv}; use crate::v1_compiler_infer_types::TextJudgment::{TextJudgedIn, TextNotAsked}; use crate::v1_compiler_infer_types::TextNotAskedReason::{ TextNotAskedForCallableComponentResidue, TextNotAskedInVariantFieldSummary, @@ -77,6 +92,7 @@ impl TypeRepr { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct TypeSummary { pub name: String, + pub key: String, pub repr: Rc, pub field_summaries: Rc>>, pub field_type_map: Rc>, @@ -86,6 +102,173 @@ pub struct TypeSummary { pub has_fn_fields: bool, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct TypeSummaryIndex { + pub by_key: Rc>>, + pub keys_by_leaf: Rc>>>, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum TypeSummaryLookup { + TypeSummaryFound { summary: Rc }, + TypeSummaryLeafAmbiguous { leaf: String }, + TypeSummaryNotDeclared, +} + +pub fn empty_type_summary_index() -> Rc { + Rc::new(TypeSummaryIndex { + by_key: v1_rt::rc_empty_map::>(), + keys_by_leaf: v1_rt::rc_empty_map::>>(), + }) +} + +pub fn type_summary_index_insert( + index: Rc, + key: String, + leaf: String, + summary: Rc, +) -> Rc { + Rc::new(TypeSummaryIndex { + by_key: v1_rt::rc_map_insert(index.by_key.clone(), key.clone(), summary.clone()), + keys_by_leaf: v1_rt::rc_map_insert( + index.keys_by_leaf.clone(), + leaf.clone(), + type_decl_leaf_identities_with( + v1_rt::map_get(&index.keys_by_leaf.clone(), leaf.clone()), + key.clone(), + ), + ), + }) +} + +pub fn type_summary_keys_of_leaf(index: Rc, leaf: String) -> Rc> { + match v1_rt::map_get(&index.keys_by_leaf.clone(), leaf.clone()) { + Some(keys) => keys.clone(), + std::option::Option::None => Rc::new(vec![]), + } +} + +pub fn type_summary_at_key(index: Rc, key: String) -> Rc { + match v1_rt::map_get(&index.by_key.clone(), key.clone()) { + Some(summary) => Rc::new(TypeSummaryLookup::TypeSummaryFound { + summary: summary.clone(), + }), + std::option::Option::None => Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared), + } +} + +pub fn type_summary_by_leaf(index: Rc, leaf: String) -> Rc { + { + let keys = type_summary_keys_of_leaf(index.clone(), leaf.clone()); + if ((keys.clone().len() as i64) == 1) { + keys.iter().cloned().fold( + Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared), + |_acc: Rc, key: String| { + type_summary_at_key(index.clone(), key.clone()) + }, + ) + } else { + if ((keys.clone().len() as i64) > 1) { + Rc::new(TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: leaf.clone() }) + } else { + Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared) + } + } + } +} + +pub fn type_summary_lookup(index: Rc, key: String) -> Rc { + stacker::maybe_grow(512 * 1024, 2 * 1024 * 1024, || { + match v1_rt::map_get(&index.by_key.clone(), key.clone()) { + Some(summary) => Rc::new(TypeSummaryLookup::TypeSummaryFound { + summary: summary.clone(), + }), + std::option::Option::None => { + if v1_rt::contains(key.clone(), "::".to_string()) { + { + let parts = Rc::new( + key.clone() + .split(&"::".to_string()) + .map(|s| s.to_string()) + .collect::>(), + ); + let enum_part = Rc::new(v1_rt::list_take( + &parts.clone(), + v1_rt::int_sub((parts.clone().len() as i64), 1), + )) + .join(&"::".to_string()); + let variant_part = match parts.clone().last().cloned() { + Some(v) => v.clone(), + std::option::Option::None => "".to_string(), + }; + match (*type_summary_lookup(index.clone(), enum_part.clone())).clone() { + TypeSummaryLookup::TypeSummaryFound { summary: owner, .. } => { + type_summary_at_key( + index.clone(), + variant_summary_key(owner.key.clone(), variant_part.clone()), + ) + } + TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: l, .. } => { + Rc::new(TypeSummaryLookup::TypeSummaryLeafAmbiguous { + leaf: l.clone(), + }) + } + TypeSummaryLookup::TypeSummaryNotDeclared => { + Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared) + } + } + } + } else { + if v1_rt::contains(key.clone(), ".".to_string()) { + Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared) + } else { + type_summary_by_leaf(index.clone(), key.clone()) + } + } + } + } + }) +} + +pub fn type_summary_of_reference( + index: Rc, + decls: Rc, + type_expr: Rc, + source_indices: Rc>>, +) -> Rc { + match (*resolve_type_decl_reference(decls.clone(), type_expr.clone(), source_indices.clone())) + .clone() + { + TypeDeclResolution::TypeDeclResolved { identity, .. } => { + type_summary_at_key(index.clone(), identity.clone()) + } + TypeDeclResolution::TypeDeclLeafAmbiguous { leaf: l, .. } => { + Rc::new(TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: l.clone() }) + } + TypeDeclResolution::TypeDeclKernelSpellingUndecided { leaf: l, .. } => { + Rc::new(TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: l.clone() }) + } + TypeDeclResolution::TypeDeclNotDeclared => { + Rc::new(TypeSummaryLookup::TypeSummaryNotDeclared) + } + } +} + +pub fn type_summary_decided(index: Rc, key: String) -> Option> { + match (*type_summary_lookup(index.clone(), key.clone())).clone() { + TypeSummaryLookup::TypeSummaryFound { + summary: summary, .. + } => Some(summary.clone()), + TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: _, .. } => std::option::Option::None, + TypeSummaryLookup::TypeSummaryNotDeclared => std::option::Option::None, + } +} + +pub fn type_summary_values(index: Rc) -> Rc>> { + Rc::new(v1_rt::map_values(&index.by_key.clone())) +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FieldTypeMapBuild { pub field_types: Rc>, @@ -231,6 +414,31 @@ pub struct TypeDeclIndex { pub qualified_names: Rc, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum TypeDeclOccurrenceBinding { + OccurrenceDeclaredHere { identity: String }, + OccurrenceImported { identity: String }, + OccurrenceIsKernelName, + OccurrenceUndecided, +} +impl TypeDeclOccurrenceBinding { + pub fn identity(&self) -> String { + match self { + TypeDeclOccurrenceBinding::OccurrenceDeclaredHere { + identity: __val, .. + } => __val.clone(), + TypeDeclOccurrenceBinding::OccurrenceImported { + identity: __val, .. + } => __val.clone(), + TypeDeclOccurrenceBinding::OccurrenceIsKernelName => { + panic!("no identity on unit variant") + } + TypeDeclOccurrenceBinding::OccurrenceUndecided => panic!("no identity on unit variant"), + } + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(tag = "_variant")] pub enum TypeDeclResolution { @@ -401,6 +609,8 @@ pub enum TypeDeclReferenceRoute { RouteDeclaringSpan, RouteReferencingModuleDeclarer, RouteQualifiedSpelling, + RouteReferencingModuleImport, + RouteKernelMint, RouteLeafSpelling, } @@ -465,7 +675,7 @@ pub fn resolve_type_decl_routed( let referencing_module_identity = match (*by_spelling.clone()).clone() { TypeDeclResolution::TypeDeclLeafAmbiguous { leaf: l, .. } => { if (authored.clone() == l.clone()) { - type_decl_referencing_module_declarer( + type_decl_occurrence_identity( index.clone(), l.clone(), type_expr.clone(), @@ -482,11 +692,12 @@ pub fn resolve_type_decl_routed( }; match referencing_module_identity.clone() { Some(identity) => Rc::new(TypeDeclRoutedResolution { - route: if (authored.clone() == leaf.clone()) { - TypeDeclReferenceRoute::RouteReferencingModuleDeclarer - } else { - TypeDeclReferenceRoute::RouteQualifiedSpelling - }, + route: type_decl_spelling_route( + index.clone(), + authored.clone(), + leaf.clone(), + type_expr.clone(), + ), resolution: type_decl_resolution_at_identity( index.clone(), identity.clone(), @@ -519,6 +730,136 @@ pub fn type_decl_qualifier_declarer(index: Rc, authored: String) } } +pub fn type_decl_kernel_mint_declarer(index: Rc, leaf: String) -> Option { + match (*crate::std_literal_elaboration::kernel_mint_declaration_for( + kernel_mint_declaration_rows(), + leaf.clone(), + )) + .clone() + { + KernelMintDeclarationLookup::KernelMintDeclarationFound { declaration: d, .. } => { + let identity = type_decl_identity(d.module_path.clone(), d.decl_name.clone()); + match v1_rt::map_get(&index.by_identity.clone(), identity.clone()) { + Some(_) => Some(identity.clone()), + std::option::Option::None => std::option::Option::None, + } + } + KernelMintDeclarationLookup::KernelMintDeclarationAbsent => std::option::Option::None, + KernelMintDeclarationLookup::KernelMintDeclarationAmbiguous { row_count: _, .. } => { + std::option::Option::None + } + } +} + +pub fn type_decl_spelling_route( + index: Rc, + authored: String, + leaf: String, + reference: Rc, +) -> TypeDeclReferenceRoute { + if (authored.clone() != leaf.clone()) { + TypeDeclReferenceRoute::RouteQualifiedSpelling + } else { + match (*type_decl_occurrence_binding(index.clone(), leaf.clone(), reference.clone())) + .clone() + { + TypeDeclOccurrenceBinding::OccurrenceDeclaredHere { identity: _, .. } => { + TypeDeclReferenceRoute::RouteReferencingModuleDeclarer + } + TypeDeclOccurrenceBinding::OccurrenceIsKernelName => { + TypeDeclReferenceRoute::RouteKernelMint + } + TypeDeclOccurrenceBinding::OccurrenceImported { identity: _, .. } => { + TypeDeclReferenceRoute::RouteReferencingModuleImport + } + TypeDeclOccurrenceBinding::OccurrenceUndecided => { + TypeDeclReferenceRoute::RouteLeafSpelling + } + } + } +} + +pub fn type_decl_occurrence_identity( + index: Rc, + leaf: String, + reference: Rc, +) -> Option { + match (*type_decl_occurrence_binding(index.clone(), leaf.clone(), reference.clone())).clone() { + TypeDeclOccurrenceBinding::OccurrenceDeclaredHere { + identity: identity, .. + } => Some(identity.clone()), + TypeDeclOccurrenceBinding::OccurrenceIsKernelName => { + type_decl_kernel_mint_declarer(index.clone(), leaf.clone()) + } + TypeDeclOccurrenceBinding::OccurrenceImported { + identity: identity, .. + } => Some(identity.clone()), + TypeDeclOccurrenceBinding::OccurrenceUndecided => std::option::Option::None, + } +} + +pub fn type_decl_occurrence_binding( + index: Rc, + leaf: String, + reference: Rc, +) -> Rc { + match (*crate::v1_compiler_infer_env::bare_occurrence_binding( + index.qualified_names.clone(), + leaf.clone(), + reference.clone(), + )) + .clone() + { + BareOccurrenceBinding::BareOccurrenceDeclaredInItsFile { + declaration: decl, .. + } => match type_decl_identity_of_declaration(index.clone(), decl.clone()) { + Some(identity) => Rc::new(TypeDeclOccurrenceBinding::OccurrenceDeclaredHere { + identity: identity.clone(), + }), + std::option::Option::None => Rc::new(TypeDeclOccurrenceBinding::OccurrenceUndecided), + }, + BareOccurrenceBinding::BareOccurrenceImportedByName { + module_path: from_module, + declaration: decl, + .. + } => match type_decl_identity_of_declaration(index.clone(), decl.clone()) { + Some(identity) => Rc::new(TypeDeclOccurrenceBinding::OccurrenceImported { + identity: identity.clone(), + }), + std::option::Option::None => { + let imported = type_decl_identity(from_module.clone(), leaf.clone()); + match v1_rt::map_get(&index.by_identity.clone(), imported.clone()) { + Some(_) => Rc::new(TypeDeclOccurrenceBinding::OccurrenceImported { + identity: imported.clone(), + }), + std::option::Option::None => { + Rc::new(TypeDeclOccurrenceBinding::OccurrenceUndecided) + } + } + } + }, + BareOccurrenceBinding::BareOccurrenceIsKernelName => { + Rc::new(TypeDeclOccurrenceBinding::OccurrenceIsKernelName) + } + BareOccurrenceBinding::BareOccurrenceUndecided => { + Rc::new(TypeDeclOccurrenceBinding::OccurrenceUndecided) + } + } +} + +pub fn type_decl_identity_of_declaration( + index: Rc, + decl: Rc, +) -> Option { + match decl.ident_span.clone() { + Some(sp) => v1_rt::map_get( + &index.identity_by_declaring_span.clone(), + declaring_span_key(sp.clone()), + ), + std::option::Option::None => std::option::Option::None, + } +} + pub fn type_decl_index_with_qualified_names( index: Rc, qualified_names: Rc, @@ -535,53 +876,12 @@ pub fn type_decl_index_with_qualified_names( pub fn referencing_module_declarer_route_dissolves_on() -> Rc { thread_local! { static CACHED: Rc = { - crate::std_dissolution::unbound_dissolution("RouteReferencingModuleDeclarer and RouteQualifiedSpelling dissolve when the type environment's declaration bodies carry Node.declaration on every member type reference, stamped by v1.compiler.infer_resolve reference_with_declaration in the DECLARING module's scope, so a bare reference resolved through lookup_type_for hands the emitter stamped members: v1.tests.claim.carrier_realization_census reports zero emitter_decl_route referencing_module_declarer or qualified_spelling rows over the self-host and dag closures. Stamping one declaration, or one closure, is not that capability. Then both arms, type_decl_referencing_module_declarer and type_decl_qualifier_declarer are deleted and such a reference refuses unless carried.".to_string()) + crate::std_dissolution::unbound_dissolution("RouteReferencingModuleDeclarer, RouteReferencingModuleImport, RouteQualifiedSpelling and RouteKernelMint dissolve when the type environment's declaration bodies carry Node.declaration on every member type reference, stamped by v1.compiler.infer_resolve reference_with_declaration in the DECLARING module's scope, so a bare reference resolved through lookup_type_for hands the emitter stamped members: v1.tests.claim.carrier_realization_census reports zero emitter_decl_route referencing_module_declarer, referencing_module_import, qualified_spelling or kernel_mint rows over the self-host and dag closures. Stamping one declaration, or one closure, is not that capability. Then type_decl_occurrence_binding and type_decl_qualifier_declarer are deleted and such a reference refuses unless carried.".to_string()) }; } CACHED.with(|c: &Rc| c.clone()) } -pub fn type_decl_referencing_module_declarer( - index: Rc, - leaf: String, - reference: Rc, -) -> Option { - { - let local = Rc::new({ - let mut __result = Vec::new(); - for identity in type_decl_identities_of_leaf(index.clone(), leaf.clone()) - .iter() - .cloned() - { - if match v1_rt::map_get(&index.by_identity.clone(), identity.clone()) { - Some(decl) => { - (decl.span.clone().file.clone() == reference.span.clone().file.clone()) - } - std::option::Option::None => false, - } { - __result.push(identity); - } - } - __result - }); - if (local - .iter() - .cloned() - .fold(0, |n: i64, _identity: String| v1_rt::int_add(n, 1)) - == 1) - { - local - .iter() - .cloned() - .fold(std::option::Option::None, |acc: _, identity: String| { - Some(identity.clone()) - }) - } else { - std::option::Option::None - } - } -} - pub fn resolve_type_decl_reference( index: Rc, type_expr: Rc, @@ -602,6 +902,10 @@ pub fn type_decl_routed_resolution_label(routed: Rc) - "referencing_module_declarer".to_string() } TypeDeclReferenceRoute::RouteQualifiedSpelling => "qualified_spelling".to_string(), + TypeDeclReferenceRoute::RouteReferencingModuleImport => { + "referencing_module_import".to_string() + } + TypeDeclReferenceRoute::RouteKernelMint => "kernel_mint".to_string(), TypeDeclReferenceRoute::RouteLeafSpelling => "leaf_spelling".to_string(), }; match (*routed.resolution.clone()).clone() { @@ -628,7 +932,7 @@ pub fn type_decl_routed_resolution_label(routed: Rc) - #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EmitGraphInfo { - pub type_summaries: Rc>>, + pub type_summaries: Rc, pub type_decl_items: Rc, pub data_variant_wire_spellings: Rc>>, pub fn_decl_items: Rc>>, @@ -654,7 +958,7 @@ pub struct EmitGraphInfo { #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct EmitInfoBuildState { - pub type_summaries: Rc>>, + pub type_summaries: Rc, pub type_decl_items: Rc, pub fn_decl_items: Rc>>, pub structural_alias_fn_surface_names: Rc>>>, @@ -664,7 +968,7 @@ pub struct EmitInfoBuildState { pub fn empty_emit_graph_info() -> Rc { Rc::new(EmitGraphInfo { item_leaf_owner_modules: v1_rt::rc_empty_map::>(), - type_summaries: v1_rt::rc_empty_map::>(), + type_summaries: empty_type_summary_index(), type_decl_items: empty_type_decl_index(), data_variant_wire_spellings: v1_rt::rc_empty_map::>(), fn_decl_items: v1_rt::rc_empty_map::>(), @@ -804,7 +1108,80 @@ pub fn lookup_emit_type_summary( emit_info: Rc, type_name: String, ) -> Option> { - v1_rt::map_get(&emit_info.type_summaries.clone(), type_name.clone()) + type_summary_decided(emit_info.type_summaries.clone(), type_name.clone()) +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum TypeSummaryQuestion { + QuestionDecided { value: bool }, + QuestionNameAmbiguous { leaf: String }, + QuestionNotDeclared, +} + +pub fn type_summary_answer( + index: Rc, + key: String, + question: impl Fn(Rc) -> bool + Clone, +) -> Rc { + match (*type_summary_lookup(index.clone(), key.clone())).clone() { + TypeSummaryLookup::TypeSummaryFound { + summary: summary, .. + } => Rc::new(TypeSummaryQuestion::QuestionDecided { + value: question(summary.clone()), + }), + TypeSummaryLookup::TypeSummaryNotDeclared => { + Rc::new(TypeSummaryQuestion::QuestionNotDeclared) + } + TypeSummaryLookup::TypeSummaryLeafAmbiguous { leaf: leaf, .. } => { + let answers = type_summary_keys_of_leaf(index.clone(), leaf.clone()) + .iter() + .cloned() + .fold( + Rc::new(vec![]), + |acc: Rc>, k: String| match v1_rt::map_get( + &index.by_key.clone(), + k.clone(), + ) { + Some(summary) => { + v1_rt::rc_list_push(acc.clone(), question(summary.clone())) + } + std::option::Option::None => acc.clone(), + }, + ); + if ((answers.clone().len() as i64) == 0) { + Rc::new(TypeSummaryQuestion::QuestionNotDeclared) + } else { + if { + let mut __all = true; + for a in answers.iter().cloned() { + if !(a.clone()) { + __all = false; + break; + } + } + __all + } { + Rc::new(TypeSummaryQuestion::QuestionDecided { value: true }) + } else { + if { + let mut __all = true; + for a in answers.iter().cloned() { + if !(!a.clone()) { + __all = false; + break; + } + } + __all + } { + Rc::new(TypeSummaryQuestion::QuestionDecided { value: false }) + } else { + Rc::new(TypeSummaryQuestion::QuestionNameAmbiguous { leaf: leaf.clone() }) + } + } + } + } + } } pub fn emit_graph_records_type_decl( @@ -826,10 +1203,8 @@ pub fn emit_graph_records_fn_decl(item: Rc) -> bool { (item.module_item_kind.clone() == ParsedModuleItemKind::ModuleItemFunction) } -pub fn derive_variant_to_enum( - type_summaries: Rc>>, -) -> Rc> { - Rc::new(v1_rt::map_values(&type_summaries)) +pub fn derive_variant_to_enum(type_summaries: Rc) -> Rc> { + type_summary_values(type_summaries.clone()) .iter() .cloned() .fold( @@ -855,7 +1230,7 @@ pub fn derive_variant_to_enum( std::option::Option::None => v1_rt::rc_map_insert( inner.clone(), vn.clone(), - summary.name.clone(), + summary.key.clone(), ), }, ) @@ -865,13 +1240,10 @@ pub fn derive_variant_to_enum( ) } -pub fn is_known_variant( - type_summaries: Rc>>, - name: String, -) -> bool { +pub fn is_known_variant(type_summaries: Rc, name: String) -> bool { { let mut __found = false; - for summary in Rc::new(v1_rt::map_values(&type_summaries)).iter().cloned() { + for summary in type_summary_values(type_summaries.clone()).iter().cloned() { if match (*summary.repr.clone()).clone() { TypeRepr::EnumRepr { unit_only: _, .. } => { crate::v1_compiler_infer_types::emit_map_has( @@ -889,54 +1261,89 @@ pub fn is_known_variant( } } +pub fn summary_is_enum_with_variant(summary: Rc, variant_name: String) -> bool { + match (*summary.repr.clone()).clone() { + TypeRepr::EnumRepr { unit_only: _, .. } => crate::v1_compiler_infer_types::emit_map_has( + summary.variant_name_set.clone(), + variant_name.clone(), + ), + _ => false, + } +} + +pub fn summary_is_enum(summary: Rc) -> bool { + match (*summary.repr.clone()).clone() { + TypeRepr::EnumRepr { unit_only: _, .. } => true, + _ => false, + } +} + pub fn variant_belongs_to_enum( - type_summaries: Rc>>, + type_summaries: Rc, variant_name: String, enum_name: String, -) -> bool { - match v1_rt::map_get(&type_summaries, enum_name.clone()) { - Some(summary) => match (*summary.repr.clone()).clone() { - TypeRepr::EnumRepr { unit_only: _, .. } => { - crate::v1_compiler_infer_types::emit_map_has( - summary.variant_name_set.clone(), - variant_name.clone(), - ) - } - _ => false, - }, - std::option::Option::None => false, - } +) -> Rc { + type_summary_answer(type_summaries.clone(), enum_name.clone(), |summary| { + summary_is_enum_with_variant(summary.clone(), variant_name.clone()) + }) } pub fn is_enum_in_summaries( - type_summaries: Rc>>, + type_summaries: Rc, type_name: String, -) -> bool { - match v1_rt::map_get(&type_summaries, type_name.clone()) { - Some(summary) => match (*summary.repr.clone()).clone() { - TypeRepr::EnumRepr { unit_only: _, .. } => true, - _ => false, - }, - std::option::Option::None => false, - } +) -> Rc { + type_summary_answer(type_summaries.clone(), type_name.clone(), |summary| { + summary_is_enum(summary.clone()) + }) } pub fn find_variant_parent( - type_summaries: Rc>>, + type_summaries: Rc, variant_name: String, scope_enums: Rc>, ) -> Option { - Rc::new({ - let mut __result = Vec::new(); + if { + let mut __found = false; for en in scope_enums.iter().cloned() { - if variant_belongs_to_enum(type_summaries.clone(), variant_name.clone(), en.clone()) { - __result.push(en); + if match (*variant_belongs_to_enum( + type_summaries.clone(), + variant_name.clone(), + en.clone(), + )) + .clone() + { + TypeSummaryQuestion::QuestionNameAmbiguous { leaf: _, .. } => true, + _ => false, + } { + __found = true; + break; } } - __result - }) - .first() - .cloned() + __found + } { + std::option::Option::None + } else { + Rc::new({ + let mut __result = Vec::new(); + for en in scope_enums.iter().cloned() { + if match (*variant_belongs_to_enum( + type_summaries.clone(), + variant_name.clone(), + en.clone(), + )) + .clone() + { + TypeSummaryQuestion::QuestionDecided { value: v, .. } => v.clone(), + _ => false, + } { + __result.push(en); + } + } + __result + }) + .first() + .cloned() + } } pub fn field_value_shape_from_type_node(type_node: Rc) -> FieldValueShape { @@ -1264,6 +1671,7 @@ pub fn build_type_summary( source_indices.clone(), item.clone(), ), + key: "".to_string(), repr: Rc::new(TypeRepr::StructRepr), field_summaries: build_struct_field_summaries( item.clone(), @@ -1293,6 +1701,7 @@ pub fn build_type_summary( source_indices.clone(), item.clone(), ), + key: "".to_string(), repr: Rc::new(TypeRepr::EnumRepr { unit_only: unit_only.clone(), }), @@ -1380,12 +1789,22 @@ pub struct FnReachRound { pub grew: bool, } +pub fn fn_reach_target_keys(index: Rc, target: String) -> Rc> { + match v1_rt::map_get(&index.by_key.clone(), target.clone()) { + Some(_) => Rc::new(vec![target.clone()]), + std::option::Option::None => v1_rt::concat( + type_summary_keys_of_leaf(index.clone(), target.clone()), + Rc::new(vec![target.clone()]), + ), + } +} + pub fn fn_reach_holds_directly( name: String, - summaries: Rc>>, + summaries: Rc, structural_alias_direct_fn_names: Rc>, ) -> bool { - match v1_rt::map_get(&summaries, name.clone()) { + match v1_rt::map_get(&summaries.by_key.clone(), name.clone()) { Some(s) => s.has_fn_fields.clone(), std::option::Option::None => { v1_rt::set_contains(&structural_alias_direct_fn_names, name.clone()) @@ -1395,26 +1814,34 @@ pub fn fn_reach_holds_directly( pub fn fn_reach_targets( name: String, - summaries: Rc>>, + summaries: Rc, structural_alias_fn_surface_names: Rc>>>, ) -> Rc> { - match v1_rt::map_get(&summaries, name.clone()) { - Some(s) => v1_rt::concat( - Rc::new(v1_rt::map_values(&s.field_type_map.clone())), - s.field_import_surface_names.clone(), - ), - std::option::Option::None => { - match v1_rt::map_get(&structural_alias_fn_surface_names, name.clone()) { - Some(targets) => targets.clone(), - std::option::Option::None => Rc::new(vec![]), + { + let spelled = match v1_rt::map_get(&summaries.by_key.clone(), name.clone()) { + Some(s) => v1_rt::concat( + Rc::new(v1_rt::map_values(&s.field_type_map.clone())), + s.field_import_surface_names.clone(), + ), + std::option::Option::None => { + match v1_rt::map_get(&structural_alias_fn_surface_names, name.clone()) { + Some(targets) => targets.clone(), + std::option::Option::None => Rc::new(vec![]), + } } - } + }; + spelled + .iter() + .cloned() + .fold(Rc::new(vec![]), |acc: Rc>, t: String| { + v1_rt::concat(acc, fn_reach_target_keys(summaries.clone(), t.clone())) + }) } } pub fn fn_reaching_names_fixpoint( mut __tco_loop_candidates: Rc>, - mut __tco_loop_summaries: Rc>>, + mut __tco_loop_summaries: Rc, mut __tco_loop_structural_alias_fn_surface_names: Rc>>>, mut __tco_loop_structural_alias_direct_fn_names: Rc>, mut __tco_loop_reached: Rc>, @@ -1497,13 +1924,13 @@ pub fn fn_reaching_names_fixpoint( } pub fn fn_reaching_names( - summaries: Rc>>, + summaries: Rc, structural_alias_fn_surface_names: Rc>>>, structural_alias_direct_fn_names: Rc>, ) -> Rc> { fn_reaching_names_fixpoint( v1_rt::concat( - Rc::new(v1_rt::map_keys(&summaries)), + Rc::new(v1_rt::map_keys(&summaries.by_key.clone())), Rc::new(v1_rt::map_keys(&structural_alias_fn_surface_names)), ), summaries.clone(), @@ -1514,45 +1941,56 @@ pub fn fn_reaching_names( } pub fn close_fn_fields( - summaries: Rc>>, + summaries: Rc, structural_alias_fn_surface_names: Rc>>>, structural_alias_direct_fn_names: Rc>, -) -> Rc>> { +) -> Rc { { let reaching = fn_reaching_names( summaries.clone(), structural_alias_fn_surface_names.clone(), structural_alias_direct_fn_names.clone(), ); - Rc::new(v1_rt::map_keys(&summaries)).iter().cloned().fold( - summaries.clone(), - |acc: Rc>>, name: String| match v1_rt::map_get( - &summaries, - name.clone(), - ) { - Some(s) => { - if (!s.has_fn_fields.clone() && v1_rt::set_contains(&reaching, name.clone())) { - v1_rt::rc_map_insert( - acc.clone(), - name.clone(), - Rc::new(TypeSummary { - name: s.name.clone(), - repr: s.repr.clone(), - field_summaries: s.field_summaries.clone(), - field_type_map: s.field_type_map.clone(), - field_import_surface_names: s.field_import_surface_names.clone(), - variant_name_set: s.variant_name_set.clone(), - generic_param_names: s.generic_param_names.clone(), - has_fn_fields: true, - }), - ) - } else { - acc.clone() - } - } - std::option::Option::None => acc.clone(), - }, - ) + Rc::new(TypeSummaryIndex { + by_key: Rc::new(v1_rt::map_keys(&summaries.by_key.clone())) + .iter() + .cloned() + .fold( + summaries.by_key.clone(), + |acc: Rc>>, key: String| match v1_rt::map_get( + &summaries.by_key.clone(), + key.clone(), + ) { + Some(s) => { + if (!s.has_fn_fields.clone() + && v1_rt::set_contains(&reaching, key.clone())) + { + v1_rt::rc_map_insert( + acc.clone(), + key.clone(), + Rc::new(TypeSummary { + name: s.name.clone(), + key: s.key.clone(), + repr: s.repr.clone(), + field_summaries: s.field_summaries.clone(), + field_type_map: s.field_type_map.clone(), + field_import_surface_names: s + .field_import_surface_names + .clone(), + variant_name_set: s.variant_name_set.clone(), + generic_param_names: s.generic_param_names.clone(), + has_fn_fields: true, + }), + ) + } else { + acc.clone() + } + } + std::option::Option::None => acc.clone(), + }, + ), + keys_by_leaf: summaries.keys_by_leaf.clone(), + }) } } @@ -1651,13 +2089,15 @@ pub fn add_emit_item_summary( state.clone() }; match build_type_summary(item.clone(), source_indices.clone()) { - Some(summary) => { + Some(built) => { + let key = type_decl_identity(module_name.clone(), built.name.clone()); + let summary = type_summary_with_key(built.clone(), key.clone()); let with_variants = match (*summary.repr.clone()).clone() { TypeRepr::EnumRepr { unit_only: _, .. } => { item.children.clone().iter().cloned().fold( state.type_summaries.clone(), - |acc: Rc>>, variant: Rc| { + |acc: Rc, variant: Rc| { if ((variant.children.clone().len() as i64) > 0) { { let v_has_fn = { @@ -1688,15 +2128,19 @@ pub fn add_emit_item_summary( summary.name.clone(), vname.clone(), ); + let variant_key = + variant_summary_key(key.clone(), vname.clone()); let variant_field_types = build_field_type_map( variant.children.clone(), source_indices.clone(), ); - v1_rt::rc_map_insert( + type_summary_index_insert( acc.clone(), + variant_key.clone(), qualified_vname.clone(), Rc::new(TypeSummary { name: qualified_vname.clone(), + key: variant_key.clone(), repr: Rc::new(TypeRepr::StructRepr), field_summaries: build_struct_field_summaries( variant.clone(), @@ -1726,8 +2170,9 @@ pub fn add_emit_item_summary( } _ => state.type_summaries.clone(), }; - let next_summaries = v1_rt::rc_map_insert( + let next_summaries = type_summary_index_insert( with_variants.clone(), + key.clone(), summary.name.clone(), summary.clone(), ); @@ -1748,6 +2193,20 @@ pub fn add_emit_item_summary( } } +pub fn type_summary_with_key(summary: Rc, key: String) -> Rc { + Rc::new(TypeSummary { + name: summary.name.clone(), + key: key.clone(), + repr: summary.repr.clone(), + field_summaries: summary.field_summaries.clone(), + field_type_map: summary.field_type_map.clone(), + field_import_surface_names: summary.field_import_surface_names.clone(), + variant_name_set: summary.variant_name_set.clone(), + generic_param_names: summary.generic_param_names.clone(), + has_fn_fields: summary.has_fn_fields.clone(), + }) +} + pub fn emit_dependency_registration_dissolve_on_note() -> Rc { thread_local! { static CACHED: Rc = { @@ -1768,4 +2227,8 @@ pub struct RouteReferencingModuleDeclarer; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RouteQualifiedSpelling; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RouteReferencingModuleImport; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RouteKernelMint; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RouteLeafSpelling; diff --git a/src/v1/stage0/src/v1_compiler_infer_env.rs b/src/v1/stage0/src/v1_compiler_infer_env.rs index 3732f70645d..aa3df4f5f50 100644 --- a/src/v1/stage0/src/v1_compiler_infer_env.rs +++ b/src/v1/stage0/src/v1_compiler_infer_env.rs @@ -1,6 +1,7 @@ // Generated by v1 compiler -- do not edit. // Source module: v1.compiler.infer_env +use self::BareOccurrenceBinding::*; use self::GlobalBareLookupState::*; use self::TypeNodeDeclarationReading::*; use self::TypeNodeUnidentifiedCause::*; @@ -351,6 +352,8 @@ pub struct SymbolIndex { pub services: Rc>>, pub transparent_alias_rep: Rc>, pub type_head_exposures: Rc>>, + pub file_modules: Rc>, + pub module_named_imports: Rc>>>, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -365,6 +368,8 @@ pub fn empty_symbol_index() -> Rc { services: v1_rt::rc_empty_map::>(), transparent_alias_rep: v1_rt::rc_empty_map::(), type_head_exposures: v1_rt::rc_empty_map::>(), + file_modules: v1_rt::rc_empty_map::(), + module_named_imports: v1_rt::rc_empty_map::>>(), }) } @@ -395,6 +400,210 @@ pub fn symbol_index_lookup(index: Rc, qualified_name: String) -> Op v1_rt::map_get(&index.entries.clone(), qualified_name.clone()) } +pub fn overlay_skips_kernel_name(name: String) -> bool { + (((((crate::std_types::is_kernel_type(name.clone()) + || crate::std_types::is_container_type(name.clone())) + || (name.clone() == "Unit".to_string())) + || (name.clone() == "Optional".to_string())) + || (name.clone() == "Present".to_string())) + || (name.clone() == "Absent".to_string())) +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum BareOccurrenceBinding { + BareOccurrenceDeclaredInItsFile { + declaration: Rc, + }, + BareOccurrenceImportedByName { + module_path: String, + declaration: Rc, + }, + BareOccurrenceIsKernelName, + BareOccurrenceUndecided, +} +impl BareOccurrenceBinding { + pub fn declaration(&self) -> Rc { + match self { + BareOccurrenceBinding::BareOccurrenceDeclaredInItsFile { + declaration: __val, .. + } => __val.clone(), + BareOccurrenceBinding::BareOccurrenceImportedByName { + declaration: __val, .. + } => __val.clone(), + BareOccurrenceBinding::BareOccurrenceIsKernelName => { + panic!("no declaration on unit variant") + } + BareOccurrenceBinding::BareOccurrenceUndecided => { + panic!("no declaration on unit variant") + } + } + } +} + +pub fn symbol_index_bare_declarations(index: Rc, name: String) -> Rc>> { + match v1_rt::map_get(&index.global_bare.clone(), name.clone()) + .as_deref() + .cloned() + { + Some(GlobalBareLookupState::GlobalBareUniqueBinding { binding: b, .. }) => { + Rc::new(vec![b.resolved.clone()]) + } + Some(GlobalBareLookupState::GlobalBareAmbiguousBinding { candidates: cs, .. }) => { + Rc::new({ + let mut __result = Vec::new(); + for c in cs.iter().cloned() { + __result.push(c.binding.clone().resolved.clone()); + } + __result + }) + } + std::option::Option::None => Rc::new(vec![]), + } +} + +pub fn bare_occurrence_import( + index: Rc, + name: String, + reference: Rc, +) -> Option> { + match v1_rt::map_get( + &index.file_modules.clone(), + reference.span.clone().file.clone(), + ) { + std::option::Option::None => std::option::Option::None, + Some(referencing_module) => match symbol_index_named_import_source( + index.clone(), + referencing_module.clone(), + name.clone(), + ) { + std::option::Option::None => std::option::Option::None, + Some(from_module) => Some(bare_occurrence_import_terminal( + index.clone(), + from_module.clone(), + name.clone(), + 16, + )), + }, + } +} + +pub fn symbol_index_named_import_source( + index: Rc, + module_path: String, + name: String, +) -> Option { + match v1_rt::map_get(&index.module_named_imports.clone(), module_path.clone()) { + std::option::Option::None => std::option::Option::None, + Some(named) => v1_rt::map_get(&named, name.clone()), + } +} + +pub fn bare_occurrence_import_terminal( + mut __tco_loop_index: Rc, + mut __tco_loop_module_path: String, + mut __tco_loop_name: String, + mut __tco_loop_fuel: i64, +) -> Rc { + loop { + #[allow(unused_mut)] + let mut index = __tco_loop_index; + #[allow(unused_mut)] + let mut module_path = __tco_loop_module_path; + #[allow(unused_mut)] + let mut name = __tco_loop_name; + #[allow(unused_mut)] + let mut fuel = __tco_loop_fuel; + if (fuel.clone() == 0) { + break Rc::new(BareOccurrenceBinding::BareOccurrenceUndecided); + } else { + match symbol_index_named_import_source(index.clone(), module_path.clone(), name.clone()) + { + Some(next) => { + let __tco_0 = index; + let __tco_1 = next.clone(); + let __tco_2 = name; + let __tco_3 = v1_rt::int_sub(fuel, 1); + __tco_loop_index = __tco_0; + __tco_loop_module_path = __tco_1; + __tco_loop_name = __tco_2; + __tco_loop_fuel = __tco_3; + continue; + } + std::option::Option::None => { + match symbol_index_lookup( + index.clone(), + v1_rt::concat( + v1_rt::concat(module_path.clone(), ".".to_string()), + name.clone(), + ), + ) { + Some(decl) => { + break Rc::new(BareOccurrenceBinding::BareOccurrenceImportedByName { + module_path: module_path.clone(), + declaration: decl.clone(), + }); + } + std::option::Option::None => { + break Rc::new(BareOccurrenceBinding::BareOccurrenceUndecided); + } + } + } + } + } + } +} + +pub fn bare_occurrence_binding( + index: Rc, + name: String, + reference: Rc, +) -> Rc { + { + let local = Rc::new({ + let mut __result = Vec::new(); + for d in symbol_index_bare_declarations(index.clone(), name.clone()) + .iter() + .cloned() + { + if (d.span.clone().file.clone() == reference.span.clone().file.clone()) { + __result.push(d); + } + } + __result + }); + let local_count = local + .iter() + .cloned() + .fold(0, |n: i64, _d: Rc| v1_rt::int_add(n, 1)); + if (local_count.clone() == 1) { + local.iter().cloned().fold( + Rc::new(BareOccurrenceBinding::BareOccurrenceUndecided), + |acc: Rc, d: Rc| { + Rc::new(BareOccurrenceBinding::BareOccurrenceDeclaredInItsFile { + declaration: d.clone(), + }) + }, + ) + } else { + if (local_count.clone() > 1) { + Rc::new(BareOccurrenceBinding::BareOccurrenceUndecided) + } else { + match bare_occurrence_import(index.clone(), name.clone(), reference.clone()) { + Some(imported) => imported.clone(), + std::option::Option::None => { + if overlay_skips_kernel_name(name.clone()) { + Rc::new(BareOccurrenceBinding::BareOccurrenceIsKernelName) + } else { + Rc::new(BareOccurrenceBinding::BareOccurrenceUndecided) + } + } + } + } + } + } +} + pub fn global_bare_candidates_contain( candidates: Rc>>, resolved: Rc, @@ -493,6 +702,8 @@ pub fn symbol_index_insert( services: index.services.clone(), transparent_alias_rep: index.transparent_alias_rep.clone(), type_head_exposures: index.type_head_exposures.clone(), + file_modules: index.file_modules.clone(), + module_named_imports: index.module_named_imports.clone(), }) } @@ -518,6 +729,8 @@ pub fn symbol_index_insert_decl( services: index.services.clone(), transparent_alias_rep: index.transparent_alias_rep.clone(), type_head_exposures: index.type_head_exposures.clone(), + file_modules: index.file_modules.clone(), + module_named_imports: index.module_named_imports.clone(), }) } @@ -540,6 +753,8 @@ pub fn symbol_index_insert_service( ), transparent_alias_rep: index.transparent_alias_rep.clone(), type_head_exposures: index.type_head_exposures.clone(), + file_modules: index.file_modules.clone(), + module_named_imports: index.module_named_imports.clone(), }) } @@ -1144,14 +1359,7 @@ pub fn segment_lcp_len(a: Rc>, b: Rc>) -> i64 { Some(bh) => { if (bh.clone() == seg.clone()) { Rc::new(SegmentLcpScan { - remaining: Rc::new( - acc.remaining - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + remaining: Rc::new(v1_rt::list_skip(&acc.remaining.clone(), 1)), matched: v1_rt::int_add(acc.matched.clone(), 1), live: true, }) @@ -1635,13 +1843,10 @@ pub fn qualified_all_but_last(name: String) -> String { "".to_string() } else { { - let scan = Rc::new( - segs.clone() - .iter() - .cloned() - .take(v1_rt::int_sub(seg_count.clone(), 1) as usize) - .collect::>(), - ) + let scan = Rc::new(v1_rt::list_take( + &segs.clone(), + v1_rt::int_sub(seg_count.clone(), 1), + )) .iter() .cloned() .fold("".to_string(), |acc: String, seg: String| { diff --git a/src/v1/stage0/src/v1_compiler_infer_lookup.rs b/src/v1/stage0/src/v1_compiler_infer_lookup.rs index 2dc2196ddc6..f35b3584317 100644 --- a/src/v1/stage0/src/v1_compiler_infer_lookup.rs +++ b/src/v1/stage0/src/v1_compiler_infer_lookup.rs @@ -1003,10 +1003,8 @@ pub fn lookup_field_type_node( let value_child = match n .children .clone() - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(child) => { crate::v1_compiler_infer_types::child_type_node( @@ -1272,14 +1270,7 @@ pub fn map_value_type_in_env(type_node: Rc, env: Rc) -> Option= 2)) { - match map_type - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + match map_type.children.clone().get((1) as usize).cloned() { Some(value_type) => Some(value_type.clone()), std::option::Option::None => std::option::Option::None, } @@ -1641,14 +1632,7 @@ pub fn declared_arg_types_for_method( std::option::Option::None => false, }; let non_receiver_templates = if first_is_self.clone() { - Rc::new( - t.param_types - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_skip(&t.param_types.clone(), 1)) } else { t.param_types.clone() }; diff --git a/src/v1/stage0/src/v1_compiler_infer_patterns.rs b/src/v1/stage0/src/v1_compiler_infer_patterns.rs index e2ab2807ad2..5e999c27c36 100644 --- a/src/v1/stage0/src/v1_compiler_infer_patterns.rs +++ b/src/v1/stage0/src/v1_compiler_infer_patterns.rs @@ -75,10 +75,8 @@ pub fn generic_use_slot_bindings( match scrut_node .children .clone() - .iter() + .get((pair.0.clone()) as usize) .cloned() - .skip(pair.0.clone() as usize) - .next() { Some(arg) => v1_rt::rc_map_insert(acc.clone(), slot.clone(), arg.clone()), std::option::Option::None => acc.clone(), @@ -1065,13 +1063,7 @@ pub fn specialize_pattern_row( ) -> Rc>>>> { { let head = pattern_row_head(row.clone()); - let rest = Rc::new( - row.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&row.clone(), 1)); if crate::v1_std_core::match_pattern_is_irrefutable(head.clone()) { Rc::new(vec![v1_rt::concat( Rc::new({ @@ -1142,10 +1134,8 @@ pub fn render_constructor_witness( Rc::new(vec![]), |acc: _, pair: (i64, Rc)| match cells .clone() - .iter() + .get((pair.0.clone()) as usize) .cloned() - .skip(pair.0.clone() as usize) - .next() { Some(cell) => { if (cell.clone() == "_".to_string()) { @@ -1210,13 +1200,7 @@ pub fn default_pattern_rows( .iter() .cloned() { - __result.push(Rc::new( - r.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - )); + __result.push(Rc::new(v1_rt::list_skip(&r.clone(), 1))); } __result }) @@ -1240,14 +1224,7 @@ pub fn exhaustiveness_witnesses( } } Some(head_type) => { - let rest_columns = Rc::new( - columns - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest_columns = Rc::new(v1_rt::list_skip(&columns.clone(), 1)); match (*constructor_roster_for(head_type.clone(), env.clone())).clone() { ConstructorRoster::ConstructorsOpen => { let defaulted = default_pattern_rows(rows.clone()); @@ -1376,24 +1353,15 @@ pub fn exhaustiveness_witnesses( Rc::new(vec![render_constructor_witness( c.clone(), fields.clone(), - Rc::new( - w.cells - .clone() - .iter() - .cloned() - .take(field_count.clone() - as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_take( + &w.cells.clone(), + field_count.clone(), + )), )]), - Rc::new( - w.cells - .clone() - .iter() - .cloned() - .skip(field_count.clone() as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip( + &w.cells.clone(), + field_count.clone(), + )), ), })); } diff --git a/src/v1/stage0/src/v1_compiler_infer_resolve.rs b/src/v1/stage0/src/v1_compiler_infer_resolve.rs index 868d3fc403a..af5f2911654 100644 --- a/src/v1/stage0/src/v1_compiler_infer_resolve.rs +++ b/src/v1/stage0/src/v1_compiler_infer_resolve.rs @@ -101,7 +101,7 @@ pub fn type_param_kind_diagnostics( { __result.extend((*match crate::v1_std_core::find_property(pair.1.clone().properties.clone(), crate::v1_std_core::type_param_kind_property_name(), env.source_indices.clone()) { std::option::Option::None => Rc::new(vec![]), - Some(kind_node) => match carrier.children.clone().iter().cloned().skip(pair.0.clone() as usize).next() { + Some(kind_node) => match carrier.children.clone().get((pair.0.clone()) as usize).cloned() { std::option::Option::None => Rc::new(vec![]), Some(arg) => { let kind_name = crate::v1_compiler_infer_env::authored_name(env.clone(), kind_node.clone()); @@ -1775,17 +1775,11 @@ Rc::new(NodeResolveResult { Some(k) => k.clone(), std::option::Option::None => unit_type(), }; - let val_child_node = match n - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { - Some(v) => v.clone(), - std::option::Option::None => unit_type(), - }; + let val_child_node = + match n.children.clone().get((1) as usize).cloned() { + Some(v) => v.clone(), + std::option::Option::None => unit_type(), + }; let key_type = crate::v1_compiler_infer_types::child_type_node( key_child_node.clone(), ); @@ -3034,25 +3028,20 @@ pub fn resolve_expr_types( }) } }; - let body_r = match arm_ch - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { - Some(b) => resolve_expr_types( - b.clone(), - env.clone(), - module_name.clone(), - ), - std::option::Option::None => { - Rc::new(ExprResolveResult { - expr: child.clone(), - diagnostics: Rc::new(vec![]), - }) - } - }; + let body_r = + match arm_ch.clone().get((1) as usize).cloned() { + Some(b) => resolve_expr_types( + b.clone(), + env.clone(), + module_name.clone(), + ), + std::option::Option::None => { + Rc::new(ExprResolveResult { + expr: child.clone(), + diagnostics: Rc::new(vec![]), + }) + } + }; Rc::new(ExprResolveResult { expr: crate::v1_std_core::make_arm_node( child.occurrence_identity.clone(), @@ -3144,14 +3133,14 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let tr = match ch.clone().iter().cloned().skip(1 as usize).next() { + let tr = match ch.clone().get((1) as usize).cloned() { Some(t) => resolve_expr_types(t.clone(), env.clone(), module_name.clone()), std::option::Option::None => Rc::new(ExprResolveResult { expr: texpr.clone(), diagnostics: Rc::new(vec![]), }), }; - let er = match ch.clone().iter().cloned().skip(2 as usize).next() { + let er = match ch.clone().get((2) as usize).cloned() { Some(e) => Some(resolve_expr_types( e.clone(), env.clone(), @@ -3189,7 +3178,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let br = match ch.clone().iter().cloned().skip(1 as usize).next() { + let br = match ch.clone().get((1) as usize).cloned() { Some(bd) => Some(resolve_expr_types( bd.clone(), env.clone(), @@ -3395,7 +3384,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let rr = match ch.clone().iter().cloned().skip(1 as usize).next() { + let rr = match ch.clone().get((1) as usize).cloned() { Some(r) => resolve_expr_types(r.clone(), env.clone(), module_name.clone()), std::option::Option::None => Rc::new(ExprResolveResult { expr: texpr.clone(), @@ -3437,15 +3426,7 @@ pub fn resolve_expr_types( }) } ExprData::ExprLambda => { - let lam_param_nodes = Rc::new( - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let lam_param_nodes = Rc::new(v1_rt::list_skip(&texpr.children.clone(), 1)); let r = match texpr.children.clone().first().cloned() { Some(b) => resolve_expr_types(b.clone(), env.clone(), module_name.clone()), std::option::Option::None => Rc::new(ExprResolveResult { @@ -3571,7 +3552,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let tr = match ch.clone().iter().cloned().skip(1 as usize).next() { + let tr = match ch.clone().get((1) as usize).cloned() { Some(target) => resolve_node(target.clone(), env.clone(), module_name.clone()), std::option::Option::None => Rc::new(NodeResolveResult { resolved: unit_type(), @@ -3598,7 +3579,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let br = match ch.clone().iter().cloned().skip(1 as usize).next() { + let br = match ch.clone().get((1) as usize).cloned() { Some(b) => resolve_expr_types(b.clone(), env.clone(), module_name.clone()), std::option::Option::None => Rc::new(ExprResolveResult { expr: texpr.clone(), @@ -3632,7 +3613,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let ir = match ch.clone().iter().cloned().skip(1 as usize).next() { + let ir = match ch.clone().get((1) as usize).cloned() { Some(index) => { resolve_expr_types(index.clone(), env.clone(), module_name.clone()) } @@ -3663,7 +3644,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let sr = match ch.clone().iter().cloned().skip(1 as usize).next() { + let sr = match ch.clone().get((1) as usize).cloned() { Some(start) => { resolve_expr_types(start.clone(), env.clone(), module_name.clone()) } @@ -3672,7 +3653,7 @@ pub fn resolve_expr_types( diagnostics: Rc::new(vec![]), }), }; - let er = match ch.clone().iter().cloned().skip(2 as usize).next() { + let er = match ch.clone().get((2) as usize).cloned() { Some(end_e) => { resolve_expr_types(end_e.clone(), env.clone(), module_name.clone()) } @@ -3775,14 +3756,10 @@ pub fn first_duplicate_type_param_name(names: Rc>) -> Option { if { let mut __found = false; - for other in Rc::new( - names - .clone() - .iter() - .cloned() - .skip(v1_rt::int_add(pair.0.clone(), 1) as usize) - .collect::>(), - ) + for other in Rc::new(v1_rt::list_skip( + &names.clone(), + v1_rt::int_add(pair.0.clone(), 1), + )) .iter() .cloned() { @@ -3968,15 +3945,7 @@ pub fn reference_param_with_declaration( binders.clone(), env.clone(), )]), - Rc::new( - param - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(¶m.children.clone(), 1)), ), param.params.clone(), param.inferred.clone(), @@ -4150,15 +4119,7 @@ pub fn binder_marked_param( binders.clone(), env.clone(), )]), - Rc::new( - param - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(¶m.children.clone(), 1)), ), param.params.clone(), param.inferred.clone(), diff --git a/src/v1/stage0/src/v1_compiler_infer_types.rs b/src/v1/stage0/src/v1_compiler_infer_types.rs index 04ee1a3e91b..f9dc4f910d1 100644 --- a/src/v1/stage0/src/v1_compiler_infer_types.rs +++ b/src/v1/stage0/src/v1_compiler_infer_types.rs @@ -1487,14 +1487,7 @@ pub fn unify_template( } std::option::Option::None => subst.clone(), }; - match concrete - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + match concrete.children.clone().get((1) as usize).cloned() { Some(v) => { if (v1_rt::map_get(&s1, "__value__".to_string()) != std::option::Option::None) @@ -1613,14 +1606,7 @@ pub fn unify_template( ), std::option::Option::None => subst.clone(), }; - match concrete - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + match concrete.children.clone().get((1) as usize).cloned() { Some(c) => unify_template( st.clone(), child_type_node(c.clone()), @@ -1656,14 +1642,7 @@ pub fn build_type_substitution( std::option::Option::None => false, }; let non_receiver_templates = if first_is_self.clone() { - Rc::new( - param_templates - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_skip(¶m_templates.clone(), 1)) } else { param_templates.clone() }; @@ -1897,46 +1876,41 @@ pub fn apply_type_substitution( } }, }, - AlgebraTypeTemplate::ReceiverValue => match receiver - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { - Some(child) => Rc::new(KernelTypeBuild { - ty: child_type_node(child.clone()), - diagnostics: Rc::new(vec![]), - }), - std::option::Option::None => { - match v1_rt::map_get(&subst, "__value__".to_string()) { - Some(val) => Rc::new(KernelTypeBuild { - ty: val.clone(), - diagnostics: Rc::new(vec![]), - }), - std::option::Option::None => { - let rname = - container_kind_canonical(crate::v1_std_core::authored_name_at( - source_indices.clone(), - receiver.clone(), - )); - match crate::std_types::container_param_name(rname.clone(), 1) { - Some(n) => Rc::new(KernelTypeBuild { - ty: type_variable_node(n.clone()), - diagnostics: Rc::new(vec![]), - }), - std::option::Option::None => Rc::new(KernelTypeBuild { - ty: missing_kernel_container_profile_type(rname.clone()), - diagnostics: Rc::new(vec![ - kernel_container_profile_miss_diagnostic(rname.clone()), - ]), - }), + AlgebraTypeTemplate::ReceiverValue => { + match receiver.children.clone().get((1) as usize).cloned() { + Some(child) => Rc::new(KernelTypeBuild { + ty: child_type_node(child.clone()), + diagnostics: Rc::new(vec![]), + }), + std::option::Option::None => { + match v1_rt::map_get(&subst, "__value__".to_string()) { + Some(val) => Rc::new(KernelTypeBuild { + ty: val.clone(), + diagnostics: Rc::new(vec![]), + }), + std::option::Option::None => { + let rname = + container_kind_canonical(crate::v1_std_core::authored_name_at( + source_indices.clone(), + receiver.clone(), + )); + match crate::std_types::container_param_name(rname.clone(), 1) { + Some(n) => Rc::new(KernelTypeBuild { + ty: type_variable_node(n.clone()), + diagnostics: Rc::new(vec![]), + }), + std::option::Option::None => Rc::new(KernelTypeBuild { + ty: missing_kernel_container_profile_type(rname.clone()), + diagnostics: Rc::new(vec![ + kernel_container_profile_miss_diagnostic(rname.clone()), + ]), + }), + } } } } } - }, + } AlgebraTypeTemplate::NamedTemplate { name: n, .. } => Rc::new(KernelTypeBuild { ty: nominal_type_ref(n.clone()), diagnostics: Rc::new(vec![]), @@ -2221,13 +2195,7 @@ pub fn node_type_shape_argument_list( match args.clone().first().cloned() { Some(a) => { let head = node_type_shape(child_type_node(a.clone()), source_indices.clone()); - let rest = Rc::new( - args.clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let rest = Rc::new(v1_rt::list_skip(&args.clone(), 1)); if ((rest.clone().len() as i64) == 0) { head.clone() } else { @@ -2830,14 +2798,7 @@ pub fn callable_signatures_agree( .iter() .cloned() { - if !(match right - .params - .clone() - .iter() - .cloned() - .skip(pair.0.clone() as usize) - .next() - { + if !(match right.params.clone().get((pair.0.clone()) as usize).cloned() { Some(right_param) => agree( crate::v1_std_core::param_node_type_expr(pair.1.clone()), crate::v1_std_core::param_node_type_expr(right_param.clone()), @@ -2940,10 +2901,8 @@ pub fn node_type_equals_core( if !(match right .children .clone() - .iter() + .get((pair.0.clone()) as usize) .cloned() - .skip(pair.0.clone() as usize) - .next() { Some(right_child) => node_type_equals( pair.1.clone(), @@ -3026,18 +2985,14 @@ pub fn node_type_equals_core( Some(right_first) => match left .children .clone() - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(left_second) => match right .children .clone() - .iter() + .get((1) as usize) .cloned() - .skip(1 as usize) - .next() { Some(right_second) => { (node_type_equals( @@ -3241,14 +3196,7 @@ pub fn method_receiver_element_node( { let normed = normalize_access_type_node(receiver_type.clone()); let maybe_element = if node_is_keyed_collection(normed.clone(), source_indices.clone()) { - match normed - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() - { + match normed.children.clone().get((1) as usize).cloned() { Some(ch) => Some(child_type_node(ch.clone())), std::option::Option::None => std::option::Option::None, } @@ -3362,13 +3310,7 @@ pub fn first_matching_algebra_field( } std::option::Option::None => { let __tco_0 = n; - let __tco_1 = Rc::new( - candidates - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ); + let __tco_1 = Rc::new(v1_rt::list_skip(&candidates, 1)); let __tco_2 = source_indices; __tco_loop_n = __tco_0; __tco_loop_candidates = __tco_1; diff --git a/src/v1/stage0/src/v1_compiler_ownership.rs b/src/v1/stage0/src/v1_compiler_ownership.rs index 12959c145bd..a69c4f25071 100644 --- a/src/v1/stage0/src/v1_compiler_ownership.rs +++ b/src/v1/stage0/src/v1_compiler_ownership.rs @@ -296,14 +296,10 @@ pub fn merge_branch_usages( let mut __result = Vec::new(); for b in branches.iter().cloned() { __result.extend( - (*Rc::new( - b.fold_call_nodes - .clone() - .iter() - .cloned() - .skip(base_fold_count.clone() as usize) - .collect::>(), - )) + (*Rc::new(v1_rt::list_skip( + &b.fold_call_nodes.clone(), + base_fold_count.clone(), + ))) .iter() .cloned(), ); @@ -1294,7 +1290,7 @@ pub fn analyze_single_fold( Some(a) => crate::v1_std_core::arg_value(a.clone()), std::option::Option::None => method_call.clone(), }; - let fold_lambda_node = match args.clone().iter().cloned().skip(1 as usize).next() { + let fold_lambda_node = match args.clone().get((1) as usize).cloned() { Some(a) => crate::v1_std_core::arg_value(a.clone()), std::option::Option::None => method_call.clone(), }; diff --git a/src/v1/stage0/src/v1_compiler_parse.rs b/src/v1/stage0/src/v1_compiler_parse.rs index 6203bf1f162..1e73153d17c 100644 --- a/src/v1/stage0/src/v1_compiler_parse.rs +++ b/src/v1/stage0/src/v1_compiler_parse.rs @@ -723,8 +723,7 @@ pub fn mint_parsed_string_part_nodes( } }; { - let __tco_0 = - Rc::new(parts.iter().cloned().skip(1 as usize).collect::>()); + let __tco_0 = Rc::new(v1_rt::list_skip(&parts, 1)); let __tco_1 = minted.ctx.clone(); let __tco_2 = span; let __tco_3 = v1_rt::rc_list_push(acc, node.clone()); @@ -3240,14 +3239,7 @@ pub fn stamp_parsed_node_list_with_head_role( } else { { let stamped_tail = stamp_parsed_node_list( - Rc::new( - nodes - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&nodes.clone(), 1)), ancestors.clone(), stamped_head.ctx.clone(), tail_role.clone(), diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 38d49cab731..9681af4d227 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -76,7 +76,7 @@ pub fn rt_string_ops() -> String { } pub fn rt_collection_ops() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("pub fn lookup(m: &HashMap, key: K) -> Option {\n".to_string(), " m.get(&key).cloned()\n".to_string()), "}\n\n".to_string()), "pub fn list_get_optional(items: &Vec, index: i64) -> Option {\n".to_string()), " if index < 0 { None } else { items.get(index as usize).cloned() }\n".to_string()), "}\n\n".to_string()), "pub fn index_by String>(list: Vec, key_fn: F) -> HashMap {\n".to_string()), " let mut map = HashMap::new();\n".to_string()), " for item in list { let key = key_fn(&item); map.insert(key, item); }\n".to_string()), " map\n".to_string()), "}\n\n".to_string()), "pub fn empty_map() -> HashMap { HashMap::new() }\n\n".to_string()), "pub fn map_insert(mut map: HashMap, key: K, value: V) -> HashMap {\n".to_string()), " map.insert(key, value); map\n".to_string()), "}\n\n".to_string()), "pub fn map_merge(mut base: HashMap, overlay: HashMap) -> HashMap {\n".to_string()), " base.extend(overlay); base\n".to_string()), "}\n\n".to_string()), "pub fn map_get(m: &HashMap, key: K) -> Option {\n".to_string()), " m.get(&key).cloned()\n".to_string()), "}\n\n".to_string()), "pub fn map_keys(m: &HashMap) -> Vec { m.keys().cloned().collect() }\n\n".to_string()), "// THE EMITTED ORDERING-KEY ADMISSION: the counterpart of the interpreter's admit_emitted_ord_keys.\n".to_string()), "// Only String, i64 and bool (and their aliases) are admitted, because on exactly those kinds the\n".to_string()), "// native Ord IS the canonical content order (std.algebra TotalOrder). Any other key type -- a\n".to_string()), "// derived-Ord record or enum (declaration order), or f64 (no total Ord) -- fails to compile here,\n".to_string()), "// at the call, rather than sorting in an order the interpreter would not produce.\n".to_string()), "#[diagnostic::on_unimplemented(message = \"EMIT REFUSED: `{Self}` is not an admitted ordering key; sorted_map_keys and sort_by admit only String, Int and Bool keys, whose order is the canonical content order in both realizations\", label = \"ordering key of a type with no canonical emitted order\")]\n".to_string()), "pub trait CanonicalOrdKey: Ord {}\n".to_string()), "impl CanonicalOrdKey for String {}\n".to_string()), "impl CanonicalOrdKey for RcStr {}\n".to_string()), "impl CanonicalOrdKey for i64 {}\n".to_string()), "impl CanonicalOrdKey for bool {}\n\n".to_string()), "pub fn canonical_key_cmp(a: &K, b: &K) -> std::cmp::Ordering { a.cmp(b) }\n\n".to_string()), "pub fn sorted_map_keys(m: &HashMap) -> Vec { let mut keys = map_keys(m); keys.sort(); keys }\n\n".to_string()), "pub fn map_is_empty(m: &HashMap) -> bool { m.is_empty() }\n\n".to_string()), "// Sound identity hint (see 04_infer.dag substitute_generics): returns true =>\n".to_string()), "// the two are structurally equal. The compiled realization answers with pointer\n".to_string()), "// identity (conservative: may say false when equal); never true-when-unequal, so\n".to_string()), "// callers that return the original on true preserve output structure. Owned Rc\n".to_string()), "// args (cheap refcount bump) keep the inner element pointers, so ptr_eq on the\n".to_string()), "// clones equals ptr_eq on the originals -- lets the .dag pass args by value and\n".to_string()), "// emit through the normal clone path (no all-args-by-ref emit rule needed).\n".to_string()), "pub fn rc_ptr_eq(a: Rc, b: Rc) -> bool { Rc::ptr_eq(&a, &b) }\n\n".to_string()), "pub fn rc_vec_ptr_eq(a: Rc>>, b: Rc>>) -> bool {\n".to_string()), " a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| Rc::ptr_eq(x, y))\n".to_string()), "}\n\n".to_string()), "pub fn map_values(m: &HashMap) -> Vec { m.values().cloned().collect() }\n\n".to_string()), "pub fn list_concat(mut a: Vec, b: Vec) -> Vec { a.append(b); a }\n\n".to_string()), "pub fn list_push(mut list: Vec, item: T) -> Vec { list.push_back(item); list }\n\n".to_string()), "pub fn append(list: Rc>, item: T) -> Vec {\n".to_string()), " let mut v = (*list).clone();\n".to_string()), " v.push_back(item);\n".to_string()), " v\n".to_string()), "}\n\n".to_string()), "pub fn chars_to_string(chars: &Rc>, start: i64, end: i64) -> String {\n".to_string()), " let len = chars.len();\n".to_string()), " let start = (start.max(0) as usize).min(len);\n".to_string()), " let end = (end.max(0) as usize).min(len).max(start);\n".to_string()), " let units = (end.saturating_sub(start)) as u64;\n".to_string()), " record_source_chars_slice_walked(units);\n".to_string()), " // Focus caches the current RRB chunk, so per-char access is contiguous-slice\n".to_string()), " // speed; a plain iter().skip(start) walks O(start) per call, which summed\n".to_string()), " // quadratically across a tokenize pass (profiled 2026-07-06).\n".to_string()), " let mut focus = chars.focus();\n".to_string()), " let mut out = String::with_capacity(end - start);\n".to_string()), " for i in start..end {\n".to_string()), " if let Some(ch) = char::from_u32(*focus.index(i) as u32) {\n".to_string()), " out.push(ch);\n".to_string()), " }\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "pub fn parse_int(s: String) -> Option { s.parse::().ok() }\n\n".to_string()), "pub fn map_contains_key(m: &HashMap, key: K) -> bool {\n".to_string()), " m.contains_key(&key)\n".to_string()), "}\n\n".to_string()), "pub fn map_has(m: &HashMap, key: K) -> bool {\n".to_string()), " m.contains_key(&key)\n".to_string()), "}\n\n".to_string()), "pub fn rc_empty_set() -> Rc> {\n".to_string()), " Rc::new(BTreeSet::new())\n".to_string()), "}\n\n".to_string()), "pub fn rc_set_insert(mut s: Rc>, x: T) -> Rc> {\n".to_string()), " Rc::make_mut(&mut s).insert(x);\n".to_string()), " s\n".to_string()), "}\n\n".to_string()), "pub fn rc_set_union(a: Rc>, b: Rc>) -> Rc> {\n".to_string()), " let mut out = a;\n".to_string()), " for x in b.iter().cloned() {\n".to_string()), " Rc::make_mut(&mut out).insert(x);\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "pub fn set_contains>>(s: S, x: T) -> bool {\n".to_string()), " s.as_ref().contains(&x)\n".to_string()), "}\n\n".to_string()), "pub fn reverse(list: Rc>) -> Rc> {\n".to_string()), " Rc::new(list.iter().cloned().rev().collect())\n".to_string()), "}\n\n".to_string()), "pub fn replace(s: String, from: String, to: String) -> String {\n".to_string()), " s.replace(&from, &to)\n".to_string()), "}\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("pub fn lookup(m: &HashMap, key: K) -> Option {\n".to_string(), " m.get(&key).cloned()\n".to_string()), "}\n\n".to_string()), "pub fn list_get_optional(items: &Vec, index: i64) -> Option {\n".to_string()), " if index < 0 { None } else { items.get(index as usize).cloned() }\n".to_string()), "}\n\n".to_string()), "pub fn index_by String>(list: Vec, key_fn: F) -> HashMap {\n".to_string()), " let mut map = HashMap::new();\n".to_string()), " for item in list { let key = key_fn(&item); map.insert(key, item); }\n".to_string()), " map\n".to_string()), "}\n\n".to_string()), "pub fn empty_map() -> HashMap { HashMap::new() }\n\n".to_string()), "pub fn map_insert(mut map: HashMap, key: K, value: V) -> HashMap {\n".to_string()), " map.insert(key, value); map\n".to_string()), "}\n\n".to_string()), "pub fn map_merge(mut base: HashMap, overlay: HashMap) -> HashMap {\n".to_string()), " base.extend(overlay); base\n".to_string()), "}\n\n".to_string()), "pub fn map_get(m: &HashMap, key: K) -> Option {\n".to_string()), " m.get(&key).cloned()\n".to_string()), "}\n\n".to_string()), "pub fn map_keys(m: &HashMap) -> Vec { m.keys().cloned().collect() }\n\n".to_string()), "// THE EMITTED ORDERING-KEY ADMISSION: the counterpart of the interpreter's admit_emitted_ord_keys.\n".to_string()), "// Only String, i64 and bool (and their aliases) are admitted, because on exactly those kinds the\n".to_string()), "// native Ord IS the canonical content order (std.algebra TotalOrder). Any other key type -- a\n".to_string()), "// derived-Ord record or enum (declaration order), or f64 (no total Ord) -- fails to compile here,\n".to_string()), "// at the call, rather than sorting in an order the interpreter would not produce.\n".to_string()), "#[diagnostic::on_unimplemented(message = \"EMIT REFUSED: `{Self}` is not an admitted ordering key; sorted_map_keys and sort_by admit only String, Int and Bool keys, whose order is the canonical content order in both realizations\", label = \"ordering key of a type with no canonical emitted order\")]\n".to_string()), "pub trait CanonicalOrdKey: Ord {}\n".to_string()), "impl CanonicalOrdKey for String {}\n".to_string()), "impl CanonicalOrdKey for RcStr {}\n".to_string()), "impl CanonicalOrdKey for i64 {}\n".to_string()), "impl CanonicalOrdKey for bool {}\n\n".to_string()), "pub fn canonical_key_cmp(a: &K, b: &K) -> std::cmp::Ordering { a.cmp(b) }\n\n".to_string()), "pub fn sorted_map_keys(m: &HashMap) -> Vec { let mut keys = map_keys(m); keys.sort(); keys }\n\n".to_string()), "pub fn map_is_empty(m: &HashMap) -> bool { m.is_empty() }\n\n".to_string()), "// Sound identity hint (see 04_infer.dag substitute_generics): returns true =>\n".to_string()), "// the two are structurally equal. The compiled realization answers with pointer\n".to_string()), "// identity (conservative: may say false when equal); never true-when-unequal, so\n".to_string()), "// callers that return the original on true preserve output structure. Owned Rc\n".to_string()), "// args (cheap refcount bump) keep the inner element pointers, so ptr_eq on the\n".to_string()), "// clones equals ptr_eq on the originals -- lets the .dag pass args by value and\n".to_string()), "// emit through the normal clone path (no all-args-by-ref emit rule needed).\n".to_string()), "pub fn rc_ptr_eq(a: Rc, b: Rc) -> bool { Rc::ptr_eq(&a, &b) }\n\n".to_string()), "pub fn rc_vec_ptr_eq(a: Rc>>, b: Rc>>) -> bool {\n".to_string()), " a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| Rc::ptr_eq(x, y))\n".to_string()), "}\n\n".to_string()), "pub fn map_values(m: &HashMap) -> Vec { m.values().cloned().collect() }\n\n".to_string()), "pub fn list_concat(mut a: Vec, b: Vec) -> Vec { a.append(b); a }\n\n".to_string()), "pub fn list_push(mut list: Vec, item: T) -> Vec { list.push_back(item); list }\n\n".to_string()), "/// std.primitives skip_contract on the persistent carrier: the suffix after `n` members,\n".to_string()), "/// sharing the receiver's tree in O(log n) instead of copying the remainder, so a walk that\n".to_string()), "/// slices by offset is O(n log n) rather than quadratic. A negative `n` skips every member, as\n".to_string()), "/// the copying form's saturating `n as usize` did.\n".to_string()), "pub fn list_skip(items: &Vec, n: i64) -> Vec {\n".to_string()), " let k = if n < 0 {\n".to_string()), " items.len()\n".to_string()), " } else {\n".to_string()), " (n as usize).min(items.len())\n".to_string()), " };\n".to_string()), " items.skip(k)\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// std.primitives take_contract on the persistent carrier: the first `n` members, sharing\n".to_string()), "/// the receiver's tree. A negative `n` keeps every member, as the copying form did.\n".to_string()), "pub fn list_take(items: &Vec, n: i64) -> Vec {\n".to_string()), " let k = if n < 0 {\n".to_string()), " items.len()\n".to_string()), " } else {\n".to_string()), " (n as usize).min(items.len())\n".to_string()), " };\n".to_string()), " items.take(k)\n".to_string()), "}\n".to_string()), "\n".to_string()), "#[cfg(test)]\n".to_string()), "mod list_slice_tests {\n".to_string()), " use super::*;\n".to_string()), "\n".to_string()), " thread_local! {\n".to_string()), " static CLONES: Cell = const { Cell::new(0) };\n".to_string()), " }\n".to_string()), "\n".to_string()), " /// A code point whose every copy is counted, so a slice's cost is read off the carrier's\n".to_string()), " /// own work rather than a clock.\n".to_string()), " #[derive(Debug, PartialEq)]\n".to_string()), " struct CountedCodePoint(u32);\n".to_string()), "\n".to_string()), " impl Clone for CountedCodePoint {\n".to_string()), " fn clone(&self) -> Self {\n".to_string()), " CLONES.with(|c| c.set(c.get() + 1));\n".to_string()), " CountedCodePoint(self.0)\n".to_string()), " }\n".to_string()), " }\n".to_string()), "\n".to_string()), " fn clones_during(f: impl FnOnce() -> R) -> (R, u64) {\n".to_string()), " let before = CLONES.with(|c| c.get());\n".to_string()), " let r = f();\n".to_string()), " (r, CLONES.with(|c| c.get()) - before)\n".to_string()), " }\n".to_string()), "\n".to_string()), " // Non-ASCII on purpose: emitted char_at / substring over a bare &str are O(offset) here,\n".to_string()), " // which is the walk the code-point slice replaces.\n".to_string()), " fn non_ascii_code_points(n: usize) -> Vec {\n".to_string()), " [0xe9u32, 0x4e2d, 0x1f600, 0x61]\n".to_string()), " .iter()\n".to_string()), " .cycle()\n".to_string()), " .take(n)\n".to_string()), " .map(|cp| CountedCodePoint(*cp))\n".to_string()), " .collect()\n".to_string()), " }\n".to_string()), "\n".to_string()), " const N: usize = 200_000;\n".to_string()), " // im's RRB split copies at most one boundary chunk per tree level; this is that bound with\n".to_string()), " // headroom, and three orders of magnitude below N.\n".to_string()), " const SLICE_COPY_BUDGET: u64 = 2_048;\n".to_string()), "\n".to_string()), " #[test]\n".to_string()), " fn a_slice_from_the_middle_copies_only_boundary_chunks() {\n".to_string()), " let cps = Rc::new(non_ascii_code_points(N));\n".to_string()), " let (slice, copied) = clones_during(|| list_take(&list_skip(&cps, (N / 2) as i64), 7));\n".to_string()), " assert!(\n".to_string()), " copied <= SLICE_COPY_BUDGET,\n".to_string()), " \"skip+take copied {} members of {}\",\n".to_string()), " copied,\n".to_string()), " N\n".to_string()), " );\n".to_string()), " let expected: std::vec::Vec = cps.iter().skip(N / 2).take(7).map(|c| c.0).collect();\n".to_string()), " assert_eq!(\n".to_string()), " slice.iter().map(|c| c.0).collect::>(),\n".to_string()), " expected\n".to_string()), " );\n".to_string()), " }\n".to_string()), "\n".to_string()), " // THE RED CONTROL: the extdeps.languages.rust.emit skip / take templates this runtime pair\n".to_string()), " // replaced, spelled as they emitted, on the same input and bound. It must exceed the budget,\n".to_string()), " // or the budget does not discriminate the quadratic.\n".to_string()), " #[test]\n".to_string()), " fn the_copying_template_form_exceeds_the_budget() {\n".to_string()), " let cps = Rc::new(non_ascii_code_points(N));\n".to_string()), " let (_, copied) = clones_during(|| {\n".to_string()), " let rest = cps.iter().cloned().skip(N / 2).collect::>();\n".to_string()), " rest.iter().cloned().take(7).collect::>()\n".to_string()), " });\n".to_string()), " assert!(\n".to_string()), " copied > SLICE_COPY_BUDGET,\n".to_string()), " \"copying form copied only {}\",\n".to_string()), " copied\n".to_string()), " );\n".to_string()), " }\n".to_string()), "\n".to_string()), " #[test]\n".to_string()), " fn negative_and_overlong_counts_keep_the_copying_forms_reading() {\n".to_string()), " let xs: Vec = (0..10).collect();\n".to_string()), " for n in [-3i64, 0, 4, 10, 11, i64::MAX] {\n".to_string()), " let copying_skip = xs.iter().cloned().skip(n as usize).collect::>();\n".to_string()), " let copying_take = xs.iter().cloned().take(n as usize).collect::>();\n".to_string()), " assert_eq!(list_skip(&xs, n), copying_skip, \"skip {}\", n);\n".to_string()), " assert_eq!(list_take(&xs, n), copying_take, \"take {}\", n);\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n".to_string()), "\n".to_string()), "pub fn append(list: Rc>, item: T) -> Vec {\n".to_string()), " let mut v = (*list).clone();\n".to_string()), " v.push_back(item);\n".to_string()), " v\n".to_string()), "}\n\n".to_string()), "pub fn chars_to_string(chars: &Rc>, start: i64, end: i64) -> String {\n".to_string()), " let len = chars.len();\n".to_string()), " let start = (start.max(0) as usize).min(len);\n".to_string()), " let end = (end.max(0) as usize).min(len).max(start);\n".to_string()), " let units = (end.saturating_sub(start)) as u64;\n".to_string()), " record_source_chars_slice_walked(units);\n".to_string()), " // Focus caches the current RRB chunk, so per-char access is contiguous-slice\n".to_string()), " // speed; a plain iter().skip(start) walks O(start) per call, which summed\n".to_string()), " // quadratically across a tokenize pass (profiled 2026-07-06).\n".to_string()), " let mut focus = chars.focus();\n".to_string()), " let mut out = String::with_capacity(end - start);\n".to_string()), " for i in start..end {\n".to_string()), " if let Some(ch) = char::from_u32(*focus.index(i) as u32) {\n".to_string()), " out.push(ch);\n".to_string()), " }\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "pub fn parse_int(s: String) -> Option { s.parse::().ok() }\n\n".to_string()), "pub fn map_contains_key(m: &HashMap, key: K) -> bool {\n".to_string()), " m.contains_key(&key)\n".to_string()), "}\n\n".to_string()), "pub fn map_has(m: &HashMap, key: K) -> bool {\n".to_string()), " m.contains_key(&key)\n".to_string()), "}\n\n".to_string()), "pub fn rc_empty_set() -> Rc> {\n".to_string()), " Rc::new(BTreeSet::new())\n".to_string()), "}\n\n".to_string()), "pub fn rc_set_insert(mut s: Rc>, x: T) -> Rc> {\n".to_string()), " Rc::make_mut(&mut s).insert(x);\n".to_string()), " s\n".to_string()), "}\n\n".to_string()), "pub fn rc_set_union(a: Rc>, b: Rc>) -> Rc> {\n".to_string()), " let mut out = a;\n".to_string()), " for x in b.iter().cloned() {\n".to_string()), " Rc::make_mut(&mut out).insert(x);\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "pub fn set_contains>>(s: S, x: T) -> bool {\n".to_string()), " s.as_ref().contains(&x)\n".to_string()), "}\n\n".to_string()), "pub fn reverse(list: Rc>) -> Rc> {\n".to_string()), " Rc::new(list.iter().cloned().rev().collect())\n".to_string()), "}\n\n".to_string()), "pub fn replace(s: String, from: String, to: String) -> String {\n".to_string()), " s.replace(&from, &to)\n".to_string()), "}\n\n".to_string()) } pub fn rt_string_carrier() -> String { @@ -135,39 +135,47 @@ pub fn rust_runtime_source() -> String { v1_rt::concat( v1_rt::concat( v1_rt::concat( - rt_header(), - rt_text_lookup_work_counter(), + v1_rt::concat( + rt_header(), + rt_text_lookup_work_counter(), + ), + rt_resolution_silent_pick_telemetry( + ), ), - rt_resolution_silent_pick_telemetry(), + rt_concat_trait(), ), - rt_concat_trait(), + rt_string_ops(), ), - rt_string_ops(), + rt_string_carrier(), ), - rt_string_carrier(), + rt_collection_ops(), ), - rt_collection_ops(), + rt_rc_container_ops(), ), - rt_rc_container_ops(), + rt_scanner_ops(), ), - rt_scanner_ops(), + rt_unicode_ops(), ), - rt_unicode_ops(), + rt_freemonoid_host_ops(), ), - rt_freemonoid_host_ops(), + rt_hash_ops(), ), - rt_hash_ops(), + rt_filesystem(), ), - rt_filesystem(), + rt_checked_int_ops(), ), - rt_checked_int_ops(), + rt_realization_measurement(), ), - rt_realization_measurement(), + rt_accelerator_demo_kernel(), ), - rt_accelerator_demo_kernel(), + rt_host_budget(), ) } +pub fn rt_host_budget() -> String { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The one host-budget precedence. `read_host_budget_bytes` and\n".to_string(), "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoinSource {\n".to_string()), " CgroupMemoryHigh { cgroup_dir: String },\n".to_string()), " CgroupMemoryMax { cgroup_dir: String },\n".to_string()), " CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n".to_string()), " DarwinPhysicalMemory,\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoinSource {\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoin {\n".to_string()), " Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n".to_string()), " DeclaredUnverified { requested_bytes: u64, reason: String },\n".to_string()), " Unreadable { reason: String },\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoin {\n".to_string()), " pub fn bytes(&self) -> Option {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n".to_string()), " HostBudgetJoin::Unreadable { .. } => None,\n".to_string()), " }\n".to_string()), " }\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n".to_string()), " Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n".to_string()), " None => source.label(),\n".to_string()), " },\n".to_string()), " HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n".to_string()), " let cgroup_v1_limit = match cgroup_v1_limit {\n".to_string()), " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n".to_string()), " return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n".to_string()), " }\n".to_string()), " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n".to_string()), " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n".to_string()), " };\n".to_string()), " let observation = [\n".to_string()), " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n".to_string()), " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n".to_string()), " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n".to_string()), " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n".to_string()), " if let Some((source, observed_bytes)) = observation {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n".to_string()), " }\n".to_string()), " if let Some(bytes) = darwin_physical {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n".to_string()), " }\n".to_string()), " if let Some(requested_bytes) = env_override {\n".to_string()), " return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n".to_string()), " }\n".to_string()), " HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n".to_string()), "}\n\n".to_string()), "pub fn read_host_budget_bytes() -> (Option, String) {\n".to_string()), " let join = resolve_host_budget_join(\n".to_string()), " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n".to_string()), " host_budget_tightest_cgroup(\"memory.high\"),\n".to_string()), " host_budget_tightest_cgroup(\"memory.max\"),\n".to_string()), " host_budget_cgroup_v1(),\n".to_string()), " host_budget_darwin_physical(),\n".to_string()), " );\n".to_string()), " (join.bytes(), join.label())\n".to_string()), "}\n\n".to_string()), "#[derive(Clone)]\n".to_string()), "pub enum HostBudgetCgroupV1 {\n".to_string()), " Limited(u64),\n".to_string()), " Unlimited,\n".to_string()), " Unparseable(String),\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup_under(self_cg: &str, root: &std::path::Path, limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let rel = self_cg\n".to_string()), " .lines()\n".to_string()), " .find_map(|l| l.strip_prefix(\"0::\"))\n".to_string()), " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n".to_string()), " let mut dir = root.join(&rel);\n".to_string()), " let mut best: Option<(u64, std::path::PathBuf)> = None;\n".to_string()), " loop {\n".to_string()), " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n".to_string()), " let s = s.trim();\n".to_string()), " if s != \"max\" {\n".to_string()), " if let Ok(v) = s.parse::() {\n".to_string()), " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n".to_string()), " if take {\n".to_string()), " best = Some((v, dir.clone()));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " if dir == root || !dir.pop() {\n".to_string()), " break;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " best.map(|(v, d)| (d.display().to_string(), v))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new(\"/sys/fs/cgroup\"), limit_file)\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 {\n".to_string()), " (i64::MAX as u64 / page_size) * page_size\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n".to_string()), " let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n".to_string()), " if page_size <= 0 {\n".to_string()), " return Some((\n".to_string()), " \"/proc/self/mountinfo\".to_string(),\n".to_string()), " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n".to_string()), " ));\n".to_string()), " }\n".to_string()), " host_budget_cgroup_v1_under(std::path::Path::new(\"/\"), &self_cg, &mountinfo, page_size as u64)\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_under(fs_root: &std::path::Path, self_cg: &str, mountinfo: &str, page_size: u64) -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?;\n".to_string()), " let dir_path = std::path::Path::new(&dir);\n".to_string()), " let joined = fs_root.join(dir_path.strip_prefix(\"/\").unwrap_or(dir_path));\n".to_string()), " let value = match std::fs::read_to_string(joined.join(\"memory.stat\")) {\n".to_string()), " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n".to_string()), " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n".to_string()), " };\n".to_string()), " Some((joined.display().to_string(), value))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n".to_string()), " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n".to_string()), " let fields: Vec<&str> = line.split(' ').collect();\n".to_string()), " let dash = fields.iter().position(|f| *f == \"-\")?;\n".to_string()), " let fstype = fields.get(dash + 1)?;\n".to_string()), " let super_opts = fields.get(dash + 3)?;\n".to_string()), " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n".to_string()), " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n".to_string()), " } else {\n".to_string()), " None\n".to_string()), " }\n".to_string()), " })?;\n".to_string()), " let path = self_cg.lines().find_map(|l| {\n".to_string()), " let mut parts = l.splitn(3, ':');\n".to_string()), " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n".to_string()), " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n".to_string()), " })?;\n".to_string()), " let rel = if mount_root == \"/\" {\n".to_string()), " path.as_str()\n".to_string()), " } else {\n".to_string()), " let rest = path.strip_prefix(mount_root.as_str())?;\n".to_string()), " if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n".to_string()), " rest\n".to_string()), " };\n".to_string()), " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n".to_string()), " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n".to_string()), " let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n".to_string()), " let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size);\n".to_string()), " match body.trim().parse::() {\n".to_string()), " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n".to_string()), " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n".to_string()), " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n".to_string()), "pub fn host_budget_darwin_physical() -> Option {\n".to_string()), " #[cfg(target_os = \"macos\")]\n".to_string()), " {\n".to_string()), " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n".to_string()), " let mut value: u64 = 0;\n".to_string()), " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n".to_string()), " let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n".to_string()), " if rc == 0 && value > 0 { Some(value) } else { None }\n".to_string()), " }\n".to_string()), " #[cfg(not(target_os = \"macos\"))]\n".to_string()), " { None }\n".to_string()), "}\n".to_string()) +} + pub fn rt_accelerator_demo_kernel() -> String { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("fn int_relu(x: i64) -> i64 {\n".to_string(), " if x > 0 { x } else { 0 }\n".to_string()), "}\n\n".to_string()), "/// Opcode wire authority: `extdeps.languages.simd.kernel.relu_mul_add_wire_op_codes`.\n".to_string()), "/// Scaffold regen-sync: see `host_kernel_relu_mul_add_op_codes_scaffold_note` until feature:dag-kernel-realization-handler.\n".to_string()), "const RELU_MUL_ADD_OP_CODES: [i64; 3] = [1, 2, 3];\n\n".to_string()), "fn assert_relu_mul_add_op_codes(op_codes: &[i64], _handler: &str) {\n".to_string()), " if op_codes != RELU_MUL_ADD_OP_CODES {\n".to_string()), " panic!(\"unsupported op_codes in accelerator demo kernel\");\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Host bridge: `.dag` passes `std.numerical_contract.FmaContractionPolicy`; interpreter maps to 0/1 until target_model emit.\n".to_string()), "pub fn contiguous_loop_elementwise_float_kernel(\n".to_string()), " op_codes: &[i64],\n".to_string()), " fma_contraction_policy: i64,\n".to_string()), " a: &[f64],\n".to_string()), " b: &[f64],\n".to_string()), " c: &[f64],\n".to_string()), ") -> Vec {\n".to_string()), " assert_relu_mul_add_op_codes(op_codes, \"contiguous_loop_elementwise_float_kernel\");\n".to_string()), " assert_eq!(a.len(), b.len());\n".to_string()), " assert_eq!(b.len(), c.len());\n".to_string()), " let mut out = Vec::with_capacity(a.len());\n".to_string()), " for i in 0..a.len() {\n".to_string()), " let elem = match fma_contraction_policy {\n".to_string()), " 0 => {\n".to_string()), " let prod = a[i] * b[i];\n".to_string()), " prod + c[i]\n".to_string()), " },\n".to_string()), " 1 => a[i].mul_add(b[i], c[i]),\n".to_string()), " _ => panic!(\"unknown fma_contraction_policy\"),\n".to_string()), " };\n".to_string()), " out.push(if elem > 0.0 { elem } else { 0.0 });\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "/// Integer oracle authority: `gunbc.accelerator_demo_eval` via interpreter `Int` binops (`*`/`+`).\n".to_string()), "/// Host kernel uses `wrapping_*` to mirror release interpreter overflow semantics; demo fixtures\n".to_string()), "/// stay in-range under `IntegerExact` — out-of-range inputs are outside the bit-exact bar.\n".to_string()), "pub fn contiguous_loop_elementwise_kernel(\n".to_string()), " op_codes: &[i64],\n".to_string()), " a: &[i64],\n".to_string()), " b: &[i64],\n".to_string()), " c: &[i64],\n".to_string()), ") -> Vec {\n".to_string()), " assert_relu_mul_add_op_codes(op_codes, \"contiguous_loop_elementwise_kernel\");\n".to_string()), " assert_eq!(a.len(), b.len());\n".to_string()), " assert_eq!(b.len(), c.len());\n".to_string()), " let mut out = Vec::with_capacity(a.len());\n".to_string()), " for i in 0..a.len() {\n".to_string()), " let tmp = a[i].wrapping_mul(b[i]).wrapping_add(c[i]);\n".to_string()), " out.push(int_relu(tmp));\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n".to_string()) } diff --git a/src/v1/stage0/src/v1_compiler_trace.rs b/src/v1/stage0/src/v1_compiler_trace.rs index 1637cff79ed..a86f63edfb6 100644 --- a/src/v1/stage0/src/v1_compiler_trace.rs +++ b/src/v1/stage0/src/v1_compiler_trace.rs @@ -98,15 +98,10 @@ pub fn trace_pop_frame(trace: Rc) -> Rc { } else { Rc::new(Trace { events: trace.events.clone(), - stack: Rc::new( - trace - .stack - .clone() - .iter() - .cloned() - .take(v1_rt::int_sub(n.clone(), 1) as usize) - .collect::>(), - ), + stack: Rc::new(v1_rt::list_take( + &trace.stack.clone(), + v1_rt::int_sub(n.clone(), 1), + )), }) } } diff --git a/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs b/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs index 645e5e50b4c..149ffd3b5f3 100644 --- a/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs +++ b/src/v1/stage0/src/v1_compiler_trait_bound_witness.rs @@ -1,7 +1,6 @@ // Generated by v1 compiler -- do not edit. // Source module: v1.compiler.trait_bound_witness -use self::TraitBoundWitnessScope::*; use crate::v1_rt; use crate::v1_rt::{VecCompat, VecJoin}; use crate::NonEmptyBTreeSet; @@ -9,14 +8,6 @@ use crate::NonEmptyVec; use im::{vector as vec, HashMap, OrdSet as BTreeSet, Vector as Vec}; use std::rc::Rc; -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] -#[serde(tag = "_variant")] -pub enum TraitBoundWitnessScope { - BoundedToDirectTopLevelMatchBody, -} - pub fn v1_rc_match_scrutinee_clone_bound_param_names( generic_param_names: Rc>, scrutinee_type_arg_names: Rc>, @@ -205,6 +196,3 @@ pub fn v1_equality_bound_param_name( } } } - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct BoundedToDirectTopLevelMatchBody; diff --git a/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs b/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs index 4f1778fa883..e022f34832a 100644 --- a/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs +++ b/src/v1/stage0/src/v1_compiler_trait_derive_emit.rs @@ -2106,22 +2106,8 @@ pub fn v1_ord_propagated_zip_loop( (here.clone() || v1_ord_propagated_zip_loop( param_name.clone(), - Rc::new( - decl_params - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - type_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&decl_params.clone(), 1)), + Rc::new(v1_rt::list_skip(&type_args.clone(), 1)), decl_set_affected_names.clone(), source_indices.clone(), )) @@ -2775,22 +2761,8 @@ pub fn v1_declared_type_app_mentions_param_non_phantom_loop( (here.clone() || v1_declared_type_app_mentions_param_non_phantom_loop( param_name.clone(), - Rc::new( - decl_params - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - type_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&decl_params.clone(), 1)), + Rc::new(v1_rt::list_skip(&type_args.clone(), 1)), phantom_slot_names.clone(), bounds.clone(), type_decl_items.clone(), @@ -3652,22 +3624,8 @@ pub fn v1_declared_type_app_clone_impl_needs_param_loop( (here.clone() || v1_declared_type_app_clone_impl_needs_param_loop( param_name.clone(), - Rc::new( - decl_params - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - type_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&decl_params.clone(), 1)), + Rc::new(v1_rt::list_skip(&type_args.clone(), 1)), phantom_slot_names.clone(), bounds.clone(), type_decl_items.clone(), @@ -3735,22 +3693,8 @@ pub fn v1_declared_arg_positions_need_clone_param( (here.clone() || v1_declared_arg_positions_need_clone_param( param_name.clone(), - Rc::new( - decl_params - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), - Rc::new( - type_args - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&decl_params.clone(), 1)), + Rc::new(v1_rt::list_skip(&type_args.clone(), 1)), bound_params.clone(), bounds.clone(), type_decl_items.clone(), diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 6ed58544e37..a18e57618cc 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -68,11 +68,11 @@ use crate::v1_std_core::{ index_base, index_expr, is_file_transport, is_rest_transport, is_shell_transport, lambda_body, lambda_param_names_at, let_binding_name_at, let_body, let_value, match_arm_nodes, match_scrutinee, method_arg_nodes, method_receiver, param_node_default_value, - param_node_name_at, qualified_last_segment, record_lit_type_name_at, return_value, slice_base, - slice_end, slice_start, transport_stdin, type_name_compatible, unaryop_operand, CallSemantics, - Cardinality, Connective, ErrorNode, ExprData, FieldAccessStyle, FieldSummary, FieldValueShape, - InferredNode, MatchPattern, MethodSemantics, NewlineIndex, Node, SourceSpan, StringPart, - UnaryOpKind, VarBindingKind, + param_node_name_at, param_node_type_expr, qualified_last_segment, record_lit_type_name_at, + return_value, slice_base, slice_end, slice_start, transport_stdin, type_name_compatible, + unaryop_operand, CallSemantics, Cardinality, Connective, ErrorNode, ExprData, FieldAccessStyle, + FieldSummary, FieldValueShape, InferredNode, MatchPattern, MethodSemantics, NewlineIndex, Node, + SourceSpan, StringPart, UnaryOpKind, VarBindingKind, }; #[path = "bounded_shell_host_drain.rs"] @@ -1100,16 +1100,74 @@ fn value_eq_calls() -> u64 { /// onto a heap worklist and dropped from there with its own children already detached. Shared /// children (`Rc` strong count above one) are left in place; their last owner detaches them. /// Class: gunbc.recurring_failure_mode recursion_over_value_depth_uncounted_by_the_call_limit. +/// +/// A PERSISTENT CARRIER IS DETACHED ONLY ONCE DROP NESTING IS DEEP. A List or Map is an RRB / HAMT +/// tree whose nodes may be shared with other values -- a `skip` or `map_insert` result shares +/// all but a logarithmic boundary with its source -- and consuming one into the worklist clones +/// every member out of the shared nodes, so dropping a slice cost its source's length and a walk +/// that slices by offset was quadratic in the drop alone. Detaching exists for host-stack depth, +/// and depth only accrues where one carrier's drop nests another's: below +/// `VALUE_DROP_INLINE_DEPTH` nested drops the carrier is released by its own drop, which frees +/// shared nodes by reference count and runs each member's `Value::drop` (itself iterative over +/// Variant / Record depth); at or beyond it the carrier is detached as before, so a list nested +/// to any depth still drops in bounded host stack (`value_depth_walker_tests::a_deep_list_drops`). impl Drop for Value { fn drop(&mut self) { + let depth = VALUE_DROP_DEPTH + .try_with(|d| { + let depth = d.get(); + d.set(depth + 1); + depth + }) + .unwrap_or(VALUE_DROP_INLINE_DEPTH); + let detach_carriers = depth >= VALUE_DROP_INLINE_DEPTH; let mut pending: Vec = Vec::new(); - detach_owned_children(self, &mut pending); + detach_owned_children(self, &mut pending, detach_carriers); while let Some(mut child) = pending.pop() { - detach_owned_children(&mut child, &mut pending); + detach_owned_children(&mut child, &mut pending, detach_carriers); } + // A carrier left attached is released HERE, while the depth is raised: drop glue would + // release it only after this function returns and the depth is restored, and a nested + // carrier's members would then never see the depth they are at. + match self { + Value::List(items) => drop(std::mem::replace(items, empty_list_carrier())), + Value::Map(entries) => drop(std::mem::replace(entries, empty_map_carrier())), + _ => {} + } + let _ = VALUE_DROP_DEPTH.try_with(|d| d.set(depth)); } } +thread_local! { + static EMPTY_LIST_CARRIER: Rc> = Rc::new(RrbVector::new()); + static EMPTY_MAP_CARRIER: Rc> = Rc::new(HamtMap::new()); +} + +fn empty_list_carrier() -> Rc> { + EMPTY_LIST_CARRIER + .try_with(Rc::clone) + .unwrap_or_else(|_| Rc::new(RrbVector::new())) +} + +fn empty_map_carrier() -> Rc> { + EMPTY_MAP_CARRIER + .try_with(Rc::clone) + .unwrap_or_else(|_| Rc::new(HamtMap::new())) +} + +const VALUE_DROP_INLINE_DEPTH: u32 = 64; + +thread_local! { + static VALUE_DROP_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +// TEST-ONLY WORK OBSERVATION: members moved out of a persistent carrier by drop, so the drop's +// cost contract is observed as work rather than time. +#[cfg(test)] +thread_local! { + static DROP_DETACHED_CARRIER_MEMBERS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + /// The portable form is a plain tree with no sharing, so it is as deep as the value it was taken /// from and needs the same iterative drop (`impl Drop for Value`): a recursive drop of a deep /// portable chain aborted the process (stack overflow) in @@ -1141,15 +1199,19 @@ fn detach_portable_children(value: &mut PortableValue, pending: &mut Vec) { +fn detach_owned_children(value: &mut Value, pending: &mut Vec, detach_carriers: bool) { match value { - Value::List(items) => { + Value::List(items) if detach_carriers => { if let Some(items) = Rc::get_mut(items) { + #[cfg(test)] + DROP_DETACHED_CARRIER_MEMBERS.with(|c| c.set(c.get() + items.len() as u64)); pending.extend(std::mem::take(items)); } } - Value::Map(entries) => { + Value::Map(entries) if detach_carriers => { if let Some(entries) = Rc::get_mut(entries) { + #[cfg(test)] + DROP_DETACHED_CARRIER_MEMBERS.with(|c| c.set(c.get() + entries.len() as u64)); pending.extend(std::mem::take(entries).into_iter().map(|(_, v)| v)); } } @@ -5530,12 +5592,6 @@ mod cross_claim_memo_tests { } } -#[derive(Default)] -struct ParseTableMemo { - map: HashMap<(String, String, i64, Symbol), Value>, - keepalive: Vec, -} - // Recompute-trace ledger (diagnostic READ mode: reports, never gates — DESIGN §5 stopped-line // audit). Counts evaluations of pure named fns (empty `uses` row) per (fn identity, argument // identity). Keying is SOUND-ONLY: an argument without a cheap sound identity (composite @@ -6518,7 +6574,6 @@ pub struct InterpContext { cast_source_name_cache: std::cell::RefCell>, cast_source_name_cache_keepalive: std::cell::RefCell>>, pure_call_memo: std::cell::RefCell, - parse_table_memo: std::cell::RefCell, eval_recompute_trace: std::cell::RefCell, eval_call_memo: std::cell::RefCell, // Effect-dispatch odometer, incremented per service-operation dispatch. The eval-call memo @@ -6944,7 +6999,6 @@ impl InterpContext { cast_source_name_cache: std::cell::RefCell::new(HashMap::new()), cast_source_name_cache_keepalive: std::cell::RefCell::new(Vec::new()), pure_call_memo: std::cell::RefCell::new(PureCallMemo::default()), - parse_table_memo: std::cell::RefCell::new(ParseTableMemo::default()), eval_recompute_trace: std::cell::RefCell::new(EvalRecomputeTrace::default()), eval_call_memo: std::cell::RefCell::new(EvalCallMemo::default()), effect_dispatch_count: std::cell::Cell::new(0), @@ -7366,6 +7420,26 @@ pub fn declared_parameter_names(ctx: &InterpContext, entry_fn: &str) -> Option Option)>> { + let fn_node = ctx.lookup_fn(entry_fn)?; + Some( + fn_node + .params + .iter() + .map(|p| { + ( + authored_name_at(ctx.si(), p.clone()), + param_node_type_expr(p.clone()), + ) + }) + .collect(), + ) +} + /// Peak parent-chain depth observed across `call_function` frames in the last /// `run_in_context*` invocation (test witness for lexical-base scoping). #[cfg(any(test, feature = "interp_test_witness"))] @@ -8463,7 +8537,24 @@ fn eval_expr_inner(node: &Rc, env: &Rc, ctx: &InterpContext) -> Inter ), }), - ExprData::NoExprData => Ok(Value::Unit), + ExprData::NoExprData => { + // A List/Set type node (make_container_type) is NoExprData. Evaluating it as + // [] was a fabricated default (review 77868, DESIGN §5/§6b). Empty list + // values keep ExprListLit and evaluate in that arm. A type used as a value + // refuses. + let ty = match node.inferred.as_deref() { + Some(InferredNode::Resolved { node: t, .. }) => t.clone(), + _ => node.clone(), + }; + if crate::v1_compiler_infer_types::node_is_element_collection(ty, ctx.si()) { + Err(InterpError::TypeError { + msg: "type node used as a value (empty list must remain ExprListLit)" + .to_string(), + }) + } else { + Ok(Value::Unit) + } + } } } @@ -11232,10 +11323,6 @@ fn eval_call(node: &Rc, env: &Rc, ctx: &InterpContext) -> InterpResul return result; } - if let Some(result) = try_parse_table_memo_dispatch(ctx, &func_name, &fn_node, &args, env)? { - return Ok(result); - } - if let Some(key) = pure_call_memo_key(&fn_node, &func_name, &args) { if let Some(v) = pure_call_memo_get(ctx, &key) { return Ok(v); @@ -11461,14 +11548,6 @@ fn eval_fold_list_right_native( Ok(acc) } -fn witness_holds(value: Value, ctx: &InterpContext) -> Value { - Value::Variant { - type_name: ctx.sym("Witness"), - variant_name: ctx.sym("Holds"), - fields: Rc::new(vec![(ctx.sym("value"), value)]), - } -} - fn witness_violates(diagnostic: Value, ctx: &InterpContext) -> Value { Value::Variant { type_name: ctx.sym("Witness"), @@ -11477,133 +11556,6 @@ fn witness_violates(diagnostic: Value, ctx: &InterpContext) -> Value { } } -fn parse_table_materialization_allows_memo(ctx: &InterpContext, table: &Value) -> bool { - // SCAFFOLD (§7 seed-retained): extdeps/realization/parse_table_memo.dag - // parse_table_memo_seed_handler_dissolution_trigger (Disposition Scaffold) — seed - // try_parse_table_memo_dispatch gates ParseTableMemo map insert/serve on Memoize; - // .dag authority: v2.compiler.materialization_allows_memo_store. - let table_fields = match table { - Value::Record { fields, .. } | Value::Variant { fields, .. } => fields, - _ => return false, - }; - let Some(mat) = ctx.field(table_fields, "materialization") else { - return false; - }; - match mat { - Value::Variant { variant_name, .. } => resolve_sym(*variant_name) == "Memoize", - _ => false, - } -} - -fn parse_table_memo_scope_and_key( - ctx: &InterpContext, - table: &Value, - key: &Value, -) -> Option<(String, String, i64, Symbol)> { - if !parse_table_materialization_allows_memo(ctx, table) { - return None; - } - let table_fields = match table { - Value::Record { fields, .. } | Value::Variant { fields, .. } => fields, - _ => return None, - }; - let grammar_digest = match ctx.field(table_fields, "grammar_digest")? { - Value::Str(s) => s.to_string(), - _ => return None, - }; - let token_stream_digest = match ctx.field(table_fields, "token_stream_digest")? { - Value::Str(s) => s.to_string(), - _ => return None, - }; - let key_fields = match key { - Value::Record { fields, .. } | Value::Variant { fields, .. } => fields, - _ => return None, - }; - let position = match fields_get(key_fields, ctx.sym("position")) { - Some(Value::Int(n)) => *n, - _ => return None, - }; - let production = match fields_get(key_fields, ctx.sym("production")) { - Some(Value::Str(s)) => ctx.sym(s.as_ref()), - _ => return None, - }; - Some((grammar_digest, token_stream_digest, position, production)) -} - -/// Handler bodies for parse-table memo dispatch. Roster authority is -/// `v1_interpreter_authored_roster_arms()`; generated lookup routes spellings -/// before this macro matches on the generated enum variant. -macro_rules! v1_parse_table_arms { - ($cb:ident, $func_name:ident, $ctx:ident, $fn_node:ident, $args:ident, $env:ident) => { - $cb! { - $func_name, $ctx, $fn_node, $args, $env; - arm "parse_table_memo.parse_table_lookup" { "parse_table_lookup" } => { - let positional: Vec<&Value> = $args.iter().map(|(_, v)| v).collect(); - let [table, key] = match positional.as_slice() { - [table, key] => [table, key], - _ => return Ok(None), - }; - let Some(memo_key) = parse_table_memo_scope_and_key($ctx, table, key) else { - return Ok(None); - }; - let allows_memo = parse_table_materialization_allows_memo($ctx, table); - let mut st = $ctx.parse_table_memo.borrow_mut(); - if allows_memo { - if let Some(v) = st.map.get(&memo_key).cloned() { - drop(st); - record_parse_memo_lookup(&memo_key, true); - return Ok(Some(witness_holds(v, $ctx))); - } - } - drop(st); - record_parse_memo_lookup(&memo_key, false); - let result = call_function($ctx, $fn_node, $args, $env)?; - Ok(Some(result)) - }, - arm "parse_table_memo.parse_table_insert" { "parse_table_insert" } => { - let positional: Vec<&Value> = $args.iter().map(|(_, v)| v).collect(); - let [table, key, value] = match positional.as_slice() { - [table, key, value] => [table, key, value], - _ => return Ok(None), - }; - let result = call_function($ctx, $fn_node, $args, $env)?; - if parse_table_materialization_allows_memo($ctx, table) { - if let Some(memo_key) = parse_table_memo_scope_and_key($ctx, table, key) { - let mut st = $ctx.parse_table_memo.borrow_mut(); - st.keepalive.push((*table).clone()); - st.keepalive.push((*key).clone()); - st.keepalive.push((*value).clone()); - st.map.insert(memo_key, (*value).clone()); - } - } - Ok(Some(result)) - }, - } - }; -} - -/// Expansion 1: the dispatch. -macro_rules! v1_parse_table_dispatch { - ($f:ident, $c:ident, $n:ident, $a:ident, $e:ident; $(arm $id:tt { $lit:literal } => $body:expr ,)*) => { - match $crate::v1_interpreter_dispatch_generated::lookup_try_parse_table_memo_dispatch(&$f) { - Some(arm) => match arm { - $( try_parse_table_memo_dispatch_arm!($id) => $body , )* - }, - None => Ok(None), - } - }; -} - -fn try_parse_table_memo_dispatch( - ctx: &InterpContext, - func_name: &str, - fn_node: &Rc, - args: &[(Option, Value)], - env: &Rc, -) -> InterpResult> { - v1_parse_table_arms!(v1_parse_table_dispatch, func_name, ctx, fn_node, args, env) -} - fn is_structural_pure_fn(name: &str) -> bool { matches!( name, @@ -13395,6 +13347,17 @@ fn lookup_type_item_across_modules(ctx: &InterpContext, type_name: &str) -> Opti ctx.indexes.type_items.get(type_name).cloned() } +/// RHS of a type declaration, when the item is an alias or resolved target. +pub fn type_declaration_rhs(item: &Rc) -> Option> { + if let Some(rhs) = item.children.iter().next().cloned() { + return Some(rhs); + } + match item.inferred.as_deref() { + Some(InferredNode::Resolved { node }) => Some(node.clone()), + _ => None, + } +} + #[cfg(test)] mod cast_scope_index_tests { use super::*; @@ -14229,17 +14192,13 @@ macro_rules! v1_algebra_method_arms { arm "method_call.skip" { "skip" } => { let items = expect_list(&$receiver, "skip")?; let n = expect_int($args.first(), "skip")?; - Ok(list_value( - items.iter().skip(n as usize).cloned().collect::>(), - )) + Ok(Value::List(Rc::new(rrb_skip(&items, n)))) }, arm "method_call.take" { "take" } => { let items = expect_list(&$receiver, "take")?; let n = expect_int($args.first(), "take")?; - Ok(list_value( - items.iter().take(n as usize).cloned().collect::>(), - )) + Ok(Value::List(Rc::new(rrb_take(&items, n)))) }, arm "method_call.enumerate" { "enumerate" } => { @@ -18076,7 +18035,7 @@ fn dispatch_shell( let (budget, source) = crate::memory_governor::read_host_budget_bytes(); Some(budget.ok_or_else(|| InterpError::TypeError { msg: format!( - "WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({source})" + "WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({source})" ), })? as usize) } @@ -23604,7 +23563,7 @@ macro_rules! v1_builtin_arms { arm "free_call.chars_to_string" { "chars_to_string" } => { let cps = match $positional.first().copied() { - Some(v) => free_monoid_to_vec(v).ok_or_else(|| InterpError::TypeError { + Some(v) => free_monoid_items(v).ok_or_else(|| InterpError::TypeError { msg: "chars_to_string expects a list of code points".to_string(), })?, None => { @@ -23621,7 +23580,9 @@ macro_rules! v1_builtin_arms { .max(0) .min(len) .max(start); - let s: String = cps[start as usize..end as usize] + let s: String = cps + .skip(start as usize) + .take((end - start) as usize) .iter() .filter_map(|v| match v { Value::Int(cp) => char::from_u32(*cp as u32), @@ -23632,6 +23593,10 @@ macro_rules! v1_builtin_arms { }, arm "free_call.get" { "get" } => match $positional.as_slice() { + [Value::List(items), idx_val] => { + let idx = expect_int(Some(idx_val), "get")?; + Ok(Some(list_get_at_or_null(items, idx))) + } [list_val, idx_val] if free_monoid_to_vec(list_val).is_some() => { let items = expect_list(list_val, "get")?; let idx = expect_int(Some(idx_val), "get")?; @@ -23712,6 +23677,7 @@ macro_rules! v1_builtin_arms { }, arm "free_call.count" { "count" } => match $positional.first() { + Some(Value::List(items)) => Ok(Some(Value::Int(items.len() as i64))), Some(v) => match free_monoid_to_vec(v) { Some(items) => Ok(Some(Value::Int(items.len() as i64))), None => Ok(None), @@ -25735,45 +25701,6 @@ static CALL_FREQUENCY_WATCHLIST: std::sync::Mutex< Option>, > = std::sync::Mutex::new(None); -/// adhoc-c328b166-bca memo-effectiveness discriminator: distinct (grammar_digest, -/// token_stream_digest, position, production) keys ever looked up vs total lookups/hits. -/// `lookups >> distinct` with `hits == 0` is the smoking gun for "memo never serves a -/// re-attempted span"; `lookups == distinct` is the benign "every position visited once" -/// signature. Global (not per-InterpContext) so the periodic dump thread -/// (GUNBC_FLATTEN_SITE_DUMP_SECS), which never enters with_active_context, can read it -- -/// survives a DNF, unlike ctx-scoped stats. -static PARSE_MEMO_LOOKUPS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); -static PARSE_MEMO_HITS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); -static PARSE_MEMO_DISTINCT_KEYS: std::sync::Mutex< - Option>, -> = std::sync::Mutex::new(None); - -fn record_parse_memo_lookup(key: &(String, String, i64, Symbol), hit: bool) { - if !residual_hunt_forensics_enabled() { - return; - } - PARSE_MEMO_LOOKUPS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - if hit { - PARSE_MEMO_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - } - let mut guard = PARSE_MEMO_DISTINCT_KEYS.lock().unwrap(); - guard - .get_or_insert_with(std::collections::HashSet::new) - .insert(key.clone()); -} - -pub fn parse_memo_global_snapshot() -> (u64, u64, u64) { - let lookups = PARSE_MEMO_LOOKUPS.load(std::sync::atomic::Ordering::Relaxed); - let hits = PARSE_MEMO_HITS.load(std::sync::atomic::Ordering::Relaxed); - let distinct = PARSE_MEMO_DISTINCT_KEYS - .lock() - .unwrap() - .as_ref() - .map(|s| s.len() as u64) - .unwrap_or(0); - (lookups, hits, distinct) -} - fn record_call_frequency(func_name: &str) { if !residual_hunt_forensics_enabled() { return; @@ -25797,17 +25724,10 @@ fn record_call_frequency(func_name: &str) { "parse_expr_sequence", "parse_expr_choice", "parse_expr_optional", - "parse_production_memo_stats", "filter", "upsert_production_first_row", "parse_current_position", - "parse_nonterminal_memoized", - "parse_nonterminal_memoized_core", - "parse_table_record_lookup_call", - "parse_table_record_hit", - "parse_table_record_miss", - "parse_table_lookup", - "parse_table_insert", + "parse_nonterminal", "parse_choice_plan", "parse_choice_ordered_backtrack", "uri_percent_encode_scalar_fragment", @@ -26486,6 +26406,183 @@ fn value_to_list_carrier(val: &Value) -> Option<(Rc>, u64)> { } } +/// The members of a code-point (or any FreeMonoid) value as the persistent carrier, sharing a +/// `Value::List`'s tree rather than flattening it. `free_monoid_to_vec` copies all n members, so a +/// consumer that reads a slice through it pays O(n) per read and O(n^2) over a walk; this reads the +/// list form in O(1) and flattens only the Str and Cons forms, which have no shared tree to borrow. +fn free_monoid_items(val: &Value) -> Option>> { + match val { + Value::List(items) => Some(Rc::clone(items)), + other => free_monoid_to_vec(other).map(|items| Rc::new(RrbVector::from(items))), + } +} + +/// std.primitives skip_contract on the persistent carrier: the suffix after `n` members, sharing +/// the receiver's tree, O(log n) rather than a copy of the remainder. A negative `n` keeps the +/// reading the copying form had (`n as usize` saturates past the end), which skips everything. +pub(crate) fn rrb_skip(items: &RrbVector, n: i64) -> RrbVector { + let k = if n < 0 { + items.len() + } else { + (n as usize).min(items.len()) + }; + items.skip(k) +} + +/// std.primitives take_contract on the persistent carrier: the first `n` members, sharing the +/// receiver's tree. A negative `n` keeps every member, as the copying form's saturating cast did. +pub(crate) fn rrb_take(items: &RrbVector, n: i64) -> RrbVector { + let k = if n < 0 { + items.len() + } else { + (n as usize).min(items.len()) + }; + items.take(k) +} + +#[cfg(test)] +mod slice_cost_tests { + use super::*; + use std::cell::Cell; + + thread_local! { + static CLONES: Cell = const { Cell::new(0) }; + } + + /// A code point whose every copy is counted, so a slice's cost is read off the carrier's + /// own work rather than a clock. + #[derive(Debug, PartialEq)] + struct CountedCodePoint(u32); + + impl Clone for CountedCodePoint { + fn clone(&self) -> Self { + CLONES.with(|c| c.set(c.get() + 1)); + CountedCodePoint(self.0) + } + } + + fn clones_during(f: impl FnOnce() -> R) -> (R, u64) { + let before = CLONES.with(|c| c.get()); + let r = f(); + (r, CLONES.with(|c| c.get()) - before) + } + + // Non-ASCII on purpose: the interpreter's string carrier is O(1) per index only on ASCII, + // which is the case a byte-offset decode is NOT linear on. + fn non_ascii_code_points(n: usize) -> RrbVector { + "\u{e9}\u{4e2d}\u{1f600}a" + .chars() + .cycle() + .take(n) + .map(|c| CountedCodePoint(c as u32)) + .collect() + } + + const N: usize = 200_000; + // im's RRB split copies at most one boundary chunk per tree level; this is that bound with + // headroom, and three orders of magnitude below N. + const SLICE_COPY_BUDGET: u64 = 2_048; + + #[test] + fn a_slice_from_the_middle_copies_only_boundary_chunks() { + let cps = non_ascii_code_points(N); + let (slice, copied) = clones_during(|| rrb_take(&rrb_skip(&cps, (N / 2) as i64), 7)); + assert!( + copied <= SLICE_COPY_BUDGET, + "skip+take copied {copied} members of {N}" + ); + let expected: Vec = cps.iter().skip(N / 2).take(7).map(|c| c.0).collect(); + assert_eq!(slice.iter().map(|c| c.0).collect::>(), expected); + } + + // THE RED CONTROL: the copying form these arms had, on the same input and bound. It must + // exceed the budget, or the budget does not discriminate the quadratic. + #[test] + fn the_copying_slice_form_exceeds_the_budget() { + let cps = non_ascii_code_points(N); + let (_, copied) = clones_during(|| { + let rest: RrbVector<_> = cps.iter().skip(N / 2).cloned().collect(); + rest.iter().take(7).cloned().collect::>() + }); + assert!( + copied > SLICE_COPY_BUDGET, + "copying form copied only {copied}" + ); + } + + #[test] + fn negative_and_overlong_counts_keep_the_copying_forms_reading() { + let cps: RrbVector = (0..10).collect(); + let copying_skip = |n: i64| { + cps.iter() + .skip(n as usize) + .cloned() + .collect::>() + }; + let copying_take = |n: i64| { + cps.iter() + .take(n as usize) + .cloned() + .collect::>() + }; + for n in [-3, 0, 4, 10, 11, i64::MAX] { + assert_eq!(rrb_skip(&cps, n), copying_skip(n), "skip {n}"); + assert_eq!(rrb_take(&cps, n), copying_take(n), "take {n}"); + } + } + + fn detached() -> u64 { + DROP_DETACHED_CARRIER_MEMBERS.with(|c| c.get()) + } + + // The slice's drop is part of the slice's cost: a walk drops one slice per step. Below the + // nesting threshold the RRB tree releases its nodes by reference count, so dropping a slice + // of a shared list moves no member; the copying drop moved the whole suffix. + #[test] + fn dropping_a_slice_of_a_shared_list_moves_no_member() { + let base = list_value((0..N as i64).map(Value::Int).collect::>()); + let Value::List(items) = &base else { + unreachable!("list_value builds a list") + }; + let slice = Value::List(Rc::new(rrb_skip(items, (N / 2) as i64))); + let before = detached(); + drop(slice); + assert_eq!(detached() - before, 0, "dropping a shared slice walked it"); + assert_eq!(items.len(), N, "the source survives its slice's drop"); + } + + // THE RED CONTROL: the same drop once nesting is past the threshold, which is the detaching + // path every list drop used to take. It moves the slice's members, so the counter discriminates. + #[test] + fn the_detaching_drop_moves_the_slice_members() { + let base = list_value((0..N as i64).map(Value::Int).collect::>()); + let Value::List(items) = &base else { + unreachable!("list_value builds a list") + }; + let mut slice = Value::List(Rc::new(rrb_skip(items, (N / 2) as i64))); + let before = detached(); + let mut pending = Vec::new(); + detach_owned_children(&mut slice, &mut pending, true); + assert_eq!(detached() - before, (N - N / 2) as u64); + } + + #[test] + fn reading_a_list_value_does_not_flatten_it() { + let list = list_value((0..N as i64).map(Value::Int).collect::>()); + let before = flatten_counters_snapshot(); + let items = free_monoid_items(&list).expect("a list is a free monoid"); + assert_eq!(items.len(), N); + assert_eq!( + flatten_counters_snapshot(), + before, + "free_monoid_items flattened a Value::List" + ); + // RED CONTROL: the flattening read the get / count / chars_to_string arms used. + free_monoid_to_vec(&list).expect("a list is a free monoid"); + assert_eq!(flatten_counters_snapshot().1 - before.1, N as u64); + } +} + fn list_get_at_or_null(items: &RrbVector, idx: i64) -> Value { items.get(idx as usize).cloned().unwrap_or(Value::Null) } diff --git a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs index 84c11837dc2..24bc0bcb70c 100644 --- a/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs +++ b/src/v1/stage0/src/v1_interpreter_dispatch_generated.rs @@ -743,24 +743,3 @@ macro_rules! eval_call_native_intercept_arm { ("native_intercept.fold_list") => { $crate::v1_interpreter_dispatch_generated::EvalCallNativeInterceptArm::NativeInterceptFoldList }; ("native_intercept.fold_list_right") => { $crate::v1_interpreter_dispatch_generated::EvalCallNativeInterceptArm::NativeInterceptFoldListRight }; } -#[rustfmt::skip] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum TryParseTableMemoDispatchArm { - ParseTableMemoParseTableLookup, - ParseTableMemoParseTableInsert, -} - -#[rustfmt::skip] -pub fn lookup_try_parse_table_memo_dispatch(spelling: &str) -> Option { - match spelling { - "parse_table_lookup" => Some(TryParseTableMemoDispatchArm::ParseTableMemoParseTableLookup), - "parse_table_insert" => Some(TryParseTableMemoDispatchArm::ParseTableMemoParseTableInsert), - _ => None, - } -} - -#[rustfmt::skip] -macro_rules! try_parse_table_memo_dispatch_arm { - ("parse_table_memo.parse_table_lookup") => { $crate::v1_interpreter_dispatch_generated::TryParseTableMemoDispatchArm::ParseTableMemoParseTableLookup }; - ("parse_table_memo.parse_table_insert") => { $crate::v1_interpreter_dispatch_generated::TryParseTableMemoDispatchArm::ParseTableMemoParseTableInsert }; -} diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index 7204701cdf4..5a77c89eb7a 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -758,6 +758,118 @@ pub fn list_push(mut list: Vec, item: T) -> Vec { list } +/// std.primitives skip_contract on the persistent carrier: the suffix after `n` members, +/// sharing the receiver's tree in O(log n) instead of copying the remainder, so a walk that +/// slices by offset is O(n log n) rather than quadratic. A negative `n` skips every member, as +/// the copying form's saturating `n as usize` did. +pub fn list_skip(items: &Vec, n: i64) -> Vec { + let k = if n < 0 { + items.len() + } else { + (n as usize).min(items.len()) + }; + items.skip(k) +} + +/// std.primitives take_contract on the persistent carrier: the first `n` members, sharing +/// the receiver's tree. A negative `n` keeps every member, as the copying form did. +pub fn list_take(items: &Vec, n: i64) -> Vec { + let k = if n < 0 { + items.len() + } else { + (n as usize).min(items.len()) + }; + items.take(k) +} + +#[cfg(test)] +mod list_slice_tests { + use super::*; + + thread_local! { + static CLONES: Cell = const { Cell::new(0) }; + } + + /// A code point whose every copy is counted, so a slice's cost is read off the carrier's + /// own work rather than a clock. + #[derive(Debug, PartialEq)] + struct CountedCodePoint(u32); + + impl Clone for CountedCodePoint { + fn clone(&self) -> Self { + CLONES.with(|c| c.set(c.get() + 1)); + CountedCodePoint(self.0) + } + } + + fn clones_during(f: impl FnOnce() -> R) -> (R, u64) { + let before = CLONES.with(|c| c.get()); + let r = f(); + (r, CLONES.with(|c| c.get()) - before) + } + + // Non-ASCII on purpose: emitted char_at / substring over a bare &str are O(offset) here, + // which is the walk the code-point slice replaces. + fn non_ascii_code_points(n: usize) -> Vec { + [0xe9u32, 0x4e2d, 0x1f600, 0x61] + .iter() + .cycle() + .take(n) + .map(|cp| CountedCodePoint(*cp)) + .collect() + } + + const N: usize = 200_000; + // im's RRB split copies at most one boundary chunk per tree level; this is that bound with + // headroom, and three orders of magnitude below N. + const SLICE_COPY_BUDGET: u64 = 2_048; + + #[test] + fn a_slice_from_the_middle_copies_only_boundary_chunks() { + let cps = Rc::new(non_ascii_code_points(N)); + let (slice, copied) = clones_during(|| list_take(&list_skip(&cps, (N / 2) as i64), 7)); + assert!( + copied <= SLICE_COPY_BUDGET, + "skip+take copied {} members of {}", + copied, + N + ); + let expected: std::vec::Vec = cps.iter().skip(N / 2).take(7).map(|c| c.0).collect(); + assert_eq!( + slice.iter().map(|c| c.0).collect::>(), + expected + ); + } + + // THE RED CONTROL: the extdeps.languages.rust.emit skip / take templates this runtime pair + // replaced, spelled as they emitted, on the same input and bound. It must exceed the budget, + // or the budget does not discriminate the quadratic. + #[test] + fn the_copying_template_form_exceeds_the_budget() { + let cps = Rc::new(non_ascii_code_points(N)); + let (_, copied) = clones_during(|| { + let rest = cps.iter().cloned().skip(N / 2).collect::>(); + rest.iter().cloned().take(7).collect::>() + }); + assert!( + copied > SLICE_COPY_BUDGET, + "copying form copied only {}", + copied + ); + } + + #[test] + fn negative_and_overlong_counts_keep_the_copying_forms_reading() { + let xs: Vec = (0..10).collect(); + for n in [-3i64, 0, 4, 10, 11, i64::MAX] { + let copying_skip = xs.iter().cloned().skip(n as usize).collect::>(); + let copying_take = xs.iter().cloned().take(n as usize).collect::>(); + assert_eq!(list_skip(&xs, n), copying_skip, "skip {}", n); + assert_eq!(list_take(&xs, n), copying_take, "take {}", n); + } + } +} + pub fn append(list: Rc>, item: T) -> Vec { let mut v = (*list).clone(); v.push_back(item); @@ -1614,3 +1726,323 @@ pub fn contiguous_loop_elementwise_kernel( } out } +/// The one host-budget precedence. `read_host_budget_bytes` and +/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HostBudgetJoinSource { + CgroupMemoryHigh { cgroup_dir: String }, + CgroupMemoryMax { cgroup_dir: String }, + CgroupV1HierarchicalMemoryLimit { cgroup_dir: String }, + DarwinPhysicalMemory, +} + +impl HostBudgetJoinSource { + pub fn label(&self) -> String { + match self { + HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => { + format!("cgroup memory.high ({})", cgroup_dir) + } + HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => { + format!("cgroup memory.max ({})", cgroup_dir) + } + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!( + "cgroup v1 memory.stat hierarchical_memory_limit ({})", + cgroup_dir + ), + HostBudgetJoinSource::DarwinPhysicalMemory => "sysctl hw.memsize".to_string(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HostBudgetJoin { + Resolved { + effective_bytes: u64, + requested_bytes: Option, + source: HostBudgetJoinSource, + observed_bytes: u64, + }, + DeclaredUnverified { + requested_bytes: u64, + reason: String, + }, + Unreadable { + reason: String, + }, +} + +impl HostBudgetJoin { + pub fn bytes(&self) -> Option { + match self { + HostBudgetJoin::Resolved { + effective_bytes, .. + } => Some(*effective_bytes), + HostBudgetJoin::DeclaredUnverified { + requested_bytes, .. + } => Some(*requested_bytes), + HostBudgetJoin::Unreadable { .. } => None, + } + } + pub fn label(&self) -> String { + match self { + HostBudgetJoin::Resolved { + effective_bytes, + requested_bytes, + source, + observed_bytes, + } => match requested_bytes { + Some(requested) => format!( + "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", + effective_bytes, + requested, + source.label(), + observed_bytes + ), + None => source.label(), + }, + HostBudgetJoin::Unreadable { reason } => format!("unreadable: {}", reason), + HostBudgetJoin::DeclaredUnverified { + requested_bytes, + reason, + } => format!( + "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}", + requested_bytes, reason + ), + } + } +} + +pub fn resolve_host_budget_join( + env_override: Option, + cgroup_high: Option<(String, u64)>, + cgroup_max: Option<(String, u64)>, + cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, + darwin_physical: Option, +) -> HostBudgetJoin { + let cgroup_v1_limit = match cgroup_v1_limit { + Some((dir, HostBudgetCgroupV1::Unparseable(body))) => { + return HostBudgetJoin::Unreadable { reason: format!("cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading", dir, body) }; + } + Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)), + Some((_, HostBudgetCgroupV1::Unlimited)) | None => None, + }; + let observation = [ + cgroup_high + .map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)), + cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)), + cgroup_v1_limit.map(|(cgroup_dir, b)| { + ( + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, + b, + ) + }), + ] + .into_iter() + .flatten() + .fold( + None::<(HostBudgetJoinSource, u64)>, + |best, cand| match best { + Some(cur) if cur.1 <= cand.1 => Some(cur), + _ => Some(cand), + }, + ); + if let Some((source, observed_bytes)) = observation { + return HostBudgetJoin::Resolved { + effective_bytes: env_override + .map(|requested| requested.min(observed_bytes)) + .unwrap_or(observed_bytes), + requested_bytes: env_override, + source, + observed_bytes, + }; + } + if let Some(bytes) = darwin_physical { + return HostBudgetJoin::Resolved { + effective_bytes: env_override + .map(|requested| requested.min(bytes)) + .unwrap_or(bytes), + requested_bytes: env_override, + source: HostBudgetJoinSource::DarwinPhysicalMemory, + observed_bytes: bytes, + }; + } + if let Some(requested_bytes) = env_override { + return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: "no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string() }; + } + HostBudgetJoin::Unreadable { reason: format!("no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", std::env::consts::OS) } +} + +pub fn read_host_budget_bytes() -> (Option, String) { + let join = resolve_host_budget_join( + std::env::var("GUNBC_MEMORY_BUDGET_BYTES") + .ok() + .and_then(|s| s.trim().parse::().ok()), + host_budget_tightest_cgroup("memory.high"), + host_budget_tightest_cgroup("memory.max"), + host_budget_cgroup_v1(), + host_budget_darwin_physical(), + ); + (join.bytes(), join.label()) +} + +#[derive(Clone)] +pub enum HostBudgetCgroupV1 { + Limited(u64), + Unlimited, + Unparseable(String), +} + +pub fn host_budget_tightest_cgroup_under( + self_cg: &str, + root: &std::path::Path, + limit_file: &str, +) -> Option<(String, u64)> { + let rel = self_cg + .lines() + .find_map(|l| l.strip_prefix("0::")) + .map(|p| p.trim().trim_start_matches('/').to_string())?; + let mut dir = root.join(&rel); + let mut best: Option<(u64, std::path::PathBuf)> = None; + loop { + if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) { + let s = s.trim(); + if s != "max" { + if let Ok(v) = s.parse::() { + let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true); + if take { + best = Some((v, dir.clone())); + } + } + } + } + if dir == root || !dir.pop() { + break; + } + } + best.map(|(v, d)| (d.display().to_string(), v)) +} + +pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { + let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; + host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new("/sys/fs/cgroup"), limit_file) +} + +pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 { + (i64::MAX as u64 / page_size) * page_size +} + +pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> { + let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; + let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + if page_size <= 0 { + return Some(( + "/proc/self/mountinfo".to_string(), + HostBudgetCgroupV1::Unparseable("sysconf(_SC_PAGESIZE) unreadable".to_string()), + )); + } + host_budget_cgroup_v1_under( + std::path::Path::new("/"), + &self_cg, + &mountinfo, + page_size as u64, + ) +} + +pub fn host_budget_cgroup_v1_under( + fs_root: &std::path::Path, + self_cg: &str, + mountinfo: &str, + page_size: u64, +) -> Option<(String, HostBudgetCgroupV1)> { + let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?; + let dir_path = std::path::Path::new(&dir); + let joined = fs_root.join(dir_path.strip_prefix("/").unwrap_or(dir_path)); + let value = match std::fs::read_to_string(joined.join("memory.stat")) { + Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size), + Err(e) => HostBudgetCgroupV1::Unparseable(format!("memory.stat: {}", e)), + }; + Some((joined.display().to_string(), value)) +} + +pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { + let (mount_root, mount_point) = mountinfo.lines().find_map(|line| { + let fields: Vec<&str> = line.split(' ').collect(); + let dash = fields.iter().position(|f| *f == "-")?; + let fstype = fields.get(dash + 1)?; + let super_opts = fields.get(dash + 3)?; + if *fstype == "cgroup" && super_opts.split(',').any(|o| o == "memory") { + Some((fields.get(3)?.to_string(), fields.get(4)?.to_string())) + } else { + None + } + })?; + let path = self_cg.lines().find_map(|l| { + let mut parts = l.splitn(3, ':'); + let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?); + controllers + .split(',') + .any(|c| c == "memory") + .then(|| path.trim().to_string()) + })?; + let rel = if mount_root == "/" { + path.as_str() + } else { + let rest = path.strip_prefix(mount_root.as_str())?; + if !(rest.is_empty() || rest.starts_with('/')) { + return None; + } + rest + }; + Some( + std::path::Path::new(&mount_point) + .join(rel.trim_start_matches('/')) + .display() + .to_string(), + ) +} + +pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 { + let hits: std::vec::Vec<&str> = memory_stat + .lines() + .filter_map(|l| l.trim().strip_prefix("hierarchical_memory_limit ")) + .collect(); + let [body] = hits.as_slice() else { + return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); + }; + let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size); + match body.trim().parse::() { + Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()), + Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited, + Ok(n) => HostBudgetCgroupV1::Limited(n as u64), + Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()), + } +} + +/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`. +pub fn host_budget_darwin_physical() -> Option { + #[cfg(target_os = "macos")] + { + let name = std::ffi::CStr::from_bytes_with_nul(b"hw.memsize\0").ok()?; + let mut value: u64 = 0; + let mut len: libc::size_t = std::mem::size_of::() as libc::size_t; + let rc = unsafe { + libc::sysctlbyname( + name.as_ptr(), + (&mut value as *mut u64).cast::(), + &mut len, + std::ptr::null_mut(), + 0, + ) + }; + if rc == 0 && value > 0 { + Some(value) + } else { + None + } + } + #[cfg(not(target_os = "macos"))] + { + None + } +} diff --git a/src/v1/stage0/src/v1_std_core.rs b/src/v1/stage0/src/v1_std_core.rs index cabb658e176..6373b9b0b0c 100644 --- a/src/v1/stage0/src/v1_std_core.rs +++ b/src/v1/stage0/src/v1_std_core.rs @@ -837,6 +837,11 @@ pub enum CompilerDiagnostic { identities: Rc>, span: Rc, }, + NativeEffectRealizationRefused { + entry: String, + refusals: Rc>, + span: Rc, + }, EffectSummaryIncompleteAtFunctionValue { caller: String, span: Rc, @@ -1078,6 +1083,7 @@ pub fn diagnostic_to_span(d: Rc) -> Rc { CompilerDiagnostic::EffectfulSelfRecursionUnrealized { span: s, .. } => s.clone(), CompilerDiagnostic::ModuleFilenameCollision { span: s, .. } => s.clone(), CompilerDiagnostic::EmittedSymbolCollision { span: s, .. } => s.clone(), + CompilerDiagnostic::NativeEffectRealizationRefused { span: s, .. } => s.clone(), CompilerDiagnostic::EffectSummaryIncompleteAtFunctionValue { span: s, .. } => s.clone(), CompilerDiagnostic::EffectSummaryIncompleteAtLocalBinding { span: s, .. } => s.clone(), CompilerDiagnostic::CallArgumentNameUnknown { span: s, .. } => s.clone(), @@ -1160,6 +1166,7 @@ pub fn diagnostic_to_message(d: Rc) -> String { CompilerDiagnostic::EffectfulSelfRecursionUnrealized { name: n, .. } => v1_rt::concat(v1_rt::concat("effectful declaration '".to_string(), n.clone()), "' calls itself outside tail position: the Rust realization lowers tail calls to a loop, but non-tail async recursion has no realization. Put the recursive call in tail position or move it into a pure helper.".to_string()), CompilerDiagnostic::ModuleFilenameCollision { filename: f, modules: ms, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("module filename collision: ".to_string(), ((ms.clone().len() as i64)).to_string()), " modules render one emitted file name '".to_string()), f.clone()), "': ".to_string()), ms.clone().join(&", ".to_string())), " — module_to_filename maps '.' to '_', so these names are indistinguishable at the emitted path; rename one module segment".to_string()), CompilerDiagnostic::EmittedSymbolCollision { symbol: s, identities: ids, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("emitted symbol collision: ".to_string(), ((ids.clone().len() as i64)).to_string()), " authored identities render one emitted symbol '".to_string()), s.clone()), "': ".to_string()), ids.clone().join(&", ".to_string())), " — two authored identities one emitted scope cannot hold apart (an undotted service with a type's name, a type spelled with `__`, dotted services differing only in first-letter case or in a segment's leading or trailing `_`); rename one".to_string()), + CompilerDiagnostic::NativeEffectRealizationRefused { entry: e, refusals: rs, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("native effect realization refused for entry '".to_string(), e.clone()), "': ".to_string()), rs.clone().join(&"; ".to_string())), " (v1.compiler.emit NativeEffectRealizationAdmission: the rendered main binds only admitted handlers, so an unadmitted effect refuses here rather than failing to build or link)".to_string()), CompilerDiagnostic::EffectSummaryIncompleteAtFunctionValue { caller: c, .. } => v1_rt::concat(v1_rt::concat("effect summary incomplete: ".to_string(), c.clone()), " calls through a function value, whose callee is chosen at runtime, so its effects are unknown rather than empty — the caller's summary is a lower bound, not the answer".to_string()), CompilerDiagnostic::EffectSummaryIncompleteAtLocalBinding { caller: c, name: n, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("effect summary incomplete: ".to_string(), c.clone()), " calls the local binding '".to_string()), n.clone()), "', whose effects this pass cannot join through the registry, so the caller's summary is a lower bound rather than the answer".to_string()), CompilerDiagnostic::CallArgumentNameUnknown { callee: c, argument: a, declared: ds, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("call shape mismatch calling '".to_string(), c.clone()), "': no parameter named '".to_string()), a.clone()), "' (declared: [".to_string()), ds.clone().join(&", ".to_string())), "])".to_string()), @@ -1447,6 +1454,10 @@ pub fn diagnostic_disposition(d: Rc) -> Rc Rc::new(DiagnosticDisposition { severity: DiagnosticSeverity::SeverityError, gate: Rc::new(DiagnosticGateDisposition::GateBlocking), +}), + CompilerDiagnostic::NativeEffectRealizationRefused { .. } => Rc::new(DiagnosticDisposition { + severity: DiagnosticSeverity::SeverityError, + gate: Rc::new(DiagnosticGateDisposition::GateBlocking), }), CompilerDiagnostic::EffectSummaryIncompleteAtFunctionValue { .. } => Rc::new(DiagnosticDisposition { severity: DiagnosticSeverity::SeverityNonError, @@ -2221,7 +2232,7 @@ pub fn param_node_type_expr(n: Rc) -> Rc { pub fn param_node_default_value(n: Rc) -> Option> { if ((n.children.clone().len() as i64) > 1) { - n.children.clone().iter().cloned().skip(1 as usize).next() + n.children.clone().get((1) as usize).cloned() } else { std::option::Option::None } @@ -2298,7 +2309,7 @@ pub fn field_node_cardinality(n: Rc) -> Cardinality { pub fn field_node_default_value(n: Rc) -> Option> { if ((n.children.clone().len() as i64) > 1) { - n.children.clone().iter().cloned().skip(1 as usize).next() + n.children.clone().get((1) as usize).cloned() } else { std::option::Option::None } @@ -2570,10 +2581,8 @@ pub fn expr_child_at(texpr: Rc, index: i64, role: String) -> Rc { match texpr .children .clone() - .iter() + .get((index.clone()) as usize) .cloned() - .skip(index.clone() as usize) - .next() { Some(v) => v.clone(), std::option::Option::None => make_expr_error_node( @@ -2831,13 +2840,7 @@ pub fn if_then_branch(texpr: Rc) -> Rc { } pub fn if_else_branch(texpr: Rc) -> Option> { - texpr - .children - .clone() - .iter() - .cloned() - .skip(2 as usize) - .next() + texpr.children.clone().get((2) as usize).cloned() } pub fn match_scrutinee(texpr: Rc) -> Rc { @@ -2845,15 +2848,7 @@ pub fn match_scrutinee(texpr: Rc) -> Rc { } pub fn match_arm_nodes(texpr: Rc) -> Rc>> { - Rc::new( - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_skip(&texpr.children.clone(), 1)) } pub fn binop_left(texpr: Rc) -> Rc { @@ -2953,15 +2948,7 @@ pub fn method_receiver(texpr: Rc) -> Rc { } pub fn method_arg_nodes(texpr: Rc) -> Rc>> { - Rc::new( - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) + Rc::new(v1_rt::list_skip(&texpr.children.clone(), 1)) } pub fn expr_method_name_at( @@ -2991,17 +2978,9 @@ pub fn lambda_param_names_at( ) -> Rc> { Rc::new({ let mut __result = Vec::new(); - for n in Rc::new( - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ) - .iter() - .cloned() + for n in Rc::new(v1_rt::list_skip(&texpr.children.clone(), 1)) + .iter() + .cloned() { __result.push(authored_name_at(source_indices.clone(), n.clone())); } @@ -3014,13 +2993,7 @@ pub fn let_value(texpr: Rc) -> Rc { } pub fn let_body(texpr: Rc) -> Option> { - texpr - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .next() + texpr.children.clone().get((1) as usize).cloned() } pub fn let_binding_name_at( @@ -5107,10 +5080,8 @@ pub fn byte_to_line_col(index: Rc, offset: i64) -> LineCol { match index .offsets .clone() - .iter() + .get((v1_rt::int_sub(line.clone(), 2)) as usize) .cloned() - .skip(v1_rt::int_sub(line.clone(), 2) as usize) - .next() { Some(o) => v1_rt::int_add(o.clone(), 1), std::option::Option::None => 0, @@ -5133,10 +5104,8 @@ pub fn source_line_at(index: Rc, line: i64) -> String { match index .offsets .clone() - .iter() + .get((v1_rt::int_sub(line.clone(), 2)) as usize) .cloned() - .skip(v1_rt::int_sub(line.clone(), 2) as usize) - .next() { Some(o) => v1_rt::int_add(o.clone(), 1), std::option::Option::None => src_len.clone(), @@ -5145,10 +5114,8 @@ pub fn source_line_at(index: Rc, line: i64) -> String { let line_end = match index .offsets .clone() - .iter() + .get((v1_rt::int_sub(line.clone(), 1)) as usize) .cloned() - .skip(v1_rt::int_sub(line.clone(), 1) as usize) - .next() { Some(o) => o.clone(), std::option::Option::None => src_len.clone(), diff --git a/src/v1/stage0/src/v1_tests_claim_generic_identity_census.rs b/src/v1/stage0/src/v1_tests_claim_generic_identity_census.rs index 5ad0c3b241f..bc8bf680a15 100644 --- a/src/v1/stage0/src/v1_tests_claim_generic_identity_census.rs +++ b/src/v1/stage0/src/v1_tests_claim_generic_identity_census.rs @@ -1576,15 +1576,7 @@ pub fn gi_owner_argument_arms_discriminate( xs_param.clone(), v1_rt::concat( Rc::new(vec![minted.clone()]), - Rc::new( - xs_param - .children - .clone() - .iter() - .cloned() - .skip(1 as usize) - .collect::>(), - ), + Rc::new(v1_rt::list_skip(&xs_param.children.clone(), 1)), ), ); (((gi_foreign_label_under_owner( diff --git a/src/v1/stage0/src/v1_tests_claim_interpolation_hole_census.rs b/src/v1/stage0/src/v1_tests_claim_interpolation_hole_census.rs index 642898b05bf..9de31d86589 100644 --- a/src/v1/stage0/src/v1_tests_claim_interpolation_hole_census.rs +++ b/src/v1/stage0/src/v1_tests_claim_interpolation_hole_census.rs @@ -418,16 +418,9 @@ pub fn ihc_template_holes(ctx: Rc, template: Rc) -> Rc>(), - )) - .iter() - .cloned(), + (*Rc::new(v1_rt::list_take(&p.children.clone(), 1))) + .iter() + .cloned(), ); } __result diff --git a/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs b/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs index 892eefcb3fe..ed8fa600c3e 100644 --- a/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs +++ b/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs @@ -17,9 +17,11 @@ pub use crate::v1_compiler_emit_rust::{ reference_derived_candidate_disposition, reference_derived_census, reference_derived_disposition_name, reference_derived_row_diagnostics, }; -pub use crate::v1_compiler_infer_emit_info::empty_emit_graph_info; use crate::v1_compiler_infer_emit_info::TypeRepr::EnumRepr; -pub use crate::v1_compiler_infer_emit_info::{TypeRepr, TypeSummary}; +pub use crate::v1_compiler_infer_emit_info::{ + empty_emit_graph_info, empty_type_summary_index, type_summary_index_insert, +}; +pub use crate::v1_compiler_infer_emit_info::{TypeRepr, TypeSummary, TypeSummaryIndex}; use crate::v1_compiler_infer_items::ItemKind::FnItem; pub use crate::v1_compiler_infer_items::{ItemInfo, ItemKind}; use crate::v1_rt; @@ -91,7 +93,7 @@ pub fn fixture_disposition( Rc::new(vec![]), v1_rt::rc_empty_map::>(), ), - v1_rt::rc_empty_map::>(), + crate::v1_compiler_infer_emit_info::empty_type_summary_index(), v1_rt::rc_empty_map::(), false, ), @@ -132,7 +134,7 @@ pub fn cross_module_candidate_with_export_proof_survives() -> bool { Rc::new(vec![]), v1_rt::rc_empty_map::>(), ), - v1_rt::rc_empty_map::>(), + crate::v1_compiler_infer_emit_info::empty_type_summary_index(), v1_rt::rc_empty_map::(), false, ) == Rc::new(ReferenceDerivedCandidateDisposition::CandidateSurvived { @@ -219,12 +221,14 @@ pub fn variant_arm_red_note() -> String { CACHED.with(|c: &String| c.clone()) } -pub fn fixture_variant_type_summaries() -> Rc>> { - v1_rt::rc_map_insert( - v1_rt::rc_empty_map::>(), +pub fn fixture_variant_type_summaries() -> Rc { + crate::v1_compiler_infer_emit_info::type_summary_index_insert( + crate::v1_compiler_infer_emit_info::empty_type_summary_index(), + "fixture.E".to_string(), "E".to_string(), Rc::new(TypeSummary { name: "E".to_string(), + key: "fixture.E".to_string(), repr: Rc::new(TypeRepr::EnumRepr { unit_only: true }), field_summaries: v1_rt::rc_empty_map::>(), field_type_map: v1_rt::rc_empty_map::(), @@ -257,7 +261,7 @@ pub fn known_variant_is_delegated_to_its_parent_not_registry_absent() -> bool { v1_rt::rc_map_insert( v1_rt::rc_empty_map::(), "V".to_string(), - "E".to_string(), + "fixture.E".to_string(), ), false, ) == Rc::new( @@ -276,12 +280,14 @@ pub fn ambiguous_parent_red_note() -> String { CACHED.with(|c: &String| c.clone()) } -pub fn fixture_colliding_variant_type_summaries() -> Rc>> { - v1_rt::rc_map_insert( +pub fn fixture_colliding_variant_type_summaries() -> Rc { + crate::v1_compiler_infer_emit_info::type_summary_index_insert( fixture_variant_type_summaries(), + "fixture.F".to_string(), "F".to_string(), Rc::new(TypeSummary { name: "F".to_string(), + key: "fixture.F".to_string(), repr: Rc::new(TypeRepr::EnumRepr { unit_only: true }), field_summaries: v1_rt::rc_empty_map::>(), field_type_map: v1_rt::rc_empty_map::(), @@ -338,7 +344,7 @@ pub fn a_known_variant_spelling_in_a_type_position_takes_the_registry_arm() -> b v1_rt::rc_map_insert( v1_rt::rc_empty_map::(), "V".to_string(), - "E".to_string(), + "fixture.E".to_string(), ), true, ), @@ -369,7 +375,7 @@ pub fn non_variant_name_still_answers_registry_absent() -> bool { v1_rt::rc_map_insert( v1_rt::rc_empty_map::(), "V".to_string(), - "E".to_string(), + "fixture.E".to_string(), ), false, ), diff --git a/src/v1/stage0/tests/interpreter_dispatch_authority.rs b/src/v1/stage0/tests/interpreter_dispatch_authority.rs index 7018f061943..0981a7dc537 100644 --- a/src/v1/stage0/tests/interpreter_dispatch_authority.rs +++ b/src/v1/stage0/tests/interpreter_dispatch_authority.rs @@ -7,9 +7,8 @@ use v1_compiler::v1_interpreter_dispatch_generated::{ lookup_eval_algebra_method_inner, lookup_eval_builtin_inner, lookup_eval_call_bridge_std_node, - lookup_eval_call_native_intercept, lookup_try_parse_table_memo_dispatch, - lookup_try_v2_std_collection_map_primitive_grounding, EvalAlgebraMethodArm, EvalBuiltinArm, - EvalCallBridgeStdNodeArm, EvalCallNativeInterceptArm, TryParseTableMemoDispatchArm, + lookup_eval_call_native_intercept, lookup_try_v2_std_collection_map_primitive_grounding, + EvalAlgebraMethodArm, EvalBuiltinArm, EvalCallBridgeStdNodeArm, EvalCallNativeInterceptArm, TryV2StdCollectionMapPrimitiveGroundingArm, }; @@ -56,7 +55,6 @@ fn generated_lookup_covers_representative_spellings_per_site() { .is_some() ); assert!(lookup_eval_call_native_intercept("fold_list").is_some()); - assert!(lookup_try_parse_table_memo_dispatch("parse_table_lookup").is_some()); } #[test] @@ -81,8 +79,4 @@ fn generated_enums_are_copy_and_distinct() { EvalCallNativeInterceptArm::NativeInterceptFoldList, EvalCallNativeInterceptArm::NativeInterceptFoldListRight ); - assert_ne!( - TryParseTableMemoDispatchArm::ParseTableMemoParseTableInsert, - TryParseTableMemoDispatchArm::ParseTableMemoParseTableLookup - ); } diff --git a/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag b/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag index 9f6d9fcb47d..69a9e3bcc39 100644 --- a/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag +++ b/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag @@ -24,7 +24,7 @@ import v1.compiler.infer_items { ItemInfo, FnItem } import v1.std.core { ErrorNode, SourceSpan, no_span, diagnostic_to_message, is_error_diagnostic } -import v1.compiler.infer_emit_info { TypeSummary, EnumRepr, empty_emit_graph_info } +import v1.compiler.infer_emit_info { TypeSummary, TypeSummaryIndex, EnumRepr, empty_emit_graph_info, empty_type_summary_index, type_summary_index_insert } import v1.compiler.compile { SourceFile, compile_to_resolved } data reference_derived_disposition_census_witness_note: String = "THE ROUTE HALF OF THIS FILE IS ABSENT AND THE REASON IS A WALL RATHER THAN A CHOICE, stated here because it is the first thing a reader of a five-row file should know: the end-to-end pair this file was designed around -- a two-module manifest whose consumer calls a peer fn in VALUE position with no import, against a control differing by one authored import line -- was authored and DOES NOT RUN. A nested compile is not evaluable in the interpreter: compile_to_resolved over an authored source vector refuses with NoSuchField Node.ident during the nested pipeline, measured 2026-08-27 on three separate subjects. CAUSE CORRECTED 2026-09-02 (quick-crab-396): THAT SENTENCE NAMES A SYMPTOM AND NOT THE CLASS. Nested compiles are one POPULATION that constructs Nodes through helpers omitting v1.std.core Node's `ident` and then reads it; the class is that the interpreter has NO DEFAULT FOR AN OMITTED OPTIONAL FIELD and refuses on read, general to any record type -- established by a two-row probe with a positive control on a fresh type unrelated to Node, filed as the fourth form of gunbc.recurring_failure_mode state_space_conflation, which is the producer to re-derive from. This matters for what to DO: the repair the symptom suggests -- route around the nested pipeline with a host builtin, as the two instruments below do -- does not touch the cause, and the capability that would is construction supplying every declared field. That is not a property of this change; it is why the two nested-compile instruments the corpus already has (gunbc.compile_diagnostic_census and tools.multi_module_compile_fixture) both route through a HOST BUILTIN rather than calling the pipeline from .dag. The rows are not retained in a form that cannot execute, because a probe that always errors is not a red -- it is an inert artifact that would be counted as coverage. NEXT-RUNG TRIGGER FOR THE ROUTE: a nested-compile builtin returning the resolved graph, the shape tools.multi_module_compile_fixture already has, widened to carry the emitter reference-derived rows.\n\nWHERE THIS RUNS, STATED FIRST AND WITHOUT FLATTERY. THIS WITNESS IS NOT RUN BY CI. gunbc.ci_layer_roots witness_discovery_scan_dirs is [dag/test/claim, src/v2/test/claim/manual, src/v2/test/claim/emit] -- src/v1/tests/claim is not among them -- and the required run invokes claim_executor with --source-root dag --source-root src/v2, which could not resolve this file's import of v1.compiler.emit_rust even if discovery reached it. The scoped batch that once ran this directory died in the 2026-08-15 floor cut; gunbc.ci_layer_roots v1_claim_scoped_witness_batch_deleted_note records that and states the entries here have no executing consumer. The roster its sibling checkpoint_identity_keying_witness_test claims to be enrolled in, v1_claim_scoped_witness_entries, is not declared anywhere in dag/gunbc/ci/ci_layer_roots.dag -- verified by grep in both directions rather than taken from the note. THE FIVE ROWS BELOW WERE RUN DIRECTLY ON 2026-08-27 AND ALL FIVE PASS. Direct execution is evidence the assertions HOLD, never evidence that CI runs them, and conflating those two is the rung inflation this directory has now recorded twice.\n\nWHAT THE SUBJECT IS. Since PR 6848 a cross-module name resolves whether or not it is imported. At TYPE positions the resolver says so advisorily (UnlistedImportUse, is_error_diagnostic false); at VALUE positions it says nothing at all, and the emitter silently synthesizes the use-line the author did not write. That synthesis is unchanged here and deliberately so. What changed is that the per-candidate decision behind it stopped being an inline flat_map whose three non-surviving arms all returned the empty list, and became a four-armed coproduct one census can count.\n\nWHY BOTH HALVES ARE HERE. The four unit rows discriminate the DECISION: each names a distinct arm, and two of them differ ONLY in whether the provider's export set proves the symbol, so no constant, disabled mechanism or name-keyed rule satisfies them together. The end-to-end rows discriminate the CENSUS ROUTE: a two-module manifest compiled through compile_to_resolved, whose consumer references a peer's fn in VALUE position with no import, against a control differing by exactly one authored import line. The unlisted arm must produce a survived row naming peer_answer; the control must produce no row for that name at all, because an already-provided name never reaches the decision. Neither can pass vacuously -- a census that always reported zero fails the first, and one that reported every reference fails the second.\n\nTHE RED IS AUTHORABLE AT THE FIXTURE BOUNDARY, which is what makes this a probe rather than a decoration. No Accepted corpus module can be relied on to hold a value-position unlisted use on demand, but a fixture manifest can author one directly, and this file does. DESIGN 4b: a check whose RED cannot be produced anywhere it runs is permanently green by construction and worse than absent, because it gets cited as coverage. What this file lacks is a RUNNER, not a reachable red.\n\nWHAT THIS DOES NOT ESTABLISH. Nothing about the SIZE of the live population -- these are fixtures; NO PRODUCER FOR A REAL FIGURE EXISTS YET, and this file does not name one: the only route from .dag to a corpus census is a nested compile, which the interpreter refuses, so any figure quoted today names nothing that can produce it. And nothing about the names the census never sees: a candidate already provided by an authored import, the prelude, a carrier use-line or the module's own declarations is filtered before the decision, so every count is a LOWER BOUND and the gap is unmeasured. Quote it as at least N.\n\nNEXT-RUNG TRIGGER FOR THIS FILE'S EVIDENCE, which is NOT the same trigger as the class's and must not be folded into it: one prepared subject spanning dag, src/v1 and src/v2 -- the restoration gunbc.ci_layer_roots already names for the deleted batch -- at which point these rows execute and this first paragraph deletes. The CLASS's own trigger (the value-position case joining the fail-closed wall, retiring the census and the synthesis together) lives on v1.compiler.emit_rust reference_derived_census_rung_note. Either can land first; neither closes the other." @@ -60,7 +60,7 @@ fn fixture_disposition(registry: Map, export_sets: Map Bool { typed_modules: [], source_indices: empty_map(), module_index: build_module_index(modules: [], leaf_owner_modules: empty_map()), - type_summaries: empty_map(), + type_summaries: empty_type_summary_index(), variant_to_enum: empty_map(), in_type_position: false ) == CandidateSurvived { provider_module: "fixture.provider" } @@ -119,9 +119,10 @@ test fn census_counts_each_arm_separately() -> Bool { data variant_arm_red_note: String = "THE DISCRIMINATING RED FOR THE FIFTH ARM, and it is the one this file most needs. Before the arm existed a bare variant name reached the registry lookup, missed, and answered registry-absent -- so the fixture below is RED against the four-arm classifier and green against the five-arm one, which is what makes it evidence rather than decoration. The positive control beside it is the census row above: a name that is NOT a known variant still answers registry-absent, so the arm narrows the class rather than emptying it." -fn fixture_variant_type_summaries() -> Map { - map_insert(empty_map(), "E", TypeSummary { +fn fixture_variant_type_summaries() -> TypeSummaryIndex { + type_summary_index_insert(index: empty_type_summary_index(), key: "fixture.E", leaf: "E", summary: TypeSummary { name: "E", + key: "fixture.E", repr: EnumRepr { unit_only: true }, field_summaries: empty_map(), field_type_map: empty_map(), @@ -143,16 +144,17 @@ test fn known_variant_is_delegated_to_its_parent_not_registry_absent() -> Bool { source_indices: empty_map(), module_index: build_module_index(modules: [], leaf_owner_modules: empty_map()), type_summaries: fixture_variant_type_summaries(), - variant_to_enum: map_insert(empty_map(), "V", "E"), + variant_to_enum: map_insert(empty_map(), "V", "fixture.E"), in_type_position: false ) == CandidateVariantDelegatedToParent { parent_enum: "E" } } data ambiguous_parent_red_note: String = "THE REACHABLE HALF of the unresolved-parent arm, and the reason it is not merely a tidier name. derive_variant_to_enum inserts the EMPTY STRING as the parent when one variant name appears in two enums, so the lookup answers Present with a parent naming nothing. Without this arm that produced CandidateVariantDelegatedToParent { parent_enum: \"\" } -- a delegation to no one, wearing the payload that was supposed to make delegation honest. This fixture authors the collision directly and is RED against the arm-less form." -fn fixture_colliding_variant_type_summaries() -> Map { - map_insert(fixture_variant_type_summaries(), "F", TypeSummary { +fn fixture_colliding_variant_type_summaries() -> TypeSummaryIndex { + type_summary_index_insert(index: fixture_variant_type_summaries(), key: "fixture.F", leaf: "F", summary: TypeSummary { name: "F", + key: "fixture.F", repr: EnumRepr { unit_only: true }, field_summaries: empty_map(), field_type_map: empty_map(), @@ -193,7 +195,7 @@ test fn a_known_variant_spelling_in_a_type_position_takes_the_registry_arm() -> source_indices: empty_map(), module_index: build_module_index(modules: [], leaf_owner_modules: empty_map()), type_summaries: fixture_variant_type_summaries(), - variant_to_enum: map_insert(empty_map(), "V", "E"), + variant_to_enum: map_insert(empty_map(), "V", "fixture.E"), in_type_position: true )) == "registry-absent" } @@ -213,7 +215,7 @@ test fn non_variant_name_still_answers_registry_absent() -> Bool { source_indices: empty_map(), module_index: build_module_index(modules: [], leaf_owner_modules: empty_map()), type_summaries: fixture_variant_type_summaries(), - variant_to_enum: map_insert(empty_map(), "V", "E"), + variant_to_enum: map_insert(empty_map(), "V", "fixture.E"), in_type_position: false )) == "registry-absent" } diff --git a/src/v1/tests/src/anonymous_record_struct_resolution_test.rs b/src/v1/tests/src/anonymous_record_struct_resolution_test.rs index f999ea14d5e..c18b1361a19 100644 --- a/src/v1/tests/src/anonymous_record_struct_resolution_test.rs +++ b/src/v1/tests/src/anonymous_record_struct_resolution_test.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use v1_compiler::v1_compiler_emit_rust::{ anonymous_record_struct_candidates, find_struct_name_by_fields, }; -use v1_compiler::v1_compiler_infer_emit_info::{TypeRepr, TypeSummary}; +use v1_compiler::v1_compiler_infer_emit_info::{TypeRepr, TypeSummary, TypeSummaryIndex}; fn summary(name: &str, fields: &[(&str, &str)]) -> Rc { let mut field_type_map = HashMap::new(); @@ -12,6 +12,7 @@ fn summary(name: &str, fields: &[(&str, &str)]) -> Rc { } Rc::new(TypeSummary { name: name.to_string(), + key: format!("fixture.{name}"), repr: Rc::new(TypeRepr::StructRepr), field_summaries: Rc::new(HashMap::new()), field_type_map: Rc::new(field_type_map), @@ -22,12 +23,23 @@ fn summary(name: &str, fields: &[(&str, &str)]) -> Rc { }) } -fn summaries(rows: &[Rc]) -> Rc>> { - let mut result = HashMap::new(); +// Summaries are keyed by declaring identity, with the leaf index beside them. +fn summaries(rows: &[Rc]) -> Rc { + let mut by_key = HashMap::new(); + let mut keys_by_leaf: HashMap>> = HashMap::new(); for row in rows { - result.insert(row.name.clone(), row.clone()); + by_key.insert(row.key.clone(), row.clone()); + let mut keys = keys_by_leaf + .get(&row.name) + .map(|k| (**k).clone()) + .unwrap_or_default(); + keys.push_back(row.key.clone()); + keys_by_leaf.insert(row.name.clone(), Rc::new(keys)); } - Rc::new(result) + Rc::new(TypeSummaryIndex { + by_key: Rc::new(by_key), + keys_by_leaf: Rc::new(keys_by_leaf), + }) } #[test] diff --git a/src/v1/trait_bound_witness.dag b/src/v1/trait_bound_witness.dag index 11633d85e97..55698a11132 100644 --- a/src/v1/trait_bound_witness.dag +++ b/src/v1/trait_bound_witness.dag @@ -57,13 +57,10 @@ module v1.compiler.trait_bound_witness // and scoped narrowly to a function body that IS DIRECTLY a top-level match expression (the exact // shape of the two real specimens this was traced from, realize_route and classify_write in // dag/std/cache_interface.dag) -- a body wrapped in Let/If/Branch before reaching the match is NOT -// walked. That is a declared boundary, not a silent gap: BoundedToDirectTopLevelMatchBody names it -// below, and the next-rung trigger is a recursive body walker mirroring -// emit_rust_expr_if/emit_rust_expr_let's accessor shapes once a second real specimen needs it -// (DESIGN.md S4b no-untracked-stall). - -type TraitBoundWitnessScope - = | BoundedToDirectTopLevelMatchBody +// walked. THAT BOUNDARY IS RETIRED: its trigger fired on gunbc.product.data_class admit_data_effect, +// whose cloned-scrutinee match is a fold step's body, and v1_fn_body_rc_match_clone_bound_param_names +// (05_emit_rust.dag) now walks every match in the body. The scope variant that named the boundary is +// deleted with it, as BoundedToDirectSingleCallLambdaBody's was. // gunbc dashboard node://adhoc-574e999b-39c, Row 1b. A second, structurally distinct trigger from // the RC-match one above: a wrapper fn whose body forwards its own arguments directly into ONE @@ -107,8 +104,8 @@ type TraitBoundWitnessScope // v1_call_forwarding_clone_bound_param_names now walks children recursively, mirroring the equality // half rather than special-casing scrutinee position -- the variant is deleted below because the // scope it named no longer exists, and a boundary declaration that outlives its boundary is the -// stale citation DESIGN section 3 forbids. BoundedToDirectTopLevelMatchBody is UNCHANGED and still -// stands: the RC-match trigger was not widened here, and its own next-rung trigger is still open. +// stale citation DESIGN section 3 forbids. The RC-match trigger was later widened the same way (see +// the retirement note above). fn v1_rc_match_scrutinee_clone_bound_param_names( generic_param_names: List, diff --git a/src/v2/cli/compile_cli.dag b/src/v2/cli/compile_cli.dag index bc326c90c25..70b0c325dbd 100644 --- a/src/v2/cli/compile_cli.dag +++ b/src/v2/cli/compile_cli.dag @@ -11,7 +11,12 @@ import v2.compiler.self_host.closure_emission { closure_emission_refused_count, emit_closure_from_ingest_located } -import v2.compiler.source_authority { SourceRootIngest, source_root_ingest_from_files } +import v2.compiler.source_authority { + SourceRootIngest, + SourceRootIngestFromFilesBuilt, + SourceRootIngestFromFilesUnrecognizedRoot, + source_root_ingest_from_files +} import v2.cli.if_arm_reader_differential { IardCensus, IardCensusTaken, @@ -373,9 +378,8 @@ type CliRunOutcome // boundary. Measured on the built binary at gunbc#11507's head -- three different entries // (`v2.std.text`, this module, `v2.compiler.compile`) all refused with the same nine words. // -// THE WHOLE ORDERED CHAIN, BECAUSE THE HEAD IS NOT THE CAUSE. Rendering only the head answers -// `parse_grammar_choice_overlap_residue` for every entry tried; that is a grammar-global ADVISORY -// and never the fatal. The fatal diagnostic is the last one, and `parse_g0_tokens_remain_diagnostic` +// THE WHOLE ORDERED CHAIN, BECAUSE THE HEAD IS NOT THE CAUSE. A head can be a non-fatal advisory +// that rejected_with_pending prepended; it is never by itself the fatal. The fatal diagnostic is the last one, and `parse_g0_tokens_remain_diagnostic` // builds its locus from the first remaining token. The fatal pick is `v2.std.diagnostic` // `diagnostics_fatal`, the chain is `lens_verdict_diagnostics_located_chain_text_in_spans` (every // link with its locus, so thirty-five advisories in front of the fatal each say where they are), @@ -543,7 +547,21 @@ fn v2_cli_main(argv: FreeMonoid) -> CliRunOutcome { CliPlanRefused { reason: reason, detail: detail } => CliUsageRefused { reason: reason, detail: detail } CliPlanParsed { verb: _ } => match source_root_read(roots: v2_cli_plan_source_roots(plan: plan)) { SourceRootFilesRead { files: files } => - v2_cli_run(plan: plan, ingest: source_root_ingest_from_files(files: files)) + match source_root_ingest_from_files(files: files) { + SourceRootIngestFromFilesBuilt { ingest: ingest } => + v2_cli_run(plan: plan, ingest: ingest) + SourceRootIngestFromFilesUnrecognizedRoot { argument: argument, path: path } => + CliUsageRefused { + reason: ^cli_source_root_unrecognized, + detail: concat( + "source root is not a dag or src/v2 tree: ", + argument, + " (file ", + path, + ")" + ) + } + } SourceRootFilesRefused { path: path, kind: kind, cause: cause } => CliUsageRefused { reason: ^cli_source_root_read_refused, detail: concat("could not read source root at ", path, " [", kind, "]: ", cause) } diff --git a/src/v2/cli/if_arm_reader_differential.dag b/src/v2/cli/if_arm_reader_differential.dag index d9347aaea27..c7e101b7a8a 100644 --- a/src/v2/cli/if_arm_reader_differential.dag +++ b/src/v2/cli/if_arm_reader_differential.dag @@ -23,6 +23,7 @@ import v2.extdeps.languages.dag { ParseSubtreeAbsent, ParseSubtreeFound, dag_lex, + dag_surface_identity_is_top_level_item, dag_surface_kw_then_ident_from_captured, parse_production_captured_child_optional, parse_production_emitted_identity_optional @@ -30,7 +31,7 @@ import v2.extdeps.languages.dag { import v2.std.algebra { fold_list, list_reverse } import v2.std.diagnostic { Accepted, Diagnostics, NonEmptyDiagnostics, Rejected, Textual, NodeLocus, SourcePortLocus, InvariantPortLocus, DeclarationLocus, ByteRange, WholeFile, bind_outcome, diagnostics_fatal_reason } import v2.std.grammar { node_atom_identity_optional } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import v2.std.node { Node, Symbol, is_empty_conj_root, symbol_lexeme } import std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.provenance { SpanIndex, node_occurrence_id_optional, span_index_textual_locus_optional } @@ -280,13 +281,7 @@ fn iard_declaration_name(shell: Node, emitted: Symbol) -> String { } fn iard_is_declaration(id: Symbol) -> Bool { - (id == ^dag_surface_fn_decl) - || (id == ^dag_surface_test_fn_decl) - || (id == ^dag_surface_data_decl) - || (id == ^dag_surface_type_decl) - || (id == ^dag_surface_alias_decl) - || (id == ^dag_surface_service_decl) - || (id == ^dag_surface_resource_decl) + dag_surface_identity_is_top_level_item(id: id) } // EVERY if_expr IN THE TREE, IN PRE-ORDER, WITH ITS ENCLOSING DECLARATION. Nested ifs are visited: @@ -344,7 +339,7 @@ fn iard_location(node: Node, spans: SpanIndex) -> String { Textual { file: _, extent: e } => match e { ByteRange { start: s, end: en } => - concat("bytes=", concat(integer_int_to_decimal_string(value: s), concat("..", integer_int_to_decimal_string(value: en)))) + concat("bytes=", concat(int_to_decimal_string(value: s), concat("..", int_to_decimal_string(value: en)))) WholeFile => "" } NodeLocus { anchor: _ } => "" @@ -362,10 +357,10 @@ type IardFileOutcome = IardFileRead { path: String, spans: SpanIndex, sites: FreeMonoid } | IardFileRefused { path: String, reason: Symbol } -fn iard_file(prepared: PreparedGrammar, residue: Diagnostics, read: DagSourceReadWitness) -> IardFileOutcome { +fn iard_file(prepared: PreparedGrammar, read: DagSourceReadWitness) -> IardFileOutcome { let parsed = bind_outcome( o: tokenize(text: read.source.carried, file: read.compilation_unit, rules: dag_lex()), - f: fn(stream) { parse_module_prepared(tokens: stream, prepared: prepared, validation_residue: residue) } + f: fn(stream) { parse_module_prepared(tokens: stream, prepared: prepared) } ) match parsed { Rejected { diagnostics: d } => IardFileRefused { path: read.artifact.file_path, reason: diagnostics_fatal_reason(d: d) } @@ -493,12 +488,12 @@ type IardCensus fn if_arm_reader_differential(ingest: SourceRootIngest) -> IardCensus { match dag_prepared_grammar() { Rejected { diagnostics: d } => IardGrammarUnprepared { reason: diagnostics_fatal_reason(d: d) } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => IardCensusTaken { tally: fold_list( xs: ingest, empty: IardTally { text: "", files_read: 0, files_refused: 0, sites: 0, else_differs: 0, then_differs: 0, sites_differing: 0 }, - cons: fn(acc, read) { iard_tally_file(t: acc, outcome: iard_file(prepared: prepared, residue: residue, read: read)) } + cons: fn(acc, read) { iard_tally_file(t: acc, outcome: iard_file(prepared: prepared, read: read)) } ) } } @@ -508,16 +503,16 @@ fn if_arm_reader_differential(ingest: SourceRootIngest) -> IardCensus { // sites can be read against how many sites and files were actually observed. fn if_arm_reader_differential_summary(t: IardTally) -> String { concat( - concat("if_arm_reader_differential files_read=", integer_int_to_decimal_string(value: t.files_read)), + concat("if_arm_reader_differential files_read=", int_to_decimal_string(value: t.files_read)), concat( - concat(" files_refused=", integer_int_to_decimal_string(value: t.files_refused)), + concat(" files_refused=", int_to_decimal_string(value: t.files_refused)), concat( - concat(" if_sites=", integer_int_to_decimal_string(value: t.sites)), + concat(" if_sites=", int_to_decimal_string(value: t.sites)), concat( - concat(" sites_differing=", integer_int_to_decimal_string(value: t.sites_differing)), + concat(" sites_differing=", int_to_decimal_string(value: t.sites_differing)), concat( - concat(" else_differs=", integer_int_to_decimal_string(value: t.else_differs)), - concat(concat(" then_differs=", integer_int_to_decimal_string(value: t.then_differs)), "\n") + concat(" else_differs=", int_to_decimal_string(value: t.else_differs)), + concat(concat(" then_differs=", int_to_decimal_string(value: t.then_differs)), "\n") ) ) ) diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index f470fa96cc2..02c080f7b1a 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -36,7 +36,7 @@ import extdeps.bazel.target_pattern { SingleTarget, PackageTargets, SubtreeTargets, parse_target_pattern, target_pattern_matches, target_pattern_refusal_text, render_target_pattern } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import v2.compiler.emit { emit } import v2.compiler.eval { eval, eval_node, eval_default_interpretation, inputs_root_only } import v2.compiler.infer { InferredTree } @@ -178,14 +178,11 @@ import v2.compiler.resolve { ResolveWalkRefused, resolve, resolved_tree_of, - ResolvedTree + ResolvedTree, + ClosureProviderRefusal } import v2.compiler.tokenize { PreparedLexRules, prepare_lex_rules, tokenize } -import v2.std.staging { then_outcome, CacheProbe, Miss } -import v2.compiler.materialization_carriers { - cached_stage_realization, - cached_stage_governed, -} +import v2.std.staging { then_outcome } import v2.std.compilers.lexing { TokenStream } import v2.std.grammar { ParseTree } import v2.std.language_model { LanguageModel } @@ -200,7 +197,8 @@ import v2.extdeps.languages.dag { import v2.lens.complexity_accumulator_copy.compile_gate { accumulator_copy_compile_gate } import v2.lens.determinism { determinism_compile_gate } import v2.lens.machine_shape { machine_shape_compile_gate } -import v2.lens.mandatory_tag { mandatory_tag_compile_gate } +import v2.lens.mandatory_tag { mandatory_tag_compile_gate, mandatory_tag_compile_gate_with_provenance } +import v2.std.member_declaration_kind { MemberKindChannel, no_member_declaration_kinds } import v2.lens.undecidable_verdict_collapse { undecidable_verdict_collapse_compile_gate } import v2.lens.fact_density { fact_density_hollow_alias_gate } import v2.lens.unit_modeling { unit_modeling_gate } @@ -209,7 +207,7 @@ import std.decl_ref { DeclarationRef, decl_ref, declaration_ref_display_key } import std.content_hash { Fnv1a64Structural, content_hash_of_value, serialize_content_hash } import std.types { NonEmptyStr, List } import std.algebra { Cons, Empty, FreeMonoid, FreeSemigroup, trim, list_append, list_snoc_item } -import v2.std.algebra { any, contains, fold_list, is_empty, length, list_find_first, list_reverse } +import v2.std.algebra { any, contains, filter, fold_list, is_empty, length, list_find_first, list_map, list_reverse, list_starts_with } import v2.std.collection { Map, empty_map, map_insert, map_lookup } import v2.std.integer { Int } import v2.std.diagnostic { @@ -795,6 +793,20 @@ fn machine_shape_root_gate(tree: InferredTree) -> Outcome> { machine_shape_compile_gate(node: tree.root) } +// The compile door's binding of the mandatory_tag gate: the same gate, handed the member-kind provenance +// the resolved subject carries (ResolvedTree.member_kinds). +fn mandatory_tag_root_lens_with_provenance(lens: CompileLens, channel: MemberKindChannel) -> CompileLens { + if lens.identity == mandatory_tag_lens.identity { + CompileLens { + identity: lens.identity, + grain: lens.grain, + gate: fn(tree) { mandatory_tag_compile_gate_with_provenance(node: tree.root, channel: channel) } + } + } else { + lens + } +} + fn mandatory_tag_root_gate(tree: InferredTree) -> Outcome> { mandatory_tag_compile_gate(node: tree.root) } @@ -1042,9 +1054,13 @@ fn validate_then_compile( bind_outcome( o: run_required_lens_gates( inferred: inferred, - lenses: list_append( - left: required_lenses_at_grain(grain: CompileLensRoot), - right: lenses_at_grain(lenses: lenses, grain: CompileLensRoot) + lenses: fold( + list_append( + left: required_lenses_at_grain(grain: CompileLensRoot), + right: lenses_at_grain(lenses: lenses, grain: CompileLensRoot) + ), + init: [], + f: fn(acc, l) { list_snoc_item(xs: acc, item: mandatory_tag_root_lens_with_provenance(lens: l, channel: source.member_kinds)) } ) ), f: fn(root_witnesses) { @@ -1239,10 +1255,6 @@ fn stage_resolve(lm: LanguageModel) -> fn(NormalizedTree) -> Outcome CacheProbe { - Miss -} - fn stage_translate(target: TargetModel) -> fn(ResolvedTree) -> Outcome> { fn(resolved) { bind_outcome( @@ -1267,34 +1279,22 @@ fn stage_translate(target: TargetModel) -> fn(ResolvedTree) -> Outcome because an unmet materialization obligation is not a licence -// to run the stage unmemoized -- it is a typed cause, and this spine already speaks Outcome, so -// it is returned as the stage outcome rather than absorbed into the recompute arm. fn compile_ingest_staging(source: Medium, target: TargetModel) -> Outcome> { - match cached_stage_realization() { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: realization, diagnostics: _ } => - let lm = dag_language_model() - let pipeline = then_outcome( - f: stage_tokenize(lm: lm), + let lm = dag_language_model() + let pipeline = then_outcome( + f: stage_tokenize(lm: lm), + g: then_outcome( + f: stage_parse(lm: lm), + g: then_outcome( + f: stage_normalize(), g: then_outcome( - f: stage_parse(lm: lm), - g: then_outcome( - f: stage_normalize(), - g: then_outcome( - f: cached_stage_governed( - realization: realization, - lookup: resolve_cache_miss, - stage: stage_resolve(lm: lm) - ), - g: stage_translate(target: target) - ) - ) + f: stage_resolve(lm: lm), + g: stage_translate(target: target) ) ) - pipeline(source) - } + ) + ) + pipeline(source) } // ═══════════════════════════════════════════════════════════════════════════════════════════ @@ -1555,14 +1555,12 @@ fn native_test_context_fold_step( read: DagSourceReadWitness, lex: PreparedLexRules, prepared: PreparedGrammar, - residue: Diagnostics, scope: Fnv1a64Structural ) -> NativeTestContextFoldState { let result = program_assembly_read_to_normalized_root_seeded( read: read, lex: lex, prepared: prepared, - residue: residue, seed: state.alloc, scope: scope ) @@ -1589,19 +1587,17 @@ type NativeTestFrontEnd { lm: LanguageModel lex: PreparedLexRules prepared: PreparedGrammar - residue: Diagnostics } fn native_test_front_end_prepare() -> Outcome { let lm = dag_language_model() match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => outcome_accepted(value: NativeTestFrontEnd { lm: lm, lex: prepare_lex_rules(rules: lm.lex), - prepared: prepared, - residue: residue + prepared: prepared }) } } @@ -1652,7 +1648,6 @@ fn native_test_context_from_ingest(ingest: SourceRootIngest) -> Outcome) -> } concat( "import-closure walk exhausted its budget of ", - integer_int_to_decimal_string(value: budget), + int_to_decimal_string(value: budget), " rounds with ", - integer_int_to_decimal_string(value: length(xs: frontier)), + int_to_decimal_string(value: length(xs: frontier)), " module(s) still unexpanded, first: ", first ) @@ -3513,6 +3508,7 @@ fn native_module_resolve_verdict( symbol_index: shared.symbol_index, closure_declarations: closure.declarations, unavailable_providers: closure.refused, + member_kinds: no_member_declaration_kinds(), lexical: lexical ) }, @@ -3756,7 +3752,7 @@ fn native_demand_census_read( match bind_outcome( o: program_assembly_phase_tokenize(read: read, lex: front.lex), f: fn(tokens) { - parse_module_prepared_seeded(tokens: tokens, prepared: front.prepared, validation_residue: front.residue, seed: state.context.alloc) + parse_module_prepared_seeded(tokens: tokens, prepared: front.prepared, seed: state.context.alloc) } ) { Rejected { diagnostics: d } => @@ -3987,12 +3983,16 @@ fn native_census_module_resolution( NativeModuleResolveAccepted { resolved: _ } => NativeCensusModuleResolved }, context: step.context, - cascade: match step.import_attribution { - NativeImportTargetRefused { target: t } => NativeCensusCascadeMemberOf { root: t.path, root_reason: native_test_file_refusal_fatal_reason(fr: t) } - NativeImportTargetsUnreadable { reason: r } => NativeCensusCascadeUnattributed { reason: r } - NativeImportTargetsAdmitted => NativeCensusCascadeNone - NativeImportTargetNotAsked => NativeCensusCascadeNone - } + cascade: native_census_import_cascade(attribution: step.import_attribution) + } +} + +fn native_census_import_cascade(attribution: NativeImportTargetAttribution) -> NativeCensusCascade { + match attribution { + NativeImportTargetRefused { target: t } => NativeCensusCascadeMemberOf { root: t.path, root_reason: native_test_file_refusal_fatal_reason(fr: t) } + NativeImportTargetsUnreadable { reason: r } => NativeCensusCascadeUnattributed { reason: r } + NativeImportTargetsAdmitted => NativeCensusCascadeNone + NativeImportTargetNotAsked => NativeCensusCascadeNone } } @@ -4014,9 +4014,13 @@ type NativeInferCensusOutcome | NativeInferCensusInferred { resolved: ResolvedTree, inferred: InferredTree } | NativeInferCensusInferRefused { resolved: ResolvedTree, chain: NonEmptyDiagnostics } +// `cascade` IS THE RESOLVE VERDICT'S IMPORT ATTRIBUTION, carried exactly as +// native_census_module_resolution carries it, so a resolve refusal reached through this walk files +// the same cascade it files through census-resolve (one attribution authority, not two). type NativeInferCensusStep { outcome: NativeInferCensusOutcome context: NativeTestContext + cascade: NativeCensusCascade } fn native_census_module_inference( @@ -4045,7 +4049,8 @@ fn native_census_module_inference( NativeInferCensusInferRefused { resolved: resolved, chain: d } } }, - context: step.context + context: step.context, + cascade: native_census_import_cascade(attribution: step.import_attribution) } } @@ -4135,17 +4140,19 @@ fn native_census_residual_rows( // THE CENSUS IS GROUPED BY THE CAUSE THAT FIRED, NEVER BY THE ONE THAT CAME FIRST. A refusal's // diagnostic list PREPENDS the advisories that were pending when the refusal fired -// (rejected_with_pending), so its head is whatever advisory was queued first -- on the dag grammar -// that is the overlap-residue advisory every file carries -- and grouping by it files every refusal -// under one advisory that refused nothing. v2.std.diagnostic diagnostics_fatal is the one authority +// (rejected_with_pending), so its head is whatever advisory was queued first, and grouping by it +// files every refusal under an advisory that refused nothing. v2.std.diagnostic diagnostics_fatal is the one authority // for which element fired: a file refusal already carries its answer as `fatal_reason` // (native_test_file_refusal), and a resolve chain is asked the same question here. The head stays on // the member as `head_reason` so the reader still sees what rode along; it is a field, not a key. // BOTH POPULATIONS LAND IN ONE GROUPING because they are one question -- what refused, and where -- // asked at two stages, and the stage is a field on the member rather than a second roster. +// INFER IS THE THIRD STAGE OF THE SAME QUESTION (census part 2, docs/plans/census-part-2-inference-design.md +// section 5): one more arm, not a second carrier, so every exhaustive match over the stage enrols it. type NativeCensusCauseStage = NativeCensusCauseFrontEnd | NativeCensusCauseResolve + | NativeCensusCauseInfer // CENSUS C: A CASCADE IS COLLAPSED TO ITS ROOT, ON THE SAME ROW. A module that refused because an // import target's FILE was refused is a symptom: it is filed ONCE (one member per module, not one @@ -4202,12 +4209,20 @@ type NativeCensusCauseGroup { // none: it is a root, and a root's own imports cannot unblock anything). A roster module that is // neither file-refused nor indexed has imports nobody can read, so the graph is INCOMPLETE and every // fan-out over it is refused with that reason -- an undercount would be the silent arm. +// `imports_of` IS THE SAME EDGE SET READ FORWARD, collected in the same pass: the subject closure +// (native_census_subject_closure) walks it, so neither direction is a second read of the graph. type NativeCensusClosureIndex - = NativeCensusClosureIndexRead { importers_of: Map>, modules: Int, refused_modules: Int } + = NativeCensusClosureIndexRead { + importers_of: Map> + imports_of: Map> + modules: Int + refused_modules: Int + } | NativeCensusClosureIndexUnavailable { reason: Symbol } type NativeCensusClosureIndexStep { importers_of: Map> + imports_of: Map> unread: Int } @@ -4222,13 +4237,13 @@ fn native_census_closure_index( Accepted { value: shared, diagnostics: _ } => let read = fold_list( xs: modules, - empty: NativeCensusClosureIndexStep { importers_of: empty_map(), unread: 0 }, + empty: NativeCensusClosureIndexStep { importers_of: empty_map(), imports_of: empty_map(), unread: 0 }, cons: fn(acc, entry) { match map_lookup(m: shared.roots.by_name, key: qualified_name_from_dotted_string(dotted: entry.module)) { Absent => match native_lane_file_refusal_for_path(index: refusal_index, path: entry.path) { Present { value: _ } => acc - Absent => NativeCensusClosureIndexStep { importers_of: acc.importers_of, unread: acc.unread + 1 } + Absent => NativeCensusClosureIndexStep { importers_of: acc.importers_of, imports_of: acc.imports_of, unread: acc.unread + 1 } } Present { value: tree } => NativeCensusClosureIndexStep { @@ -4244,6 +4259,10 @@ fn native_census_closure_index( } } ), + imports_of: map_insert(acc.imports_of, entry.module, list_map( + xs: normalized_tree_import_bindings(tree: tree), + f: fn(row) { qualified_name_to_dotted_string(qn: qualified_name_init(qn: row.target)) } + )), unread: acc.unread } } @@ -4254,6 +4273,7 @@ fn native_census_closure_index( } else { NativeCensusClosureIndexRead { importers_of: read.importers_of, + imports_of: read.imports_of, modules: length(xs: modules), refused_modules: fold_list(xs: context.file_refusals, empty: 0, cons: fn(n, r) { match r.module { @@ -4270,10 +4290,16 @@ fn native_census_closure_index( // reverse graph from the root: one walk, two directions, no second closure fold. Its budget is the // roster size plus one, which bounds the rounds of any walk over that roster; exhaustion refuses. fn native_census_closure_fan_out(index: NativeCensusClosureIndex, root: NativeTestFileRefusal) -> NativeCensusClosureFanOut { + native_census_closure_fan_out_of(index: index, root: root.module) +} + +// THE SAME COUNT FOR ANY ROOT MODULE, whatever stage refused it: a resolve or infer root blocks its +// importers exactly as a file root does, and the reverse walk does not care which stage it was. +fn native_census_closure_fan_out_of(index: NativeCensusClosureIndex, root: Optional) -> NativeCensusClosureFanOut { match index { NativeCensusClosureIndexUnavailable { reason: r } => NativeCensusClosureFanOutUnavailable { reason: r } - NativeCensusClosureIndexRead { importers_of: importers_of, modules: modules, refused_modules: refused_modules } => - match root.module { + NativeCensusClosureIndexRead { importers_of: importers_of, imports_of: _, modules: modules, refused_modules: refused_modules } => + match root { Absent => NativeCensusClosureFanOutUnavailable { reason: ^native_census_closure_root_declares_no_module } Present { value: qn } => let seed = [qualified_name_to_dotted_string(qn: qn)] @@ -4411,41 +4437,322 @@ fn native_census_cause_groups_add_cascade_member( } } +// `first_cascade` IS FILED ON THE FIRST CHAIN AND `cascade` ON EVERY OTHER. A module that is its own +// root carries the Root arm (with its fan-out) on ONE row, so the ranking names it once however many +// chains it refused at; the rest of its chains are its own refusals and read None. fn native_census_cause_groups_add_chains( groups: NativeCensusCauseGroups, entry: NativeCensusModule, first: NonEmptyDiagnostics, rest: List, + first_cascade: NativeCensusCascade, cascade: NativeCensusCascade ) -> NativeCensusCauseGroups { fold_list( - xs: native_census_residual_rows(entry: entry, first: first, rest: rest), - empty: groups, - cons: fn(acc, row) { native_census_cause_groups_add_residual(groups: acc, cascade: cascade, row: row) } + xs: rest, + empty: native_census_cause_groups_add_residual( + groups: groups, + cascade: first_cascade, + row: NativeCensusResidualRow { module: entry.module, path: entry.path, chain: first } + ), + cons: fn(acc, c) { + native_census_cause_groups_add_residual( + groups: acc, + cascade: cascade, + row: NativeCensusResidualRow { module: entry.module, path: entry.path, chain: c } + ) + } ) } +// A MODULE THAT REFUSED ON ITS OWN ACCOUNT IS A ROOT, AND ITS ROW SAYS WHAT FIXING IT UNBLOCKS: the +// same closure fan-out a file root carries (census C), counted over the same reverse import graph. +fn native_census_own_root(closure: NativeCensusClosureIndex, entry: NativeCensusModule) -> NativeCensusCascade { + NativeCensusCascadeRoot { + closure_fan_out: native_census_closure_fan_out_of( + index: closure, + root: optional_present(value: qualified_name_from_dotted_string(dotted: entry.module)) + ), + resolve_refused_importers: optional_absent() + } +} + // ONE CENSUS MODULE'S CONTRIBUTION TO THE GROUPS, decided here rather than in the emitted main: a -// cascade member is filed once under its root; any other resolve refusal files every chain. +// cascade member is filed once under its root; any other resolve refusal files every chain, the +// first as the module's root row. fn native_census_cause_groups_add_module( groups: NativeCensusCauseGroups, + closure: NativeCensusClosureIndex, entry: NativeCensusModule, step: NativeCensusModuleStep ) -> NativeCensusCauseGroups { match step.outcome { NativeCensusModuleResolveRefused { first: f, rest: r, observation: _ } => - match step.cascade { - NativeCensusCascadeMemberOf { root: root, root_reason: root_reason } => - native_census_cause_groups_add_cascade_member(groups: groups, entry: entry, root: root, root_reason: root_reason) - NativeCensusCascadeNone => native_census_cause_groups_add_chains(groups: groups, entry: entry, first: f, rest: r, cascade: step.cascade) - NativeCensusCascadeUnattributed { reason: _ } => native_census_cause_groups_add_chains(groups: groups, entry: entry, first: f, rest: r, cascade: step.cascade) - NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => native_census_cause_groups_add_chains(groups: groups, entry: entry, first: f, rest: r, cascade: step.cascade) - } + native_census_cause_groups_add_resolve_refused(groups: groups, closure: closure, entry: entry, first: f, rest: r, cascade: step.cascade) NativeCensusModuleFileRefused => groups NativeCensusModuleResolved => groups } } +fn native_census_cause_groups_add_resolve_refused( + groups: NativeCensusCauseGroups, + closure: NativeCensusClosureIndex, + entry: NativeCensusModule, + first: NonEmptyDiagnostics, + rest: List, + cascade: NativeCensusCascade +) -> NativeCensusCauseGroups { + match cascade { + NativeCensusCascadeMemberOf { root: root, root_reason: root_reason } => + native_census_cause_groups_add_cascade_member(groups: groups, entry: entry, root: root, root_reason: root_reason) + NativeCensusCascadeNone => + native_census_cause_groups_add_chains(groups: groups, entry: entry, first: first, rest: rest, first_cascade: native_census_own_root(closure: closure, entry: entry), cascade: NativeCensusCascadeNone) + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => + native_census_cause_groups_add_chains(groups: groups, entry: entry, first: first, rest: rest, first_cascade: native_census_own_root(closure: closure, entry: entry), cascade: NativeCensusCascadeNone) + NativeCensusCascadeUnattributed { reason: _ } => + native_census_cause_groups_add_chains(groups: groups, entry: entry, first: first, rest: rest, first_cascade: cascade, cascade: cascade) + } +} + +// ── census part 2: the infer stage ───────────────────────────────────────────────────────────── +// +// THE INFER CASCADE IS THE ONE CROSS-MODULE CAUSATION v2 INFER ACTUALLY HAS, AND NOTHING ELSE. +// docs/plans/census-part-2-inference-design.md section 4 is SUPERSEDED on this point (sharp-raven-357 +// ruling, 2026-10-04). That section assumed an infer->infer edge: M's infer refusing because an imported +// T's infer refused. No such edge exists. infer_and_discharge reads ONE module's ResolvedTree, and the +// declarations it reads from imports come from the closure index at RESOLVE grain +// (v2.compiler.name_resolve closure_declarations_demand). T's inferred tree is never an input, so T +// refusing infer cannot make M refuse, and a control expecting that cascade would have a red no +// program can author (DESIGN section 4b). Its proposed edge, a fatal located at T's declaration, is +// not readable either, because every infer diagnostic is located at a node of M's own tree. +// What does exist is the edge v2.compiler.infer infer_closure_miss_refusal builds: when a closure +// provider's own RESOLVE refused (ResolvedTree unavailable_providers), M's projection through one of +// its declarations refuses as a closure miss, with the provider's refusal chain carried immediately +// ahead of that miss in M's chain. M +// resolved, so it IS an infer subject. That is a cross-stage cascade the design said could not +// happen. Such a module is filed ONCE, as a member of the provider's root, in census C's shape. +// Every other infer refusal is a root of its own. +// TRIGGER for an infer->infer edge: v2 infer reading another module's INFERRED facts. Until then, +// none is attributed. +// +// THE MATCH IS BY REASON SUFFIX AND FATAL, NOT BY GUESS. The chain must end in the closure-miss reason, +// and the reasons immediately before it must be exactly the provider's chain. That is the shape +// infer_closure_miss_refusal builds. The binds enclosing it may prepend M's own pending advisories, +// so the provider chain sits at the end of M's chain, not at its head (measured on the cascade control +// in v2.test.claim.native_census.infer_cascade: a seven-element chain carrying a one-element provider +// chain). A chain that two providers would both match is Unattributed, never assigned to the first. The provider's own root is one hop +// further when the provider is itself a cascade member of a refused file: that is read through the +// same attribution authority census C uses (native_import_target_attribution), so the member names the +// earliest refused module, exactly as a resolve cascade does. +fn native_census_infer_cascade( + context: NativeTestContext, + module_paths: Map, + resolved: ResolvedTree, + chain: NonEmptyDiagnostics +) -> NativeCensusCascade { + native_census_infer_cascade_of( + module_paths: module_paths, + providers: resolved.unavailable_providers, + chain: chain, + attribution: fn(provider) { native_import_target_attribution(context: context, module: provider) } + ) +} + +// THE ATTRIBUTION DECISION ITSELF, over the values it reads: the census roster's paths, the tree's refused +// providers, the module's chain, and the import attribution of a provider. The wrapper above binds the +// last to the context's own authority; a claim supplies all four at this interface (DESIGN section 3). +fn native_census_infer_cascade_of( + module_paths: Map, + providers: List, + chain: NonEmptyDiagnostics, + attribution: fn(QualifiedName) -> NativeImportTargetAttribution +) -> NativeCensusCascade { + if diagnostics_fatal_reason(d: chain) == ^infer_reason_projection_receiver_declaration_unavailable { + let reversed = list_reverse(xs: native_census_chain_reasons(chain: chain)) + let leading = filter(xs: providers, predicate: fn(p) { + list_starts_with( + xs: reversed, + prefix: Cons { head: ^infer_reason_projection_receiver_declaration_unavailable, tail: list_reverse(xs: native_census_chain_reasons(chain: p.refusal)) }, + eq: fn(a, b) { a == b } + ) + }) + match leading { + Empty => NativeCensusCascadeNone + Cons { head: provider, tail: others } => + if length(xs: others) > 0 { + NativeCensusCascadeUnattributed { reason: ^native_census_infer_cascade_provider_ambiguous } + } else { + match map_lookup(module_paths, qualified_name_to_dotted_string(qn: provider.module)) { + Absent => NativeCensusCascadeUnattributed { reason: ^native_census_infer_cascade_provider_not_censused } + Present { value: provider_path } => + match attribution(provider.module) { + NativeImportTargetRefused { target: t } => NativeCensusCascadeMemberOf { root: t.path, root_reason: native_test_file_refusal_fatal_reason(fr: t) } + NativeImportTargetsUnreadable { reason: r } => NativeCensusCascadeUnattributed { reason: r } + NativeImportTargetsAdmitted => NativeCensusCascadeMemberOf { root: provider_path, root_reason: diagnostics_fatal_reason(d: provider.refusal) } + NativeImportTargetNotAsked => NativeCensusCascadeMemberOf { root: provider_path, root_reason: diagnostics_fatal_reason(d: provider.refusal) } + } + } + } + } + } else { + NativeCensusCascadeNone + } +} + +fn native_census_chain_reasons(chain: NonEmptyDiagnostics) -> List { + Cons { head: chain.head.reason, tail: list_map(xs: chain.tail, f: fn(d) { d.reason }) } +} + +// THE CENSUS ROSTER'S MODULE NAMES TO THEIR PATHS, read once per census, so an attributed root is named +// by the same path identity every other root carries. +fn native_census_module_paths(modules: List) -> Map { + fold_list(xs: modules, empty: empty_map(), cons: fn(m, entry) { map_insert(m, entry.module, entry.path) }) +} + +// ONE census-infer MODULE'S CONTRIBUTION. A resolve refusal is filed exactly as census-resolve files it, +// because it is the same decision. An infer refusal is ONE row: infer stops at its first fatal, so +// native_census_infer_completeness reports that module's observation as a lower bound. The row is either +// a member of the closure provider's root that led its chain, or a root of its own carrying its fan-out. +fn native_census_cause_groups_add_infer_module( + groups: NativeCensusCauseGroups, + closure: NativeCensusClosureIndex, + module_paths: Map, + entry: NativeCensusModule, + step: NativeInferCensusStep +) -> NativeCensusCauseGroups { + match step.outcome { + NativeInferCensusFileRefused => groups + NativeInferCensusResolveRefused { first: f, rest: r, observation: _ } => + native_census_cause_groups_add_resolve_refused(groups: groups, closure: closure, entry: entry, first: f, rest: r, cascade: step.cascade) + NativeInferCensusInferred { resolved: _, inferred: _ } => groups + NativeInferCensusInferRefused { resolved: resolved, chain: chain } => + match native_census_infer_cascade(context: step.context, module_paths: module_paths, resolved: resolved, chain: chain) { + NativeCensusCascadeMemberOf { root: root, root_reason: root_reason } => + native_census_infer_member(groups: groups, fatal_reason: root_reason, entry: entry, chain: chain, cascade: NativeCensusCascadeMemberOf { root: root, root_reason: root_reason }) + NativeCensusCascadeUnattributed { reason: r } => + native_census_infer_member(groups: groups, fatal_reason: diagnostics_fatal_reason(d: chain), entry: entry, chain: chain, cascade: NativeCensusCascadeUnattributed { reason: r }) + NativeCensusCascadeNone => + native_census_infer_member(groups: groups, fatal_reason: diagnostics_fatal_reason(d: chain), entry: entry, chain: chain, cascade: native_census_own_root(closure: closure, entry: entry)) + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => + native_census_infer_member(groups: groups, fatal_reason: diagnostics_fatal_reason(d: chain), entry: entry, chain: chain, cascade: native_census_own_root(closure: closure, entry: entry)) + } + } +} + +fn native_census_infer_member( + groups: NativeCensusCauseGroups, + fatal_reason: Symbol, + entry: NativeCensusModule, + chain: NonEmptyDiagnostics, + cascade: NativeCensusCascade +) -> NativeCensusCauseGroups { + native_census_cause_groups_add( + groups: groups, + fatal_reason: fatal_reason, + member: NativeCensusCauseMember { + stage: NativeCensusCauseInfer, + path: entry.path, + module: optional_present(value: qualified_name_from_dotted_string(dotted: entry.module)), + head_reason: chain.head.reason, + cascade: cascade + } + ) +} + +// WHETHER THE census-infer WALK OBSERVED EVERY REFUSAL OF A MODULE. infer_and_discharge returns at its +// first fatal, so an infer refusal is a LOWER BOUND and says so; the resolve arms answer as census-resolve +// answers them. +fn native_census_infer_completeness(outcome: NativeInferCensusOutcome) -> NativeCensusOccurrenceCompleteness { + match outcome { + NativeInferCensusFileRefused => + NativeCensusCompletenessUnavailable { reason: ^native_census_file_never_reached_resolve } + NativeInferCensusResolveRefused { first: _, rest: _, observation: o } => + NativeCensusCompletenessObserved { observation: o } + NativeInferCensusInferred { resolved: _, inferred: _ } => + NativeCensusCompletenessObserved { observation: ObservationComplete } + NativeInferCensusInferRefused { resolved: _, chain: _ } => + NativeCensusCompletenessObserved { observation: ObservationIncomplete { reason: ^native_census_infer_stops_at_first_fatal } } + } +} + +// THE NATIVE TEST TARGET THE INFER CENSUS MARKS ITS ROOTS AGAINST: roadmap node `native-seven` +// (gunbc.roadmap.roadmap_authority), whose boundary names this label and rules that its population is +// DERIVED from the label, never kept as a list. So there is no roster of its tests here. The label is +// parsed by the one target-pattern authority, its package names the subject module by the same +// correspondence the native lane uses (gunbc.witness_v2_native_route native_route_module_package_text), +// and the subject's closure is walked over the census's own import graph. A root is "in the N7 closure" +// exactly when the subject module reaches it. The label is the instrument's own fact, never a CLI option. +data native_census_subject_label: String = "//v2/test/parse/expression_bodied_fn_decl_parse:all" + +type NativeCensusSubjectClosure + = NativeCensusSubjectClosureRead { subject: String, members: Map } + | NativeCensusSubjectClosureUnavailable { reason: Symbol } + +fn native_census_subject_closure( + index: NativeCensusClosureIndex, + modules: List, + label: String +) -> NativeCensusSubjectClosure { + match parse_target_pattern(text: label) { + TargetPatternRefused { cause: _ } => + NativeCensusSubjectClosureUnavailable { reason: ^native_census_subject_label_refused } + TargetPatternParsed { pattern: pattern } => + match pattern { + SingleTarget { label: _ } => NativeCensusSubjectClosureUnavailable { reason: ^native_census_subject_label_names_no_module } + SubtreeTargets { package: _ } => NativeCensusSubjectClosureUnavailable { reason: ^native_census_subject_label_names_no_module } + PackageTargets { package: package } => + match list_find_first(xs: modules, predicate: fn(m) { native_route_module_package_text(module_qn: m.module) == render_package(p: package) }) { + Absent => NativeCensusSubjectClosureUnavailable { reason: ^native_census_subject_module_not_censused } + Present { value: subject } => + match index { + NativeCensusClosureIndexUnavailable { reason: r } => NativeCensusSubjectClosureUnavailable { reason: r } + NativeCensusClosureIndexRead { importers_of: _, imports_of: imports_of, modules: n, refused_modules: _ } => + match native_lane_module_reachable( + index: imports_of, + frontier: [subject.module], + seen: [subject.module], + seen_set: map_insert(empty_map(), subject.module, true), + fuel: n + 1, + budget: n + 1 + ) { + Rejected { diagnostics: _ } => NativeCensusSubjectClosureUnavailable { reason: ^native_census_closure_walk_exhausted } + Accepted { value: reached, diagnostics: _ } => + NativeCensusSubjectClosureRead { + subject: subject.module, + members: fold_list(xs: reached, empty: empty_map(), cons: fn(m, r) { map_insert(m, r, true) }) + } + } + } + } + } + } +} + +// WHETHER ONE RANKED ROOT IS IN THE SUBJECT'S CLOSURE. Three answers: a root whose module is unknown, or a +// subject closure that could not be read, is Unavailable, never "outside". +type NativeCensusSubjectMembership + = NativeCensusInSubjectClosure + | NativeCensusOutsideSubjectClosure + | NativeCensusSubjectMembershipUnavailable { reason: Symbol } + +fn native_census_root_subject_membership( + subject: NativeCensusSubjectClosure, + root: NativeCensusCauseMember +) -> NativeCensusSubjectMembership { + match subject { + NativeCensusSubjectClosureUnavailable { reason: r } => NativeCensusSubjectMembershipUnavailable { reason: r } + NativeCensusSubjectClosureRead { subject: _, members: members } => + match root.module { + Absent => NativeCensusSubjectMembershipUnavailable { reason: ^native_census_closure_root_declares_no_module } + Present { value: qn } => + match map_lookup(members, qualified_name_to_dotted_string(qn: qn)) { + Present { value: _ } => NativeCensusInSubjectClosure + Absent => NativeCensusOutsideSubjectClosure + } + } + } +} + fn native_census_cause_groups_from_file_refusals( refusals: FreeMonoid, closure: NativeCensusClosureIndex, diff --git a/src/v2/compiler/01_tokenize.dag b/src/v2/compiler/01_tokenize.dag index 3c5077ef47e..cf6306d9966 100644 --- a/src/v2/compiler/01_tokenize.dag +++ b/src/v2/compiler/01_tokenize.dag @@ -16,6 +16,7 @@ import v2.std.compilers.lexing { AnnotationRule, BlockCommentTextChar, DigitChar, + EmptyPattern, HexDigitChar, IdentContinueChar, IdentStartChar, diff --git a/src/v2/compiler/02_parse.dag b/src/v2/compiler/02_parse.dag index 90c473d8748..4d3fcc026e8 100644 --- a/src/v2/compiler/02_parse.dag +++ b/src/v2/compiler/02_parse.dag @@ -1,12 +1,6 @@ module v2.compiler.parse import std.types { List } -import v2.compiler.parse_diagnostic { - NonEmptyParseStageDiagnostics, - non_empty_parse_stage_diagnostics_from_overlap_roster, - parse_overlap_residue_stage_diagnostic_at, - parse_stage_diagnostic_core -} import std.optional { Absent, Optional, @@ -29,13 +23,12 @@ import v2.std.diagnostic { Textual, Unavailable, UserInputBoundary, - diagnostics_merge, diagnostics_singleton, node_locus, outcome_accepted, + declaration_locus, invariant_port_locus, port_refusal_locus, - rejected_with_pending, source_port_locus } import v2.std.grammar_choice_ambiguity { GrammarChoiceAmbiguityRow } @@ -43,24 +36,18 @@ import v2.std.node { Atom, Conj, Edge, - Hash, Authored, Node, Symbol, TypeNode, - combine_hash, - content_hash, node_synthetic, node_with_occurrence_id, - symbol_identity_digest, well_formed, symbol_intern_lexeme, symbol_lexeme, } import v2.std.occurrence_id { alloc_occurrence_id, - authored_token_ordinal_space_from_allocator, - occurrence_id_allocator_advance_to, occurrence_id_allocator_initial } import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId, OccurrenceIdAllocator, OccurrenceMinted, OccurrencePending, OccurrenceProjected, OccurrenceSynthetic, node_occurrence_identity_minted } @@ -77,14 +64,13 @@ import v2.std.provenance { SpanIndex, origin_event_from_source, span_index_empty, - span_index_adopt, span_index_record } import v2.std.collection { Map, empty_map, list_at_optional, - map_get, + map_get_checked, map_insert, map_lookup } @@ -96,16 +82,6 @@ import std.optional { import std.algebra { Cons, Empty, FreeSemigroup, list_append, list_snoc_item } import v2.std.algebra { HeadAbsent, HeadFound, TailAbsent, TailFound, any, contains, filter, fold_list, fold_list_right, is_empty, length, list_head, list_reverse, list_tail, unique, zip_eq } import v2.std.witness { Holds, Violates, Witness, witness_from_optional } -import std.cache_identity { ArtifactKindId, CacheInterfaceId } -import std.realization { Materialization } -import v2.std.integer { integer_int_content_digest } -import v2.compiler.materialization_carriers { - parse_table_memo_artifact_kind, - parse_table_memo_provider_id, - ParseMemoRecurrence, - parse_table_memo_materialization_for, - materialization_allows_memo_store, -} import v2.extdeps.languages.dag { dag_float_literal_node_from_lexeme, dag_int_literal_node_from_lexeme, @@ -125,7 +101,7 @@ import v2.std.compilers.lexing { token_stream_remaining_count, token_stream_layout, } -import v2.std.parse_refusal_reason { LayoutUnknownWithoutSource, ParseRefusalReason, TokenStartsNotIncreasing, parse_refusal_reason_symbol } +import v2.std.parse_refusal_reason { LayoutUnknownWithoutSource, NonAssociativeOperatorChain, ParseRefusalReason, TokenStartsNotIncreasing, parse_refusal_reason_symbol } import v2.std.grammar { AfterLineBreak, Choice, @@ -138,6 +114,10 @@ import v2.std.grammar { GrammarExprFold, GrammarProduction, GrammarRoot, + InfixLeft, + InfixNonAssociative, + InfixOperatorRow, + InfixTable, ModeledGrammar, Nonterminal, ParseTree, @@ -152,26 +132,22 @@ import v2.std.grammar { StampLexeme, fold_grammar_expr, grammar_empty_node, + grammar_infix_right_min_power, + grammar_infix_row_binding_power, + grammar_infix_row_class, + grammar_infix_table_language, grammar_named_edge, grammar_to_node } -// THE MEMO FRAME. `frame` names every occurrence id recorded since the innermost memoized -// production started: a mint is one FrameMinted, and a memo hit or miss inside the frame is one -// FrameNested sharing the inner frame's list (a snoc, never a copy). A memo hit replays exactly -// its stored frame into the current index instead of merging the stored index whole. That is -// exact for every consumer: a node the hit returns carries only ids minted or replayed inside -// the memoized frame, so every id it can look up is in the replayed frame; entries the stored -// index held from before the frame belong to a different parse path, no node on this path -// references them, and the span index is read only by lookup(id). -type ParseProvFrameItem - = FrameMinted { id: OccurrenceId } - | FrameNested { frame: List } - +// THE PARSE'S PROVENANCE IS ONE PATH'S STATE. Every backtracking construct restores the whole +// value on a failed attempt, so the allocator and the span index describe exactly the surviving +// path: an id minted on an abandoned attempt is minted again by the next one and recorded once. +// Nothing is served across paths -- the packrat memo that did so was deleted (operator ruling R2, +// 2026-10-08; gunbc.recurring_failure_mode parse_memo_hit_serves_occurrences_minted_on_an_abandoned_path). type ParseProvenanceState { alloc: OccurrenceIdAllocator index: v2.std.provenance.SpanIndex - frame: List } // THE ALLOCATOR RIDES BESIDE THE INDEX BECAUSE THEY ARE ONE FACT. `span_index` says where each @@ -267,12 +243,6 @@ type ParseStampResult { prov: ParseProvenanceState } -type ParseMemoStats { - hits: Int - misses: Int - lookup_calls: Int -} - // THE FURTHEST POSITION A REJECTED ALTERNATIVE REACHED, which is where a refusal is located. // `parse_g0_tokens_remain` used to report the FIRST LEFTOVER token: the start of the module item // whose production failed as a whole, after `repeat` stopped. That names the symptom item, not @@ -291,17 +261,6 @@ type ParseFurthestFailure | FurthestAtToken { token: Token, expectation: Optional } | FurthestAtEnd { expectation: Optional } -type ParseMemoEntry - = ParseMemoAccepted { - remaining: List, - captured: Node, - diagnostics: List, - prov: ParseProvenanceState, - furthest: ParseFurthestFailure, - refusal: Optional - } - | ParseMemoRejected { diagnostics: List, furthest: ParseFurthestFailure, refusal: Optional } - type ParseExprResult = ParseExprAccepted { remaining: List, @@ -331,21 +290,7 @@ type GrammarExprCornerFold { nullable: Bool } -type ParsePositionKey { - position: Int - production: Symbol -} - type ParseTableRealization { - artifact_kind: std.cache_identity.ArtifactKindId - provider_id: std.cache_identity.CacheInterfaceId - materialization: Materialization - grammar_digest: Hash - token_stream_digest: Hash - entries: Map - memo_hits: Int - memo_misses: Int - memo_lookup_calls: Int furthest: ParseFurthestFailure nullable_set: List first_rows: Map> @@ -360,14 +305,13 @@ type ParseTable = ParseTableRealization type PreparedModeledGrammar { validated: ValidatedGrammarRoot analysis: GrammarFirstAnalysis - overlap_residue_stage: Optional } type PreparedGrammar = PreparedVoidGrammar - | PreparedModeled { modeled: PreparedModeledGrammar, materialization: Materialization } + | PreparedModeled { modeled: PreparedModeledGrammar } -data prepared_grammar_carrier_note: String = "PreparedGrammar is the once-per-grammar carrier: well-formedness, the five grammar_validate_for_parse checks, and the FIRST/nullable analysis (GrammarFirstAnalysis) are pure functions of the grammar, so prepare_grammar runs them ONCE and parse_module_prepared reuses the result per module. Previously parse_module re-ran the whole 5-check validation AND compute_grammar_first_analysis was computed twice (once in the ambiguity check, once in parse_table_for_production) on EVERY parse. Hoisting prepare_grammar above the assemble fold makes a door call over a K-module closure validate once, not K times — correctness-by-construction (the prepare sits above fold_list), no timing gate. The cache-interface fields on ParseTableRealization (grammar_digest/provider_id/materialization) stay for the cross-call content-addressed memo, which is gated on wiring the currently Miss-stubbed cache backend." +data prepared_grammar_carrier_note: String = "PreparedGrammar is the once-per-grammar carrier: well-formedness, the five grammar_validate_for_parse checks, and the FIRST/nullable analysis (GrammarFirstAnalysis) are pure functions of the grammar, so prepare_grammar runs them ONCE and parse_module_prepared reuses the result per module. Previously parse_module re-ran the whole 5-check validation AND compute_grammar_first_analysis was computed twice (once in the ambiguity check, once in parse_table_for_production) on EVERY parse. Hoisting prepare_grammar above the assemble fold makes a door call over a K-module closure validate once, not K times — correctness-by-construction (the prepare sits above fold_list), no timing gate." fn parse_grammar_not_well_formed_diagnostic(at: Locus) -> Diagnostic { Diagnostic { @@ -496,6 +440,14 @@ fn parse_grammar_duplicate_production_diagnostic(at: Locus) -> Diagnostic { } } +fn parse_grammar_infix_operator_duplicate_diagnostic(at: Locus) -> Diagnostic { + Diagnostic { + reason: ^parse_grammar_infix_operator_duplicate, + at: port_refusal_locus(at: at, port: ^parse_stage_locus_port, invariant: ^parse_grammar_infix_operator_duplicate), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + fn parse_e1_syntax_error_diagnostic(at: Locus) -> Diagnostic { Diagnostic { reason: ^parse_e1_syntax_error, @@ -542,16 +494,15 @@ fn grammar_lookup_production(root: GrammarRoot, name: Symbol) -> Witness, not a second // production roster (§3). Every ParseTableRealization construction site -// (parse_table_for_production_analyzed, the real-parse path; parse_table_empty; -// parse_table_carry_analysis, the pass-through on every table mutation) populates this field, so +// (parse_table_for_stream_analyzed, the real-parse path; parse_table_empty; the furthest and +// refusal updates, which pass it through) populates this field, so // the O(P)->O(1) change is not opt-in per call site. Measured effect (this host, // dag/test/claim/long/dag_arrow_lambda_witness_test.dag witness_single_param_lambda_parses via // `gunbc run --claim-run`, fix stashed vs applied around the identical whole-process invocation): @@ -559,8 +510,8 @@ fn grammar_lookup_production(root: GrammarRoot, name: Symbol) -> WitnessO(1) asymptotic claim is the primary evidence -// (grammar_lookup_production is an unmemoized linear filter reached before every memo-table check, -// so even a memo HIT paid the full scan), not the crude wall-clock ratio. This is the class +// (grammar_lookup_production is a linear filter, so every nonterminal dispatch paid the full scan), +// not the crude wall-clock ratio. This is the class // docs/plans/measurements/floor-slow-rows-2026-08-18.md records dag_arrow_lambda_witness_test's // rows landing 1-4ms over its 1552ms ceiling for. @@ -581,85 +532,6 @@ fn grammar_lookup_production_indexed(table: ParseTable, name: Symbol) -> Witness ) } -fn parse_table_memo_miss_diagnostic(at: Locus) -> Diagnostic { - Diagnostic { - reason: ^parse_table_memo_miss, - at: port_refusal_locus(at: at, port: ^parse_stage_locus_port, invariant: ^parse_table_memo_miss), - correction: Unavailable { reason: ExternalContractUnknown } - } -} - -fn parse_table_entries_lookup(table: ParseTable, key: ParsePositionKey) -> Witness { - witness_from_optional( - opt: table.entries.lookup(key), - absent: parse_table_memo_miss_diagnostic(at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_table_memo_miss)) - ) -} - -fn parse_table_lookup(table: ParseTable, key: ParsePositionKey) -> Witness { - match v2.compiler.materialization_carriers.materialization_allows_memo_store(m: table.materialization) { - true => { parse_table_entries_lookup(table: table, key: key) } - false => { - Violates { - diagnostic: parse_table_memo_store_refused_diagnostic( - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_table_memo_store_refused) - ) - } - } - } -} - -fn parse_table_memo_store_refused_diagnostic(at: Locus) -> Diagnostic { - Diagnostic { - reason: ^parse_table_memo_store_refused, - at: port_refusal_locus(at: at, port: ^parse_stage_locus_port, invariant: ^parse_table_memo_store_refused), - correction: Unavailable { reason: ExternalContractUnknown } - } -} - -fn parse_table_memo_store_refused_reason(d: Diagnostic) -> Bool { - match d { - Diagnostic { reason: r, at: _, correction: _ } => - r == ^parse_table_memo_store_refused - } -} - -fn parse_table_memo_lookup_refuses_store(table: ParseTable, key: ParsePositionKey) -> Bool { - match parse_table_lookup(table: table, key: key) { - Violates { diagnostic: d } => parse_table_memo_store_refused_reason(d: d) - Holds { value: _ } => false - } -} - -fn parse_table_memo_insert_refused( - table: ParseTable, - key: ParsePositionKey, - value: ParseMemoEntry -) -> Bool { - let after = parse_table_insert(table: table, key: key, value: value) - match v2.std.collection.map_get(m: after.entries, key: key) { - Accepted { value: Absent } => true - Accepted { value: Present { value: _ } } => false - Rejected { diagnostics: _ } => false - } -} - -fn parse_memo_entry_from_expr(result: ParseExprResult) -> ParseMemoEntry { - match result { - ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: diagnostics, prov: prov, table: _ } => - ParseMemoAccepted { - remaining: remaining, - captured: captured, - diagnostics: diagnostics, - prov: prov, - furthest: parse_expr_result_table(result: result).furthest, - refusal: parse_expr_result_table(result: result).refusal - } - ParseExprRejected { diagnostics: diagnostics, table: table } => - ParseMemoRejected { diagnostics: diagnostics, furthest: table.furthest, refusal: table.refusal } - } -} - fn parse_expr_result_table(result: ParseExprResult) -> ParseTable { match result { ParseExprAccepted { remaining: _, captured: _, diagnostics: _, prov: _, table: table } => table @@ -667,67 +539,6 @@ fn parse_expr_result_table(result: ParseExprResult) -> ParseTable { } } -fn parse_table_memo_stats(table: ParseTable) -> ParseMemoStats { - ParseMemoStats { - hits: table.memo_hits, - misses: table.memo_misses, - lookup_calls: table.memo_lookup_calls - } -} - -fn parse_memo_stats_empty() -> ParseMemoStats { - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } -} - -// THE PARSE'S OWN MEMO ACCOUNTING, CARRIED BESIDE ITS RESULT AND NOT INSIDE IT. -// -// WHY NOT A FIELD ON ParseArtifact, which is the shorter change. ParseArtifact answers WHAT was -// parsed; the memo counters answer HOW the parse was executed, which is a realization fact about -// one run and not a property of the tree. DESIGN section 3 keeps interface and realization as two -// facts, so putting a counter on the semantic artifact would make every consumer of a parse result -// carry a measurement it has no use for, and would make the artifact's identity depend on how many -// times a memo happened to be consulted. -// -// AND THE COUNTERS MUST SURVIVE A REFUSAL, which a field on the accepted value cannot do. The -// expensive case this instrument exists to read is a file that spends seconds in parse and THEN -// rejects; an Outcome's Rejected arm carries no ParseArtifact, so the stats would be lost exactly -// where they are most wanted. This carrier holds the outcome and the accounting side by side, so -// both arms report. -// -// THE UNMEASURED ENTRY POINT IS A PROJECTION OF THIS ONE, never a second parse: parse_module_prepared -// is `.outcome` of the measured fold, so there is one implementation and no caller pays a second -// parse to be measured. -type ParseProductionMeasured { - outcome: Outcome - memo: ParseMemoStats -} - -fn parse_table_carry_analysis(table: ParseTable, entries: Map, hits: Int, misses: Int, lookups: Int) -> ParseTable { - ParseTableRealization { - artifact_kind: table.artifact_kind, - provider_id: table.provider_id, - materialization: table.materialization, - grammar_digest: table.grammar_digest, - token_stream_digest: table.token_stream_digest, - entries: entries, - memo_hits: hits, - memo_misses: misses, - memo_lookup_calls: lookups, - furthest: table.furthest, - nullable_set: table.nullable_set, - first_rows: table.first_rows, - productions_by_name: table.productions_by_name, - prepared_exprs: table.prepared_exprs, - layout: table.layout, - refusal: table.refusal - } -} - -// A MEMO ENTRY CARRIES ITS OWN SUBPARSE'S FURTHEST FAILURE, and a hit joins it back in. The -// seed serves entries across parses of one token stream (v1_interpreter -// parse_table_memo_scope_and_key), so a table-only record would lose the deep rejection inside a -// served subparse and the refusal's locus would depend on cache warmth. The miss path runs the -// subparse from the unit, stores what that subparse alone reached, then joins it with the caller's. fn parse_expectation_first(left: Optional, right: Optional) -> Optional { match left { Present { value: _ } => left @@ -778,15 +589,6 @@ fn parse_furthest_expected_at(tokens: List, reason: ParseRefusalReason) - fn parse_table_with_furthest(table: ParseTable, furthest: ParseFurthestFailure) -> ParseTable { ParseTableRealization { - artifact_kind: table.artifact_kind, - provider_id: table.provider_id, - materialization: table.materialization, - grammar_digest: table.grammar_digest, - token_stream_digest: table.token_stream_digest, - entries: table.entries, - memo_hits: table.memo_hits, - memo_misses: table.memo_misses, - memo_lookup_calls: table.memo_lookup_calls, furthest: furthest, nullable_set: table.nullable_set, first_rows: table.first_rows, @@ -802,7 +604,7 @@ fn parse_table_join_furthest(table: ParseTable, furthest: ParseFurthestFailure) } // THE FIRST REFUSAL STANDS. A table that already refused keeps its refusal; one that has not takes -// the incoming one (a memo hit replaying the refusal its subparse recorded). +// the incoming one. fn parse_table_join_refusal(table: ParseTable, refusal: Optional) -> ParseTable { match table.refusal { Present { value: _ } => table @@ -816,15 +618,6 @@ fn parse_table_join_refusal(table: ParseTable, refusal: Optional) -> fn parse_table_with_refusal(table: ParseTable, refusal: Diagnostic) -> ParseTable { ParseTableRealization { - artifact_kind: table.artifact_kind, - provider_id: table.provider_id, - materialization: table.materialization, - grammar_digest: table.grammar_digest, - token_stream_digest: table.token_stream_digest, - entries: table.entries, - memo_hits: table.memo_hits, - memo_misses: table.memo_misses, - memo_lookup_calls: table.memo_lookup_calls, furthest: table.furthest, nullable_set: table.nullable_set, first_rows: table.first_rows, @@ -835,141 +628,28 @@ fn parse_table_with_refusal(table: ParseTable, refusal: Diagnostic) -> ParseTabl } } -fn parse_table_record_lookup_call(table: ParseTable) -> ParseTable { - parse_table_carry_analysis( - table: table, - entries: table.entries, - hits: table.memo_hits, - misses: table.memo_misses, - lookups: table.memo_lookup_calls + 1 - ) -} - -fn parse_table_record_hit(table: ParseTable) -> ParseTable { - parse_table_carry_analysis( - table: table, - entries: table.entries, - hits: table.memo_hits + 1, - misses: table.memo_misses, - lookups: table.memo_lookup_calls - ) -} - -fn parse_table_record_miss(table: ParseTable) -> ParseTable { - parse_table_carry_analysis( - table: table, - entries: table.entries, - hits: table.memo_hits, - misses: table.memo_misses + 1, - lookups: table.memo_lookup_calls - ) -} - -fn parse_table_insert(table: ParseTable, key: ParsePositionKey, value: ParseMemoEntry) -> ParseTable { - match v2.compiler.materialization_carriers.materialization_allows_memo_store(m: table.materialization) { - true => - parse_table_carry_analysis( - table: table, - entries: map_insert(m: table.entries, key: key, value: value), - hits: table.memo_hits, - misses: table.memo_misses, - lookups: table.memo_lookup_calls - ) - false => table - } -} - -fn parse_grammar_digest(validated: ValidatedGrammarRoot) -> Hash { - content_hash( - n: grammar_to_node(grammar: ModeledGrammar { root: validated.root }) - ) -} - -// ONE DIGEST PER TOKEN FIELD, NOT ONE HASH STEP PER UNIT OF MAGNITUDE. The stream digest used to -// send both offsets and every lexeme codepoint through the single-byte-limb digest: unary in the -// value (a codepoint near 100 cost ~100 combine_hash calls), and one shared tag for every value -// above 255. On the native route that made the eager fill in parse_table_for_production_analyzed -// 28% of the process (perf --children, bounded K=16 root, 2026-09-14), and it made offsets past -// byte 255 and every non-Latin-1 codepoint collide in the seed interpreter's cross-parse memo key -// (v1_interpreter parse_table_memo_scope_and_key reads this field). Offsets and positions now go -// through the Int content authority and the lexeme through its interned symbol. -// The token digest, combined with the stream layout's own digest (folded once, in the layout walk, -// over the line-break-preceded tokens' starts and lines). Only a sourceless stream keys by the token -// digest alone. -fn parse_token_stream_layout_digest(all_tokens: List, layout: Optional) -> Hash { - match layout { - Absent => parse_token_stream_digest(all_tokens: all_tokens) - Present { value: l } => combine_hash(a: parse_token_stream_digest(all_tokens: all_tokens), b: l.digest) - } -} - -fn parse_token_stream_digest(all_tokens: List) -> Hash { - fold_list( - xs: all_tokens, - empty: symbol_identity_digest(sym: ^parse_token_stream_digest_nil), - cons: fn(acc, tok) { - combine_hash( - a: acc, - b: combine_hash( - a: combine_hash( - a: symbol_identity_digest(sym: tok.class), - b: symbol_identity_digest(sym: tok.file) - ), - b: combine_hash( - a: combine_hash( - a: integer_int_content_digest(value: tok.start), - b: integer_int_content_digest(value: tok.end) - ), - b: symbol_identity_digest(sym: symbol_intern_lexeme(lexeme: tok.lexeme)) - ) - ) - ) - } - ) -} - -// THE MATERIALIZATION ARRIVES ADMITTED, it is not read here. This constructor runs once per -// production parse; the admission is a function of the ladder verdicts alone, so reading the -// door here would ask one question per table and give every caller its own place to mishandle -// a refusal. prepare_grammar asks once per grammar and refuses there, and what reaches here is -// a value that has already been admitted -- so this signature cannot express an unadmitted one. fn parse_table_for_production_analyzed( validated: ValidatedGrammarRoot, analysis: GrammarFirstAnalysis, - all_tokens: List, - materialization: Materialization + all_tokens: List ) -> ParseTable { parse_table_for_stream_analyzed( validated: validated, analysis: analysis, all_tokens: all_tokens, - layout: optional_absent(), - materialization: materialization + layout: optional_absent() ) } // The stream's layout rides on the table so a layout-conditioned production can ask its question at -// the point of use (v2.std.compilers.lexing token_stream_line_break_before), and its layout (the starts of -// the line-break-preceded tokens) is part of the -// stream's digest: two sources can lex to byte-identical tokens and differ only in layout, and a memo -// entry must never serve one's answer to the other. +// the point of use (v2.std.compilers.lexing token_stream_line_break_before). fn parse_table_for_stream_analyzed( validated: ValidatedGrammarRoot, analysis: GrammarFirstAnalysis, all_tokens: List, - layout: Optional, - materialization: Materialization + layout: Optional ) -> ParseTable { ParseTableRealization { - artifact_kind: v2.compiler.materialization_carriers.parse_table_memo_artifact_kind(), - provider_id: v2.compiler.materialization_carriers.parse_table_memo_provider_id(), - materialization: materialization, - grammar_digest: analysis.grammar_digest, - token_stream_digest: parse_token_stream_layout_digest(all_tokens: all_tokens, layout: layout), - entries: empty_map(), - memo_hits: 0, - memo_misses: 0, - memo_lookup_calls: 0, furthest: FurthestNone, nullable_set: analysis.nullable_set, first_rows: analysis.first_rows, @@ -982,32 +662,18 @@ fn parse_table_for_stream_analyzed( fn parse_table_for_production( validated: ValidatedGrammarRoot, - all_tokens: List, - materialization: Materialization + all_tokens: List ) -> ParseTable { parse_table_for_production_analyzed( validated: validated, analysis: compute_grammar_first_analysis(root: validated.root), - all_tokens: all_tokens, - materialization: materialization + all_tokens: all_tokens ) } -// The empty grammar has no productions, so its recurrence census would be zero on both counts -// and its demand case the ladder's single-demand arm. It is a test-only constructor with no -// analysis to carry, so the materialization is SUPPLIED at the boundary rather than derived here -// (DESIGN section 3's witness rule); the derived route is parse_table_for_production_analyzed. -fn parse_table_empty(materialization: Materialization) -> ParseTable { +// A test-only constructor with no analysis to carry: no productions, no FIRST rows, no layout. +fn parse_table_empty() -> ParseTable { ParseTableRealization { - artifact_kind: v2.compiler.materialization_carriers.parse_table_memo_artifact_kind(), - provider_id: v2.compiler.materialization_carriers.parse_table_memo_provider_id(), - materialization: materialization, - grammar_digest: content_hash(n: grammar_empty_node()), - token_stream_digest: symbol_identity_digest(sym: ^parse_table_empty_token_stream), - entries: empty_map(), - memo_hits: 0, - memo_misses: 0, - memo_lookup_calls: 0, furthest: FurthestNone, nullable_set: std.algebra.Empty, first_rows: empty_map(), @@ -1018,16 +684,6 @@ fn parse_table_empty(materialization: Materialization) -> ParseTable { } } -fn parse_table_subject(table: ParseTable, key: ParsePositionKey) -> Hash { - combine_hash( - a: combine_hash(a: table.grammar_digest, b: table.token_stream_digest), - b: combine_hash( - a: integer_int_content_digest(value: key.position), - b: symbol_identity_digest(sym: key.production) - ) - ) -} - fn parse_current_position(tokens: List) -> Int { match list_head(xs: tokens) { HeadFound { value: t } => t.start + 1 @@ -1056,8 +712,7 @@ fn parse_non_empty_diagnostics_to_list(ned: NonEmptyDiagnostics) -> List ParseProvenanceState { ParseProvenanceState { alloc: seed, - index: span_index_empty(), - frame: Empty + index: span_index_empty() } } @@ -1065,55 +720,6 @@ fn parse_prov_initial() -> ParseProvenanceState { parse_prov_seeded(seed: occurrence_id_allocator_initial()) } -fn parse_prov_replay_frame( - base: v2.std.provenance.SpanIndex, - incoming: v2.std.provenance.SpanIndex, - frame: List -) -> v2.std.provenance.SpanIndex { - fold_list( - xs: frame, - empty: base, - cons: fn(acc, item) { - match item { - FrameMinted { id: id } => span_index_adopt(base: acc, incoming: incoming, id: id) - FrameNested { frame: inner } => parse_prov_replay_frame(base: acc, incoming: incoming, frame: inner) - } - } - ) -} - -// A memo hit: `incoming` is the stored result of the memoized frame, whose `frame` is that -// frame's own record (the miss started it empty). -fn parse_prov_merge( - base: ParseProvenanceState, - incoming: ParseProvenanceState -) -> ParseProvenanceState { - ParseProvenanceState { - alloc: occurrence_id_allocator_advance_to( - alloc: base.alloc, - min_next: authored_token_ordinal_space_from_allocator( - alloc: incoming.alloc - ) - ), - index: parse_prov_replay_frame(base: base.index, incoming: incoming.index, frame: incoming.frame), - frame: list_snoc_item(xs: base.frame, item: FrameNested { frame: incoming.frame }) - } -} - -// A memo miss: the production parses in a fresh frame so its memo entry records only its own ids. -fn parse_prov_enter_frame(prov: ParseProvenanceState) -> ParseProvenanceState { - ParseProvenanceState { alloc: prov.alloc, index: prov.index, frame: Empty } -} - -// Returning from a miss: the caller's frame gains the inner frame as one nested item. -fn parse_prov_leave_frame(outer: ParseProvenanceState, inner: ParseProvenanceState) -> ParseProvenanceState { - ParseProvenanceState { - alloc: inner.alloc, - index: inner.index, - frame: list_snoc_item(xs: outer.frame, item: FrameNested { frame: inner.frame }) - } -} - fn parse_mint_from_source( prov: ParseProvenanceState, file: Symbol, @@ -1131,8 +737,7 @@ fn parse_mint_from_source( id: minted.id, prov: ParseProvenanceState { alloc: minted.alloc, - index: index, - frame: list_snoc_item(xs: prov.frame, item: FrameMinted { id: minted.id }) + index: index } } } @@ -1233,6 +838,35 @@ fn parse_stamp_derived_conj( ], occurrence_id: node_occurrence_identity_minted(id: minted.id) ), + prov: ParseProvenanceState { + alloc: minted.alloc, + index: index + } + } +} + +// ONE OPERATOR APPLICATION (v2.std.grammar InfixTable): its left operand, the operator's terminal and +// its right operand, under one minted id derived from the three. +fn parse_stamp_infix_application( + left: Node, + operator: Node, + right: Node, + prov: ParseProvenanceState +) -> ParseStampResult { + let minted = alloc_occurrence_id(alloc: prov.alloc) + let from = parse_minted_id_list(nodes: [left, operator, right]) + let event = DerivedBy { producer: ^parse_stage_producer, from: from } + let index = span_index_record(index: prov.index, id: minted.id, event: event) + ParseStampResult { + node: node_with_occurrence_id( + kind: TypeNode { connective: Conj }, + children: [ + grammar_named_edge(name: ^grammar_infix_left_node_projection, target: left), + grammar_named_edge(name: ^grammar_infix_operator_node_projection, target: operator), + grammar_named_edge(name: ^grammar_infix_right_node_projection, target: right) + ], + occurrence_id: node_occurrence_identity_minted(id: minted.id) + ), prov: ParseProvenanceState { alloc: minted.alloc, index: index, @@ -1256,16 +890,16 @@ fn parse_stamp_derived_repeat( ), prov: ParseProvenanceState { alloc: minted.alloc, - index: index, - frame: list_snoc_item(xs: prov.frame, item: FrameMinted { id: minted.id }) + index: index } } } // THE PARSE TREE'S PROJECTION EDGES, AS A CLOSED ROSTER OWNED BY THEIR PRODUCER. A parse tree -// carries exactly four Named edges that are parse-projection structure rather than authored +// carries exactly seven Named edges that are parse-projection structure rather than authored // program data: the production identity and captured pair stamped by parse_wrap_production_captured, -// and the sequence left/right pair stamped by the sequence and list spines. Every later stage that +// the sequence left/right pair stamped by the sequence and list spines, and the left/operator/right +// triple of one operator application stamped by parse_stamp_infix_application. Every later stage that // must see through projection structure (namespace_graft, name_resolve, symbol_index_fill) asks // THIS predicate. It was a lexeme-suffix test on "_node_projection" before, and a suffix is a // heuristic (DESIGN section 4): an authored fn or data declaration whose name ends in @@ -1278,6 +912,9 @@ fn parse_tree_projection_edge(name: Symbol) -> Bool { || (name == ^grammar_production_captured_node_projection) || (name == ^grammar_sequence_left_node_projection) || (name == ^grammar_sequence_right_node_projection) + || (name == ^grammar_infix_left_node_projection) + || (name == ^grammar_infix_operator_node_projection) + || (name == ^grammar_infix_right_node_projection) } fn parse_wrap_production_captured( @@ -1300,8 +937,7 @@ fn parse_wrap_production_captured( ), prov: ParseProvenanceState { alloc: minted.alloc, - index: index, - frame: list_snoc_item(xs: prov.frame, item: FrameMinted { id: minted.id }) + index: index } } } @@ -1335,9 +971,12 @@ fn parse_tokens_drop(tokens: List, count: Int) -> List { } } -// A MEMO MISS JOINS CALLER FIRST: the caller's furthest record was made before the subparse ran, so -// it is the left operand of parse_furthest_join -- the order an expectation tie is broken by. -fn parse_nonterminal_memoized( +// A NONTERMINAL IS PARSED WHERE IT IS DEMANDED, every time it is demanded. The table and the +// provenance thread straight through the production's prepared expression: the furthest-failure +// record is a join (parse_furthest_join is associative), so accumulating it across the subparse is +// the same value the deleted memo's reset-then-join computed, and the provenance is the surviving +// path's own -- nothing minted on another path is served here. +fn parse_nonterminal( all_tokens: List, tokens: List, root: GrammarRoot, @@ -1345,74 +984,32 @@ fn parse_nonterminal_memoized( prod_name: Symbol, prov: ParseProvenanceState ) -> ParseExprResult { - let key = ParsePositionKey { - position: parse_current_position(tokens: tokens), - production: prod_name - } match grammar_lookup_production_indexed(table: table, name: prod_name) { Violates { diagnostic: d } => ParseExprRejected { diagnostics: [d], table: table } Holds { value: production } => - let table_at_lookup = parse_table_record_lookup_call(table: table) - match parse_table_lookup(table: table_at_lookup, key: key) { - Holds { value: inner } => - let table_hit = parse_table_record_hit(table: table_at_lookup) - match inner { - ParseMemoRejected { diagnostics: diags, furthest: served, refusal: served_refusal } => - ParseExprRejected { - diagnostics: diags, - table: parse_table_join_refusal(table: parse_table_join_furthest(table: table_hit, furthest: served), refusal: served_refusal) - } - ParseMemoAccepted { remaining: remaining, captured: captured, diagnostics: diags, prov: inner_prov, furthest: served, refusal: served_refusal } => - let merged_prov = parse_prov_merge(base: prov, incoming: inner_prov) - let stamped = parse_wrap_production_captured( - production: production, - captured: captured, - prov: merged_prov - ) - ParseExprAccepted { - remaining: remaining, - captured: stamped.node, - diagnostics: diags, - prov: stamped.prov, - table: parse_table_join_refusal(table: parse_table_join_furthest(table: table_hit, furthest: served), refusal: served_refusal) - } - } - Violates { diagnostic: _ } => - let table_miss = parse_table_record_miss(table: table_at_lookup) - let inner = parse_expr_with_first( - expr: parse_table_prepared_expr(table: table_miss, production: production), - tokens: tokens, - all_tokens: all_tokens, - root: root, - table: parse_table_with_furthest(table: table_miss, furthest: FurthestNone), - prov: parse_prov_enter_frame(prov: prov) - ) - let table_inserted = parse_table_insert( - table: parse_expr_result_table(result: inner), - key: key, - value: parse_memo_entry_from_expr(result: inner) - ) - let table_after_memo = parse_table_with_furthest( - table: table_inserted, - furthest: parse_furthest_join(left: table_miss.furthest, right: table_inserted.furthest) + match parse_expr_with_first( + expr: parse_table_prepared_expr(table: table, production: production), + tokens: tokens, + all_tokens: all_tokens, + root: root, + table: table, + prov: prov + ) { + ParseExprRejected { diagnostics: diags, table: table1 } => + ParseExprRejected { diagnostics: diags, table: table1 } + ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: diags, prov: inner_prov, table: table1 } => + let stamped = parse_wrap_production_captured( + production: production, + captured: captured, + prov: inner_prov ) - match inner { - ParseExprRejected { diagnostics: diags, table: _ } => - ParseExprRejected { diagnostics: diags, table: table_after_memo } - ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: diags, prov: inner_prov, table: _ } => - let stamped = parse_wrap_production_captured( - production: production, - captured: captured, - prov: parse_prov_leave_frame(outer: prov, inner: inner_prov) - ) - ParseExprAccepted { - remaining: remaining, - captured: stamped.node, - diagnostics: diags, - prov: stamped.prov, - table: table_after_memo - } + ParseExprAccepted { + remaining: remaining, + captured: stamped.node, + diagnostics: diags, + prov: stamped.prov, + table: table1 } } } @@ -1440,7 +1037,8 @@ fn expr_is_nullable_given_set(expr: GrammarExpr, nullable_set: List) -> expect: fn(element_nullable, _reason) { element_nullable }, refuse_on_match: fn(element_nullable, _reason) { element_nullable }, after_line_break: fn(element_nullable) { element_nullable }, - not_predicate: fn(_) { true } + not_predicate: fn(_) { true }, + infix_table: fn(operand_nullable, _guard, _operators) { operand_nullable } } ) } @@ -1594,18 +1192,12 @@ type GrammarExprFirstFold { // every parse of `.dag` source demands could never store, and every demanding claim recomputed // it until its own CPU budget stopped the fill mid-flight. The field is the enumeration it // always was; `first_rows` beside it was already enumeration-shaped. -// THE GRAMMAR DIGEST AND THE PRODUCTION INDEX ARE ONCE-PER-GRAMMAR FACTS AND LIVE HERE, beside -// the FIRST analysis they were already computed next to. Until this carrier held them, -// parse_table_for_production_analyzed re-derived both on EVERY parse call -- a content hash of -// the whole grammar node tree (grammar_to_node over the 200-production dag grammar, then -// content_hash) plus an O(P) map build -- for a value that prepare_grammar had fixed before the -// first token was read. The required floor's own cross-claim demand census (main run -// 34556223550, required_floor_cross_claim_demand.tsv) named it: parse_grammar_digest at -// claims=19 evals=19, ~128ms per claim on the lane's runner, every one of them a claim that -// parses `.dag` text against the warm-shared dag_prepared_grammar -- so the preparation was -// shared and the invariant derived FROM it was not, which is the DESIGN §6 per-call rebuild -// #10992's write-up flagged. prepared_exprs is the precedent: a per-grammar derivation carried -// once and read per parse. Nothing about the digest's value changes; only where it is paid. +// THE PRODUCTION INDEX IS A ONCE-PER-GRAMMAR FACT AND LIVES HERE, beside the FIRST analysis it was +// already computed next to. Before this carrier held it, parse_table_for_production_analyzed +// re-derived it (and the grammar digest the deleted parse memo keyed on) on EVERY parse call -- +// an O(P) map build for a value prepare_grammar had fixed before the first token was read; the +// required floor's cross-claim demand census (main run 34556223550) named the per-call rebuild. +// prepared_exprs is the precedent: a per-grammar derivation carried once and read per parse. // The same per-parse rebuild had a second site: parse_production_prepared derived the grammar's // own locus eagerly (node_locus over grammar_to_node of the whole grammar) although only the // rejection arm that carried no diagnostic of its own ever read it; it is now derived on that @@ -1614,10 +1206,8 @@ type GrammarFirstAnalysis { nullable_set: List first_rows: Map> prepared_exprs: Map - grammar_digest: Hash productions_by_name: Map choice_residue: List - memo_recurrence: ParseMemoRecurrence } fn grammar_empty_terminal_list() -> List { @@ -1651,7 +1241,7 @@ fn first_list_intersect(left: List, right: List) -> List>, name: Symbol) -> List { - match v2.std.collection.map_get(m: rows, key: name) { + match v2.std.collection.map_get_checked(m: rows, key: name) { Accepted { value: Present { value: terminals }, diagnostics: _ } => terminals Accepted { value: Absent, diagnostics: _ } => grammar_empty_terminal_list() Rejected { diagnostics: _ } => grammar_empty_terminal_list() @@ -1734,11 +1324,27 @@ fn expr_first_fold( expect: fn(element, _reason) { element }, refuse_on_match: fn(element, _reason) { element }, after_line_break: fn(element) { element }, - not_predicate: fn(_element) { expr_first_fold_predicate() } + not_predicate: fn(_element) { expr_first_fold_predicate() }, + infix_table: fn(operand, _guard, operators) { expr_first_fold_infix(operand: operand, operators: operators) } } ) } +// A table is its operand followed by any number of `guard operator operand` continuations; the guard +// is a predicate, so a continuation begins with one of the rows' operator classes. +fn expr_first_fold_infix(operand: GrammarExprFirstFold, operators: List) -> GrammarExprFirstFold { + expr_first_fold_sequence( + left: operand, + right: expr_first_fold_repeat( + element: fold_list( + xs: operators, + empty: GrammarExprFirstFold { terminals: grammar_empty_terminal_list(), nullable: false }, + cons: fn(acc, row) { expr_first_fold_choice(left: acc, right: expr_first_fold_terminal(token_class: grammar_infix_row_class(row: row))) } + ) + ) + ) +} + fn compute_production_first_row_terminals( rows: Map>, p: GrammarProduction, @@ -1872,6 +1478,12 @@ type PreparedGrammarExpr | PreparedRefuseOnMatch { element: PreparedGrammarExpr, reason: ParseRefusalReason } | PreparedAfterLineBreak { element: PreparedGrammarExpr } | PreparedNotPredicate { element: PreparedGrammarExpr, first: GrammarExprFirstFold } + | PreparedInfixTable { + operand: PreparedGrammarExpr + continuation_guard: PreparedGrammarExpr + operators: Map + first: GrammarExprFirstFold + } type PreparedExprFold { tree: PreparedGrammarExpr @@ -1918,6 +1530,7 @@ fn flatten_choice_exprs(expr: GrammarExpr) -> List { RefuseOnMatch { element: _, reason: _ } => Cons { head: expr, tail: std.algebra.Empty } AfterLineBreak { element: _ } => Cons { head: expr, tail: std.algebra.Empty } NotPredicate { element: _ } => Cons { head: expr, tail: std.algebra.Empty } + InfixTable { operand: _, continuation_guard: _, operators: _ } => Cons { head: expr, tail: std.algebra.Empty } } } @@ -1960,6 +1573,7 @@ fn prepared_prefix_atom_eq(left: PreparedGrammarExpr, right: PreparedGrammarExpr PreparedRefuseOnMatch { element: _, reason: _ } => false PreparedAfterLineBreak { element: _ } => false PreparedNotPredicate { element: _, first: _ } => false + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => false } PreparedLiteralTerminal { token_class: lc, lexeme: lw } => match right { @@ -1977,6 +1591,7 @@ fn prepared_prefix_atom_eq(left: PreparedGrammarExpr, right: PreparedGrammarExpr PreparedRefuseOnMatch { element: _, reason: _ } => false PreparedAfterLineBreak { element: _ } => false PreparedNotPredicate { element: _, first: _ } => false + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => false } PreparedNonterminal { production: lp } => match right { @@ -1991,6 +1606,7 @@ fn prepared_prefix_atom_eq(left: PreparedGrammarExpr, right: PreparedGrammarExpr PreparedRefuseOnMatch { element: _, reason: _ } => false PreparedAfterLineBreak { element: _ } => false PreparedNotPredicate { element: _, first: _ } => false + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => false } PreparedSequence { left: _, right: _ } => false PreparedChoice { plan: _, first: _ } => false @@ -2000,6 +1616,7 @@ fn prepared_prefix_atom_eq(left: PreparedGrammarExpr, right: PreparedGrammarExpr PreparedRefuseOnMatch { element: _, reason: _ } => false PreparedAfterLineBreak { element: _ } => false PreparedNotPredicate { element: _, first: _ } => false + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => false } } @@ -2018,6 +1635,7 @@ fn prepared_sequence_left_atom(expr: PreparedGrammarExpr) -> Optional optional_absent() PreparedAfterLineBreak { element: _ } => optional_absent() PreparedNotPredicate { element: _, first: _ } => optional_absent() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => optional_absent() } PreparedTerminal { token_class: _, stamp: _ } => optional_absent() PreparedLiteralTerminal { token_class: _, lexeme: _ } => optional_absent() @@ -2029,6 +1647,7 @@ fn prepared_sequence_left_atom(expr: PreparedGrammarExpr) -> Optional optional_absent() PreparedAfterLineBreak { element: _ } => optional_absent() PreparedNotPredicate { element: _, first: _ } => optional_absent() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => optional_absent() } } @@ -2161,6 +1780,7 @@ fn prepared_terminal_prefix(expr: PreparedGrammarExpr, need: Int) -> Optional

  • optional_absent() PreparedAfterLineBreak { element: _ } => optional_absent() PreparedNotPredicate { element: _, first: _ } => optional_absent() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => optional_absent() } PreparedNonterminal { production: _ } => optional_absent() PreparedChoice { plan: _, first: _ } => optional_absent() @@ -2170,6 +1790,7 @@ fn prepared_terminal_prefix(expr: PreparedGrammarExpr, need: Int) -> Optional
  • optional_absent() PreparedAfterLineBreak { element: _ } => optional_absent() PreparedNotPredicate { element: _, first: _ } => optional_absent() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => optional_absent() } } } @@ -2292,6 +1913,7 @@ fn choice_shared_prefix_suffixes_from( PreparedRefuseOnMatch { element: _, reason: _ } => optional_absent() PreparedAfterLineBreak { element: _ } => optional_absent() PreparedNotPredicate { element: _, first: _ } => optional_absent() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: _ } => optional_absent() } } } @@ -2329,6 +1951,7 @@ fn expr_first_fold_from_prepared( PreparedAfterLineBreak { element: element } => expr_first_fold_from_prepared(expr: element, nullable_set: nullable_set, first_rows: first_rows) PreparedNotPredicate { element: _, first: _ } => expr_first_fold_predicate() + PreparedInfixTable { operand: _, continuation_guard: _, operators: _, first: first } => first } } @@ -2517,6 +2140,14 @@ fn lookahead_prefixes( AfterLineBreak { element: element } => lookahead_prefixes(expr: element, depth: depth, head_keys: head_keys, visiting: visiting, ctx: ctx) NotPredicate { element: _ } => lookahead_empty_only() + InfixTable { operand: operand, continuation_guard: guard, operators: operators } => + lookahead_prefixes( + expr: grammar_infix_table_language(operand: operand, continuation_guard: guard, operators: operators), + depth: depth, + head_keys: head_keys, + visiting: visiting, + ctx: ctx + ) } } @@ -2583,6 +2214,7 @@ fn lookahead_leading_guard(expr: GrammarExpr) -> Optional { Expect { element: _, reason: _ } => Absent RefuseOnMatch { element: _, reason: _ } => Absent AfterLineBreak { element: _ } => Absent + InfixTable { operand: _, continuation_guard: _, operators: _ } => Absent } } @@ -2795,6 +2427,7 @@ fn lookahead_guard_strings(expr: GrammarExpr, visiting: List, ctx: Looka RefuseOnMatch { element: _, reason: _ } => Absent AfterLineBreak { element: _ } => Absent NotPredicate { element: _ } => Absent + InfixTable { operand: _, continuation_guard: _, operators: _ } => Absent } } @@ -2830,6 +2463,7 @@ fn lookahead_guard_token_set(expr: GrammarExpr, visiting: List, ctx: Loo RefuseOnMatch { element: _, reason: _ } => Absent AfterLineBreak { element: _ } => Absent NotPredicate { element: _ } => Absent + InfixTable { operand: _, continuation_guard: _, operators: _ } => Absent } } @@ -3090,6 +2724,7 @@ fn lookahead_leading_guard_length(expr: GrammarExpr, ctx: LookaheadContext) -> I RefuseOnMatch { element: _, reason: _ } => 0 AfterLineBreak { element: _ } => 0 NotPredicate { element: _ } => 0 + InfixTable { operand: _, continuation_guard: _, operators: _ } => 0 } } @@ -3435,6 +3070,8 @@ fn compile_choice_plan( } } +// An operator table's rows become an index keyed by token class, so selecting the operator at a +// continuation is one lookup; no Choice is prepared and no choice-site residue can arise. fn prepare_grammar_expr( expr: GrammarExpr, nullable_set: List, @@ -3547,6 +3184,24 @@ fn prepare_grammar_expr( first: expr_first_fold_predicate(), residue: element_prepared.residue } + InfixTable { operand: operand, continuation_guard: guard, operators: operators } => + let operand_prepared = prepare_grammar_expr(expr: operand, nullable_set: nullable_set, first_rows: first_rows, productions: productions, production: production, token_dispatch: token_dispatch) + let guard_prepared = prepare_grammar_expr(expr: guard, nullable_set: nullable_set, first_rows: first_rows, productions: productions, production: production, token_dispatch: token_dispatch) + let first = expr_first_fold_infix(operand: operand_prepared.first, operators: operators) + PreparedExprFold { + tree: PreparedInfixTable { + operand: operand_prepared.tree, + continuation_guard: guard_prepared.tree, + operators: fold_list( + xs: operators, + empty: empty_map(), + cons: fn(acc, row) { map_insert(m: acc, key: grammar_infix_row_class(row: row), value: row) } + ), + first: first + }, + first: first, + residue: list_append(left: operand_prepared.residue, right: guard_prepared.residue) + } } } @@ -3565,7 +3220,7 @@ fn parse_table_token_dispatch(table: ParseTable, production: Symbol) -> Bool { // When the key is absent the Choice plan is ordered backtrack, the pre-cutover // interpreter. Analyzed tables always resolve Present here. fn parse_table_prepared_expr(table: ParseTable, production: GrammarProduction) -> PreparedGrammarExpr { - match v2.std.collection.map_get(m: table.prepared_exprs, key: production.name) { + match v2.std.collection.map_get_checked(m: table.prepared_exprs, key: production.name) { Accepted { value: Present { value: prepared }, diagnostics: _ } => prepared Accepted { value: Absent, diagnostics: _ } => prepare_grammar_expr( @@ -3621,84 +3276,8 @@ fn compute_grammar_first_analysis_given( nullable_set: nullable_set, first_rows: first_rows, prepared_exprs: prep.prepared_exprs, - grammar_digest: parse_grammar_digest(validated: ValidatedGrammarRoot { root: root }), productions_by_name: productions, - choice_residue: prep.residue, - memo_recurrence: parse_memo_recurrence_census(root: root, nullable_set: nullable_set) - } -} - -// THE PARSE MEMO'S DEMAND CASE, READ OFF THE GRAMMAR ONCE PER PREPARATION. The meaning of both -// counts, and why they are the whole of within-parse recurrence, is stated on -// v2.compiler.materialization_carriers ParseMemoRecurrence. This fold only reads the structure: -// a site is DISCARDING when the branch it may abandon -- a Choice's left, an Optional's or a -// Repeat's element -- demands a nonterminal, since only an abandoned nonterminal demand can be -// asked again at the position the parse resumes from. -type ParseDemandFold { - demands_nonterminal: Bool - discarding_sites: Int -} - -fn parse_demand_fold_site(attempted: ParseDemandFold) -> Int { - if attempted.demands_nonterminal { 1 } else { 0 } -} - -fn parse_demand_fold_leaf() -> ParseDemandFold { - ParseDemandFold { demands_nonterminal: false, discarding_sites: 0 } -} - -fn expr_parse_demand_fold(expr: GrammarExpr) -> ParseDemandFold { - fold_grammar_expr( - expr: expr, - algebra: GrammarExprFold { - terminal: fn(_, _) { parse_demand_fold_leaf() }, - literal_terminal: fn(_, _) { parse_demand_fold_leaf() }, - nonterminal: fn(_) { ParseDemandFold { demands_nonterminal: true, discarding_sites: 0 } }, - sequence: fn(l, r) { - ParseDemandFold { - demands_nonterminal: l.demands_nonterminal || r.demands_nonterminal, - discarding_sites: l.discarding_sites + r.discarding_sites - } - }, - choice: fn(l, r) { - ParseDemandFold { - demands_nonterminal: l.demands_nonterminal || r.demands_nonterminal, - discarding_sites: l.discarding_sites + r.discarding_sites + parse_demand_fold_site(attempted: l) - } - }, - optional: fn(e) { - ParseDemandFold { - demands_nonterminal: e.demands_nonterminal, - discarding_sites: e.discarding_sites + parse_demand_fold_site(attempted: e) - } - }, - repeat: fn(e) { - ParseDemandFold { - demands_nonterminal: e.demands_nonterminal, - discarding_sites: e.discarding_sites + parse_demand_fold_site(attempted: e) - } - }, - expect: fn(e, _reason) { e }, - refuse_on_match: fn(e, _reason) { e }, - after_line_break: fn(e) { e }, - not_predicate: fn(e) { - ParseDemandFold { - demands_nonterminal: e.demands_nonterminal, - discarding_sites: e.discarding_sites + parse_demand_fold_site(attempted: e) - } - } - } - ) -} - -fn parse_memo_recurrence_census(root: GrammarRoot, nullable_set: List) -> ParseMemoRecurrence { - ParseMemoRecurrence { - discarding_backtrack_sites: fold_list( - xs: root.productions, - empty: 0, - cons: fn(acc, p) { acc + expr_parse_demand_fold(expr: p.expression).discarding_sites } - ), - nullable_nonterminals: length(xs: nullable_set) + choice_residue: prep.residue } } @@ -3728,52 +3307,31 @@ fn grammar_choice_ambiguity_count(root: GrammarRoot) -> Int { length(xs: grammar_choice_ambiguity_roster(root: root)) } -fn parse_grammar_choice_ambiguity_diagnostic(at: Locus, count: Int) -> Diagnostic { +// A grammar choice whose alternatives overlap is refused AT THE PRODUCTION that owns the choice. +// There is no accepting arm: an ordered choice resolved by authoring order is a reading the grammar +// does not declare, so a grammar carrying one is not a grammar the parser may run. +fn parse_grammar_choice_ambiguity_row_diagnostic(row: GrammarChoiceAmbiguityRow) -> Diagnostic { Diagnostic { reason: ^parse_grammar_choice_ambiguity, - at: port_refusal_locus(at: at, port: ^parse_stage_locus_port, invariant: ^parse_grammar_choice_ambiguity), - correction: match count > 0 { - true => Unavailable { reason: ExternalContractUnknown } - false => Unavailable { reason: UserInputBoundary } - } + at: declaration_locus(declaration: Cons { head: row.production, tail: std.algebra.Empty }), + correction: Unavailable { reason: ExternalContractUnknown } } } -fn parse_grammar_choice_ambiguity_row_diagnostic(at: Locus, row: GrammarChoiceAmbiguityRow) -> Diagnostic { - let overlap_count = length(xs: row.overlap_terminals) - parse_grammar_choice_ambiguity_diagnostic( - at: at, - count: if row.both_nullable && overlap_count == 0 { 1 } else { overlap_count } - ) -} - -fn grammar_choice_ambiguity_row_diagnostic_list( - at: Locus, - roster: List -) -> List { - fold_list( - xs: roster, - empty: std.algebra.Empty, - cons: fn(acc, row) { - list_snoc_item( - xs: acc, - item: parse_grammar_choice_ambiguity_row_diagnostic(at: at, row: row) - ) - } - ) -} - fn grammar_choice_ambiguity_rejection_diagnostics( - at: Locus, - roster: List + first: GrammarChoiceAmbiguityRow, + rest: List ) -> NonEmptyDiagnostics { - parse_diags_to_non_empty( - diags: grammar_choice_ambiguity_row_diagnostic_list(at: at, roster: roster), - default: parse_grammar_choice_ambiguity_diagnostic( - at: at, - count: length(xs: roster) + NonEmptyDiagnostics { + head: parse_grammar_choice_ambiguity_row_diagnostic(row: first), + tail: fold_list( + xs: rest, + empty: std.algebra.Empty, + cons: fn(acc, row) { + list_snoc_item(xs: acc, item: parse_grammar_choice_ambiguity_row_diagnostic(row: row)) + } ) - ) + } } fn grammar_empty_corner_list() -> List { @@ -3841,7 +3399,8 @@ fn expr_corner_nullable_fold(expr: GrammarExpr, nullable_set: List) -> G expect: fn(element, _reason) { element }, refuse_on_match: fn(element, _reason) { element }, after_line_break: fn(element) { element }, - not_predicate: expr_corner_fold_optional + not_predicate: expr_corner_fold_optional, + infix_table: fn(operand, _guard, _operators) { operand } } ) } @@ -3892,7 +3451,7 @@ fn left_corner_direct_rows(root: GrammarRoot, nullable_set: List) -> Map } fn left_corner_row(rows: Map>, name: Symbol) -> List { - match v2.std.collection.map_get(m: rows, key: name) { + match v2.std.collection.map_get_checked(m: rows, key: name) { Accepted { value: Present { value: reached }, diagnostics: _ } => reached Accepted { value: Absent, diagnostics: _ } => std.algebra.Empty Rejected { diagnostics: _ } => std.algebra.Empty @@ -4008,7 +3567,8 @@ fn expr_has_undefined_nonterminal(expr: GrammarExpr, root: GrammarRoot) -> Bool expect: fn(element_undef, _reason) { element_undef }, refuse_on_match: fn(element_undef, _reason) { element_undef }, after_line_break: fn(element_undef) { element_undef }, - not_predicate: fn(element_undef) { element_undef } + not_predicate: fn(element_undef) { element_undef }, + infix_table: fn(operand_undef, guard_undef, _operators) { operand_undef || guard_undef } } ) } @@ -4024,6 +3584,37 @@ fn grammar_has_undefined_nonterminals(root: GrammarRoot) -> Bool { } } +// AN OPERATOR TABLE NAMES EACH TOKEN CLASS ONCE (v2.std.grammar InfixTable). Its rows are an index +// keyed by class, so a class named twice would be two answers for one lookup -- the overlap an +// ordered choice would carry, here refused where the grammar is validated rather than resolved by +// row order. With every class distinct the rows are disjoint by construction, and the table holds no +// Choice for the overlap roster to inspect. +fn grammar_infix_tables_well_formed(expr: GrammarExpr) -> Bool { + fold_grammar_expr( + expr: expr, + algebra: GrammarExprFold { + terminal: fn(_, _) { true }, + literal_terminal: fn(_, _) { true }, + nonterminal: fn(_) { true }, + sequence: fn(l, r) { l && r }, + choice: fn(l, r) { l && r }, + optional: fn(e) { e }, + repeat: fn(e) { e }, + expect: fn(e, _reason) { e }, + refuse_on_match: fn(e, _reason) { e }, + after_line_break: fn(e) { e }, + not_predicate: fn(e) { e }, + infix_table: fn(operand, guard, operators) { + operand && guard && unique(xs: operators, eq: fn(a, b) { grammar_infix_row_class(row: a) == grammar_infix_row_class(row: b) }) + } + } + ) +} + +fn grammar_has_malformed_infix_table(root: GrammarRoot) -> Bool { + fold_list(xs: root.productions, empty: false, cons: fn(acc, p) { acc || !grammar_infix_tables_well_formed(expr: p.expression) }) +} + fn grammar_has_duplicate_productions(root: GrammarRoot) -> Bool { match unique(xs: root.productions, eq: fn(a, b) { a.name == b.name }) { true => false @@ -4055,40 +3646,6 @@ fn parse_diags_to_non_empty(diags: List, default: Diagnostic) -> Non } } -fn grammar_nullable_ambiguity_rows(roster: List) -> List { - filter(xs: roster, predicate: fn(row) { row.both_nullable }) -} - -fn parse_grammar_choice_overlap_residue_diagnostic(at: Locus, row: GrammarChoiceAmbiguityRow) -> Diagnostic { - parse_stage_diagnostic_core( - d: parse_overlap_residue_stage_diagnostic_at(at: at, row: row) - ) -} - -fn diagnostics_from_diagnostic_list(items: List) -> Diagnostics { - match list_at_optional(xs: items, index: 0) { - Absent => None - Present { value: first } => - match list_tail(xs: items) { - TailAbsent => Some { diagnostics: NonEmptyDiagnostics { head: first, tail: std.algebra.Empty } } - TailFound { tail: rest } => - Some { diagnostics: NonEmptyDiagnostics { head: first, tail: rest } } - } - } -} - -fn grammar_overlap_residue_diagnostics(at: Locus, roster: List) -> Diagnostics { - diagnostics_from_diagnostic_list( - items: fold_list( - xs: roster, - empty: std.algebra.Empty, - cons: fn(acc, row) { - list_snoc_item(xs: acc, item: parse_grammar_choice_overlap_residue_diagnostic(at: at, row: row)) - } - ) - ) -} - // ONE nullable fixpoint for the whole preparation. The left-recursion check and the FIRST analysis // both need the nullable set, and computing it here rather than inside each of them is the // difference between one fixpoint per preparation and three. @@ -4098,30 +3655,25 @@ fn grammar_validate_and_analyze(root: GrammarRoot, at: Locus) -> Outcome + Rejected { diagnostics: grammar_choice_ambiguity_rejection_diagnostics(first: first, rest: rest) } + Empty => + Accepted { + value: PreparedModeledGrammar { + validated: ValidatedGrammarRoot { root: root }, + analysis: analysis + }, + diagnostics: None + } } } } @@ -4131,8 +3683,8 @@ fn grammar_validate_and_analyze(root: GrammarRoot, at: Locus) -> Outcome Outcome { match grammar_validate_and_analyze(root: root, at: at) { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: modeled, diagnostics: residue } => - Accepted { value: modeled.validated, diagnostics: residue } + Accepted { value: modeled, diagnostics: _ } => + Accepted { value: modeled.validated, diagnostics: None } } } @@ -4605,10 +4157,9 @@ fn parse_expr_optional( } } -// PROGRESS IS READ OFF THE MEMO KEY'S POSITION, not off two list lengths. `rest` is a suffix of +// PROGRESS IS READ OFF THE STREAM POSITION, not off two list lengths. `rest` is a suffix of // `acc.remaining`, so the element consumed a token exactly when the position changed, and -// parse_current_position is already the authority that identifies a stream position (it keys the -// memo, ParsePositionKey). Comparing lengths walked both lists on every accepted iteration -- O(n) +// parse_current_position is the authority that identifies a stream position. Comparing lengths walked both lists on every accepted iteration -- O(n) // per iteration, O(n^2) over a long repeat -- to answer a question the head token answers. A // position identifies one token because the parse admits only strictly increasing starts // (parse_token_start_order_violation). @@ -4751,8 +4302,8 @@ fn parse_expr_refuse_on_match( // attempted at this position; where it fails the predicate accepts with the input, provenance and // capture it was given (the empty capture), and where it matches the predicate rejects. Either way // the attempt was speculative, so the furthest-failure record is returned to what it was before the -// attempt: a probe reaching past this position is not the place the parse stopped. The memo the -// attempt filled is kept, and a refusal it raised is kept, since a refusal stops the parse. +// attempt: a probe reaching past this position is not the place the parse stopped. A refusal it +// raised is kept, since a refusal stops the parse. fn parse_expr_not_predicate( element: PreparedGrammarExpr, first: GrammarExprFirstFold, @@ -4929,7 +4480,7 @@ fn parse_expr_dispatch( } } PreparedNonterminal { production: prod_name } => - parse_nonterminal_memoized( + parse_nonterminal( all_tokens: all_tokens, tokens: tokens, root: root, @@ -5013,6 +4564,171 @@ fn parse_expr_dispatch( table: table, prov: prov ) + PreparedInfixTable { operand: operand, continuation_guard: guard, operators: operators, first: _ } => + parse_expr_infix_climb( + table_expr: PreparedInfixClimb { operand: operand, continuation_guard: guard, operators: operators }, + min_power: 0, + tokens: tokens, + all_tokens: all_tokens, + root: root, + table: table, + prov: prov + ) + } +} + +// PRECEDENCE CLIMBING (v2.std.grammar InfixTable). One operand is parsed; then, at each continuation, +// the guard runs, the next token's class is LOOKED UP among the rows, and an operator whose binding +// power reaches `min_power` is consumed and its right operand climbed at the power the row's +// associativity demands. An operand with no operator after it returns as itself: nothing is attempted +// or stamped for the precedence levels it does not use. Where an operator was consumed and its right +// operand fails, the continuation is abandoned BEFORE the operator, with the input, capture and +// provenance it had, exactly as a Repeat abandons an element that fails. +type PreparedInfixClimb { + operand: PreparedGrammarExpr + continuation_guard: PreparedGrammarExpr + operators: Map +} + +fn parse_expr_infix_climb( + table_expr: PreparedInfixClimb, + min_power: Int, + tokens: List, + all_tokens: List, + root: GrammarRoot, + table: ParseTable, + prov: ParseProvenanceState +) -> ParseExprResult { + match parse_expr_with_first(expr: table_expr.operand, tokens: tokens, all_tokens: all_tokens, root: root, table: table, prov: prov) { + ParseExprRejected { diagnostics: diags, table: table1 } => ParseExprRejected { diagnostics: diags, table: table1 } + ParseExprAccepted { remaining: rest, captured: lhs, diagnostics: diags, prov: prov1, table: table1 } => + parse_expr_infix_continue( + table_expr: table_expr, + min_power: min_power, + chained_power: optional_absent(), + lhs: lhs, + diagnostics: diags, + tokens: rest, + all_tokens: all_tokens, + root: root, + table: table1, + prov: prov1 + ) + } +} + +// `chained_power` is the binding power of a NON-ASSOCIATIVE operator just applied at this level: a +// second operator of that power refuses with NonAssociativeOperatorChain at the second operator. +fn parse_expr_infix_continue( + table_expr: PreparedInfixClimb, + min_power: Int, + chained_power: Optional, + lhs: Node, + diagnostics: List, + tokens: List, + all_tokens: List, + root: GrammarRoot, + table: ParseTable, + prov: ParseProvenanceState +) -> ParseExprResult { + match parse_expr_with_first(expr: table_expr.continuation_guard, tokens: tokens, all_tokens: all_tokens, root: root, table: table, prov: prov) { + ParseExprRejected { diagnostics: _, table: guarded } => + ParseExprAccepted { remaining: tokens, captured: lhs, diagnostics: diagnostics, prov: prov, table: guarded } + ParseExprAccepted { remaining: _, captured: _, diagnostics: _, prov: _, table: guarded } => + match parse_infix_row_at(operators: table_expr.operators, tokens: tokens) { + Absent => ParseExprAccepted { remaining: tokens, captured: lhs, diagnostics: diagnostics, prov: prov, table: guarded } + Present { value: row } => + if grammar_infix_row_binding_power(row: row) < min_power { + ParseExprAccepted { remaining: tokens, captured: lhs, diagnostics: diagnostics, prov: prov, table: guarded } + } else if parse_infix_chains(chained_power: chained_power, row: row) { + ParseExprRejected { + diagnostics: [], + table: parse_table_join_refusal( + table: guarded, + refusal: optional_present(value: parse_refusal_at(reason: NonAssociativeOperatorChain, tokens: tokens, all_tokens: all_tokens)) + ) + } + } else { + parse_expr_infix_apply( + table_expr: table_expr, + min_power: min_power, + row: row, + lhs: lhs, + diagnostics: diagnostics, + tokens: tokens, + all_tokens: all_tokens, + root: root, + table: guarded, + prov: prov + ) + } + } + } +} + +fn parse_infix_row_at(operators: Map, tokens: List) -> Optional { + match parse_token_first(tokens: tokens) { + Absent => optional_absent() + Present { value: tok } => map_lookup(m: operators, key: tok.class) + } +} + +fn parse_infix_chains(chained_power: Optional, row: InfixOperatorRow) -> Bool { + match chained_power { + Absent => false + Present { value: power } => power == grammar_infix_row_binding_power(row: row) + } +} + +fn parse_infix_chained_after(row: InfixOperatorRow) -> Optional { + match row.associativity { + InfixLeft => optional_absent() + InfixNonAssociative => optional_present(value: grammar_infix_row_binding_power(row: row)) + } +} + +fn parse_expr_infix_apply( + table_expr: PreparedInfixClimb, + min_power: Int, + row: InfixOperatorRow, + lhs: Node, + diagnostics: List, + tokens: List, + all_tokens: List, + root: GrammarRoot, + table: ParseTable, + prov: ParseProvenanceState +) -> ParseExprResult { + match parse_expr_terminal(token_class: grammar_infix_row_class(row: row), stamp: StampClass, tokens: tokens, table: table, prov: prov) { + ParseExprRejected { diagnostics: _, table: table1 } => + ParseExprAccepted { remaining: tokens, captured: lhs, diagnostics: diagnostics, prov: prov, table: table1 } + ParseExprAccepted { remaining: after_operator, captured: operator, diagnostics: _, prov: prov1, table: table1 } => + match parse_expr_infix_climb( + table_expr: table_expr, + min_power: grammar_infix_right_min_power(row: row), + tokens: after_operator, + all_tokens: all_tokens, + root: root, + table: table1, + prov: prov1 + ) { + ParseExprRejected { diagnostics: _, table: table2 } => + ParseExprAccepted { remaining: tokens, captured: lhs, diagnostics: diagnostics, prov: prov, table: table2 } + ParseExprAccepted { remaining: rest, captured: rhs, diagnostics: rhs_diags, prov: prov2, table: table2 } => + let stamped = parse_stamp_infix_application(left: lhs, operator: operator, right: rhs, prov: prov2) + parse_expr_infix_continue( + table_expr: table_expr, + min_power: min_power, + chained_power: parse_infix_chained_after(row: row), + lhs: stamped.node, + diagnostics: list_append(left: diagnostics, right: rhs_diags), + tokens: rest, + all_tokens: all_tokens, + root: root, + table: table2, + prov: stamped.prov + ) + } } } @@ -5044,91 +4760,36 @@ fn parse_expr( ) } -fn parse_production_memo_stats( - tokens: List, - validated: ValidatedGrammarRoot, - production_name: Symbol, - materialization: Materialization -) -> ParseMemoStats { - let root = validated.root - match grammar_lookup_production(root: root, name: production_name) { - Violates { diagnostic: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Holds { value: production } => - let prov = parse_prov_initial() - let table = parse_table_for_production( - validated: validated, - all_tokens: tokens, - materialization: materialization - ) - match parse_expr( - expr: production.expression, - tokens: tokens, - all_tokens: tokens, - root: root, - table: table, - prov: prov - ) { - ParseExprAccepted { remaining: _, captured: _, diagnostics: _, prov: _, table: final_table } => - parse_table_memo_stats(table: final_table) - ParseExprRejected { diagnostics: _, table: final_table } => - parse_table_memo_stats(table: final_table) - } - } -} - fn parse_production_prepared( tokens: List, validated: ValidatedGrammarRoot, analysis: GrammarFirstAnalysis, - production_name: Symbol, - materialization: Materialization + production_name: Symbol ) -> Outcome { - parse_production_prepared_measured( - tokens: tokens, - validated: validated, - analysis: analysis, - production_name: production_name, - materialization: materialization - ).outcome -} - -fn parse_production_prepared_measured( - tokens: List, - validated: ValidatedGrammarRoot, - analysis: GrammarFirstAnalysis, - production_name: Symbol, - materialization: Materialization -) -> ParseProductionMeasured { - parse_production_prepared_measured_seeded( + parse_production_prepared_seeded( tokens: tokens, validated: validated, analysis: analysis, production_name: production_name, seed: occurrence_id_allocator_initial(), - layout: optional_absent(), - materialization: materialization + layout: optional_absent() ) } -fn parse_production_prepared_measured_seeded( +fn parse_production_prepared_seeded( tokens: List, validated: ValidatedGrammarRoot, analysis: GrammarFirstAnalysis, production_name: Symbol, seed: OccurrenceIdAllocator, - layout: Optional, - materialization: Materialization -) -> ParseProductionMeasured { + layout: Optional +) -> Outcome { match parse_token_start_order_violation(tokens: tokens) { Present { value: tok } => - ParseProductionMeasured { - outcome: Rejected { - diagnostics: diagnostics_singleton( - d: parse_refusal_reason_diagnostic(reason: TokenStartsNotIncreasing, file: tok.file, start: tok.start, end: tok.end) - ) - }, - memo: parse_memo_stats_empty() + Rejected { + diagnostics: diagnostics_singleton( + d: parse_refusal_reason_diagnostic(reason: TokenStartsNotIncreasing, file: tok.file, start: tok.start, end: tok.end) + ) } Absent => parse_production_prepared_ordered( @@ -5137,17 +4798,16 @@ fn parse_production_prepared_measured_seeded( analysis: analysis, production_name: production_name, seed: seed, - layout: layout, - materialization: materialization + layout: layout ) } } // A STREAM POSITION IS ITS TOKEN'S START, so the parse admits only a stream whose starts strictly -// increase (v2.std.compilers.lexing Token). parse_current_position keys the memo by that start and -// decides repeat progress by it; two tokens at one start would be one position, and the memo would -// serve one's answer for the other with no diagnostic. The order is checked once here, at the -// parse's entry, and a stream that violates it refuses located at the first offending token. +// increase (v2.std.compilers.lexing Token). parse_current_position decides repeat progress by that +// start; two tokens at one start would be one position, and a repeat could stop or continue on the +// wrong one with no diagnostic. The order is checked once here, at the parse's entry, and a stream +// that violates it refuses located at the first offending token. fn parse_token_start_order_violation(tokens: List) -> Optional { fold_list( xs: tokens, @@ -5178,7 +4838,7 @@ type ParseStartOrderAcc { // THE ENTRY PRODUCTION IS READ OFF THE PREPARED GRAMMAR, never re-derived per parse: its lookup is // the analysis's productions_by_name index and its prepared tree (choice plans, FIRST unions) is the // analysis's prepared_exprs row, exactly as every nested nonterminal already reads it -// (parse_nonterminal_memoized). Routing the entry through parse_expr re-ran prepare_grammar_expr -- +// (parse_nonterminal). Routing the entry through parse_expr re-ran prepare_grammar_expr -- // choice-plan compilation plus the authored-choice overlap residue, which the parse then discarded -- // and an O(P) non-short-circuiting fold over every production, on EVERY parse, re-deriving a // property of the grammar that prepare_grammar already computed once. @@ -5188,16 +4848,14 @@ fn parse_production_prepared_ordered( analysis: GrammarFirstAnalysis, production_name: Symbol, seed: OccurrenceIdAllocator, - layout: Optional, - materialization: Materialization -) -> ParseProductionMeasured { + layout: Optional +) -> Outcome { let root = validated.root let table = parse_table_for_stream_analyzed( validated: validated, analysis: analysis, layout: layout, - all_tokens: tokens, - materialization: materialization + all_tokens: tokens ) match grammar_lookup_production_indexed(table: table, name: production_name) { Holds { value: production } => @@ -5212,17 +4870,11 @@ fn parse_production_prepared_ordered( ) match parse_expr_result_table(result: parsed).refusal { Present { value: refusal } => - ParseProductionMeasured { - outcome: Rejected { diagnostics: diagnostics_singleton(d: refusal) }, - memo: parse_table_memo_stats(table: parse_expr_result_table(result: parsed)) - } - Absent => parse_production_measured_unrefused(parsed: parsed, production: production, root: root) + Rejected { diagnostics: diagnostics_singleton(d: refusal) } + Absent => parse_production_unrefused(parsed: parsed, production: production, root: root) } Violates { diagnostic: d } => - ParseProductionMeasured { - outcome: Rejected { diagnostics: diagnostics_singleton(d: d) }, - memo: parse_memo_stats_empty() - } + Rejected { diagnostics: diagnostics_singleton(d: d) } } } @@ -5230,11 +4882,11 @@ fn parse_production_prepared_ordered( // happened to parse around it: RefuseOnMatch names source the seed refuses although another reading // succeeds, so an Accepted here would be exactly the success it exists to deny. Only an unrefused // parse reaches the ordinary accept / leftover-token / no-match reading below. -fn parse_production_measured_unrefused( +fn parse_production_unrefused( parsed: ParseExprResult, production: GrammarProduction, root: GrammarRoot -) -> ParseProductionMeasured { +) -> Outcome { match parsed { ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: diags, prov: inner_prov, table: final_table } => if is_empty(xs: remaining) { @@ -5243,34 +4895,25 @@ fn parse_production_measured_unrefused( captured: captured, prov: inner_prov ) - ParseProductionMeasured { - outcome: Accepted { - value: ParseArtifact { tree: stamped.node, span_index: stamped.prov.index, alloc: stamped.prov.alloc }, - diagnostics: parse_diags_to_diagnostics(diags: diags) - }, - memo: parse_table_memo_stats(table: final_table) + Accepted { + value: ParseArtifact { tree: stamped.node, span_index: stamped.prov.index, alloc: stamped.prov.alloc }, + diagnostics: parse_diags_to_diagnostics(diags: diags) } } else { - ParseProductionMeasured { - outcome: Rejected { - diagnostics: diagnostics_singleton( - d: parse_g0_tokens_remain_diagnostic_at(tokens: remaining, furthest: final_table.furthest) - ) - }, - memo: parse_table_memo_stats(table: final_table) + Rejected { + diagnostics: diagnostics_singleton( + d: parse_g0_tokens_remain_diagnostic_at(tokens: remaining, furthest: final_table.furthest) + ) } } - ParseExprRejected { diagnostics: diags, table: final_table } => - ParseProductionMeasured { - outcome: Rejected { - diagnostics: parse_diags_to_non_empty( - diags: diags, - default: parse_e1_syntax_error_diagnostic( - at: node_locus(node: grammar_to_node(grammar: ModeledGrammar { root: root })) - ) + ParseExprRejected { diagnostics: diags, table: _ } => + Rejected { + diagnostics: parse_diags_to_non_empty( + diags: diags, + default: parse_e1_syntax_error_diagnostic( + at: node_locus(node: grammar_to_node(grammar: ModeledGrammar { root: root })) ) - }, - memo: parse_table_memo_stats(table: final_table) + ) } } } @@ -5280,18 +4923,12 @@ fn parse_production( validated: ValidatedGrammarRoot, production_name: Symbol ) -> Outcome { - let analysis = compute_grammar_first_analysis(root: validated.root) - match parse_table_memo_materialization_for(r: analysis.memo_recurrence) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: materialization, diagnostics: _ } => - parse_production_prepared( - tokens: tokens, - validated: validated, - analysis: analysis, - production_name: production_name, - materialization: materialization - ) - } + parse_production_prepared( + tokens: tokens, + validated: validated, + analysis: compute_grammar_first_analysis(root: validated.root), + production_name: production_name + ) } fn parse_production_tree( @@ -5310,33 +4947,10 @@ fn parse_production_tree( Rejected { diagnostics: r } } } -// WHERE THE PARSE-TABLE MATERIALIZATION IS ADMITTED, and the only place it is. PreparedGrammar -// is already the once-per-grammar carrier and the admission is a once-per-grammar fact, so the -// join belongs here: a refused admission REFUSES THE PREPARE, which is how the unmet obligation -// reaches the caller as a located diagnostic instead of as a quiet recompute. -// -// THE PARAMETER IS THE DOOR, NOT ITS ANSWER, and that is forced by the derivation rather than -// chosen. The parse demand's site is read off the grammar's own recurrence census, so the -// admission is a FUNCTION of the analysis and cannot be evaluated before the analysis exists -- -// an already-computed Outcome passed in here would have to come from some other grammar. Passing -// the door keeps the control's grip: a control supplies a door built on a refusing verdict list -// and drives THIS body, the one parse_module runs, rather than a copy of it. -fn prepare_grammar(grammar: ParseGrammar) -> Outcome { - prepare_grammar_with_admission( - admit: fn(r) { parse_table_memo_materialization_for(r: r) }, - grammar: grammar - ) -} -// A VOID GRAMMAR CONSULTS NO MEMO, so it does not consult the admission either. Refusing a void -// parse because the parse-table obligation is unmet would forbid more than the obligation covers -// (DESIGN section 4d): no table is built on that arm, so there is nothing for a provider to -// discharge. The admission is demanded exactly where the table is, on the modeled arm -- and -// there, only once the analysis has produced the census the door is asked about. -fn prepare_grammar_with_admission( - admit: fn(ParseMemoRecurrence) -> Outcome, - grammar: ParseGrammar -) -> Outcome { +// PreparedGrammar is the once-per-grammar carrier: well-formedness, validation and the FIRST +// analysis run here once, and every parse of every module reads the result. +fn prepare_grammar(grammar: ParseGrammar) -> Outcome { let grammar_node = grammar_to_node(grammar: grammar) let at = node_locus(node: grammar_node) if well_formed(n: grammar_node) { @@ -5346,14 +4960,10 @@ fn prepare_grammar_with_admission( ModeledGrammar { root: root } => match grammar_validate_and_analyze(root: root, at: at) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: modeled, diagnostics: validation_residue } => - match admit(modeled.analysis.memo_recurrence) { - Rejected { diagnostics: refusal } => Rejected { diagnostics: refusal } - Accepted { value: materialization, diagnostics: _ } => - Accepted { - value: PreparedModeled { modeled: modeled, materialization: materialization }, - diagnostics: validation_residue - } + Accepted { value: modeled, diagnostics: _ } => + Accepted { + value: PreparedModeled { modeled: modeled }, + diagnostics: None } } } @@ -5362,61 +4972,14 @@ fn prepare_grammar_with_admission( } } -fn overlap_residue_stage_from_prepared(prepared: PreparedGrammar) -> Optional { - match prepared { - PreparedVoidGrammar => optional_absent() - PreparedModeled { modeled: modeled, materialization: _ } => modeled.overlap_residue_stage - } -} - -fn overlap_residue_stage_from_parse_grammar(grammar: ParseGrammar) -> Optional { - match prepare_grammar(grammar: grammar) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: prepared, diagnostics: _ } => - overlap_residue_stage_from_prepared(prepared: prepared) - } -} - -// The unmeasured door, retained for every caller that does not own a clock or a relay, and defined -// as the projection of the measured fold rather than restated beside it. +// The unseeded door: one file, its own isolated occurrence scope. fn parse_module_prepared( tokens: TokenStream, - prepared: PreparedGrammar, - validation_residue: Diagnostics + prepared: PreparedGrammar ) -> Outcome { - parse_module_prepared_measured( + parse_module_prepared_seeded( tokens: tokens, prepared: prepared, - validation_residue: validation_residue - ).outcome -} - -fn parse_module_prepared_seeded( - tokens: TokenStream, - prepared: PreparedGrammar, - validation_residue: Diagnostics, - seed: OccurrenceIdAllocator -) -> Outcome { - parse_module_prepared_measured_seeded( - tokens: tokens, - prepared: prepared, - validation_residue: validation_residue, - seed: seed - ).outcome -} - -// A VOID GRAMMAR CONSULTS NO MEMO, so its accounting is empty rather than absent: the counters are -// zero because nothing was looked up, which is an answer. Reporting no stats at all would make a -// void parse indistinguishable from a parse whose stats were dropped. -fn parse_module_prepared_measured( - tokens: TokenStream, - prepared: PreparedGrammar, - validation_residue: Diagnostics -) -> ParseProductionMeasured { - parse_module_prepared_measured_seeded( - tokens: tokens, - prepared: prepared, - validation_residue: validation_residue, seed: occurrence_id_allocator_initial() ) } @@ -5425,70 +4988,44 @@ fn parse_module_prepared_measured( // the same source graph left behind, so this file's occurrence ids continue where that one stopped // and no two files in one ingest mint the same id. The returned artifact carries the mark this // parse reached, which is what lets the assembly fold thread it to the next read. The unseeded -// projection above is every other caller: one file, its own isolated scope, byte-identical to the -// fold it was before this parameter existed. -fn parse_module_prepared_measured_seeded( +// projection above is every other caller: one file, its own isolated scope. +fn parse_module_prepared_seeded( tokens: TokenStream, prepared: PreparedGrammar, - validation_residue: Diagnostics, seed: OccurrenceIdAllocator -) -> ParseProductionMeasured { +) -> Outcome { let ntok = token_stream_remaining_count(stream: tokens) match prepared { PreparedVoidGrammar => if ntok == 0 { - ParseProductionMeasured { - outcome: Accepted { - value: ParseArtifact { tree: grammar_to_node(grammar: VoidGrammar), span_index: span_index_empty(), alloc: seed }, - diagnostics: validation_residue - }, - memo: parse_memo_stats_empty() + Accepted { + value: ParseArtifact { tree: grammar_to_node(grammar: VoidGrammar), span_index: span_index_empty(), alloc: seed }, + diagnostics: None } } else { - ParseProductionMeasured { - outcome: Rejected { - diagnostics: diagnostics_singleton( - d: parse_g0_tokens_remain_diagnostic(tokens: token_stream_remaining(stream: tokens)) - ) - }, - memo: parse_memo_stats_empty() + Rejected { + diagnostics: diagnostics_singleton( + d: parse_g0_tokens_remain_diagnostic(tokens: token_stream_remaining(stream: tokens)) + ) } } - PreparedModeled { modeled: modeled, materialization: materialization } => - let measured = parse_production_prepared_measured_seeded( + PreparedModeled { modeled: modeled } => + parse_production_prepared_seeded( tokens: token_stream_remaining(stream: tokens), validated: modeled.validated, analysis: modeled.analysis, production_name: modeled.validated.root.start, seed: seed, - layout: token_stream_layout(stream: tokens), - materialization: materialization + layout: token_stream_layout(stream: tokens) ) - match measured.outcome { - Accepted { value: artifact, diagnostics: parse_diags } => - ParseProductionMeasured { - outcome: Accepted { - value: artifact, - diagnostics: diagnostics_merge(outer: validation_residue, inner: parse_diags) - }, - memo: measured.memo - } - Rejected { diagnostics: parse_rejected } => - ParseProductionMeasured { - outcome: Rejected { - diagnostics: rejected_with_pending(pending: validation_residue, rejected: parse_rejected) - }, - memo: measured.memo - } - } } } fn parse_module(tokens: TokenStream, grammar: ParseGrammar) -> Outcome { match prepare_grammar(grammar: grammar) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: prepared, diagnostics: validation_residue } => - parse_module_prepared(tokens: tokens, prepared: prepared, validation_residue: validation_residue) + Accepted { value: prepared, diagnostics: _ } => + parse_module_prepared(tokens: tokens, prepared: prepared) } } diff --git a/src/v2/compiler/03_ingest.dag b/src/v2/compiler/03_ingest.dag index 070cf7ea6e3..68fd2df9a66 100644 --- a/src/v2/compiler/03_ingest.dag +++ b/src/v2/compiler/03_ingest.dag @@ -10,7 +10,7 @@ import v2.std.collection { Map, Present, empty_map, - map_get, + map_get_checked, map_insert } import v2.std.grammar { @@ -38,6 +38,7 @@ import v2.compiler.translate { coerce_grounded_node } import v2.compiler.target_serialize { target_bundle_child } import v2.std.inhabitant_neutralization { neutralize_core_for_target } import v2.std.node { Edge, Authored, StructuralLabel, Node, Positional, Symbol, TypeNode, Conj } +import v2.std.member_declaration_kind { no_member_declaration_kinds } import v2.std.diagnostic { Accepted, Diagnostic, @@ -69,7 +70,7 @@ fn parse_atom_frontier_class_direct( token_classes: Map ) -> Outcome { bind_outcome( - o: map_get(token_classes, atom_identity), + o: map_get_checked(token_classes, atom_identity), f: fn(direct) { match direct { Present { value: token_class } => outcome_accepted(value: token_class) @@ -86,7 +87,7 @@ fn parse_atom_frontier_class( token_classes: Map ) -> Outcome { bind_outcome( - o: map_get(binding_to_class, atom_identity), + o: map_get_checked(binding_to_class, atom_identity), f: fn(bound) { match bound { Present { value: token_class } => outcome_accepted(value: token_class) @@ -337,7 +338,7 @@ fn parse_tree_to_target_model_bridge(parse_tree: ParseTree, source_model: Target o: parse_tree_to_emitted_node(parse_tree: parse_tree, source_model: source_model), f: fn(emitted) { bind_outcome( - o: infer_and_discharge(tree: ResolvedTree { root: emitted, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }), + o: infer_and_discharge(tree: ResolvedTree { root: emitted, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [], member_kinds: no_member_declaration_kinds() }), f: fn(inferred) { bind_outcome( o: coerce_grounded_node(source: emitted, tree: inferred, target: source_model), @@ -362,7 +363,7 @@ fn cross_language_compile( o: neutralize_core_for_target(core: core, target: target_model), f: fn(neutralized) { bind_outcome( - o: infer_and_discharge(tree: ResolvedTree { root: neutralized, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }), + o: infer_and_discharge(tree: ResolvedTree { root: neutralized, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [], member_kinds: no_member_declaration_kinds() }), f: fn(inferred) { emit(tree: inferred, target: target_model) } diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 04c93a6c8ca..8d7eea51705 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -2,6 +2,7 @@ module v2.compiler.name_resolve import std.types { List } import v2.std.language_model { LanguageModel } +import v2.std.member_declaration_kind { MemberKindChannel, no_member_declaration_kinds } import v2.compiler.symbol_index_fill { symbol_index_fill_module_declarations, symbol_index_fill_module_roots } import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_binding_sources } @@ -16,6 +17,7 @@ import v2.compiler.resolve { ResolveNodeWalk, ResolveWalkRefused, ClosureProviderRefusal, + member_kind_channel_of_tree, ResolvedTree, resolved_tree_outcome, resolve_walk_prefix_pending, @@ -38,7 +40,7 @@ import v2.compiler.namespace_graft { import v2.compiler.parse { parse_tree_projection_edge } import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.algebra { fold_list } -import v2.std.collection { Absent, Map, Present, PointwisePower, empty_map, map_get, map_lookup, map_insert, optional_absent, optional_present } +import v2.std.collection { Absent, Map, Present, PointwisePower, empty_map, map_get_checked, map_lookup, map_insert, optional_absent, optional_present } import std.optional { Optional } import v2.std.diagnostic { Accepted, @@ -288,7 +290,7 @@ fn admit_export_symbol( AdmissionRejected { diagnostics: d } AdmissionAccepted { bindings: b, origins: os } => - match v2.std.collection.map_get(os, sym) { + match v2.std.collection.map_get_checked(os, sym) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: _ } => AdmissionRejected { @@ -695,6 +697,16 @@ fn subject_module_root_for_name( } } +// The subject root's member-kind channel, read from the validated roots by the subject's own name. A subject +// with no root there has no provenance to give, which is Unavailable and not a refusal here: the module-not-found +// refusal already belongs to admission. +fn subject_member_kind_channel(roots: ValidatedModuleRoots, name: QualifiedName) -> Outcome { + match map_lookup(m: roots.by_name, key: name) { + Present { value: root } => member_kind_channel_of_tree(tree: root) + Absent => Accepted { value: no_member_declaration_kinds(), diagnostics: v2.std.diagnostic.None } + } +} + fn namespace_admission_initial_state( policy: NameResolutionPolicy, canonical_symbols: PointwisePower, @@ -936,12 +948,17 @@ fn resolve_in_context(context: Outcome, admission: Admission) Accepted { value: shared, diagnostics: d } => let closure = closure_declarations_demand(shared: shared) ContextResolved { - resolved: resolved_tree_outcome( - w: walked.walk, - symbol_index: shared.symbol_index, - closure_declarations: closure.declarations, - unavailable_providers: closure.refused - ), + resolved: match subject_member_kind_channel(roots: shared.roots, name: admission.subject.name) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: channel, diagnostics: _ } => + resolved_tree_outcome( + w: walked.walk, + symbol_index: shared.symbol_index, + closure_declarations: closure.declarations, + unavailable_providers: closure.refused, + member_kinds: channel + ) + }, context: Accepted { value: closure.context, diagnostics: d } } } diff --git a/src/v2/compiler/03_normalize.dag b/src/v2/compiler/03_normalize.dag index 6f596597c4d..970b46b85aa 100644 --- a/src/v2/compiler/03_normalize.dag +++ b/src/v2/compiler/03_normalize.dag @@ -45,6 +45,7 @@ import v2.std.compilers.sugar { } import v2.compiler.body_lowering_fold { BodyLowering, + body_term_of, BodyLoweredTree, WrapperRetention, body_lowering_after_pending, @@ -75,15 +76,24 @@ import v2.compiler.normalized_tree { NormalizedTree, admit_census_tree, admit_normalized_tree, - admit_retention_free + admit_retention_free, + normalized_tree_with_member_declarations } +import v2.std.member_declaration_kind { DeclaredDataMember, DeclaredFnMember, DeclaredMember } import v2.compiler.test_marker_capture { test_marker_capture } import v2.compiler.namespace_graft { module_header_containment_graft, - namespace_graft_is_graft_candidate + namespace_graft_collect_unit_nodes_against, + namespace_graft_is_graft_candidate, + namespace_graft_top_level_item_alternative_emitted_ids } import v2.extdeps.languages.dag { + dag_grammar_root, + dag_surface_kw_then_ident_from_captured, + alias_binding_row_from_decl_node, import_binding_rows_from_decl_node, + namespace_graft_unwrap_module_working_root, + parse_production_captured_child_optional, parse_production_emitted_identity_optional } import std.types { List } @@ -315,12 +325,42 @@ fn normalize_module_graft_finish(folded: Node, cd: Diagnostics) -> Outcome } } +// A LEAF HAS NO INTERIOR (MQ-5). A literal token the parse minted with its payload already attached -- +// an Int literal's magnitude, a fold-list spine of digits (v2.std.integer) -- is one term +// (body_term_of answers RawLeaf), and body_lower_fold already takes it whole. This +// outer fold used to descend into its payload anyway, so the digit spine reached body_term_of as a +// composite with no projection edge and no seal and refused as an unrecognised lowered shape; every +// `data x: Int = ` refused (the native door's accepting control, #12942). A leaf's edges are +// kept as they stand; the leaf itself is still lowered below, as every node is. The question is asked +// of the term authority, never of a second leaf predicate; a node it refuses is still descended, so +// the refusal is raised, located, by the reader that lowers it rather than absorbed here. +// ONE CLASSIFICATION PER PARENT (review 75069). Whether a child is kept whole depends only on the +// parent n, so body_term_of is asked once here and the answer is carried into the fold, rather +// than re-read once per child edge (DESIGN section 6, bare minimum cost: a quadratic fold is always +// fixed). A RawLeaf parent keeps its payload whole; every other arm, Rejected included, descends. fn normalize_node_fold_children_ctx(n: Node, under_fn_decl: Bool) -> Outcome { + normalize_node_fold_children_classified( + n: n, + under_fn_decl: under_fn_decl, + keep_children_whole: match body_term_of(node: n) { + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => true + Accepted { value: RawParse { node: _ }, diagnostics: _ } => false + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => false + Rejected { diagnostics: _ } => false + } + ) +} + +fn normalize_node_fold_children_classified(n: Node, under_fn_decl: Bool, keep_children_whole: Bool) -> Outcome { match fold(n.children, init: normalize_children_init(), f: fn(acc, e) { normalize_children_step( acc: acc, edge: e, - child: normalize_node_ctx(n: e.target, under_fn_decl: under_fn_decl) + child: if keep_children_whole { + Accepted { value: body_lowered_tree_over(node: e.target, retained: no_wrapper_retentions()), diagnostics: None } + } else { + normalize_node_ctx(n: e.target, under_fn_decl: under_fn_decl) + } ) }) { Rejected { diagnostics: r } => Rejected { diagnostics: r } @@ -344,6 +384,11 @@ fn normalize_node_fold_children_ctx(n: Node, under_fn_decl: Bool) -> Outcome) -> Outcome> { fold_list( xs: nodes, @@ -355,7 +400,13 @@ fn import_binding_capture(tree: ParseTree, nodes: List) -> Outcome acc Present { value: id } => - if id != ^dag_surface_import_decl { + if id == ^dag_surface_alias_decl { + match alias_binding_row_from_decl_node(decl: n, module_root: tree) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: row, diagnostics: _ } => + Accepted { value: list_snoc_item(xs: rows, item: row), diagnostics: v2.std.diagnostic.None } + } + } else if id != ^dag_surface_import_decl { acc } else { match import_binding_rows_from_decl_node(decl: n, module_root: tree) { @@ -408,6 +459,47 @@ fn type_declaration_kind_capture(nodes: List) -> FreeMonoid T { v }` into one shape (a named nullary Arrow), so +// after normalize nothing in the Node says which was authored. The population is the graft's own unit +// census (namespace_graft_collect_unit_nodes_against over the module's working root, the same units the +// graft partitions into members), NOT every node of the parse: a declaration nested in another unit is +// not a member of the module. A data or fn unit whose name cannot be read contributes no row, so a +// consumer asking for it finds it not declared and refuses, never a fabricated kind. +// v2.std.member_declaration_kind MemberKindChannel is what carries these to a later grain. +fn member_declaration_capture(tree: ParseTree) -> FreeMonoid { + let unit_ids = namespace_graft_top_level_item_alternative_emitted_ids(productions: dag_grammar_root().productions) + fold_list( + xs: namespace_graft_collect_unit_nodes_against(node: namespace_graft_unwrap_module_working_root(root: tree), unit_ids: unit_ids), + empty: Empty, + cons: fn(acc, unit) { + match parse_production_emitted_identity_optional(node: unit) { + Absent => acc + Present { value: id } => + if (id == ^dag_surface_data_decl) || (id == ^dag_surface_fn_decl) { + match parse_production_captured_child_optional(node: unit) { + Absent => acc + Present { value: captured } => + match dag_surface_kw_then_ident_from_captured(captured: captured) { + Absent => acc + Present { value: name } => + list_snoc_item( + xs: acc, + item: DeclaredMember { + name: name, + kind: if id == ^dag_surface_data_decl { DeclaredDataMember } else { DeclaredFnMember } + } + ) + } + } + } else { + acc + } + } + } + ) +} + // WHICH MODIFIERS EACH TYPE DECLARATION'S SLOT CARRIES, read from the parse tree for the reason // type_declaration_kind_capture is: lowering leaves no trace of the slot in the node. The question is // answered by the lowering's own reader (v2.compiler.body_lowering_fold @@ -507,7 +599,13 @@ fn normalize_with(parse_tree: ParseTree, source: NormalizeAllocatorSource) -> Ou ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: tree, diagnostics: d2 } => - Accepted { value: ScopedNormalizedTree { tree: tree, allocator: allocated.allocator }, diagnostics: d2 } + Accepted { + value: ScopedNormalizedTree { + tree: normalized_tree_with_member_declarations(tree: tree, members: member_declaration_capture(tree: parse_tree)), + allocator: allocated.allocator + }, + diagnostics: d2 + } } } } diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index cda409b2df4..f3301a605f7 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -1,7 +1,16 @@ module v2.compiler.resolve import v2.std.language_model { LanguageModel } -import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_resource_declarations } +import v2.compiler.normalized_tree { normalized_tree_record_declarations, MemberDeclarationsNotRead, MemberDeclarationsRead, NormalizedTree, normalized_tree_resource_declarations } +import v2.std.member_declaration_kind { + DeclaredMember, + declared_members_have_duplicate, + MemberKindChannel, + MemberKindsCaptured, + MemberKindsUnavailable, + QualifiedDeclaredMember, + no_member_declaration_kinds +} import v2.compiler.namespace_graft { namespace_graft_module_body_optional } import v2.std.declaration_marker { ReachesNoTestCode, @@ -37,13 +46,13 @@ import v2.extdeps.languages.dag { import v2.compiler.parse { parse_tree_projection_edge } import v2.std.anonymous_binder { is_anonymous_param_label } import std.algebra { Cons, Empty, FreeMonoid, list_append, list_snoc_item } -import v2.std.algebra { any, fold_list, length, skip } +import v2.std.algebra { any, fold_list, length, list_find_first, skip } import v2.std.collection { Map, PointwisePower, empty_map, list_at_optional, - map_get, + map_get_checked, map_insert, map_lookup } @@ -120,6 +129,7 @@ import v2.std.symbol_index { symbol_index_declaring_path_of, symbol_index_lexical_lookup, symbol_index_declared_binders_at, + symbol_index_declared_type_params_at, symbol_index_lookup, symbol_index_renaming_alias_at } @@ -161,8 +171,9 @@ import v2.std.node { symbol_lexeme, arrow_domain_binds_a_name, arrow_first_positional_target, - arrow_named_edge_is_contract, + arrow_named_edge_is_metadata, ArrowResourceRequirementsEdge, + AdmitCallersEdge, FirstPositionalAbsent, FirstPositionalFound, edge_label_of, @@ -207,6 +218,8 @@ import v2.std.node_query { NotAConstruct, construct_tag_optional, field_projection_node, + construct_tag_path_optional, + declared_field_named, node_positional_child_targets, find_named_child, is_wildcard_pattern, @@ -258,6 +271,7 @@ type ResolvedTree { resolved_declarations: SymbolIndex lexical_bindings: Map unavailable_providers: List + member_kinds: MemberKindChannel } // A CLOSURE ROOT WHOSE OWN RESOLVE REFUSED, AND WHY. v2.compiler.name_resolve closure_declarations_demand @@ -328,6 +342,7 @@ fn resolved_tree_of( symbol_index: SymbolIndex, closure_declarations: SymbolIndex, unavailable_providers: List, + member_kinds: MemberKindChannel, lexical: List ) -> ResolvedTree { ResolvedTree { @@ -335,7 +350,49 @@ fn resolved_tree_of( symbol_index: symbol_index, resolved_declarations: resolved_declarations_over(closure: closure_declarations, subject: root), lexical_bindings: fold(lexical, init: empty_map(), f: fn(m, e) { map_insert(m, e.reference, e.binding) }), - unavailable_providers: unavailable_providers + unavailable_providers: unavailable_providers, + member_kinds: member_kinds + } +} + +// THE SUBJECT'S MEMBER-KIND CHANNEL, keyed by the DECLARING path (this module's qualified name + the member's +// name). The channel covers the subject module only -- the one root mandatory_tag runs on -- so resolve pays +// one pass over one module's members, not one per root in the closure. A tree normalize read no members for +// (a hand-built root) is Unavailable, never an empty captured list. Roots are unique by name already +// (ModuleRootsDuplicate refuses a second root for one module), so the only merge that can conflict is two +// members of one module sharing a name: that refuses, located at the root, and is never first-wins. +fn member_kind_channel_of_tree(tree: NormalizedTree) -> Outcome { + match tree.member_declarations { + MemberDeclarationsNotRead => Accepted { value: no_member_declaration_kinds(), diagnostics: None } + MemberDeclarationsRead { members: members } => + match qualified_name_from_module_node(root: tree.root) { + Rejected { diagnostics: _ } => Accepted { value: no_member_declaration_kinds(), diagnostics: None } + Accepted { value: module_qn, diagnostics: _ } => + let duplicated = declared_members_have_duplicate(members: members) + if duplicated { + Rejected { + diagnostics: diagnostics_singleton( + d: Diagnostic { + reason: ^resolve_reason_member_kind_duplicate_declaration, + at: node_locus(node: tree.root), + correction: Unavailable { reason: ExternalContractUnknown } + } + ) + } + } else { + Accepted { + value: MemberKindsCaptured { + covered: [module_qn], + members: fold_list( + xs: members, + empty: [], + cons: fn(acc, m) { list_snoc_item(xs: acc, item: QualifiedDeclaredMember { module: module_qn, name: m.name, kind: m.kind }) } + ) + }, + diagnostics: None + } + } + } } } @@ -418,6 +475,7 @@ type FrameKind type LexicalBinding { binder_site: NodeOccurrenceIdentity declared: Optional + value: Optional } // One answered lexical reference: the reference's own minted occurrence and the binding that @@ -681,18 +739,6 @@ fn gate_value_binders(names: List, outer: Scope, at: Node) -> BinderGate } } -// A LOOP'S CARRIER IS A BINDER, SO IT OPENS A FRAME AND IS NOT WALKED AS A REFERENCE. v2.std.node says -// it outright -- "^loop_carrier_edge targets the loop-carried state's BINDER, never a value" -- and -// resolve did neither: a Loop fell to resolve_children_homogeneous_scope, which opens a frame only for -// an Arrow domain, so the carrier bound nothing and the iterated body's uses of it reached the bare-name -// census. That is the whole of the `found` refusal in `fold(xs, init: false, f: fn(found, e) { ... })`: -// v2.compiler.fold_lowering destructures the step literal into an iteration body and ONE carrier symbol -// and builds this Loop, so the step's Arrow is never constructed and its second binder is dropped. -// -// The carrier edge is carried UNWALKED for the same reason the Arrow's declared-order edge is -// (resolve_arrow_node_in): its target names a binder, not a reference, so resolving it would ask the -// scope to answer for a name the scope is being opened to introduce. - fn harvest_conj_named_bindings(root: Node, acc: Map) -> Map { match root.kind { TypeNode { connective: Conj } => @@ -720,7 +766,7 @@ data resolve_variant_visibility_disposition: Disposition = Scaffold { dissolves_to: SingleAuthority, bind: DeclarationRef { module_path: "v2.compiler.symbol_index_fill", - decl_name: "symbol_index_fill_unique_variant_aliases", + decl_name: "symbol_index_fill_containment_node", field: WholeDeclaration } } @@ -823,10 +869,15 @@ fn arrow_domain_frame_binders(n: Node) -> FrameBinders { fold(domain.children, init: frame_binders_empty(), f: fn(fb, e) { match try_edge_declared_binding(e: e) { Present { value: b } => - frame_binders_add(fb: fb, name: b.name, binding: LexicalBinding { - binder_site: n.occurrence_id, - declared: member_edge_type_node(e: e) - }) + frame_binders_add( + fb: fb, + name: b.name, + binding: LexicalBinding { + binder_site: n.occurrence_id, + declared: member_edge_type_node(e: e), + value: optional_absent() + } + ) Absent => fb } }) @@ -847,10 +898,15 @@ fn bind_frame_binders(n: Node) -> FrameBinders { Present { value: e0 } => match e0.target.kind { TypeNode { connective: Atom { identity: sym } } => - frame_binders_add(fb: frame_binders_empty(), name: sym, binding: LexicalBinding { - binder_site: e0.target.occurrence_id, - declared: optional_absent() - }) + frame_binders_add( + fb: frame_binders_empty(), + name: sym, + binding: LexicalBinding { + binder_site: e0.target.occurrence_id, + declared: optional_absent(), + value: list_at_optional(xs: node_positional_child_targets(node: n), index: 1) + } + ) _ => frame_binders_empty() } Absent => frame_binders_empty() @@ -874,6 +930,12 @@ fn bind_frame_binders(n: Node) -> FrameBinders { // symbol_index_fill_module_declarations) over its one root, keyed at its module's paths. Parameter // references are verified against it (resolve_frame_bound_reference), so both routes verify against // the index each resolves through -- one authority, not a second table beside it. +// THE INDEX'S KEYS AND THE LOOKUP'S POSITION ARE THE SAME PATH. The fill keys every declaration under +// the module's own path, so module_qn -- the position an index lookup starts from -- is that path too, +// as it is for the admitted subject. With module_qn left Empty, a name that is not a root binding (a +// variant in value position) was looked up from the corpus root and refused unbound. And the fill takes +// the record names the tree carries, as it takes its resource names: with Empty, no declaration was +// marked RecordTypePayload, so every headless record literal refused as not a record. fn build_program_namespace(tree: NormalizedTree, lm: LanguageModel) -> Namespace { let declared_in = namespace_tree_module_qn(tree: tree) let bindings = namespace_bindings_augment_from_module_atoms( @@ -883,8 +945,8 @@ fn build_program_namespace(tree: NormalizedTree, lm: LanguageModel) -> Namespace Namespace { bindings: bindings, canonical_symbols: lm.canonical_symbols, - symbol_index: symbol_index_fill_module_declarations(index: empty_symbol_index(), root: tree.root, record_declarations: Empty, resource_declarations: normalized_tree_resource_declarations(t: tree)), - module_qn: Empty, + symbol_index: symbol_index_fill_module_declarations(index: empty_symbol_index(), root: tree.root, record_declarations: normalized_tree_record_declarations(t: tree), resource_declarations: normalized_tree_resource_declarations(t: tree)), + module_qn: declared_in, test_code: test_code_index_add_module( index: test_code_index_empty(), module_qn: declared_in, @@ -895,9 +957,9 @@ fn build_program_namespace(tree: NormalizedTree, lm: LanguageModel) -> Namespace } } -// A single-tree namespace has no module path of its own (module_qn stays Empty, as before), but its -// test-code index still needs the path its declarations live under. A root that names no module -// keys them under Empty, which is the same key both sides of the lookup use. +// The path a single tree's declarations live under: its module_qn, its index keys and its test-code +// index all read it. A root that names no module keys them under Empty, which is the same key both +// sides of the lookup use. fn namespace_tree_module_qn(tree: NormalizedTree) -> QualifiedName { match qualified_name_from_module_node(root: tree.root) { Accepted { value: qn, diagnostics: _ } => qn @@ -1087,7 +1149,7 @@ type ScopeBinding fn lookup_chain(s: Scope, name: Symbol) -> Outcome { match s { ScopeFrame { locals: m, outer: o, kind: kind } => - match v2.std.collection.map_get(m, name) { + match v2.std.collection.map_get_checked(m, name) { Accepted { value: opt, diagnostics: d_here } => match opt { Present { value: v } => Accepted { value: BoundInFrame { canonical: v, kind: kind }, diagnostics: d_here } @@ -1107,7 +1169,7 @@ fn lookup_chain(s: Scope, name: Symbol) -> Outcome { Rejected { diagnostics: ds } => Rejected { diagnostics: ds } } ScopeRoot { module: mod } => - match v2.std.collection.map_get(mod.bindings, name) { + match v2.std.collection.map_get_checked(mod.bindings, name) { Accepted { value: opt, diagnostics: d } => match opt { Present { value: v } => Accepted { value: BoundAtRoot { canonical: v }, diagnostics: d } Absent => Accepted { value: ScopeUnbound, diagnostics: d } @@ -1925,8 +1987,8 @@ fn kernel_type_spelling_foreign_diagnostic(n: Node) -> Diagnostic { } // WHETHER A DECLARATION IS THE REFERENCING MODULE'S OWN, read from where the namespace records its -// owner. declared_in is that owner on both namespace routes: build_program_namespace (single tree, -// whose module_qn stays Empty) and the admitted subject (where it equals module_qn). root_binding_origin +// owner. declared_in is that owner on both namespace routes: build_program_namespace (single tree) +// and the admitted subject, and on both it equals module_qn. root_binding_origin // reads the same field, so the root door and the symbol-index door agree on ownership. fn namespace_owns_declaration(namespace: Namespace, path: QualifiedName) -> Bool { qualified_name_init(qn: path) == namespace.declared_in @@ -2355,12 +2417,13 @@ fn resolved_tree_outcome( w: ResolveNodeWalk, symbol_index: SymbolIndex, closure_declarations: SymbolIndex, - unavailable_providers: List + unavailable_providers: List, + member_kinds: MemberKindChannel ) -> Outcome { match w { ResolveWalkAccepted { value: v, diagnostics: d, lexical: l } => Accepted { - value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_declarations: closure_declarations, unavailable_providers: unavailable_providers, lexical: l), + value: resolved_tree_of(root: v, symbol_index: symbol_index, closure_declarations: closure_declarations, unavailable_providers: unavailable_providers, member_kinds: member_kinds, lexical: l), diagnostics: d } ResolveWalkRefused { first: f, rest: _, observation: _ } => Rejected { diagnostics: f } @@ -2662,29 +2725,253 @@ fn resolve_pattern_atom_names_constructor(ctx: ResolveContext, id: Symbol) -> Bo } } -fn resolve_pattern_binders(ctx: ResolveContext, pat: Node, acc: FrameBinders) -> FrameBinders { +fn resolve_pattern_binders(ctx: ResolveContext, pat: Node, acc: PatternBindersOutcome) -> PatternBindersOutcome { match construct_tag_optional(n: pat) { Absent => acc Present { value: _ } => fold(construct_field_edges(n: pat), init: acc, f: fn(m, e) { - match node_atom_identity_optional(node: e.target) { - Present { value: id } => - if is_wildcard_pattern(pattern: e.target) { - m - } else if resolve_pattern_atom_names_constructor(ctx: ctx, id: id) { - m - } else { - frame_binders_add(fb: m, name: id, binding: LexicalBinding { - binder_site: e.target.occurrence_id, - declared: optional_absent() - }) + match m { + PatternBindersRefused { diagnostic: _ } => m + PatternBindersAdmitted { binders: fb } => + match node_atom_identity_optional(node: e.target) { + Present { value: id } => + if is_wildcard_pattern(pattern: e.target) { + m + } else if resolve_pattern_atom_names_constructor(ctx: ctx, id: id) { + m + } else { + match resolve_pattern_binder_declared_type(ctx: ctx, pat: pat, field: e) { + PatternBinderFormal { at: at } => + PatternBindersRefused { + diagnostic: pattern_payload_formal_uninstantiated_diagnostic(at: at) + } + PatternBinderTyped { node: t } => + PatternBindersAdmitted { + binders: frame_binders_add( + fb: fb, + name: id, + binding: LexicalBinding { + binder_site: e.target.occurrence_id, + declared: optional_present(value: t), + value: optional_absent() + } + ) + } + PatternBinderUntyped => + PatternBindersAdmitted { + binders: frame_binders_add( + fb: fb, + name: id, + binding: LexicalBinding { + binder_site: e.target.occurrence_id, + declared: optional_absent(), + value: optional_absent() + } + ) + } + } + } + Absent => resolve_pattern_binders(ctx: ctx, pat: e.target, acc: m) } - Absent => resolve_pattern_binders(ctx: ctx, pat: e.target, acc: m) } }) } } +// A PATTERN BINDER'S TYPE IS THE VARIANT FIELD IT CAPTURES (v2.compiler.body_lowering_fold +// body_lower_field_decl_optional). The arm the pattern's tag names declares the field -- positional +// "0" and named alike, one grammar -- and the binder's declared type is that field's type node, so +// the body's reference derives from its binding (v2.compiler.infer infer_lexical_reference_facts) +// instead of staying on the frontier with the match spine. A tag that names no declared arm, or a +// field the arm does not declare, leaves the binder untyped exactly as before: the match row judges +// the pattern against the scrutinee's coproduct and refuses the mismatch at the arm row. +// +// A field whose type names the DECLARING TYPE's formal is neither of those: the type node exists +// but names a type nothing instantiates at this stage (the scrutinee's arguments flow into the arm +// frame only with central deep substitution -- the derivation follow-up). Minting the formal +// fabricates a type the binder does not have (DESIGN 5), and leaving the binder untyped lets the +// match frontier ACCEPT the arm as an advisory frontier -- the module prepares and the native lane +// reports a prepared module whose binder has no type. Both fail open. The binders fold therefore +// refuses the arm walk here, with the payload field as the location +// (^resolve_reason_pattern_payload_formal_uninstantiated), and the deep-instantiation follow-up +// flips this refusal to grounding by installing the scrutinee's arguments. +type PatternBinderDeclared + = PatternBinderTyped { node: Node } + | PatternBinderUntyped + | PatternBinderFormal { at: Node } + +// The binders fold carries the mint's outcome so a formal-typed payload field refuses the walk +// before any binder is admitted under an invented scope. +type PatternBindersOutcome + = PatternBindersAdmitted { binders: FrameBinders } + | PatternBindersRefused { diagnostic: Diagnostic } + +fn pattern_payload_formal_uninstantiated_diagnostic(at: Node) -> Diagnostic { + Diagnostic { + reason: ^resolve_reason_pattern_payload_formal_uninstantiated, + at: node_locus(node: at), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +// A payload field whose type still names the declaring type's formal is detected against the +// declaring type's OWN parameter list -- `symbol_index_declared_type_params_at` at the arm's +// parent path -- so a concrete user type or another declaration's parameter never trips this gate. +fn resolve_payload_field_type_holds_declaring_formal( + ctx: ResolveContext, + arm_path: QualifiedName, + t: Node +) -> Bool { + resolve_type_node_mentions_any_name( + t: t, + names: symbol_index_declared_type_params_at( + index: ctx.namespace.symbol_index, + qualified_path: qualified_name_init(qn: arm_path) + ) + ) +} + +fn resolve_type_node_mentions_any_name(t: Node, names: List) -> Bool { + match t.kind { + TypeNode { connective: Atom { identity: id } } => + any(xs: names, predicate: fn(n) { n == id }) + TypeNode { connective: _ } => + any( + xs: node_positional_child_targets(node: t), + predicate: fn(c) { resolve_type_node_mentions_any_name(t: c, names: names) } + ) + ComputationNode { behavior: _ } => + any( + xs: node_positional_child_targets(node: t), + predicate: fn(c) { resolve_type_node_mentions_any_name(t: c, names: names) } + ) + } +} + +fn resolve_pattern_binder_declared_type(ctx: ResolveContext, pat: Node, field: Edge) -> PatternBinderDeclared { + match resolve_pattern_tag_declared_payload_path_optional(ctx: ctx, pat: pat) { + Absent => PatternBinderUntyped + Present { value: arm_path } => + match symbol_index_lookup(index: ctx.namespace.symbol_index, qualified_path: arm_path) { + Absent => PatternBinderUntyped + Present { value: payload } => + match field.label { + Authored { name: field_name } => + match declared_field_named(payload: payload, name: field_name) { + Present { value: f } => + if resolve_payload_field_type_holds_declaring_formal( + ctx: ctx, + arm_path: arm_path, + t: f.type_node + ) { + PatternBinderFormal { at: field.target } + } else { + PatternBinderTyped { node: f.type_node } + } + Absent => PatternBinderUntyped + } + StructuralLabel { label: _ } => PatternBinderUntyped + Positional => PatternBinderUntyped + } + } + } +} + +// The declared arm a pattern's tag names, read BEFORE the walk (the binders mint first, so a +// refused pattern never leaves the body under an invented scope). A one-segment tag looks up +// lexically exactly as resolve_pattern_atom_names_constructor does; a qualified tag looks up +// absolutely; only a candidate the declared-payload authority answers for names an arm. +fn resolve_pattern_tag_declared_payload_path_optional(ctx: ResolveContext, pat: Node) -> Optional { + match construct_tag_path_optional(n: pat) { + Absent => optional_absent() + Present { value: path } => + match path { + Cons { head: only, tail: rest } => + match rest { + Empty => resolve_declared_payload_path_lookup(ctx: ctx, name: only) + Cons { head: _, tail: _ } => resolve_qualified_tag_declared_payload(ctx: ctx, path: path) + } + Empty => resolve_qualified_tag_declared_payload(ctx: ctx, path: path) + } + } +} + +// A tag naming more than one segment is a qualified path; a zero-segment tag cannot happen at a +// pattern (the parser requires a tag), so both non-solo shapes share this lookup. +fn resolve_qualified_tag_declared_payload(ctx: ResolveContext, path: List) -> Optional { + match resolve_declared_payload_candidates( + ctx: ctx, + paths: list_map( + xs: symbol_index_absolute_candidates(index: ctx.namespace.symbol_index, qualified_path: path), + f: fn(c) { c.path } + ) + ) { + Cons { head: only, tail: rest } => + match rest { + Empty => optional_present(value: only) + Cons { head: _, tail: _ } => optional_absent() + } + Empty => optional_absent() + } +} + +fn resolve_declared_payload_path_lookup(ctx: ResolveContext, name: Symbol) -> Optional { + match symbol_index_lexical_lookup( + index: ctx.namespace.symbol_index, + position: symbol_index_lookup_position(ctx: ctx), + name: name + ) { + LexicalHit { node: _, path: path } => + if resolve_path_is_declared_payload(ctx: ctx, path: path) { + optional_present(value: path) + } else { + optional_absent() + } + LexicalAmbiguous { candidates: candidates } => + resolve_sole_declared_payload_path(ctx: ctx, paths: list_map(xs: candidates, f: fn(c) { c.path })) + LexicalUnbound => + match symbol_index_global_unique_lookup(index: ctx.namespace.symbol_index, name: name) { + GlobalBareHit { node: _, path: path } => + if resolve_path_is_declared_payload(ctx: ctx, path: path) { + optional_present(value: path) + } else { + optional_absent() + } + GlobalBareLookupAmbiguous { paths: paths } => resolve_sole_declared_payload_path(ctx: ctx, paths: paths) + GlobalBareLookupUnbound => optional_absent() + } + } +} + +// The declared-payload paths among a candidate set, in lookup order. +fn resolve_declared_payload_candidates(ctx: ResolveContext, paths: FreeMonoid) -> FreeMonoid { + match paths { + Empty => Empty + Cons { head: p, tail: rest } => + if resolve_path_is_declared_payload(ctx: ctx, path: p) { + Cons { head: p, tail: resolve_declared_payload_candidates(ctx: ctx, paths: rest) } + } else { + resolve_declared_payload_candidates(ctx: ctx, paths: rest) + } + } +} + +// An ambiguous tag must not name an arm by candidate order: a binder typed by the first of several +// declared payloads fabricates a plausible output (DESIGN §5 -- a failure arm refuses, never +// widens). Exactly one declared-payload candidate names the arm; a wider set leaves the binder +// untyped, the behaviour that preceded binder typing. The arm the binder means is settled by the +// scrutinee's coproduct at infer, not by name order here. +fn resolve_sole_declared_payload_path(ctx: ResolveContext, paths: List) -> Optional { + match resolve_declared_payload_candidates(ctx: ctx, paths: paths) { + Cons { head: only, tail: rest } => + match rest { + Empty => optional_present(value: only) + Cons { head: _, tail: _ } => optional_absent() + } + Empty => optional_absent() + } +} + // The pat resolves under the ARM frame: the tag and any nested constructor resolve as // references, a binder answers BoundInFrame and stays itself, and the wildcard is kept as it is. // @@ -3294,15 +3581,21 @@ fn resolve_match_arm_walk(ctx: ResolveContext, arm: Node) -> ResolveNodeWalk { Accepted { value: pat, diagnostics: _ } => match gate_value_binders(names: pattern_binder_name_list(ctx: ctx, pat: pat), outer: ctx.scope, at: pat) { BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) - BinderGateAdmitted => resolve_match_arm_admitted(ctx: ctx, arm: arm, pat: pat) + BinderGateAdmitted => + match resolve_pattern_binders(ctx: ctx, pat: pat, acc: PatternBindersAdmitted { binders: frame_binders_empty() }) { + PatternBindersRefused { diagnostic: d } => + resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) + PatternBindersAdmitted { binders: fb } => + resolve_match_arm_admitted(ctx: ctx, arm: arm, pat: pat, fb: fb) + } } } } -fn resolve_match_arm_admitted(ctx: ResolveContext, arm: Node, pat: Node) -> ResolveNodeWalk { +fn resolve_match_arm_admitted(ctx: ResolveContext, arm: Node, pat: Node, fb: FrameBinders) -> ResolveNodeWalk { let arm_ctx = resolve_ctx_with_scope( ctx: ctx, - scope: lexical_frame(fb: resolve_pattern_binders(ctx: ctx, pat: pat, acc: frame_binders_empty()), outer: ctx.scope) + scope: lexical_frame(fb: fb, outer: ctx.scope) ) child_walk_node(n: arm, w: fold(arm.children, init: child_walk_init(), f: fn(acc, e) { child_walk_step( @@ -3529,9 +3822,11 @@ fn resolve_type_application_diagnostic(reason: Symbol, at: Node) -> Diagnostic { // it must name a coproduct, and an argument inhabits it exactly when the argument resolves to one of // that coproduct's variants -- by the DECLARATION the index holds, never by leaf spelling. A unique // variant referenced bare resolves to its module-level path (p.Pointer), which -// v2.compiler.symbol_index_fill symbol_index_fill_unique_variant_aliases files as a second ENTRY over -// the same arm node rather than as a binding to p.Resolution.Pointer, so -// symbol_index_declaring_path_of cannot relate the two paths; the arm node they share can. A Nat literal and a type +// v2.compiler.symbol_index_fill symbol_index_fill_containment_node files as a BINDING to the +// variant's declaring path (p.Resolution.Pointer) -- the repair the receipt +// a_binding_position_recorded_as_a_declaring_identity names, landed here: the alias was a second +// ENTRY over the same arm node, which was the double-entry defect that row describes, and +// symbol_index_declaring_path_of now relates the two paths with no parallel carrier map. A Nat literal and a type // parameter of an enclosing declaration are not judged (v2.std.type_binder KindInhabitance). fn resolve_kind_inhabitance(ctx: ResolveContext, declaration: QualifiedName, argument: Node, kind: Node) -> KindInhabitance { if dag_node_is_int_literal_atom(node: argument) { @@ -4113,6 +4408,12 @@ fn resolve_node_consumes_expectation(n: Node) -> Bool { // parameters under -- so its value frame is a ParameterFrame over that path. Nothing below an Arrow // is containment: its body, codomain and domain walk NotDeclaring, so an Arrow met there (a lambda) // opens a LexicalFrame. +// THE DECLARED PERMITTED-CALLERS CLAUSE IS CARRIED UNWALKED, like the declared order below it: +// its target is the clause's preserved spelling, evidence about the source rather than value +// content, and walking it would resolve the list's decl_ref constructor as a reference. Its +// reader is the caller-admission consumer (v2.compiler.infer infer_caller_admission_diags) and +// its structural check is the Arrow wall's at-most-once guard +// (v2.std.node arrow_signature_edges_conform). fn resolve_arrow_node_in(outer_ctx: ResolveContext, ctx: ResolveContext, n: Node) -> ResolveNodeWalk { match gate_value_binders(names: arrow_domain_binder_name_list(n: n), outer: ctx.scope, at: n) { BinderGateRefused { diagnostic: d } => resolve_walk_refused_one(chain: diagnostics_singleton(d: d)) @@ -4140,7 +4441,7 @@ fn resolve_arrow_node_admitted(outer_ctx: ResolveContext, ctx: ResolveContext, n child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None, lexical: [] }) } else if edge_is_core(e: e, marker: ArrowResourceRequirementsEdge) { child_walk_step(w: acc, e: e, r: resolve_arrow_resource_requirements(ctx: resolve_ctx_with_scope(ctx: inner_ctx, scope: scope_type), requirements: e.target)) - } else if arrow_named_edge_is_contract(e: e) { + } else if arrow_named_edge_is_metadata(e: e) { child_walk_step(w: acc, e: e, r: ResolveWalkAccepted { value: e.target, diagnostics: None, lexical: [] }) } else { child_walk_step( @@ -4409,7 +4710,10 @@ fn resolve_with_namespace_policy( lm: LanguageModel, policy: NameResolutionPolicy ) -> Outcome { - resolved_tree_outcome( + match member_kind_channel_of_tree(tree: tree) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: channel, diagnostics: _ } => + resolved_tree_outcome( w: resolve_walk_with_namespace_policy( tree: tree, namespace: namespace, @@ -4418,8 +4722,10 @@ fn resolve_with_namespace_policy( ), symbol_index: namespace.symbol_index, closure_declarations: empty_symbol_index(), - unavailable_providers: [] + unavailable_providers: [], + member_kinds: channel ) + } } fn resolve_walk_with_namespace_policy( diff --git a/src/v2/compiler/04_infer.dag b/src/v2/compiler/04_infer.dag index 93fcded72d2..e5a5eea7553 100644 --- a/src/v2/compiler/04_infer.dag +++ b/src/v2/compiler/04_infer.dag @@ -4,8 +4,8 @@ import std.types { List } import std.occurrence_identity { OccurrenceSynthetic } import std.kind { Kind, RosterKindIndex, TypeDenotationKind, kind_denoted_by_node, kind_node, roster_kind_index, roster_kind_index_lookup } -import v2.std.algebra { any, Empty, TailAbsent, TailFound, length, list_map, list_tail, zip_map } -import std.algebra { Cons, Empty, list_snoc_item } +import v2.std.algebra { any, Empty, TailAbsent, TailFound, length, list_map, list_tail, skip, zip_map } +import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.bounded_lattice_completeness { infer_bounded_lattice_consumer_gate, partial_bounded_lattice_instances_in_tree @@ -17,9 +17,11 @@ import v2.std.compilers.target_model { canonical_operation_op_add, canonical_operation_op_coerce, canonical_operation_wire_matches_operation, + canonical_operation_op_sub, target_model_canonical_operation_member_declared_type, } import v2.compiler.resolve { ResolvedTree, resolved_tree_lexical_binding, resolved_tree_unavailable_provider } +import v2.std.grammar { node_atom_identity_optional } import v2.compiler.inferred_tree { DerivedGrounding, GroundingNotDerived, @@ -31,6 +33,9 @@ import v2.compiler.inferred_tree { } import v2.std.algebra_structure_signature { FieldDeclared, FieldNotDeclared, InhabitanceFieldRead, InhabitanceFieldReadMalformed, InhabitanceMalformed, algebra_inhabitance_field_reading } import v2.std.model_core { AlgebraInhabitanceDecl } +import std.literal_elaboration { DirectLiteral, HomomorphismDuplicated, KernelGroundingAbsent, KernelGroundingAmbiguous, KernelGroundingFound, KernelIntLiteral, LiteralElaborationRefusal, LiteralElaborationRefused, ViaHomomorphism, elaborate_literal_at, kernel_grounding_admits, kernel_grounding_for } +import gunbc.structural_realization_bindings { kernel_grounding_rows, literal_homomorphism_rows } +import std.decl_ref { DeclarationRef, decl_ref } import v2.std.grammar { node_atom_identity_optional } import v2.extdeps.languages.dag { dag_algebra_inhabitance_decls, @@ -40,10 +45,13 @@ import v2.extdeps.languages.dag { DagCanonicalStringLiteral, DagCanonicalLiteral, dag_binding_denotation, + dag_kernel_type_declaration_binding_optional, dag_canonical_literal_from_node, dag_declared_inhabitants_root, dag_declared_inhabitants_subject_field, - dag_grammar_production_member_declared_type + dag_grammar_production_member_declared_type, + parse_production_captured_child_optional, + qualified_name_from_module_node } import v2.extdeps.languages.python { python_declared_inhabitants_root, @@ -69,12 +77,13 @@ import std.optional { optional_absent, optional_present } -import v2.std.qualified_name { QualifiedName, declaration_reference_node, declaration_reference_path_optional, qualified_name_to_dotted_string, lexical_reference_label_optional, parameter_reference_path_optional } -import v2.std.node_query { BinderParts, binder_node, binder_node_parts, construct_field_edges, construct_tag_optional, declared_field_from_edge, find_labeled_child } +import v2.std.qualified_name { QualifiedName, declaration_reference_node, declaration_reference_path_optional, qualified_name_to_dotted_string, lexical_reference_label_optional, parameter_reference_path_optional, qualified_name_init, qualified_name_last_segment } +import v2.std.node_query { BinderParts, binder_node, binder_node_parts, construct_field_edges, construct_tag_optional, construct_tag_path_optional, declared_field_from_edge, declared_field_named, find_labeled_child, find_named_child, arrow_constant_member_codomain } +import v2.std.compilers.sugar { sugar_sequence_pair_optional } import v2.std.symbol_index { RecordTypePayload, SymbolIndex, VariantArmPayload, symbol_index_declared_payload_at, symbol_index_declared_type_params_at, symbol_index_lookup } import v2.std.grammar { node_atom_identity_optional } import v2.compiler.fold_lowering { FoldMemberFormal, RosterSignatureLookup, NotARosterPrimitiveCallRow, RosterPrimitiveCallSignature, RosterPrimitiveCallSignatureUnreadable, collection_roster_primitive_call_signature_at, FoldMemberRole, FoldStepTakesCollectionMember, FoldStepTakesNoMember, fold_collection_member_type, fold_loop_step, fold_realized_member_role, fold_step_body_node, fold_step_carrier_formal, fold_step_declared_return, fold_step_member_formal } -import v2.std.node { LoopBoundMeasure, LoopCarrierBinder, LoopDomainValue, LoopEdgeUnrecognized, LoopIteratedBody, LoopRealizedDeclaration, loop_edge_role, NodeFoldDependent, fold_node_dependent, loop_domain_value_target, loop_realized_declaration_target, Ambiguous, Found, NotMarkedReference, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_signature_order_edge, match_arm_pattern_edge, resolved_reference_body_kind, edge_label_of, MatchArmBodyEdge, MatchArmPatternEdge, production_edge_label } +import v2.std.node { symbol_lexeme, LoopBoundMeasure, LoopCarrierBinder, LoopDomainValue, LoopEdgeUnrecognized, LoopIteratedBody, LoopRealizedDeclaration, loop_edge_role, NodeFoldDependent, fold_node_dependent, loop_domain_value_target, loop_realized_declaration_target, AdmitCallersEdge, Ambiguous, DeclarationReferenceKind, Found, MarkedReference, NodeFoldTopDown, NotMarkedReference, Symbol, arrow_body_target_lookup, arrow_named_edge_is_contract, arrow_named_edge_is_metadata, arrow_signature_order_edge, core_edge_target_lookup, edge_label_of, fold_node_topdown, match_arm_pattern_edge, MatchArmBodyEdge, MatchArmPatternEdge, node_subtree_nodes, production_edge_label, resolved_reference_body_kind, symbol_eq, symbol_intern_lexeme } import v2.std.list_introduction { list_introduction_elements_optional, list_introduction_head_path } import v2.std.arrow_signature { ApplicationBindingRefused, @@ -120,6 +129,7 @@ import v2.std.inhabitance { position_record_field, position_let_annotation, position_direct_call_argument, + position_match_coverage, undecidable_argument_type_not_derived, undecidable_formal_unresolved, undecidable_where_predicate_subject_unmodelled @@ -130,6 +140,7 @@ import v2.std.diagnostic { ExternalContractUnknown, Locus, NonEmptyDiagnostics, + Suggested, UserInputBoundary, node_locus, Outcome, @@ -146,11 +157,15 @@ import v2.std.diagnostic { outcome_accepted, rejected_with_pending } +import v2.std.integer { integer_int_type_node } import v2.std.logic { bool_node } import v2.std.node { StructuralLabel, + symbol_intern_lexeme, host_text_type_node, symbol_type_node, + node_rebuild, + node_with_occurrence_id, Arrow, Atom, Bind, @@ -170,6 +185,7 @@ import v2.std.node { Symbol, Transform, TypeNode, + is_empty_conj_root, Value, Ambiguous, Found, @@ -185,11 +201,15 @@ import v2.std.type_binder { GenericTypeDecl, OpaqueTypeDecl, PlainTypeDecl, Type import v2.std.coercion { coercion_cast_crossing } import v2.std.node_query { FieldProjection, + NamedChildAmbiguous, + NamedChildFound, + NamedChildMissing, construct_tag_path_optional, declared_field_named, field_projection_optional, find_core_child, find_named_child, + named_child_lookup, node_positional_child_targets, pattern_wildcard_name } @@ -413,6 +433,17 @@ fn infer_match_non_exhaustive_diagnostic(node: Node) -> Diagnostic { } } +// Undecided exhaustiveness is not exhaustiveness: coverage fails closed (DESIGN 5 -- a failure arm +// refuses, never widens), and the refusal names its own reason rather than the non-exhaustive one, +// because the relation did not decide the arms wrong; it could not decide them. +fn infer_match_coverage_undecidable_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_match_coverage_undecidable, + at: node_locus(node: node), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + fn infer_transform_shape_invalid_diagnostic(node: Node) -> Diagnostic { Diagnostic { reason: ^infer_transform_shape_invalid, @@ -710,7 +741,7 @@ fn infer_product_child_evidence_edges( infer_formation_child_evidence_edges( children: node.children, entries: entries, - is_metadata: fn(edge) { arrow_signature_order_edge(parent: node, edge: edge) || arrow_named_edge_is_contract(e: edge) } + is_metadata: fn(edge) { arrow_signature_order_edge(parent: node, edge: edge) || arrow_named_edge_is_metadata(e: edge) } ) } @@ -1156,14 +1187,16 @@ fn infer_field_projection_facts( outcome_rejected(infer_receiver_declares_no_fields_diagnostic(node: node)) ReceiverDeclarationUnavailable { path: path } => Rejected { diagnostics: infer_closure_miss_refusal(resolved: resolved, path: path, miss: infer_receiver_declaration_unavailable_diagnostic(node: node)) } - ReceiverPayload { payload: payload } => + ReceiverArityMismatch => + outcome_rejected(infer_receiver_arity_mismatch_diagnostic(node: node)) + ReceiverPayload { payload: payload, instances: instances } => match declared_field_named(payload: payload, name: projection.field) { Present { value: declared } => bind_outcome_accepted( od: cd, inner: inferred_facts_from_derived_type( node: node, - derived_type: declared.type_node, + derived_type: infer_projected_field_type(field_type: declared.type_node, instances: instances), descent: Holds { value: descent_proof } ) ) @@ -1175,6 +1208,30 @@ fn infer_field_projection_facts( } } +// A FIELD OF AN INSTANTIATED GENERIC RECORD HAS ITS DECLARED TYPE AT THIS INSTANTIATION. A receiver +// typed Box reads Box's payload and binds Box's type parameters to the application's arguments in +// declared order, the same pairing infer_match_coproduct_of_type uses for a matched coproduct, so a field +// declared `item: T` projects as Int. A non-generic receiver binds nothing and keeps its field type +// exactly as declared. +fn infer_projected_field_type(field_type: Node, instances: List) -> Node { + if length(xs: instances) == 0 { + field_type + } else { + infer_type_instantiated(t: field_type, instances: instances) + } +} + +// AN APPLICATION WHOSE ARGUMENT COUNT DIFFERS FROM ITS HEAD'S DECLARED PARAMETERS has no instantiation +// to read a field at; that is a decided defect of the receiver's type, refused at the projection, never +// read past or left at the frontier. +fn infer_receiver_arity_mismatch_diagnostic(node: Node) -> Diagnostic { + Diagnostic { + reason: ^infer_reason_projection_receiver_type_arity_mismatch, + at: node_locus(node: node), + correction: Unavailable { reason: UserInputBoundary } + } +} + fn infer_field_not_declared_diagnostic(node: Node) -> Diagnostic { Diagnostic { reason: ^infer_reason_field_not_declared_on_receiver, @@ -1222,8 +1279,9 @@ fn infer_field_not_declared_diagnostic(node: Node) -> Diagnostic { // never a claim about the receiver's fields. The arm carries the path so a reader can name the declaration // that could not be retrieved. type ProjectionReceiver - = ReceiverPayload { payload: Node } + = ReceiverPayload { payload: Node, instances: List } | ReceiverTypeNotADeclaration + | ReceiverArityMismatch | ReceiverDeclarationUnavailable { path: QualifiedName } | ReceiverTypeUnderived @@ -1238,13 +1296,37 @@ fn infer_projection_receiver( match inferred_facts_resolved_type(facts: base_facts) { Violates { diagnostic: _ } => ReceiverTypeUnderived Holds { value: receiver_type } => - match declaration_reference_path_optional(node: receiver_type) { - Absent => ReceiverTypeNotADeclaration - Present { value: path } => - match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { - Absent => ReceiverDeclarationUnavailable { path: path } - Present { value: payload } => ReceiverPayload { payload: payload } + infer_projection_receiver_of_type(receiver_type: receiver_type, declarations: resolved.resolved_declarations) + } + } +} + +// THE RECEIVER'S DECLARATION AND INSTANTIATION, FROM ITS ESTABLISHED TYPE. A plain declaration +// reference binds nothing; an application binds the head's declared type parameters to its arguments +// in declared order. Separate from the facts lookup above so the type-to-payload step is judged on its +// own inputs. +fn infer_projection_receiver_of_type(receiver_type: Node, declarations: SymbolIndex) -> ProjectionReceiver { + let head_args = infer_type_head_and_args(t: receiver_type) + match declaration_reference_path_optional(node: head_args.head) { + Absent => ReceiverTypeNotADeclaration + Present { value: path } => + match symbol_index_lookup(index: declarations, qualified_path: path) { + Absent => ReceiverDeclarationUnavailable { path: path } + Present { value: payload } => + if length(xs: head_args.args) == 0 { + ReceiverPayload { payload: payload, instances: infer_no_type_variable_instances() } + } else { + let params = symbol_index_declared_type_params_at(index: declarations, qualified_path: path) + if length(xs: params) != length(xs: head_args.args) { + ReceiverArityMismatch + } else { + ReceiverPayload { + payload: payload, + instances: zip_map(a: params, b: head_args.args, f: fn(p, arg) { + TypeVariableInstance { binder: p, instance: arg } + }) } + } } } } @@ -1445,7 +1527,7 @@ fn infer_declaration_reference_facts_indexed( o: infer_facts_denoting( o: inferred_facts_from_derived_type( node: n, - derived_type: callable, + derived_type: infer_reference_value_type(callable: callable), descent: Holds { value: descent_proof } ), declaration: declared @@ -1457,6 +1539,18 @@ fn infer_declaration_reference_facts_indexed( } } +// A REFERENCE TO A `data` CONSTANT USES ITS VALUE, so it is typed at the constant's declared type, the +// codomain of the Arrow body lowering realizes it as (v2.std.node_query arrow_constant_member_codomain +// reads the ConstantMemberEdge marker that arm writes). Any other callable -- a nullary fn included -- +// denotes its function and is typed as the Arrow, exactly as before. The denotation recorded beside +// either is the declaration, so eval applies the constant through the same declaration infer typed. +fn infer_reference_value_type(callable: Node) -> Node { + match arrow_constant_member_codomain(arrow: callable) { + Present { value: declared_type } => declared_type + Absent => callable + } +} + // THE DENOTATION IS RECORDED HERE AND NOWHERE ELSE, because here is where the GUARDED reader answered. // eval cannot ask symbol_index_lookup -- it holds no index -- so without this the only routes open to // it are a weaker second resolution authority over the tree (which would accept references the guard @@ -1676,6 +1770,45 @@ fn infer_arrow_declared_return_type(arrow: Node) -> Optional { } } +// A CALL'S RESULT HAS THE TYPE ITS CALLEE DECLARES, KERNEL OR NOT. infer_arrow_declared_return_type +// knows only the kernel value types (and Bool by authority), so a callee declared to return a +// corpus type -- Nat, Outcome -- answered Absent, the call stayed underived, and every +// declared position it reached accepted it as undecidable. Every other declared type in this +// stage is already used as written (a parameter's declared node, a lexical binding's instantiated +// declaration); this reader now does the same. The kernel answer is consulted FIRST, so the Int and +// Bool paths are byte-identical. Otherwise the declared return node, alias-normalized through the one +// normalization (#13187) and instantiated with the instances the argument walk bound at THIS site, is +// the result. A return that still mentions one of the callee's own type parameters after +// instantiation is not derived (the counted frontier) -- never a fabricated instance; nor is a return +// that is not a type node. +fn infer_application_result_type( + arrow: Node, + instances: List, + declarations: SymbolIndex +) -> Optional { + match infer_arrow_declared_return_type(arrow: arrow) { + Present { value: kernel } => Present { value: kernel } + Absent => + match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) { + Absent => Absent + Present { value: ret } => + match ret.kind { + TypeNode { connective: _ } => + let declared = infer_type_instantiated( + t: infer_type_alias_normalized(t: ret, declarations: declarations), + instances: instances + ) + if inhabitance_node_mentions_type_variable(n: declared, type_variables: type_param_names(n: arrow)) { + optional_absent() + } else { + optional_present(value: declared) + } + ComputationNode { behavior: _ } => optional_absent() + } + } + } +} + fn infer_atom_binding_sym(node: Node) -> Optional { node_atom_identity_optional(node: node) } @@ -1780,15 +1913,194 @@ fn refinement_declared_carrier(index: SymbolIndex, source_type: Node) -> Optiona // unequal and were reported as an arm-type mismatch, while two structurally identical arms // compared equal and were reported as unified — both verdicts about node identity, not types. +// A LET BINDER CARRIES NO DECLARED TYPE; ITS TYPE IS ITS VALUE'S. The binding names the binder +// occurrence (binder_site), and the Bind node that owns it holds the value expression whose facts +// the fold already derived — so an operand that reads the binder derives the value's type instead +// of the GroundingNotDerived frontier (gunbc.recurring_failure_mode +// native_infer_accepts_only_bool_and_int_match_scrutinees: the enrolled variant claims' scrutinees +// are let-bound). A pattern binder or any other declared-less binding that is not a Bind still +// falls through to the facts. +// THE PUBLIC READ stays three-argument: an external caller holds the InferredTree only, whose +// facts are an opaque partial function, so the construct-facts entries thread is not recoverable +// there and the read falls back to the facts alone (the let-typed rule needs the entries). The +// four-argument read below is the inference-internal form, threaded through construct reads. fn infer_branch_operand_resolved_type_in_tree( node: Node, facts: InferredFacts, resolved: ResolvedTree, +) -> Witness { + infer_branch_operand_resolved_type_with_entries( + node: node, + facts: facts, + entries: Empty, + instances: infer_no_type_variable_instances(), + resolved: resolved + ) +} + +fn infer_branch_operand_resolved_type_with_entries( + node: Node, + facts: InferredFacts, + entries: List, + instances: List, + resolved: ResolvedTree, ) -> Witness { match dag_canonical_literal_from_node(node: node) { Accepted { value: literal, diagnostics: _ } => infer_binding_value_type_witness(binding: infer_literal_type_binding(literal: literal), at: node) - Rejected { diagnostics: _ } => inferred_facts_resolved_type(facts: facts) + Rejected { diagnostics: _ } => + match resolved_tree_lexical_binding(tree: resolved, reference: node) { + Present { value: binding } => + match binding.declared { + Present { value: declared } => + match infer_operand_declared_type_instantiated(declared: declared, instances: instances) { + Present { value: instantiated } => Holds { value: instantiated } + Absent => + match infer_let_binder_value_type_optional(entries: entries, binding: binding) { + Present { value: value_type } => Holds { value: value_type } + Absent => inferred_facts_resolved_type(facts: facts) + } + } + Absent => + match infer_let_binder_value_type_optional(entries: entries, binding: binding) { + Present { value: value_type } => Holds { value: value_type } + Absent => inferred_facts_resolved_type(facts: facts) + } + } + Absent => inferred_facts_resolved_type(facts: facts) + } + } +} + +// THE DECLARED TYPE AN OPERAND READS IS THE INSTANTIATED ONE. The lexical facts instantiate a +// binding's declared type with infer_frame_instantiated before deriving from it; a read that +// preempts the facts with the raw declared node skips exactly that step, so a generic coproduct's +// field (`Some { value: T }`) would hand the arm body the formal `T` instead of the scrutinee's +// argument (DESIGN 5: no fabricated plausible output). The instantiation is the one the construct +// read holds -- at a match, the scrutinee coproduct's instances. An atom no instance binds is held +// only when it names a type-denoting binding (a concrete literal type, by the same +// dag_binding_denotation test the facts fold applies); a free formal is NOT held -- the read falls +// through to the facts, the answer the caller had before the read existed. +fn infer_operand_declared_type_instantiated(declared: Node, instances: List) -> Optional { + match declared.kind { + TypeNode { connective: Atom { identity: id } } => + match type_variable_instance_lookup(instances: instances, binder: id) { + Present { value: instance } => optional_present(value: instance) + Absent => + match dag_binding_denotation(sym: id) { + Present { value: _ } => + match infer_declared_type_children_instantiated(children: declared.children, instances: instances) { + Present { value: children } => + if length(xs: children) == 0 { + optional_present(value: declared) + } else { + optional_present( + value: node_with_occurrence_id( + kind: TypeNode { connective: Atom { identity: id } }, + children: children, + occurrence_id: declared.occurrence_id + ) + ) + } + Absent => optional_absent() + } + Absent => optional_absent() + } + } + TypeNode { connective: _ } => + if any( + xs: node_positional_child_targets(node: declared), + predicate: fn(c) { infer_declared_type_mentions_free_formal(declared: c, instances: instances) } + ) { + optional_absent() + } else { + optional_present(value: declared) + } + ComputationNode { behavior: _ } => + if any( + xs: node_positional_child_targets(node: declared), + predicate: fn(c) { infer_declared_type_mentions_free_formal(declared: c, instances: instances) } + ) { + optional_absent() + } else { + optional_present(value: declared) + } + } +} + +// Whether a type node still names a formal no instance binds, anywhere in it. A concrete type +// (every atom either instantiated or type-denoting) is safe to hold as written; a node carrying a +// free formal is not, whatever connective wraps it. +fn infer_declared_type_mentions_free_formal(declared: Node, instances: List) -> Bool { + match declared.kind { + TypeNode { connective: Atom { identity: id } } => + match type_variable_instance_lookup(instances: instances, binder: id) { + Present { value: _ } => false + Absent => + match dag_binding_denotation(sym: id) { + Present { value: _ } => false + Absent => true + } + } + TypeNode { connective: _ } => + any( + xs: node_positional_child_targets(node: declared), + predicate: fn(c) { infer_declared_type_mentions_free_formal(declared: c, instances: instances) } + ) + ComputationNode { behavior: _ } => + any( + xs: node_positional_child_targets(node: declared), + predicate: fn(c) { infer_declared_type_mentions_free_formal(declared: c, instances: instances) } + ) + } +} + +// Substitution descends through the arguments: a compound declared type +// (`Box`, `FreeMonoid`) is not held with a formal riding inside it -- an argument no +// instance binds leaves the whole declared type unheld, and the read falls through to the facts +// rather than fabricate a partially formal type. +fn infer_declared_type_children_instantiated( + children: FreeMonoid, + instances: List +) -> Optional> { + match children { + Empty => optional_present(value: Empty) + Cons { head: c, tail: rest } => + match infer_operand_declared_type_instantiated(declared: c.target, instances: instances) { + Present { value: c2 } => + match infer_declared_type_children_instantiated(children: rest, instances: instances) { + Present { value: rest2 } => + optional_present(value: Cons { head: Edge { label: c.label, target: c2 }, tail: rest2 }) + Absent => Absent + } + Absent => Absent + } + } +} + +// THE LET'S VALUE IS THE LET'S TYPE. The binding carries the value node beside the binder site -- +// resolve's Bind frame mints it where the Bind node is in hand -- so the read is one map entry and +// one fold-entry lookup, never a tree walk. Absent -- the binding carries no value (a parameter or +// pattern binder), or the value's own facts are underived, as for a let over a still-frontier +// expression -- leaves the caller's answer exactly as it stood. +fn infer_let_binder_value_type_optional( + entries: List, + binding: LexicalBinding, +) -> Optional { + match binding.value { + Absent => optional_absent() + Present { value: value_target } => + match lookup_inferred_facts_in_entries(entries: entries, key: value_target) { + Absent => optional_absent() + Present { value: facts } => inferred_facts_resolved_type_optional(facts: facts) + } + } +} + +fn inferred_facts_resolved_type_optional(facts: InferredFacts) -> Optional { + match facts.grounding { + DerivedGrounding { grounding: g } => optional_present(value: g.witness.structural.evidence) + GroundingNotDerived { node: _ } => optional_absent() } } @@ -1853,9 +2165,9 @@ fn infer_branch_type_is_int(type_node: Node) -> Bool { // 0/1 authority as eval allocate_literal), not a post-unification rewrite. dissolve-on: bool // keywords carry Int type in Int-valued match bodies at normalize/ingest. scaffold-widen: Int // scrutinee on bool-literal match emits infer_match_int_scrutinee_bool_literal_scaffold diagnostic -// (countable on Accepted path). paired-scaffold: emit half = -// target_value_expr_int_bool_octet_channel_dissolution_note (target_model.dag) — dissolve both -// halves together on octet↔bool codec inhabitation. +// (countable on Accepted path). emit match-seam production was uprooted after the Bool de-fork +// (target_value_expr_int_bool_octet_channel_dissolution_note). Dissolve this diagnostic on +// octet↔bool codec inhabitation. fn infer_match_bool_consumer_diagnostics_with_int_scrutinee_scaffold( consumer: Node, @@ -1965,6 +2277,71 @@ fn infer_match_bool_arm_rows_accept( } } +// THE ARMS' COVERED CARRIER, JUDGED BY THE ONE RELATION. The covered carrier is the scrutinee +// coproduct's own alternative edges, rebuilt with only the alternatives the arms name, in +// declaration order -- arm order and arm repetition cannot flatter the answer, and the relation +// (declared_type_inhabitance over PositionMatchCoverage) answers exhaustive exactly when the +// carrier structurally equals the whole coproduct. A wildcard arm covers every alternative, so it +// contributes the whole coproduct. This replaces the per-tag inline count +// (infer_coproduct_rows_cover) that stood in while the relation's carrier was missing: +// gunbc.recurring_failure_mode native_infer_accepts_only_bool_and_int_match_scrutinees. +// +// The obligation carries no type variables because neither carrier poses an instantiation +// question: both are projections of the same declaration (the declared Disj and a subset of its +// edges), so the exact-structural-equality fold compares atom-for-atom within one declaration and +// its verdict cannot depend on how the parameters are instantiated at the scrutinee. +fn infer_coproduct_coverage_obligation( + node: Node, + scrutinee_target: Node, + coproduct: InferMatchCoproduct, + rows: List, +) -> Outcome { + let covered_children = fold(coproduct.variants.children, init: Empty, f: fn(acc, e) { + match e.label { + Authored { name: variant_tag } => + if any(xs: rows, predicate: fn(row) { + match row.covers { + CoversAll => true + CoversVariant { tag: t } => t == variant_tag + } + }) { + list_snoc_item(xs: acc, item: e) + } else { + acc + } + StructuralLabel { label: _ } => acc + Positional => acc + } + }) + let covered_carrier = node_rebuild(n: coproduct.variants, children: covered_children) + match declared_type_inhabitance( + obligation: DeclaredTypeObligation { + position: position_match_coverage(match_node: node, scrutinee: scrutinee_target), + parameter_identity: infer_coproduct_coverage_parameter_identity(coproduct: coproduct), + declared: coproduct.variants, + produced: covered_carrier, + produced_declared_carrier: Absent, + application: node, + type_variables: Empty + } + ) { + Inhabits { homomorphism: _ } => outcome_accepted(value: covered_carrier) + InhabitanceUndecidable { reason: _ } => + outcome_rejected(infer_match_coverage_undecidable_diagnostic(node: node)) + InhabitanceRefused { find_witness_reason: _ } => + outcome_rejected(infer_match_non_exhaustive_diagnostic(node: node)) + } +} + +// The obligation's parameter identity names the variant in a refusal: the declaration path the +// scrutinee's type resolved through, by its last segment. +fn infer_coproduct_coverage_parameter_identity(coproduct: InferMatchCoproduct) -> Symbol { + match qualified_name_last_segment(qn: coproduct.path) { + Present { value: s } => s + Absent => symbol_intern_lexeme(lexeme: "coproduct_coverage") + } +} + fn infer_unify_branch_arm_types( then_node: Node, then_facts: InferredFacts, @@ -2234,9 +2611,11 @@ fn infer_match_bool( match infer_bounded_lattice_consumer_gate(consumer: node, partials: partials) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: _, diagnostics: cd } => - match infer_branch_operand_resolved_type_in_tree( + match infer_branch_operand_resolved_type_with_entries( node: scrutinee_target, facts: scrutinee_facts, + entries: entries, + instances: infer_no_type_variable_instances(), resolved: resolved ) { Violates { diagnostic: d } => outcome_rejected(d) @@ -2289,7 +2668,7 @@ fn infer_match_arm_pattern_is_literal(arm: Node) -> Bool { // patterns are neither literals nor constructors -- bare atoms naming nothing, say -- keeps the literal // family's refusal (infer_match_pattern_not_bool_literal): routing it here instead would meet an // untyped scrutinee and accept it at the frontier, widening a refusal into an admission -// (v2.test.claim.manual.match_infer_fail_open_audit complement_body_real_infer_refuses_unsupported_pattern_holds). +// (v2.test.claim.manual.match_infer_fail_open_audit match_infer_fail_open_audit_rejects_holds). fn infer_match_arm_pattern_is_constructor(arm: Node) -> Bool { match find_core_child(root: arm, marker: MatchArmPatternEdge) { Rejected { diagnostics: _ } => false @@ -2333,6 +2712,7 @@ fn infer_match( // index does not hold -- declares no variants to eliminate, and a count of arguments different from the // count of parameters binds no instance at all. type InferMatchCoproduct { + path: QualifiedName variants: Node instances: List } @@ -2369,17 +2749,18 @@ fn infer_match_coproduct_of_type(scrutinee_type: Node, resolved: ResolvedTree) - match declaration_reference_path_optional(node: head_args.head) { Absent => CoproductNotDeclared Present { value: path } => - match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { Absent => CoproductNotDeclared Present { value: declared } => match declared.kind { TypeNode { connective: Disj } => - let params = symbol_index_declared_type_params_at(index: resolved.symbol_index, qualified_path: path) + let params = symbol_index_declared_type_params_at(index: resolved.resolved_declarations, qualified_path: path) if length(xs: params) != length(xs: head_args.args) { CoproductArityMismatch } else { CoproductRead { coproduct: InferMatchCoproduct { + path: path, variants: declared, instances: zip_map(a: params, b: head_args.args, f: fn(p, arg) { TypeVariableInstance { binder: p, instance: arg } @@ -2413,15 +2794,6 @@ fn infer_match_variant_payload(variants: Node, tag: Symbol) -> Optional { }) } -fn infer_match_variant_tags(variants: Node) -> List { - fold(variants.children, init: Empty, f: fn(acc, e) { - match e.label { - Authored { name: tag } => list_snoc_item(xs: acc, item: tag) - StructuralLabel { label: _ } => acc - Positional => acc - } - }) -} // A VARIANT FIELD'S TYPE AT THIS INSTANTIATION, OR NOTHING. A field declared as one of the // declaration's type parameters is that parameter's argument -- a type the USE SITE resolved. A field @@ -2652,14 +3024,6 @@ fn infer_coproduct_arm_row(arm: Node, coproduct: InferMatchCoproduct) -> Outcome } } -fn infer_coproduct_rows_cover(rows: List, tag: Symbol) -> Bool { - any(xs: rows, predicate: fn(row) { - match row.covers { - CoversAll => true - CoversVariant { tag: t } => t == tag - } - }) -} // THE ARM BODIES' ONE TYPE. Two DERIVED body types that differ are a mismatch and refuse; a body whose // type is not derived leaves the match at the frontier with one located advisory per such body, and @@ -2674,13 +3038,14 @@ type InferArmBodyTypes { fn infer_coproduct_arm_body_types( rows: List, entries: List, + instances: List, resolved: ResolvedTree ) -> InferArmBodyTypes { fold(rows, init: InferArmBodyTypes { derived: Absent, mismatch: false, underived: None }, f: fn(acc, row) { let body_type = match lookup_inferred_facts_in_entries(entries: entries, key: row.body) { Absent => Absent Present { value: facts } => - match infer_branch_operand_resolved_type_in_tree(node: row.body, facts: facts, resolved: resolved) { + match infer_branch_operand_resolved_type_with_entries(node: row.body, facts: facts, entries: entries, instances: instances, resolved: resolved) { Holds { value: t } => Present { value: t } Violates { diagnostic: _ } => Absent } @@ -2728,6 +3093,7 @@ fn infer_match_coproduct_rows( coproduct: InferMatchCoproduct, entries: List, resolved: ResolvedTree, + scrutinee_target: Node, cd: Diagnostics ) -> Outcome { match fold(arms, init: Accepted { value: Empty, diagnostics: None }, f: fn(acc, arm) { @@ -2742,32 +3108,32 @@ fn infer_match_coproduct_rows( }) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: rows, diagnostics: _ } => - if any(xs: infer_match_variant_tags(variants: coproduct.variants), predicate: fn(tag) { - !infer_coproduct_rows_cover(rows: rows, tag: tag) - }) { - outcome_rejected(infer_match_non_exhaustive_diagnostic(node: node)) - } else { - let bodies = infer_coproduct_arm_body_types(rows: rows, entries: entries, resolved: resolved) - if bodies.mismatch { - outcome_rejected(infer_match_arm_type_mismatch_diagnostic(node: node)) - } else { - match bodies.underived { - Some { diagnostics: _ } => infer_match_frontier(node: node, cd: cd, advisories: bodies.underived) - None => - match bodies.derived { - Absent => infer_match_frontier(node: node, cd: cd, advisories: None) - Present { value: t } => - match infer_descent_witness_for_node(n: node) { - Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } - Holds { value: descent_proof } => - bind_outcome( - o: inferred_facts_from_derived_type(node: node, derived_type: t, descent: Holds { value: descent_proof }), - f: fn(facts) { Accepted { value: facts, diagnostics: cd } } - ) + match infer_coproduct_coverage_obligation(node: node, scrutinee_target: scrutinee_target, coproduct: coproduct, rows: rows) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: _, diagnostics: _ } => + { + let bodies = infer_coproduct_arm_body_types(rows: rows, entries: entries, instances: coproduct.instances, resolved: resolved) + if bodies.mismatch { + outcome_rejected(infer_match_arm_type_mismatch_diagnostic(node: node)) + } else { + match bodies.underived { + Some { diagnostics: _ } => infer_match_frontier(node: node, cd: cd, advisories: bodies.underived) + None => + match bodies.derived { + Absent => infer_match_frontier(node: node, cd: cd, advisories: None) + Present { value: t } => + match infer_descent_witness_for_node(n: node) { + Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } + Holds { value: descent_proof } => + bind_outcome( + o: inferred_facts_from_derived_type(node: node, derived_type: t, descent: Holds { value: descent_proof }), + f: fn(facts) { Accepted { value: facts, diagnostics: cd } } + ) + } } } + } } - } } } } @@ -2794,7 +3160,7 @@ fn infer_match_coproduct( match lookup_inferred_facts_in_entries(entries: entries, key: scrutinee_target) { Absent => outcome_rejected(infer_facts_lookup_miss_diagnostic(key: scrutinee_target)) Present { value: scrutinee_facts } => - match infer_branch_operand_resolved_type_in_tree(node: scrutinee_target, facts: scrutinee_facts, resolved: resolved) { + match infer_branch_operand_resolved_type_with_entries(node: scrutinee_target, facts: scrutinee_facts, entries: entries, instances: infer_no_type_variable_instances(), resolved: resolved) { Violates { diagnostic: _ } => infer_match_frontier( node: node, @@ -2806,7 +3172,15 @@ fn infer_match_coproduct( CoproductNotDeclared => outcome_rejected(infer_match_scrutinee_not_coproduct_diagnostic(node: scrutinee_target)) CoproductArityMismatch => outcome_rejected(infer_match_type_argument_arity_mismatch_diagnostic(node: scrutinee_target)) CoproductRead { coproduct: coproduct } => - infer_match_coproduct_rows(node: node, arms: arms, coproduct: coproduct, entries: entries, resolved: resolved, cd: cd) + infer_match_coproduct_rows( + node: node, + arms: arms, + coproduct: coproduct, + scrutinee_target: scrutinee_target, + entries: entries, + resolved: resolved, + cd: cd + ) } } } @@ -3041,14 +3415,17 @@ fn infer_unify_transform_operand_types( left_facts: InferredFacts, right_node: Node, right_facts: InferredFacts, + entries: List, resolved: ResolvedTree, ) -> Outcome { - match infer_branch_operand_resolved_type_in_tree(node: left_node, facts: left_facts, resolved: resolved) { + match infer_branch_operand_resolved_type_with_entries(node: left_node, facts: left_facts, entries: entries, instances: infer_no_type_variable_instances(), resolved: resolved) { Violates { diagnostic: d } => outcome_rejected(d) Holds { value: left_type } => - match infer_branch_operand_resolved_type_in_tree( + match infer_branch_operand_resolved_type_with_entries( node: right_node, facts: right_facts, + entries: entries, + instances: infer_no_type_variable_instances(), resolved: resolved ) { Violates { diagnostic: d } => outcome_rejected(d) @@ -3379,6 +3756,11 @@ fn infer_judge_declared_position( reason: reason ) Absent => + match infer_literal_elaborates_at(literal_node: produced_at, expected: infer_declared_type_denotation(declared: formal.declared), declarations: declarations) { + InferLiteralElaboratesTo { destination_type: _ } => Accepted { value: true, diagnostics: None } + InferLiteralElaborationRefused { cause: cause } => + outcome_rejected(Diagnostic { reason: cause, at: node_locus(node: produced_at), correction: Unavailable { reason: CorrectionNotModeled } }) + InferLiteralNotElaborated => match lookup_inferred_facts_in_entries(entries: entries, key: produced_at) { Absent => infer_inhabitance_undecidable_accepted( @@ -3412,8 +3794,10 @@ fn infer_judge_declared_position( } } } + } } + fn infer_no_type_variable_instances() -> List { Empty } @@ -3525,22 +3909,29 @@ fn infer_type_alias_unfolded(t: Node, declarations: SymbolIndex) -> Optional Node { - match infer_type_alias_unfolded(t: t, declarations: declarations) { - Present { value: unfolded } => infer_type_alias_normalized(t: unfolded, declarations: declarations) + match infer_kernel_value_type_optional(t: t) { + Present { value: k } => k Absent => - match t.kind { - TypeNode { connective: Instantiation } => - Node { - kind: t.kind, - children: fold(t.children, init: Empty, f: fn(acc, e) { - list_snoc_item(xs: acc, item: Edge { label: e.label, target: infer_type_alias_normalized(t: e.target, declarations: declarations) }) - }), - occurrence_id: t.occurrence_id + match infer_type_alias_unfolded(t: t, declarations: declarations) { + Present { value: unfolded } => infer_type_alias_normalized(t: unfolded, declarations: declarations) + Absent => + match t.kind { + TypeNode { connective: Instantiation } => + Node { + kind: t.kind, + children: fold(t.children, init: Empty, f: fn(acc, e) { + list_snoc_item(xs: acc, item: Edge { label: e.label, target: infer_type_alias_normalized(t: e.target, declarations: declarations) }) + }), + occurrence_id: t.occurrence_id + } + TypeNode { connective: _ } => t + ComputationNode { behavior: _ } => t } - TypeNode { connective: _ } => t - ComputationNode { behavior: _ } => t } } } @@ -3563,6 +3954,36 @@ fn infer_match_generic_formal_or_carrier(formal: Node, produced: Node, type_para } } +// THE KERNEL VALUE TYPE AND THE CORPUS DECLARATION ARE ONE TYPE. `v2.std.integer.Int` +// (`type Int = GroupCompletion`) and the kernel atom `dag_binding_type_int` +// (integer_int_type_node) answer the same value-type question. Alias-normalizing first unfolds +// that alias to GroupCompletion and then the judge compares that product to the kernel atom. +// Recognition is BY AUTHORITY, NOT BY SPELLING: a declaration reference joins only when +// dag_kernel_type_declaration_binding_optional admits the FULL path (the same roster resolve +// uses). A last-segment `Int` would recapture a module's own `type Int = | Mine` and admit +// `let y: Int = 1` (gunbc.recurring_failure_mode kernel_type_spelling_captured_a_module_declaration; +// review 77259). An atom joins only through dag_binding_denotation (binding -> value type). +fn infer_kernel_value_type_optional(t: Node) -> Optional { + if infer_type_equal_ignoring_provenance(a: t, b: integer_int_type_node()) { + optional_present(value: integer_int_type_node()) + } else if infer_type_equal_ignoring_provenance(a: t, b: bool_node()) { + optional_present(value: bool_node()) + } else { + match declaration_reference_path_optional(node: t) { + Present { value: path } => + match dag_kernel_type_declaration_binding_optional(path: path) { + Present { value: binding } => dag_binding_denotation(sym: binding) + Absent => optional_absent() + } + Absent => + match node_atom_identity_optional(node: t) { + Present { value: id } => dag_binding_denotation(sym: id) + Absent => optional_absent() + } + } + } +} + // THE ONE PRODUCER OF A DeclaredTypeObligation in infer: every obligation -- a call argument, a declared return, a // fold member or carrier, a generic formal -- is built here, so the ONE alias rule (infer_type_alias_normalized) // reaches every judgment: the declared type, the produced type and the produced type's refinement carrier are @@ -3591,6 +4012,138 @@ fn infer_declared_type_obligation( } } +// A LITERAL ELABORATES AT AN EXPECTED TYPE THROUGH THE DECLARED TABLES, at a declared position (the declared-type +// obligation, infer_judge_declared_position). Two tables, which partition the declarations they name +// (std.literal_elaboration, "a declaration is never both"): +// - KERNEL GROUNDING (gunbc.structural_realization_bindings kernel_grounding_rows): the carrier IS the kernel value, +// so a literal at it is its own value (DirectLiteral) when the row's declared range admits it +// (std.literal_elaboration kernel_grounding_admits). std.nat Nat is here with min 0: 0 and 3 elaborate at Nat, +// and `-1` refuses, DERIVED from the range (infer_reason_literal_outside_kernel_grounding_range) -- calm-boar-904, +// ruling A on review 77219. v2 infer keeps Nat and Int distinct TYPES; the grounding only says which literals +// denote a value of Nat, and Nat is realized as the kernel integer, so the literal's value does not move. +// - LITERAL HOMOMORPHISM (literal_homomorphism_rows): a STRUCTURAL destination supplies the literal through its own +// constructors (test.fixture.structural_peano_nat StructuralNat), via std.literal_elaboration elaborate_literal_at, +// the same function and rows the seed consults. A negative literal has no image under a Peano unfold. +// NOT READ BY THE OPERATOR UNIFY, AND THAT IS STATED: an operand partner would elaborate a literal only beside a +// Nat, and Nat carries no algebra row, so `n + 1` refuses at the operator either way, while `==`/`<` are not +// judged at all (gunbc.recurring_failure_mode comparison_operands_never_judged_in_v2_infer). +// +// SCOPE, STATED: integer literals. A string literal at the free monoid over Char (the Unicode scalar row) is +// not elaborated here; that crossing stays the text-boundary lane's (DESIGN section 4, Ruling 3). +type InferLiteralElaboration + = InferLiteralElaboratesTo { destination_type: Node } + | InferLiteralNotElaborated + | InferLiteralElaborationRefused { cause: Symbol } + +fn infer_type_declaration_ref(t: Node) -> Optional { + match declaration_reference_path_optional(node: infer_type_head_and_args(t: t).head) { + Absent => optional_absent() + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Absent => optional_absent() + Present { value: leaf } => + optional_present(value: decl_ref(module_path: qualified_name_to_dotted_string(qn: qualified_name_init(qn: path)), decl_name: symbol_lexeme(sym: leaf))) + } + } +} + +// A NEGATED INTEGER LITERAL (`-1`) lowers to a unary Transform whose operator is the `-` token or the subtraction +// wire resolve canonicalizes it to, over one integer literal. It is not a literal node, so without this reading it +// reached the judge underived and was admitted at ANY declared type -- including Nat, which has no negative +// values. Its value is the negated magnitude, judged like any literal's: a grounded destination's range decides +// it, a Peano unfold has no image for it, and a destination with no row (Int) is not this function's subject. +fn infer_negated_int_literal_value(node: Node) -> Optional { + match node.kind { + ComputationNode { behavior: Transform } => + let targets = node_positional_child_targets(node: node) + if length(xs: targets) != 2 { + optional_absent() + } else { + match list_at_optional(xs: targets, index: 0) { + Absent => optional_absent() + Present { value: op } => + if infer_operator_is_minus_token(op_node: op) || canonical_operation_wire_matches_operation(wire: op, operation: canonical_operation_op_sub()) { + match list_at_optional(xs: targets, index: 1) { + Absent => optional_absent() + Present { value: operand } => + match dag_canonical_literal_from_node(node: operand) { + Accepted { value: DagCanonicalIntLiteral { magnitude: m }, diagnostics: _ } => optional_present(value: 0 - m) + Accepted { value: _, diagnostics: _ } => optional_absent() + Rejected { diagnostics: _ } => optional_absent() + } + } + } else { + optional_absent() + } + } + } + ComputationNode { behavior: _ } => optional_absent() + TypeNode { connective: _ } => optional_absent() + } +} + +fn infer_operator_is_minus_token(op_node: Node) -> Bool { + match node_atom_identity_optional(node: op_node) { + Present { value: id } => id == ^dag_token_minus + Absent => false + } +} + +// The table's own refusal, carried through as its own typed reason rather than flattened to a generic one +// (review 76326): a duplicated homomorphism row is an authoring defect in literal_homomorphism_rows. +fn infer_literal_elaboration_refusal_reason(cause: LiteralElaborationRefusal) -> Symbol { + match cause { + HomomorphismDuplicated { source_kind: _, destination: _, row_count: _ } => ^infer_reason_literal_homomorphism_duplicated + } +} + +fn infer_literal_elaborates_at(literal_node: Node, expected: Node, declarations: SymbolIndex) -> InferLiteralElaboration { + match dag_canonical_literal_from_node(node: literal_node) { + Rejected { diagnostics: _ } => + match infer_negated_int_literal_value(node: literal_node) { + Absent => InferLiteralNotElaborated + Present { value: v } => infer_int_literal_value_at(value: v, expected: expected, declarations: declarations) + } + Accepted { value: literal, diagnostics: _ } => + match literal { + DagCanonicalIntLiteral { magnitude: m } => infer_int_literal_value_at(value: m, expected: expected, declarations: declarations) + DagCanonicalBoolLiteral { value: _ } => InferLiteralNotElaborated + DagCanonicalSymbolLiteral { symbol: _ } => InferLiteralNotElaborated + DagCanonicalStringLiteral { value: _ } => InferLiteralNotElaborated + } + } +} + +// An integer value at an expected type: the grounding row first (the carrier IS the kernel value, its range decides), +// else the homomorphism table (a structural destination). The two never both answer, by the partition law. +fn infer_int_literal_value_at(value: Int, expected: Node, declarations: SymbolIndex) -> InferLiteralElaboration { + let wanted = infer_type_alias_normalized(t: expected, declarations: declarations) + match infer_type_declaration_ref(t: wanted) { + Absent => InferLiteralNotElaborated + Present { value: destination } => + match kernel_grounding_for(rows: kernel_grounding_rows, source_kind: KernelIntLiteral, carrier: destination) { + KernelGroundingFound { row: row } => + if kernel_grounding_admits(row: row, value: value) { + InferLiteralElaboratesTo { destination_type: wanted } + } else { + InferLiteralElaborationRefused { cause: ^infer_reason_literal_outside_kernel_grounding_range } + } + KernelGroundingAmbiguous { row_count: _ } => InferLiteralElaborationRefused { cause: ^infer_reason_kernel_grounding_duplicated } + KernelGroundingAbsent => + match elaborate_literal_at(rows: literal_homomorphism_rows, source_kind: KernelIntLiteral, destination: destination, element: none, destination_realizes_natively: false) { + ViaHomomorphism { homomorphism: _ } => + if value < 0 { + InferLiteralElaborationRefused { cause: ^infer_reason_negative_literal_has_no_image_in_destination } + } else { + InferLiteralElaboratesTo { destination_type: wanted } + } + DirectLiteral => InferLiteralNotElaborated + LiteralElaborationRefused { cause: c } => InferLiteralElaborationRefused { cause: infer_literal_elaboration_refusal_reason(cause: c) } + } + } + } +} + // A REFINEMENT IS NOT A TRANSPARENT ALIAS. `type Pos = Int where positive` -- and a nominal brand, which is // spelled the same way (`where brand(..)`, std.types WherePredicateMarker) -- denotes a type distinct from its // carrier, so it never unfolds: normalizing it would admit a raw carrier where the refined or branded type is @@ -3760,17 +4313,26 @@ fn infer_judge_application_argument_instantiating( } } +// THE SITE'S INSTANTIATION IS KEPT, NOT DISCARDED. The argument walk already ends with the call's +// TypeVariableInstance list (infer_judge_formal_args); the result step reads it to instantiate a +// generic declared return (infer_application_result_type), so the one instantiation that judged the +// arguments is the one that types the result. An unresolved formal set judges nothing and binds no +// instance. fn infer_application_argument_inhabitance( node: Node, entries: List, declarations: SymbolIndex -) -> Outcome { +) -> Outcome> { match infer_application_formals(node: node, entries: entries) { FormalsUnresolved => - infer_inhabitance_undecidable_accepted( + match infer_inhabitance_undecidable_accepted( application: node, reason: undecidable_formal_unresolved() - ) + ) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: _, diagnostics: d } => + Accepted { value: infer_no_type_variable_instances(), diagnostics: d } + } FormalsOrderRefused { reason: reason } => outcome_rejected(application_parameter_order_diagnostic(application: node, reason: reason)) FormalsInDeclaredOrder { formals: formals } => @@ -3786,7 +4348,7 @@ fn infer_application_argument_inhabitance( type_params: infer_application_type_params(node: node, entries: entries) ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: _, diagnostics: d } => Accepted { value: true, diagnostics: d } + Accepted { value: instances, diagnostics: d } => Accepted { value: instances, diagnostics: d } } } } @@ -3975,6 +4537,7 @@ fn infer_transform_binary_infix( left_facts: left_facts, right_node: right_target, right_facts: right_facts, + entries: entries, resolved: resolved ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } @@ -4309,7 +4872,7 @@ fn infer_list_element_type(element: Node, entries: List, res match lookup_inferred_facts_in_entries(entries: entries, key: element) { Absent => outcome_rejected(infer_facts_lookup_miss_diagnostic(key: element)) Present { value: facts } => - match infer_branch_operand_resolved_type_in_tree(node: element, facts: facts, resolved: resolved) { + match infer_branch_operand_resolved_type_with_entries(node: element, facts: facts, entries: entries, instances: infer_no_type_variable_instances(), resolved: resolved) { Violates { diagnostic: d } => outcome_rejected(d) Holds { value: t } => outcome_accepted(value: t) } @@ -4456,14 +5019,15 @@ fn infer_gather_application_row_on_entries( await_transform_row: false, children_remaining: 0 ) - Accepted { value: _, diagnostics: id } => + Accepted { value: instances, diagnostics: id } => let merged_pending = diagnostics_merge(outer: pending, inner: id) match infer_transform_derived_optional( node: node, partials: partials, entries: entries, resolved: resolved, - arguments_decided: infer_diagnostics_none(d: id) + arguments_decided: infer_diagnostics_none(d: id), + instances: instances ) { Present { value: derived } => match derived { @@ -4599,13 +5163,14 @@ fn infer_transform_derived_optional( entries: List, resolved: ResolvedTree, arguments_decided: Bool, + instances: List, ) -> Optional> { if infer_transform_is_binary_infix_algebra_shape(node: node) { optional_present(value: infer_transform_binary_infix(node: node, partials: partials, entries: entries, resolved: resolved)) } else if infer_transform_is_cast(node: node) { infer_transform_cast_optional(node: node, entries: entries, resolved: resolved) } else if arguments_decided { - infer_transform_application_optional(node: node, entries: entries) + infer_transform_application_optional(node: node, entries: entries, instances: instances, declarations: resolved.resolved_declarations) } else { optional_absent() } @@ -4617,7 +5182,12 @@ fn infer_transform_derived_optional( // derived -- so arrow introduction has checked its body against that return. An underived callee, // a non-Arrow operator, or a return the language join does not denote stays on the counted // frontier: Absent here, never an admission. -fn infer_transform_application_optional(node: Node, entries: List) -> Optional> { +fn infer_transform_application_optional( + node: Node, + entries: List, + instances: List, + declarations: SymbolIndex, +) -> Optional> { match infer_application_callee_arrow_with_facts(node: node, entries: entries) { Absent => optional_absent() Present { value: arrow } => @@ -4630,7 +5200,7 @@ fn infer_transform_application_optional(node: Node, entries: List optional_absent() Present { value: return_type } => optional_present( @@ -5300,7 +5870,7 @@ fn infer_gather_fold_step( if acc.failed { acc } else if arrow_signature_order_edge(parent: acc.node, edge: edge) - || arrow_named_edge_is_contract(e: edge) + || arrow_named_edge_is_metadata(e: edge) || infer_declaration_binder_edge(parent: acc.node, edge: edge) || match_arm_pattern_edge(parent: acc.node, edge: edge) || (resolved_reference_body_kind(target: acc.node) != NotMarkedReference) { @@ -5483,10 +6053,13 @@ fn infer_gather_fold_step_merged( // trigger are rostered in gunbc.recurring_failure_mode // construct_field_value_not_typed_against_the_record_declaration. // -// ONLY A DECLARED RECORD IS READ HERE. The index says which kind of payload a path denotes -// (symbol_index_declared_payload_at); a variant arm's construct, a tag the guarded reader does not -// answer for, and a Conj with no tag at all are ConstructNotARecord and keep product introduction, -// where an underived tag leaves them on the counted frontier as before. +// ONLY A DECLARED RECORD OR VARIANT ARM IS READ HERE. The index says which kind of payload a path +// denotes (symbol_index_declared_payload_at); a tag the guarded reader does not answer for and a +// Conj with no tag at all are ConstructNotARecord and keep product introduction, where an underived +// tag leaves them on the counted frontier as before. A VARIANT ARM's construct is read at its own +// path (the arm declares the fields) and derives the VARIANT's type: the declaration reference of +// the arm path's parent (v2.compiler.body_lowering_fold variant_fields_unlowered_on_the_native_route +// is retired by this read plus the variant match row beside it). type InferRecordFormals = RecordFormalsResolved { payload: Node } | RecordFormalsAuthored { payload: Node } @@ -5498,8 +6071,20 @@ type InferRecordConstruct { type_params: List } +// A variant arm's construct carries the arm path it was read at and the variant path whose +// declaration reference names the construction's type. The arm's fields are the formals; the +// variant's declared type params are the ones its fields may cite. +type InferVariantArmConstruct { + record: Node + arm_path: QualifiedName + variant_path: QualifiedName + formals: InferRecordFormals + type_params: List +} + type InferConstructRead = ConstructIsRecord { construct: InferRecordConstruct } + | ConstructIsVariantArm { construct: InferVariantArmConstruct } | ConstructNotARecord fn infer_record_formals(resolved: ResolvedTree, path: QualifiedName, authored: Node) -> InferRecordFormals { @@ -5516,6 +6101,46 @@ fn infer_record_formals_payload(formals: InferRecordFormals) -> Node { } } +// ONE MINT FOR BOTH PATH FORMS. The tag's path is either the variant's declared path (the +// containment form: `p.Seal.Boxed`) or the module-level unique-variant alias (`p.Boxed`), whose +// carrier the index carries beside it; by here the path IS the declared path either way, so the +// construction types by the declaring coproduct, its params, and the variant's own payload. +fn infer_variant_arm_construct( + reference: Node, + resolved: ResolvedTree, + declared_path: QualifiedName +) -> InferConstructRead { + match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: declared_path) { + Absent => ConstructNotARecord + Present { value: resolved_decl } => + match qualified_name_init(qn: declared_path) { + Empty => ConstructNotARecord + Cons { head: _, tail: _ } => + ConstructIsVariantArm { + construct: InferVariantArmConstruct { + record: reference, + arm_path: declared_path, + variant_path: qualified_name_init(qn: declared_path), + formals: infer_record_formals(resolved: resolved, path: declared_path, authored: resolved_decl), + type_params: symbol_index_declared_type_params_at( + index: resolved.resolved_declarations, + qualified_path: qualified_name_init(qn: declared_path) + ) + } + } + } + } +} + +// A CONSTRUCTION'S TAG IS A RESOLVED REFERENCE, BARE OR QUALIFIED ALIKE: resolve rewrites the tag +// to the declaration it binds (the qualified door and the atom walk meet in resolve_atom_bound, the +// same producer the bare door uses), so the variant's declaring path is the reference's own path -- +// no second lookup beside it. Reads go through resolved_declarations: declarations with resolved +// bodies (gunbc#12629). The global bare index is dead as a mechanism (03_resolve at the global_bare +// comment) and names no read here. THE DECLARING PATH OF A VARIANT-ARM PAYLOAD PATH: for the +// variant's own row this is the row itself (no claimants); for its module-level alias the binding +// filed by symbol_index_fill_containment_node answers the variant's declaring path. One authority, +// no parallel carrier map. fn infer_record_construct_read(node: Node, resolved: ResolvedTree) -> InferConstructRead { match construct_tag_optional(n: node) { Absent => ConstructNotARecord @@ -5523,21 +6148,32 @@ fn infer_record_construct_read(node: Node, resolved: ResolvedTree) -> InferConst match declaration_reference_path_optional(node: reference) { Absent => ConstructNotARecord Present { value: path } => - match symbol_index_declared_payload_at(index: resolved.symbol_index, qualified_path: path) { + match symbol_index_declared_payload_at(index: resolved.resolved_declarations, qualified_path: path) { Absent => ConstructNotARecord Present { value: kind } => match kind { - VariantArmPayload => ConstructNotARecord + VariantArmPayload => + infer_variant_arm_construct( + reference: reference, + resolved: resolved, + declared_path: symbol_index_declaring_path_of( + index: resolved.resolved_declarations, + target: path + ) + ) RecordTypePayload => - match symbol_index_lookup(index: resolved.symbol_index, qualified_path: path) { + match symbol_index_lookup(index: resolved.resolved_declarations, qualified_path: path) { Absent => ConstructNotARecord - Present { value: authored } => + Present { value: resolved_decl } => ConstructIsRecord { construct: InferRecordConstruct { record: reference, path: path, - formals: infer_record_formals(resolved: resolved, path: path, authored: authored), - type_params: symbol_index_declared_type_params_at(index: resolved.symbol_index, qualified_path: path) + formals: infer_record_formals(resolved: resolved, path: path, authored: resolved_decl), + type_params: symbol_index_declared_type_params_at( + index: resolved.resolved_declarations, + qualified_path: path + ) } } } @@ -5634,7 +6270,7 @@ fn infer_record_first_positional_actual(actuals: List) -> Optional { fn infer_record_construct_binding_refusal( node: Node, - construct: InferRecordConstruct, + record: Node, formals: List, actuals: List, plan_reason: Symbol, @@ -5647,7 +6283,7 @@ fn infer_record_construct_binding_refusal( infer_record_construct_refusal( reason: reason, at: node, - record: construct.record, + record: record, field: Edge { label: Authored { name: missing.parameter_identity }, target: missing.declared } ) Absent => application_parameter_order_diagnostic(application: node, reason: application_parameter_order_malformed_reason()) @@ -5655,7 +6291,7 @@ fn infer_record_construct_binding_refusal( } else { match list_at_optional(xs: actuals, index: at) { Present { value: actual } => - infer_record_construct_refusal(reason: reason, at: actual.target, record: construct.record, field: actual) + infer_record_construct_refusal(reason: reason, at: actual.target, record: record, field: actual) Absent => application_parameter_order_diagnostic(application: node, reason: application_parameter_order_malformed_reason()) } } @@ -5669,8 +6305,8 @@ type InferRecordFieldBinding | RecordFieldsRefused { diagnostic: Diagnostic } | RecordPayloadUnread -fn infer_record_field_binding(node: Node, construct: InferRecordConstruct) -> InferRecordFieldBinding { - match infer_formals_from_domain(domain: infer_record_formals_payload(formals: construct.formals)) { +fn infer_record_field_binding(node: Node, record: Node, formals: InferRecordFormals) -> InferRecordFieldBinding { + match infer_formals_from_domain(domain: infer_record_formals_payload(formals: formals)) { Absent => RecordPayloadUnread Present { value: formals } => let actuals = construct_field_edges(n: node) @@ -5680,7 +6316,7 @@ fn infer_record_field_binding(node: Node, construct: InferRecordConstruct) -> In diagnostic: infer_record_construct_refusal( reason: ^record_construct_field_not_named, at: unnamed.target, - record: construct.record, + record: record, field: unnamed ) } @@ -5690,7 +6326,7 @@ fn infer_record_field_binding(node: Node, construct: InferRecordConstruct) -> In RecordFieldsRefused { diagnostic: infer_record_construct_binding_refusal( node: node, - construct: construct, + record: record, formals: formals, actuals: actuals, plan_reason: reason, @@ -5713,19 +6349,20 @@ fn infer_record_field_binding(node: Node, construct: InferRecordConstruct) -> In // field obligation, or a binder no field value fixed, stays on the frontier, exactly as an // application does (infer_transform_derived_optional arguments_decided). fn infer_record_construct_type( - construct: InferRecordConstruct, + type_path: QualifiedName, + type_params: List, instances: List, decided: Bool ) -> Optional { if !decided { optional_absent() } else { - let reference = declaration_reference_node(qn: construct.path, occurrence_id: OccurrenceSynthetic) - if length(xs: construct.type_params) == 0 { + let reference = declaration_reference_node(qn: type_path, occurrence_id: OccurrenceSynthetic) + if length(xs: type_params) == 0 { optional_present(value: reference) } else { match fold( - construct.type_params, + type_params, init: optional_present(value: [Edge { label: Positional, target: reference }]), f: fn(acc, binder) { match acc { @@ -5764,6 +6401,50 @@ fn infer_record_construct_facts( entries: List, declarations: SymbolIndex, partials: List, +) -> Outcome { + infer_construct_facts_on_entries( + node: node, + record: construct.record, + formals: construct.formals, + type_path: construct.path, + type_params: construct.type_params, + entries: entries, + declarations: declarations, + partials: partials + ) +} + +// A VARIANT ARM's construction derives the VARIANT's type (the arm path's parent declaration +// reference), with the arm's fields as the formals: `Boxed(41)` is a value of `Seal`, judged at the +// arm the construction names (v2.compiler.infer infer_record_construct_read reads the arm's path). +fn infer_variant_construct_facts( + node: Node, + construct: InferVariantArmConstruct, + entries: List, + declarations: SymbolIndex, + partials: List, +) -> Outcome { + infer_construct_facts_on_entries( + node: node, + record: construct.record, + formals: construct.formals, + type_path: construct.variant_path, + type_params: construct.type_params, + entries: entries, + declarations: declarations, + partials: partials + ) +} + +fn infer_construct_facts_on_entries( + node: Node, + record: Node, + formals: InferRecordFormals, + type_path: QualifiedName, + type_params: List, + entries: List, + declarations: SymbolIndex, + partials: List, ) -> Outcome { match infer_bounded_lattice_consumer_gate(consumer: node, partials: partials) { Rejected { diagnostics: r } => Rejected { diagnostics: r } @@ -5772,12 +6453,12 @@ fn infer_record_construct_facts( Violates { diagnostic: d } => Rejected { diagnostics: diagnostics_singleton(d: d) } Holds { value: descent_proof } => - match infer_record_field_binding(node: node, construct: construct) { + match infer_record_field_binding(node: node, record: record, formals: formals) { RecordFieldsRefused { diagnostic: d } => outcome_rejected(d) RecordPayloadUnread => bind_outcome_accepted(od: cd, inner: inferred_facts_not_derived(node: node, descent: Holds { value: descent_proof })) RecordFieldsBound { pairs: pairs } => - match construct.formals { + match formals { RecordFormalsAuthored { payload: _ } => bind_outcome_accepted( od: diagnostics_merge(outer: cd, inner: infer_record_fields_unresolved_diagnostics(pairs: pairs)), @@ -5785,19 +6466,20 @@ fn infer_record_construct_facts( ) RecordFormalsResolved { payload: _ } => match infer_judge_formal_args( - position: position_record_field(record: construct.record), + position: position_record_field(record: record), site: JudgedAtEachActual, entries: entries, declarations: declarations, pairs: pairs, - type_params: construct.type_params + type_params: type_params ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: instances, diagnostics: jd } => bind_outcome_accepted( od: diagnostics_merge(outer: cd, inner: jd), inner: match infer_record_construct_type( - construct: construct, + type_path: type_path, + type_params: type_params, instances: instances, decided: infer_diagnostics_none(d: jd) ) { @@ -5849,6 +6531,22 @@ fn infer_gather_record_construct_row_on_entries( ) } +fn infer_gather_variant_construct_row_on_entries( + node: Node, + construct: InferVariantArmConstruct, + entries: List, + declarations: SymbolIndex, + pending: Diagnostics, + partials: List, +) -> InferGatherFoldAcc { + infer_gather_formed_facts_row( + node: node, + formed: infer_variant_construct_facts(node: node, construct: construct, entries: entries, declarations: declarations, partials: partials), + entries: entries, + pending: pending + ) +} + // THE LAST CHILD OF A NODE NO BEHAVIOR ROW AWAITED. An annotated Bind (v2.std.type_binder // type_annotation_optional) checks its bound value against the annotation; a product or Arrow still // missing its own entry takes the product row; anything else stays as folded. @@ -6074,6 +6772,15 @@ fn infer_gather_settled_unannotated_row( pending: merged_pending, partials: partials ) + ConstructIsVariantArm { construct: construct } => + infer_gather_variant_construct_row_on_entries( + node: acc.node, + construct: construct, + entries: merged_entries, + declarations: resolved.resolved_declarations, + pending: merged_pending, + partials: partials + ) ConstructNotARecord => infer_gather_product_row_on_entries( node: acc.node, @@ -6475,21 +7182,625 @@ fn infer_malformed_tree_diagnostic(n: Node) -> Diagnostic { } } +// THE CALLER-ADMISSION WALL RUNS AFTER THE FACTS GATHER, with the gather's entries in hand: a +// callee is reached as a resolved reference (a Conj, not an Arrow), and the one authority for the +// arrow behind such a reference is the use's facts entry (infer_application_callee_arrow_with_facts) +// — the same accessor every other callee-arrow consumer (formals, type parameters, argument +// inhabitance) runs through. Before this reorder the wall read infer_operator_arrow on the +// reference itself and answered Absent for every name-resolved callee, admitting them unjudged — +// the exact silent widen DESIGN section 5 refuses. Refusal keeps the gather's diagnostics, so no +// evidence is dropped by the short-circuit. fn infer_well_formed(tree: ResolvedTree) -> Outcome { match infer_entries_for_tree(tree: tree) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: entries, diagnostics: d } => - match facts_map_from_entries(entries: entries) { - Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: facts_map, diagnostics: md } => - Accepted { - value: ObligatedInferredTree { - root: tree.root, - facts: facts_map, - obligations: Empty + match infer_caller_admission_diags(tree: tree, entries: entries) { + Some { diagnostics: ned } => + match d { + None => Rejected { diagnostics: ned } + Some { diagnostics: da } => + Rejected { diagnostics: non_empty_diagnostics_append(a: da, b: ned) } + } + None => + match facts_map_from_entries(entries: entries) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: facts_map, diagnostics: md } => + Accepted { + value: ObligatedInferredTree { + root: tree.root, + facts: facts_map, + obligations: Empty + }, + diagnostics: diagnostics_merge(outer: d, inner: md) + } + } + } + } +} + +// =================================================================== +// CALLER ADMISSION — the admit_callers wall. +// +// Restoration of the former drop gunbc.rung_drop.admit_callers_discarded_on_the_native_route: +// a v2 stage (this one) now checks caller admission on the native route from the fn's declared +// admit_callers list, refusing a call from a declaration outside it. The clause was parsed by +// the v2 grammar and carried onto the Arrow by v2.compiler.body_lowering_fold +// body_lower_fn_admit_clause_edges — the carrier added together with this consumer, as that +// former drop's trigger requires. +// +// THE SEMANTICS ARE THE SEED'S, copied symbol for symbol from v1's wall +// (src/v1/stage0/src/v1_compiler_infer.rs constructor_call_admission_diags, gated by +// constructor_reference_admission_enforced() -> true): +// - a permitted caller is named by a decl_ref(module_path: "...", decl_name: "...") entry; +// an entry that cannot be read as that call refuses (v1 AdmitCallersEntryNotDeclRef) rather +// than shrinking the roster silently; +// - the caller's coords are its enclosing declaration's (module, name) — v1's +// caller_decl_coords read the enclosing fn scope; here the enclosing module member, keyed in +// infer_admission_child_context — and membership is v1's decl_ref_coords_equal; +// - a call under no declaration has no caller coords and refuses (v1's None arm, the recorded +// behavior of gunbc.recurring_failure_mode +// admit_callers_names_no_caller_for_a_data_declaration); +// - no same-module or test-module exemption exists in the seed and none is added here: the +// corpus's forged-proof probe modules are negative fixtures whose compile refusal is the +// designed and enrolled behavior (measured, ruling C of lively-ram-153, 2026-10-02: all five +// dag/test/probe modules refuse under the v1 seed with ConstructorCallAdmissionRefused among +// their blocking errors). +// =================================================================== + +// THE ADMITTED CALLER'S COORDS, AS THE CLAUSE AUTHORS THEM (the ROSTER ENTRY's image): the +// declaring module's dotted spelling and the declaration's name, both as interned lexemes. The +// module path is a plain Symbol, not an Optional: every authored entry names its module, so no +// Absent case can be WRITTEN here -- an entry whose module does not read is uninterpretable and +// refuses at the entry (the record demand), never silently skipped (review 77558: the dead +// `Absent => acc` arm the Optional invited). +type InferAdmitCallerCoords { + module_path: Symbol + decl_name: Symbol +} + +// THE CALLER'S KEY, AS DESCENT KEYED IT: the innermost enclosing declaration's name. The caller's +// MODULE is the tree's module and is judged separately -- infer_admission_from_roster unwraps it +// first and refuses ^infer_reason_caller_admission_module_unreadable when the root carries none. +// A caller key without a module field cannot fall out of the judgment as a silent skip: the module +// comparison cannot be written wrong at the point of judgment. +type InferAdmissionCallerKey { + decl_name: Symbol +} + +type InferAdmissionCtx { + caller: Optional + module_symbol: Optional +} + +type InferAdmitEntry + = AdmitEntryCoords { coords: InferAdmitCallerCoords } + | AdmitEntryUninterpretable + +// THE RECORD SHAPE DEMAND: [ decl_ref -> atom(^decl_ref), module_path -> atom, decl_name -> atom ] — +// the AUTHORED operator edge demands the authored spelling (^decl_ref, v1's gate: only a decl_ref +// call states coordinates), and both atom identities must be present. Absent = the child is not such +// a record. +fn infer_admit_record_check(record: Node) -> Optional { + match infer_admit_record_operator_spelling(record: record) { + Absent => optional_absent() + Present { value: _ } => + match infer_admit_entry_field_symbol(record: record, field: ^module_path) { + Absent => optional_absent() + Present { value: mp } => + match infer_admit_entry_field_symbol(record: record, field: ^decl_name) { + Absent => optional_absent() + Present { value: dn } => + optional_present(value: InferAdmitCallerCoords { module_path: mp, decl_name: dn }) + } + } + } +} + +// THE RECORD'S OPERATOR SPELLING: the AUTHORED `decl_ref` edge the lowering carried from the authored +// call must name the ^decl_ref atom. A record without it is not a decl_ref call's image. +fn infer_admit_record_operator_spelling(record: Node) -> Optional { + match named_child_lookup(root: record, name: ^decl_ref) { + NamedChildFound { target: target } => + match infer_admit_atom_identity(node: target) { + Absent => optional_absent() + Present { value: op } => + if symbol_eq(a: op, b: ^decl_ref) { + optional_present(value: op) + } else { + optional_absent() + } + } + NamedChildMissing => optional_absent() + NamedChildAmbiguous => optional_absent() + } +} + +// ONE ENTRY OF THE LIST, READ OR REFUSED. The carrier re-hung the authored call as a POSITIONAL +// record conj [ module_path -> atom, decl_name -> atom ]; the record's atom identities ARE the +// coords. Anything else is uninterpretable and refuses at the entry — an entry that cannot be +// interpreted would otherwise be dropped, silently shrinking the permitted-caller roster below +// what was authored. +fn infer_admit_entry_interpret(entry: Node) -> InferAdmitEntry { + match infer_admit_record_check(record: entry) { + Absent => AdmitEntryUninterpretable + Present { value: coords } => AdmitEntryCoords { coords: coords } + } +} + +// THE ONE NAMED FIELD'S STRING VALUE, as an interned lexeme: the record conj's authored edge +// `field` carries an atom whose identity is the interned lexeme. +fn infer_admit_entry_field_symbol(record: Node, field: Symbol) -> Optional { + match named_child_lookup(root: record, name: field) { + NamedChildFound { target: target } => infer_admit_atom_identity(node: target) + NamedChildMissing => optional_absent() + NamedChildAmbiguous => optional_absent() + } +} + +fn infer_admit_atom_identity(node: Node) -> Optional { + match node.kind { + TypeNode { connective: Atom { identity: id } } => optional_present(value: id) + TypeNode { connective: Conj } => optional_absent() + TypeNode { connective: Disj } => optional_absent() + TypeNode { connective: Arrow } => optional_absent() + TypeNode { connective: Cardinality } => optional_absent() + TypeNode { connective: Instantiation } => optional_absent() + ComputationNode { behavior: _ } => optional_absent() + } +} + +// THE ROSTER'S RECORDS, read from the core edge target the carrier hung on the Arrow. The carrier +// re-hung each authored entry as a record conj in a left-named spine — @left carries the record, @right +// the rest, the empty conj root ends the spine (a conforming conj: every edge authored and distinct +// within its node). A decl_ref call's image is a record conj; an entry that reads but is not such a +// call is a marker record the record demand refuses at the entry (DESIGN §5: a failure arm must +// refuse, never widen). A target that is NOT a walkable spine is the carrier's fail-closed fallback — +// the authored spine did not read — and REFUSES with ^infer_reason_admit_callers_entries_unreadable +// at the clause: a malformed clause is never a shorter roster, and the refusal names the cause instead +// of the downstream refused-call verdict. +fn infer_admit_clause_entries(roster: Node) -> Outcome> { + infer_admit_spine_entries(spine: roster) +} + +fn infer_admit_spine_entries(spine: Node) -> Outcome> { + match spine.kind { + TypeNode { connective: Conj } => + if is_empty_conj_root(n: spine) { + outcome_accepted(value: Empty) + } else { + match sugar_sequence_pair_optional(node: spine) { + Absent => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + Present { value: pair } => + match infer_admit_record_check(record: pair.left) { + Absent => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entry_not_decl_ref, + at: node_locus(node: pair.left), + correction: Unavailable { reason: UserInputBoundary } + }) + Present { value: _ } => + match infer_admit_spine_entries(spine: pair.right) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: rest, diagnostics: _ } => + outcome_accepted(value: Cons { head: pair.left, tail: rest }) + } + } + } + } + TypeNode { connective: Atom { identity: _ } } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + TypeNode { connective: Arrow } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + TypeNode { connective: Cardinality } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + TypeNode { connective: Instantiation } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + TypeNode { connective: Disj } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + ComputationNode { behavior: _ } => + outcome_rejected(Diagnostic { + reason: ^infer_reason_admit_callers_entries_unreadable, + at: node_locus(node: spine), + correction: Unavailable { reason: ExternalContractUnknown } + }) + } +} + +// EVERY DIAGNOSTIC THE LIST CARRIES, folded into the stage's Diagnostics carrier. +fn infer_admission_diags_of(ds: List) -> Diagnostics { + fold(ds, init: None, f: fn(acc, d) { + diagnostics_merge( + outer: acc, + inner: Some { diagnostics: NonEmptyDiagnostics { head: d, tail: [] } } + ) + }) +} + +type InferAdmitJudgement { + matched: Bool + diags: List +} + +// EVERY ENTRY PLACED, THEN THE LIST'S VERDICT: uninterpretable entries refuse beside whatever +// they sit with; the call refuses when no entry names its caller (v1: ConstructorCallAdmissionRefused). +fn infer_admission_judge_entries( + application: Node, + entries: List, + caller: InferAdmissionCallerKey, + module_symbol: Symbol +) -> Diagnostics { + let j = fold(entries, init: InferAdmitJudgement { matched: false, diags: [] }, f: fn(acc, entry) { + match infer_admit_entry_interpret(entry: entry) { + AdmitEntryUninterpretable => + InferAdmitJudgement { + matched: acc.matched, + diags: list_snoc_item( + xs: acc.diags, + item: Diagnostic { + reason: ^infer_reason_admit_callers_entry_not_decl_ref, + at: node_locus(node: entry), + correction: Unavailable { reason: UserInputBoundary } + } + ) + } + AdmitEntryCoords { coords: coords } => + if symbol_eq(a: coords.module_path, b: module_symbol) && symbol_eq(a: coords.decl_name, b: caller.decl_name) { + InferAdmitJudgement { matched: true, diags: acc.diags } + } else { + acc + } + } + }) + if j.matched { + infer_admission_diags_of(ds: j.diags) + } else { + infer_admission_diags_of(ds: list_snoc_item( + xs: j.diags, + item: Diagnostic { + reason: ^infer_reason_caller_admission_refused, + at: node_locus(node: application), + correction: Unavailable { reason: UserInputBoundary } + } + )) + } +} + +// ONE APPLICATION'S VERDICT UNDER `caller`. Only a callee whose Arrow carries the admit edge is +// judged; an unrostered callee imposes nothing (scope rule: the clause rides the fn Arrow, and a +// fn whose Arrow does not carry it is not restricted). An Ambiguous admit edge refuses at the +// application. The entries come from the facts gather, so an imported callee's clause is read the +// same way a same-module one is. +fn infer_caller_admission_application_diags( + application: Node, + caller: Optional, + module_symbol: Optional, + entries: List, + declarations: SymbolIndex +) -> Diagnostics { + match infer_application_callee_arrow_with_facts(node: application, entries: entries) { + Absent => + match list_at_optional(xs: node_positional_child_targets(node: application), index: 0) { + Absent => None + Present { value: child0 } => + match resolved_reference_body_kind(target: child0) { + NotMarkedReference => None + MarkedReference { kind: DeclarationReferenceKind } => + match infer_callee_declaration_from_declarations(declarations: declarations, callee_ref: child0) { + Absent => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_callee_unresolved, + at: node_locus(node: application), + correction: Suggested { node: child0 } + } + ]) + Present { value: decl } => + match infer_operator_arrow(operator: decl) { + Absent => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_callee_unreadable, + at: node_locus(node: application), + correction: Suggested { node: child0 } + } + ]) + Present { value: arrow } => + infer_caller_admission_judged_diags( + application: application, + caller: caller, + module_symbol: module_symbol, + arrow: arrow, + declarations: declarations + ) + } + } + MarkedReference { kind: _ } => None + } + } + Present { value: arrow } => + infer_caller_admission_judged_diags( + application: application, + caller: caller, + module_symbol: module_symbol, + arrow: arrow, + declarations: declarations + ) + } +} + +// THE CALLEE'S DECLARATION FROM THE RESOLVED-DECLARATIONS INDEX: a cross-module callee's facts are +// never in the subject's gather (the gather walks the subject root), so the wall reads the callee's +// own declaration node from resolved_declarations -- declarations WITH RESOLVED BODIES, imported +// modules included. THE BOUNDARY BETWEEN NO JUDGMENT AND REFUSAL: an operator child that is not +// marked as a declaration reference is no judgment -- the ordinary grounding machinery owns that +// call, no clause can exist for it, the wall imposes nothing (NotMarkedReference). But a child +// MARKED as a declaration reference (MarkedReference{DeclarationReferenceKind}) whose path has no +// entry in the index is NOT "not declared": the mark asserts a declaration stands behind the +// reference, so an unavailable one is an unresolved admission authority and the caller admission +// REFUSES on it (RosterLookupUnresolved -> infer_reason_caller_admission_callee_unresolved, fail +// closed). A FOUND declaration whose arrow cannot be read is the same kind of failure one step +// later and refuses separately (infer_reason_caller_admission_callee_unreadable). The two are +// distinct facts about the world -- the declaration missing versus the declaration present but +// unreadable -- and both refuse; neither shortens the roster nor admits. +fn infer_callee_declaration_from_declarations(declarations: SymbolIndex, callee_ref: Node) -> Optional { + match declaration_reference_path_optional(node: callee_ref) { + Absent => optional_absent() + Present { value: qn } => symbol_index_lookup(index: declarations, qualified_path: qn) + } +} + +fn infer_caller_admission_judged_diags( + application: Node, + caller: Optional, + module_symbol: Optional, + arrow: Node, + declarations: SymbolIndex +) -> Diagnostics { + match core_edge_target_lookup(children: arrow.children, marker: AdmitCallersEdge) { + Ambiguous => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_admit_callers_edge_ambiguous, + at: node_locus(node: application), + correction: Unavailable { reason: UserInputBoundary } + } + ]) + Absent => + match infer_caller_admission_roster_from_declarations( + application: application, + declarations: declarations + ) { + RosterLookupAbsent => None + RosterLookupAmbiguous => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_admit_callers_edge_ambiguous, + at: node_locus(node: application), + correction: Unavailable { reason: UserInputBoundary } + } + ]) + RosterLookupUnreadable => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_callee_unreadable, + at: node_locus(node: application), + correction: match list_at_optional(xs: node_positional_child_targets(node: application), index: 0) { + Absent => Unavailable { reason: ExternalContractUnknown } + Present { value: callee_ref } => Suggested { node: callee_ref } + } + } + ]) + RosterLookupUnresolved => + infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_callee_unresolved, + at: node_locus(node: application), + correction: match list_at_optional(xs: node_positional_child_targets(node: application), index: 0) { + Absent => Unavailable { reason: ExternalContractUnknown } + Present { value: callee_ref } => Suggested { node: callee_ref } + } + } + ]) + RosterLookupFound { target: roster } => infer_admission_from_roster( + application: application, + caller: caller, + module_symbol: module_symbol, + roster: roster + ) + } + Found { target: roster } => infer_admission_from_roster( + application: application, + caller: caller, + module_symbol: module_symbol, + roster: roster + ) + } +} + +// THE ADMIT ROSTER'S ONE HOME IS THE RESOLVED TREE'S ARROW. The facts arrow is infer's CANONICAL +// GROUNDING of the callee reference -- a rebuilt copy whose formals and body survive but whose core +// metadata edges (the carrier's AdmitCallersEdge among them) do not. When the facts copy is +// unmarked, read the roster off the resolved-declarations arrow for the same declared callee. +// Absent when the application's operator child is not a marked reference at all, or the resolved +// arrow carries no roster (a fn without the clause -- the legitimate admit). Unresolved when the +// child IS marked as a declaration reference but no declaration stands behind it: that is not +// "not declared", it is a grounding failure, and the caller admission refuses on it (fail closed). +type InferAdmitRosterLookup + = RosterLookupAbsent + | RosterLookupAmbiguous + | RosterLookupUnreadable + | RosterLookupUnresolved + | RosterLookupFound { target: Node } + +fn infer_caller_admission_roster_from_declarations( + application: Node, + declarations: SymbolIndex +) -> InferAdmitRosterLookup { + match list_at_optional(xs: node_positional_child_targets(node: application), index: 0) { + Absent => RosterLookupAbsent + Present { value: child0 } => + match resolved_reference_body_kind(target: child0) { + NotMarkedReference => RosterLookupAbsent + MarkedReference { kind: DeclarationReferenceKind } => + match infer_callee_declaration_from_declarations(declarations: declarations, callee_ref: child0) { + Absent => RosterLookupUnresolved + Present { value: decl } => + match infer_operator_arrow(operator: decl) { + Absent => RosterLookupUnreadable + Present { value: resolved_arrow } => + match core_edge_target_lookup(children: resolved_arrow.children, marker: AdmitCallersEdge) { + Absent => RosterLookupAbsent + Ambiguous => RosterLookupAmbiguous + Found { target: roster } => RosterLookupFound { target: roster } + } + } + } + MarkedReference { kind: _ } => RosterLookupAbsent + } + } +} + +// THE JUDGMENT FROM A MINTED ROSTER. THE MODULE CHECK HAPPENS HERE, AT THE POINT OF JUDGMENT: +// comparing the roster's entry coordinates against the caller's module is the judgment; a +// fn-rooted grounding context carries no tree module, so the comparison is undetermined and the +// call refuses (module_unreadable, located at the call) rather than judge with a fabricated +// module -- fail closed where it matters, never at the root (a root-level refusal would refuse +// every fn-rooted context wholesale). +fn infer_admission_from_roster( + application: Node, + caller: Optional, + module_symbol: Optional, + roster: Node +) -> Diagnostics { + match infer_admit_clause_entries(roster: roster) { + Rejected { diagnostics: d } => Some { diagnostics: d } + Accepted { value: records, diagnostics: _ } => + match caller { + Absent => infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_caller_unkeyed, + at: node_locus(node: application), + correction: Unavailable { reason: UserInputBoundary } + } + ]) + Present { value: coords } => + match module_symbol { + Absent => infer_admission_diags_of([ + Diagnostic { + reason: ^infer_reason_caller_admission_module_unreadable, + at: node_locus(node: application), + correction: Unavailable { reason: UserInputBoundary } + } + ]) + Present { value: tree_module } => + infer_admission_judge_entries( + application: application, + entries: records, + caller: coords, + module_symbol: tree_module + ) + } + } + } +} + +// DESCENT KEYING. The namespace graft DISSOLVES the declaration member wrap and re-hangs each +// member's Arrow DIRECTLY under the module body conj, under an authored edge named for the +// declaration (v2.compiler.body_lowering_fold body_lower_fn_decl_named_member_wrap: "the wrap is a +// carrier, not the image root ... re-hangs the Arrow under the module; the Arrow is what stands for +// the authored declaration in the admitted tree"). So an AUTHORED edge whose target is an Arrow +// inside the grafted body names the declaration the calls below it sit in — the same edge resolve +// harvests the binding from (v2.compiler.03_resolve harvest_module_root_named_bindings / +// try_edge_declared_binding). Keying REPLACES the caller, so a nested declaration keys its own +// calls once the descent passes its authored edge (innermost enclosing declaration wins, which is +// the caller v1 judged). Descending through any other edge keeps the enclosing caller. +fn infer_admission_child_context(self: Node, ctx: InferAdmissionCtx, edge: Edge) -> InferAdmissionCtx { + match edge.label { + Authored { name: member_name } => + match edge.target.kind { + TypeNode { connective: Arrow } => + InferAdmissionCtx { + caller: Present { + value: InferAdmissionCallerKey { decl_name: member_name } }, - diagnostics: diagnostics_merge(outer: d, inner: md) + module_symbol: ctx.module_symbol } + TypeNode { connective: Atom { identity: _ } } => ctx + TypeNode { connective: Conj } => ctx + TypeNode { connective: Disj } => ctx + TypeNode { connective: Cardinality } => ctx + TypeNode { connective: Instantiation } => ctx + ComputationNode { behavior: _ } => ctx } + Positional => ctx + StructuralLabel { label: _ } => ctx } } + +// THE ROOT'S MODULE NAME, when the root is a module: the caller-module key for module-rooted +// trees. A sub-tree root (a fn-rooted grounding context) reads as no module and the wall still +// runs -- see the judgment-point check in infer_admission_from_roster. +fn infer_admission_root_module_symbol(root: Node) -> Optional { + match qualified_name_from_module_node(root: root) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: module_qn, diagnostics: _ } => + optional_present(value: symbol_intern_lexeme(lexeme: qualified_name_to_dotted_string(qn: module_qn))) + } +} + +// THE WALL, over the whole resolved tree, top-down so each application is judged under the +// declaration it sits in. The tree is judged however it is rooted: a module-rooted tree carries +// its module name in the descent context (it keys the caller's module); a sub-tree root (a +// fn-rooted grounding context -- the cross-language and self-host fixtures run infer on slices) +// carries NO module name, and the wall still runs. Failure arms FAIL CLOSED (DESIGN.md §5: +// refuse, never widen) -- but where they fail matters: an unreadable module name is not a +// tree-level fault, it is unreadable only where a judgment needs it, so the refusal fires at the +// application being judged (module_unreadable, located at the call), never as a root-level +// precondition that would refuse every fn-rooted context wholesale. +fn infer_caller_admission_diags(tree: ResolvedTree, entries: List) -> Diagnostics { + fold_node_topdown( + n: tree.root, + ctx: InferAdmissionCtx { + caller: Absent, + module_symbol: infer_admission_root_module_symbol(root: tree.root) + }, + algebra: NodeFoldTopDown { + init: fn(self, ctx) { + infer_caller_admission_application_diags( + application: self, + caller: ctx.caller, + module_symbol: ctx.module_symbol, + entries: entries, + declarations: tree.resolved_declarations + ) + }, + child_context: fn(self, ctx, edge) { infer_admission_child_context(self: self, ctx: ctx, edge: edge) }, + step: fn(acc, _edge, sub) { diagnostics_merge(outer: acc, inner: sub) } + } + ) +} diff --git a/src/v2/compiler/05_eval.dag b/src/v2/compiler/05_eval.dag index 3950432e690..267a87e4890 100644 --- a/src/v2/compiler/05_eval.dag +++ b/src/v2/compiler/05_eval.dag @@ -147,6 +147,7 @@ import v2.std.node { well_formed, } import v2.std.node_query { + arrow_constant_member_codomain, FieldProjection, field_projection_edge_base_optional, field_projection_optional, @@ -527,37 +528,13 @@ type TestClaimEvalSubject { input: TestClaimTypedInput } -type TestClaimCacheKey { - eval_subject: TestClaimEvalSubject -} - -fn test_claim_cache_key_claim_hash(key: TestClaimCacheKey) -> Hash { - test_claim_claim_hash_digest(c: key.eval_subject.claim) -} - -fn test_claim_cache_key_interpretation_hash_node(key: TestClaimCacheKey) -> Hash { - test_claim_cache_interpretation_hash_node(subject: key.eval_subject) -} - -type TestClaimCacheReceipt { - key: TestClaimCacheKey -} - type TestClaimRun { - cache: TestClaimCacheReceipt + claim: TestClaim verdict: Verdict, A> } -fn test_claim_cache_key_claim(key: TestClaimCacheKey) -> TestClaim { - key.eval_subject.claim -} - -fn test_claim_cache_receipt_claim(receipt: TestClaimCacheReceipt) -> TestClaim { - test_claim_cache_key_claim(key: receipt.key) -} - fn test_claim_run_claim(run: TestClaimRun) -> TestClaim { - test_claim_cache_receipt_claim(receipt: run.cache) + run.claim } fn test_claim_run_verdict(run: TestClaimRun) -> Verdict, A> { @@ -1008,18 +985,6 @@ fn test_claim_cache_interpretation_hash_node(subject: TestClaimEvalSubject ) } -fn test_claim_cache_receipt_node(subject: TestClaimEvalSubject) -> TestClaimCacheReceipt { - TestClaimCacheReceipt { - key: TestClaimCacheKey { - eval_subject: subject - } - } -} - -fn test_claim_cache_receipt(subject: TestClaimEvalSubject) -> TestClaimCacheReceipt { - test_claim_cache_receipt_node(subject: subject) -} - fn eval_diagnostic(reason: Symbol, node: Node) -> Diagnostic { Diagnostic { reason: reason, @@ -2509,6 +2474,63 @@ fn eval_interpret_node( ) } +// THE CONSTANT'S BODY RUNS THROUGH THE SAME BODY-FORM DISPATCH A NULLARY CALL USES (eval_transform_node's +// callee arm): v2.std.node arrow_body_admits_eval_entry decides whether the form may enter, a direct atom +// (a literal) is allocated by eval_type_node_atom without a facts lookup, and every other admitted form +// is walked by eval_runtime_node. A form that may not enter refuses with eval_callee_body_refusal_reason, +// the reason the call route gives for the same form, so the two routes cannot disagree about a body. +fn eval_constant_member_value( + body: Node, + tree: InferredTree, + interpretation: InterpretationAlgebra, + environment: EvaluationEnvironment, + runtime: V4EvaluatorRuntime, + effect_io: EffectIoEvalContext +) -> Outcome { + let form = classify_arrow_body_form(target: body) + if !arrow_body_admits_eval_entry(form: form) { + outcome_rejected(d: eval_diagnostic(reason: eval_callee_body_refusal_reason(form: form), node: body)) + } else { + match form { + DirectAtomBody => eval_type_node_atom(node: body, interpretation: interpretation, environment: environment) + RecordConstructBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + DeclarationReferenceBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + ParameterReferenceBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + LexicalReferenceBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + FunctionValueBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + ComputationBody { behavior: _ } => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + UnsupportedArrowBody => eval_runtime_node(node: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + } + } +} + +// A REFERENCE TO A `data` CONSTANT EVALUATES TO THE CONSTANT'S VALUE: the body of the nullary Arrow body +// lowering realizes it as, evaluated as the nullary application the seed performs at every reference +// (v1.compiler.emit_rust emits a data item as a nullary fn and calls it). The declaration is the +// denotation infer recorded beside the reference, and it is a constant exactly when its Arrow carries +// v2.std.node ConstantMemberEdge -- the same reader (v2.std.node_query arrow_constant_member_codomain) +// infer typed the reference by, so a reference infer typed at T is the one eval applies, and every other +// value-position declaration reference keeps the binding-miss refusal in eval_type_node_atom. The empty +// domain binds nothing, so the body runs in the environment of the reference. +fn eval_constant_member_body(node: Node, facts: InferredFacts) -> Optional { + match declaration_reference_path_optional(node: node) { + Absent => Absent + Present { value: _ } => + match facts.denotation { + Absent => Absent + Present { value: declaration } => + match arrow_constant_member_codomain(arrow: declaration) { + Absent => Absent + Present { value: _ } => + match find_arrow_body_child(root: declaration) { + Accepted { value: body, diagnostics: _ } => Present { value: body } + Rejected { diagnostics: _ } => Absent + } + } + } + } +} + // A FIELD PROJECTION SELECTS A FIELD FROM THE RECEIVER'S VALUE, and refuses on anything else. Without // this arm a projection reached the default TypeNode route and was handed to allocate_literal, which // would fabricate a literal out of a two-edge Conj -- a value for an expression that selects, not one @@ -2864,6 +2886,53 @@ fn eval_fold_tree( environment: EvaluationEnvironment, runtime: V4EvaluatorRuntime, effect_io: EffectIoEvalContext +) -> EvalFoldState { + match eval_constant_reference_body(tree: tree, node: n) { + Present { value: body } => + eval_fold_constant_reference_state(node: n, body: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + Absent => eval_fold_tree_walk(n: n, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + } +} + +// A REFERENCE TO A `data` CONSTANT IS A LEAF OF THE WALK, AS A PARAMETER REFERENCE IS (eval_runtime_node's +// first arm). Its only child is its declaring path spine, which is identity data built at +// OccurrenceSynthetic with no facts entry of its own, so descending into it refused with +// infer_facts_lookup_miss on the spine before the reference was ever valued. The leaf's value is the +// constant's, through eval_constant_member_value; its structural check is the reference's own facts. +fn eval_fold_constant_reference_state( + node: Node, + body: Node, + tree: InferredTree, + interpretation: InterpretationAlgebra, + environment: EvaluationEnvironment, + runtime: V4EvaluatorRuntime, + effect_io: EffectIoEvalContext +) -> EvalFoldState { + EvalFoldState { + node: node, + progress: Interpreted, + structural: eval_structural_node_for_eval(tree: tree, node: node), + child_values: Accepted { value: empty_runtime_values(), diagnostics: None }, + value: eval_constant_member_value(body: body, tree: tree, interpretation: interpretation, environment: environment, runtime: runtime, effect_io: effect_io) + } +} + +// The constant body a node names when it is a declaration reference whose facts record a marked +// declaration, Absent for every other node (so every other node keeps the ordinary walk). +fn eval_constant_reference_body(tree: InferredTree, node: Node) -> Optional { + match tree.facts.lookup(node) { + Absent => Absent + Present { value: facts } => eval_constant_member_body(node: node, facts: facts) + } +} + +fn eval_fold_tree_walk( + n: Node, + tree: InferredTree, + interpretation: InterpretationAlgebra, + environment: EvaluationEnvironment, + runtime: V4EvaluatorRuntime, + effect_io: EffectIoEvalContext ) -> EvalFoldState { if eval_runtime_node_is_control_transfer(node: n) { eval_fold_control_transfer_state( @@ -3494,7 +3563,7 @@ fn run_test_claim_runtime_evaluated( inputs: inputs_root_only(root: subject.input.evaluator_input) ) TestClaimRun { - cache: test_claim_cache_receipt_node(subject: subject), + claim: subject.claim, verdict: aggregate_verdicts( rs: [ run_test_claim_runtime_assert( @@ -3517,7 +3586,7 @@ fn run_test_claim( match subject.claim { RoundTripClaim { label: _, anchor: _, input: _, classification: _ } => TestClaimRun { - cache: test_claim_cache_receipt_node(subject: subject), + claim: subject.claim, verdict: run_test_claim_round_trip_verdict_runtime( actual: eval_round_trip_claim_input_for_verdict( input: subject.input.evaluator_input @@ -3528,7 +3597,7 @@ fn run_test_claim( StructuralEqualsClaim { rhs: rhs, label: _, anchor: _, lhs: _, classification: _ } => TestClaimRun { - cache: test_claim_cache_receipt_node(subject: subject), + claim: subject.claim, verdict: run_test_claim_structural_equals_verdict( actual: eval_round_trip_claim_input_for_verdict( input: subject.input.evaluator_input diff --git a/src/v2/compiler/06_translate.dag b/src/v2/compiler/06_translate.dag index 230d2bcae89..e26c628038b 100644 --- a/src/v2/compiler/06_translate.dag +++ b/src/v2/compiler/06_translate.dag @@ -86,7 +86,7 @@ import v2.std.node { Positional, Symbol, TypeNode, - arrow_named_edge_is_contract, + arrow_named_edge_is_metadata, content_hash, fold_node, node_rebuild, @@ -365,7 +365,7 @@ fn translate_derived_parent_underived_child_gate( // only there: the same label elsewhere is not a contract edge and is gated like any child. fn translate_edge_is_arrow_contract(parent: Node, edge: Edge) -> Bool { match parent.kind { - TypeNode { connective: Arrow } => arrow_named_edge_is_contract(e: edge) + TypeNode { connective: Arrow } => arrow_named_edge_is_metadata(e: edge) _ => false } } diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 6386e88a2cc..6b8f5d7e72a 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -68,6 +68,7 @@ import v2.extdeps.languages.dag { dag_string_literal_value_symbol_optional, dag_symbol_literal_name_optional, dag_symbol_literal_node, + parse_repeat_capture_items_with, dag_match_arm_node, dag_resolve_preserve_module_metadata_subtree, parse_production_captured_child_optional, @@ -99,18 +100,49 @@ import std.optional { optional_present } import v2.std.compilers.body_lowering { + BodyTerm, + Lowered, + RawLeaf, + RawParse, lower_binary_infix, lower_branch, lower_bind, lower_bind_annotated, lower_loop, lower_match, - lower_unary_prefix + lower_unary_prefix, + application_head_read, + ApplicationHeadFound, + ApplicationHeadRefused, + ArrowHead, + AtomHead, + NotApplicationHead +} +import v2.std.compilers.lowered_shape { + LoweredBranch, + LoweredDataDeclaration, + LoweredFnBody, + LoweredFnDeclaration, + LoweredFoldEncoding, + LoweredFunctionValue, + LoweredLet, + LoweredLoop, + LoweredMatch, + LoweredOperand, + LoweredOperatorExpression, + LoweredPostfix, + LoweredPrimary, + LoweredResourceDeclaration, + LoweredServiceDeclaration, + LoweredShape, + LoweredStatementSequence, + LoweredTestDeclaration, + LoweredTypeDeclaration } import v2.std.list_introduction { lower_list_introduction } import v2.std.arrow_signature { anonymous_signature_arrow, declared_signature, signature_order_edge } -import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional } -import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr } +import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional, sugar_named_child_optional, sugar_type_node } +import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr, grammar_infix_row_class } import v2.std.diagnostic { Accepted, Diagnostic, @@ -143,9 +175,15 @@ import v2.std.node { ArrowSignatureOrderEdge, ArrowExecutionModeClaimEdge, ArrowResourceRequirementsEdge, + BodyTermLoweredEdge, + BodyTermLoweredSeal, + CoreMarker, + body_term_lowered_seal, + AdmitCallersEdge, MapLiteralEntryEdge, MapLiteralEntriesEdge, MatchArmScopeExitEdge, + ConstantMemberEdge, core_edge, labeled_core_is, StructuralLabel, @@ -196,6 +234,7 @@ import v2.std.arrow_contract { effect_claim_label, hermetic_claim_label } import std.effects { EffectClaim, ReadonlyClaim, IdempotentClaim } import v2.std.node_query { binder_edge, + construct_field_edges, member_edge_type_node, node_positional_child_targets, construct_node, @@ -620,80 +659,7 @@ fn body_lower_wrapper_retained_shell(shell: Node) -> Outcome { } } -// THE QUALIFIED-NAME SPINE IS LOWERED SUBSTRATE. body_lower_reduce runs bottom-up, so by the time -// a fn declaration's body is read, a dotted chain in it has already been lowered to the spine -// body_lower_qualified_name_spine_node builds -- a TypeNode Conj under the fold_list labels that no -// grammar production emits. Before this arm the spine was not recognised as lowered, and the -// expression-body readers (body_lower_eq_expr_to_body through body_lower_operand_ref_optional) -// decomposed it to its FIRST ATOM: `fn f() -> Bool = wall.peer.peer_leaf` reached the arrow body -// as the atom `wall`, measured on the normalize route by -// v2.test.claim.body_lowering_qualified_path, while a call through the same chain survived only -// because a Transform already counted as substrate. One shape, one verdict: what the spine -// builder emits, every later reader keeps. -// A CONSTRUCT IS LOWERED SUBSTRATE. The record-literal producer answers a Conj (v2.std.node_query -// construct_node: tag edge, then field edges), and every reducer above it -- the postfix reduce -// first -- must pass it through as it passes an Arrow or a Transform; the first-atom fallback -// they otherwise take would answer the construct's tag and drop its fields, which is exactly how -// `data d: R = R { .. }` reached the graft as the atom R before this arm. -// A FIELD PROJECTION IS LOWERED SUBSTRATE, for the construct's reason. The postfix chain fold -// answers `f(x).a` as v2.std.node_query field_projection_node, a two-edge Conj (base, field), and -// until this arm nothing above the postfix reduce recognised it: an operator operand, a paren -// group or a unary operand re-read the Conj as unlowered, and the first-child descent answered its -// BASE -- `p + g(x).a` lowered to `p + g(x)` with no diagnostic, silent wrongness below the ladder -// (DESIGN 4b). The producer and this predicate now agree on one shape, field_projection_optional, -// the same reader the projection's consumers use. -// A LOWERED INT LITERAL IS SUBSTRATE, AND ITS PAYLOAD IS NOT A NAME. body_lower_int_literal_with_magnitude_optional -// produces an Atom(dag_token_int_literal) carrying the integer authority's decimal digit list as -// its magnitude field, and that list is a FreeMonoid cons spine: the same head/tail shape -// qualified_name_spine_shape_present recognizes. Until 2026-09-23 the literal atom was not -// recognized here and its payload was, so a deep search from a `= expr` body (a data initializer, -// or `fn f() -> Int = 606060`) stepped past the literal and returned its digit list as the body, -// which resolve then refused as an unbound qualified name. Recognizing the literal stops every -// search at the literal, before it reaches the payload. -fn body_lower_is_core_substrate_leaf(node: Node) -> Bool { - (match body_lower_int_literal_with_magnitude_optional(node: node) { Present { value: lit } => lit == node Absent => false }) - || (match body_lower_string_literal_value_optional(node: node) { Present { value: lit } => lit == node Absent => false }) - || (match dag_symbol_literal_name_optional(node: node) { Present { value: _ } => true Absent => false }) - || qualified_name_spine_shape_present(root: node) - || (match construct_tag_reading(n: node) { ConstructTagAuthored { reference: _ } => true ConstructTagElided => true NotAConstruct => false }) - || (match field_projection_optional(n: node) { Present { value: _ } => true Absent => false }) -} - -fn body_lower_is_core_substrate(node: Node) -> Bool { - match node.kind { - TypeNode { connective: Arrow } => true - ComputationNode { behavior: _ } => true - TypeNode { connective: Atom { identity: _ } } => - body_lower_is_core_substrate_leaf(node: node) - TypeNode { connective: Conj } => - body_lower_is_core_substrate_leaf(node: node) - TypeNode { connective: Disj } => - body_lower_is_core_substrate_leaf(node: node) - TypeNode { connective: Cardinality } => - body_lower_is_core_substrate_leaf(node: node) - TypeNode { connective: Instantiation } => - body_lower_is_core_substrate_leaf(node: node) - } -} - -// A LOWERED SHAPE THE ALLOWLIST DOES NOT NAME REFUSES; IT IS NEVER RE-READ AS RAW PARSE. Every reader -// that consults body_lower_is_core_substrate used to treat a node it did not recognise as unlowered -// and walk it again with the parse readers, whose first-match descents answered a PART of it -- the -// silent part-for-whole truncation behind the #12145 class (gunbc.recurring_failure_mode -// lowered_output_reread_as_raw_parse_at_v2_body_lowering); #12269's field projection was one -// instance, closed by growing the allowlist. The raw parse tree is recognisable by construction: every -// non-atom node the parser emits carries at least one parse projection edge, and the roster of those -// edges is v2.compiler.parse parse_tree_projection_edge (a production's identity or capture, a -// sequence's sides -- what the parse stage's grammar_named_edge sites mint), while a producer's -// output carries none. So a non-atom node with children that the allowlist rejects and -// that carries no grammar projection edge is lowered output that no reader recognises, and it refuses -// here, located at that node, instead of being re-read. The MQ-5 census (wise-ant-748, 2026-09-30: -// the same test at these sites over the native population, positive control verified, baseline -// identical) found no site taking the re-read today, so this refusal changes no verdict; it makes the -// NEXT unrecognised shape loud. RUNG, HONESTLY: the diagnostic names the node, not its producer -- -// nothing on a Node says which lowering built it. The producer-declared shape (LoweredShape carried -// in a BodyTerm through the reducers, neat-boar-16 ruling 2026-09-30) is the next rung, and it -// deletes both this test and the allowlist. + fn body_lower_has_grammar_projection_edge(node: Node) -> Bool { fold(node.children, init: false, f: fn(acc, e) { acc || (match e.label { @@ -704,55 +670,232 @@ fn body_lower_has_grammar_projection_edge(node: Node) -> Bool { }) } -fn body_lower_is_unrecognised_lowered_shape(node: Node) -> Bool { - if body_lower_is_core_substrate(node: node) { - false +type BodyTermWrapper { + node: Node + shape: LoweredShape +} + +// Whether an edge carries the authored label `name`. The leaf reader uses it to admit a parse-minted +// literal token's single payload edge (body_term_literal_token_payload_field) and nothing beside it. +fn body_term_edge_named(e: Edge, name: Symbol) -> Bool { + match e.label { + Authored { name: n } => n == name + StructuralLabel { label: _ } => false + Positional => false + } +} + +// THE STAGE'S TERM IS READ OFF THE TREE, AND ITS LOWERED ARM IS WRITTEN BY THE PRODUCER (MQ-5). +// Body lowering is a bottom-up fold over a tree holding raw parse and lowered output at once. Every +// reader used to ask a hand-kept shape allowlist, body_lower_is_core_substrate, whether a node was +// lowered, and re-read anything the allowlist missed as raw parse: the parse readers' first-match +// descents then answered a PART of it (gunbc.recurring_failure_mode +// lowered_output_reread_as_raw_parse_at_v2_body_lowering, the #12145 class). That allowlist, the +// unrecognised-shape test beside it and the descendant search body_lower_find_core_substrate_optional +// are deleted. A producer's result now leaves it inside the stage's Lowered wrapper, minted only by +// body_term_seal with the producer's own LoweredShape arm (v2.std.compilers.lowered_shape, carried by the seal itself), and every +// reader asks body_term_of, which reads the wrapper back and identifies raw parse POSITIVELY: a +// composite carrying a grammar projection edge (v2.compiler.parse parse_tree_projection_edge, the +// roster the parse stage mints), or a childless leaf. A node that is neither wrapped, nor stamped by +// the parse, nor a childless leaf refuses where it is read, located at it -- it is lowered output no +// producer declared, and it is never re-read as raw parse. +// +// RUNG, HONESTLY (gunbc.recurring_failure_mode lowered_output_reread_as_raw_parse_at_v2_body_lowering, +// which carries this class's rung per path; operator approval msg_63aee66e; STACK ruling, neat-boar-16): the wrapper is a Node, so the tag lives in the tree the +// fold threads but is not a type. Its one edge is the v2.std.node core marker BodyTermLoweredEdge, +// whose BodyTermLoweredSeal is sole_constructor and minted only through body_term_lowered_seal, +// admit_callers-sealed to body_term_seal, so a lookalike does not compile on the seed and interpreted +// path (structural). On the native route admit_callers is discarded (gunbc.rung_drop +// admit_callers_discarded_on_the_native_route), so there the sole construction site is the wall. +// The ceiling is a sealed BodyTerm term tree the fold produces and normalize consumes. The leaf arm +// is bounded by today's occurrence model: a leaf inherits its source occurrence and has no +// projection edge, so its provenance is not on it; gunbc#12790 (lowered nodes mint +// OccurrenceProjected) puts it there. +fn body_term_seal_shape(seal: BodyTermLoweredSeal) -> LoweredShape { + seal.shape +} + +// The wrapper read back: one child, under the sealed core marker body_term_seal writes. The arm is +// matched, never a spelling, and its seal names the producer's arm. +fn body_term_wrapper_optional(n: Node) -> Optional { + match n.kind { + TypeNode { connective: Conj } => + if length(xs: n.children) == 1 { + match list_at_optional(xs: n.children, index: 0) { + Absent => optional_absent() + Present { value: e } => + match e.label { + StructuralLabel { label: CoreMarker { marker: marker } } => + match marker { + BodyTermLoweredEdge { seal: seal } => + optional_present(value: BodyTermWrapper { node: e.target, shape: body_term_seal_shape(seal: seal) }) + _ => optional_absent() + } + StructuralLabel { label: ProductionEdge { language: _, edge: _ } } => optional_absent() + Authored { name: _ } => optional_absent() + Positional => optional_absent() + } + } + } else { + optional_absent() + } + TypeNode { connective: Atom { identity: _ } } => optional_absent() + TypeNode { connective: Disj } => optional_absent() + TypeNode { connective: Arrow } => optional_absent() + TypeNode { connective: Cardinality } => optional_absent() + TypeNode { connective: Instantiation } => optional_absent() + ComputationNode { behavior: _ } => optional_absent() + } +} + +// THE ONE MINT, AND ITS TWO OUTCOME-SHAPED DOORS (body_term_sealed_outcome, body_term_sealed_lowering). +// Each door is itself sealed to the producer dispatch that calls it, so neither is a proxy through +// which a caller outside the producers could mint. A producer hands back what it built with the arm naming itself. A result that is +// already a term keeps the term it is: raw parse the producer passed through stays raw parse (it is +// identified positively, never wrapped), and a Lowered value a producer passed through keeps the arm +// of the producer that built it. Everything else the producer returned -- a lowered composite, a +// lowered atom, or an atom it passed through unchanged -- is that producer's lowered output. +fn body_term_seal(node: Node, shape: LoweredShape) -> Node + admit_callers: [ + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_term_sealed_outcome"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_term_sealed_lowering"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_fold_identity_absent"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_reduce_emitted") + ] +{ + if body_lower_has_grammar_projection_edge(node: node) { + node } else { - match node.kind { - TypeNode { connective: Atom { identity: _ } } => false - _ => (length(xs: node.children) > 0) && !body_lower_has_grammar_projection_edge(node: node) + match body_term_wrapper_optional(n: node) { + Present { value: _ } => node + Absent => + node_lowered_from( + kind: TypeNode { connective: Conj }, + children: [ + core_edge(marker: BodyTermLoweredEdge { seal: body_term_lowered_seal(shape: shape) }, target: node) + ], + source: node + ) } } } -fn body_lower_unrecognised_lowered_shape_diagnostics(node: Node) -> NonEmptyDiagnostics { - diagnostics_singleton( - d: body_lower_diagnostic(reason: ^body_lowering_reason_unrecognised_lowered_shape, n: node) - ) +fn body_term_sealed_outcome(o: Outcome, shape: LoweredShape) -> Outcome + admit_callers: [ + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_fold_by_identity"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_fold_dispatch"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_fold_raw"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_function_value_body"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_reduce_emitted") + ] +{ + match o { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: n, diagnostics: d } => Accepted { value: body_term_seal(node: n, shape: shape), diagnostics: d } + } } -// The node body_lower_find_core_substrate_optional would step PAST on its own first-match path -- -// an unrecognised lowered node -- to answer a recognised DESCENDANT of it: a part for the whole. -fn body_lower_find_core_substrate_passes_unrecognised(root: Node) -> Optional { - if body_lower_is_core_substrate(node: root) { - optional_absent() +fn body_term_sealed_lowering(o: Outcome, shape: LoweredShape) -> Outcome + admit_callers: [ + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_forward_row_apply"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_lower_production_emitted") + ] +{ + match o { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: BodyLowered { node: n }, diagnostics: d } => + Accepted { value: BodyLowered { node: body_term_seal(node: n, shape: shape) }, diagnostics: d } + Accepted { value: retained, diagnostics: d } => Accepted { value: retained, diagnostics: d } + } +} + +// A LEAF IS A TOKEN AS THE PARSE WROTE IT: an atom or a childless conj, or one of the three literal +// tokens the parse mints with their payload already attached (v2.compiler.parse parse_expr_terminal: +// v2.extdeps.languages.dag dag_int_literal_node_from_lexeme, dag_string_literal_node_from_lexeme, +// dag_float_literal_node_from_lexeme) -- that token's atom with exactly its own payload edge. +fn body_term_literal_token_payload_field(id: Symbol) -> Optional { + if id == ^dag_token_int_literal { + optional_present(value: ^integer_literal_magnitude_field) + } else if id == ^dag_token_string_literal { + optional_present(value: ^string_literal_value_field) + } else if id == ^dag_token_float_literal { + optional_present(value: ^float_literal_lexeme_field) } else { - match body_lower_find_core_substrate_optional(root: root) { - Absent => optional_absent() - Present { value: _ } => - if body_lower_is_unrecognised_lowered_shape(node: root) { - optional_present(value: root) - } else { - match body_lower_find_core_substrate_first_hit_child(root: root) { - Absent => optional_absent() - Present { value: child } => body_lower_find_core_substrate_passes_unrecognised(root: child) - } - } - } + optional_absent() } } -fn body_lower_find_core_substrate_first_hit_child(root: Node) -> Optional { - fold(root.children, init: Absent, f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => - match body_lower_find_core_substrate_optional(root: e.target) { - Present { value: _ } => optional_present(value: e.target) - Absent => optional_absent() +fn body_term_is_leaf(n: Node) -> Bool + admit_callers: [ + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_term_of") + ] +{ + match n.kind { + TypeNode { connective: Atom { identity: id } } => + if length(xs: n.children) == 0 { + true + } else if length(xs: n.children) == 1 { + match body_term_literal_token_payload_field(id: id) { + Absent => false + Present { value: field } => + match list_at_optional(xs: n.children, index: 0) { + Present { value: e } => body_term_edge_named(e: e, name: field) + Absent => false + } } - } - }) + } else { + false + } + TypeNode { connective: Conj } => length(xs: n.children) == 0 + TypeNode { connective: Disj } => false + TypeNode { connective: Arrow } => false + TypeNode { connective: Cardinality } => false + TypeNode { connective: Instantiation } => false + ComputationNode { behavior: _ } => false + } +} + +// THE ONE READER. Four answers, each positive; the fourth refuses. +fn body_term_of(node: Node) -> Outcome { + match body_term_wrapper_optional(n: node) { + Present { value: w } => outcome_accepted(value: Lowered { node: w.node, shape: w.shape }) + Absent => + if body_lower_has_grammar_projection_edge(node: node) { + outcome_accepted(value: RawParse { node: node }) + } else if body_term_is_leaf(n: node) { + outcome_accepted(value: RawLeaf { node: node }) + } else { + Rejected { diagnostics: body_term_unrecognised_diagnostics(node: node) } + } + } +} + +fn body_term_unrecognised_diagnostics(node: Node) -> NonEmptyDiagnostics { + diagnostics_singleton( + d: body_lower_diagnostic(reason: ^body_lowering_reason_unrecognised_lowered_shape, n: node) + ) +} + +// THE ONE EGRESS. The stage leaves the term vocabulary exactly once, where normalize's module root +// admits the lowered tree (v2.compiler.normalized_tree admit_retention_free): every wrapper anywhere +// in the tree is replaced by the node it holds. Nothing else may erase a term, so a second exit +// refuses at the call edge. +fn body_term_erase_tree(n: Node) -> Node + admit_callers: [ + decl_ref(module_path: "v2.compiler.normalized_tree", decl_name: "admit_retention_free"), + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_term_erase_tree") + ] +{ + match body_term_wrapper_optional(n: n) { + Present { value: w } => body_term_erase_tree(n: w.node) + Absent => + node_rebuild( + n: n, + children: fold(n.children, init: Empty, f: fn(acc, e) { + list_snoc_item(xs: acc, item: Edge { label: e.label, target: body_term_erase_tree(n: e.target) }) + }) + ) + } } fn body_lower_is_pass_through_emitted(emitted: Symbol) -> Bool { @@ -1619,8 +1762,8 @@ fn body_lower_operand_from_shell_read(shell: Node) -> OperandRefRead { // operator expression, read whole by body_lower_read_operator_expression, or it is not an operand // this reader recognises. The arm this replaces returned pair.left for every sequence, so `a && b` // read as `a` and `!a` read as `!` (gunbc.recurring_failure_mode -// lowering_accessor_collapses_a_sequence_operand). An operator expression the tower reader refuses is -// a refusal here too, carrying the tower reader's cause at its position. +// lowering_accessor_collapses_a_sequence_operand). An operator expression the application reader refuses is +// a refusal here too, carrying that reader's cause at its position. fn body_lower_operand_ref_sequence_read(node: Node) -> OperandRefRead { match body_lower_read_operator_expression(node: node) { OperatorExpressionRead { lowered: lowered } => OperandRefLowered { lowered: lowered } @@ -1667,12 +1810,15 @@ fn body_lower_operand_read_outcome(read: OperandRefRead, diagnostics: Diagnostic // happened to every literal argument (`f(1)` carried a bare dag_token_int_literal) until that // arm was ordered first. fn body_lower_operand_ref_read(node: Node) -> OperandRefRead { - if body_lower_is_unrecognised_lowered_shape(node: node) { - OperandRefRefused { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: node) } - } else { - match body_lower_is_core_substrate(node: node) { - true => OperandRefLowered { lowered: node } - false => + match body_term_of(node: node) { + Rejected { diagnostics: r } => OperandRefRefused { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => OperandRefLowered { lowered: lowered } + Accepted { value: _, diagnostics: _ } => body_lower_operand_raw_read(node: node) + } +} + +// The operand read of raw parse: a production, a token, or a sequence the parse stamped. +fn body_lower_operand_raw_read(node: Node) -> OperandRefRead { match parse_production_emitted_identity_optional(node: node) { Present { value: _emitted } => match parse_production_captured_child_optional(node: node) { @@ -1734,8 +1880,6 @@ fn body_lower_operand_ref_read(node: Node) -> OperandRefRead { } } } - } - } } fn body_lower_is_block_delimiter_token(id: Symbol) -> Bool { @@ -1825,29 +1969,22 @@ fn body_lower_unwrap_captured(shell: Node) -> Optional { parse_production_captured_child_optional(node: shell) } -// AN OPERATOR EXPRESSION IS READ OFF THE GRAMMAR'S OWN PRECEDENCE TOWER, WHOLE, AND A POSITION THE +// AN OPERATOR EXPRESSION IS READ OFF THE GRAMMAR'S OWN APPLICATION NODES, WHOLE, AND A POSITION THE // READER CANNOT READ REFUSES. // -// v2.extdeps.languages.dag dag_grammar_binary_expr_expr is a tower of seven levels, each -// level = sequence(next_level, tail) -// whose tail is a REPEAT of sequence(operator, next_level) for the pipe, or, and, additive and -// multiplicative levels and an OPTIONAL of the same for equality and comparison. The parse captures -// a repeat as a cons spine -- sequence(element, rest), ending in the empty conj -- and an optional -// as its element or the empty conj, so a tail is exactly one of -// the empty conj (no operator at this level) -// sequence(operator, operand) (an optional that matched) -// sequence(sequence(operator, operand), rest) (a repeat element and the rest of the repeat) -// and the bottom of the tower is dag_grammar_unary_expr_expr: a prefix `!` or `-` over another -// unary, or a postfix expression. +// v2.extdeps.languages.dag dag_grammar_binary_expr_expr is an operator table (v2.std.grammar +// InfixTable): the parse captures each operator APPLICATION as one node carrying three projections +// -- grammar_infix_left, grammar_infix_operator, grammar_infix_right -- and an operand with no +// operator after it as the operand itself. Precedence and associativity are already decided by the +// tree, so this reader lowers an application's two operands and applies its operator; it neither +// re-folds a continuation spine nor strips precedence levels, because the tree carries neither +// (docs/plans/precedence-climbing-tree-contract.md). The operand is dag_grammar_unary_expr_expr: a +// prefix `!` or `-` over another unary, or a postfix expression. // -// THE READER IT REPLACES READ ONE OPERATOR AND DROPPED THE REST, SILENTLY. It lowered the first -// sequence(operator, operand) it found and returned, so `a && b && c` lowered to `a && b` with `c` -// gone; its operand reader, body_lower_operand_ref_optional, narrowed any other sequence to its -// LEFT element, so `!a && b` lost `a` (read as the bare `!`) and, in a body the bottom-up fold had -// not lowered, `a && b` became `a`. Measured on the real tokenize -> parse -> normalize route: an -// atom only on the right of those shapes reached no reference site -// (gunbc.recurring_failure_mode lowering_accessor_collapses_a_sequence_operand). Every operand is -// now lowered and folded left-associatively, and an unreadable position is a located refusal. +// THE READER BEFORE THE TOWER READER READ ONE OPERATOR AND DROPPED THE REST, SILENTLY: `a && b && c` +// lowered to `a && b`, and its operand reader narrowed any sequence to its LEFT element, so `!a && b` +// lost `a` (gunbc.recurring_failure_mode lowering_accessor_collapses_a_sequence_operand). Every +// operand is lowered and an unreadable position is a located refusal. type OperatorExpressionRead = OperatorExpressionAbsent | OperatorExpressionRead { lowered: Node } @@ -1873,7 +2010,10 @@ fn body_lower_grammar_terminals(expr: GrammarExpr) -> List { expect: fn(e, _reason) { e }, refuse_on_match: fn(e, _reason) { e }, after_line_break: fn(e) { e }, - not_predicate: fn(_e) { Empty } + not_predicate: fn(_e) { Empty }, + infix_table: fn(operand, _guard, operators) { + list_append(left: operand, right: list_map(xs: operators, f: fn(row) { grammar_infix_row_class(row: row) })) + } } ) } @@ -1920,13 +2060,15 @@ fn body_lower_operator_token_optional(node: Node, binary: Bool) -> Optional Node { +// A SEQUENCE WHOSE RIGHT IS THE EMPTY CONJ IS ITS LEFT: a production whose trailing repeat or +// optional matched nothing (a postfix expression with no suffix) captures sequence(head, empty), and +// the operand it denotes is the head. +fn body_lower_strip_empty_tails(node: Node) -> Node { let stripped = body_lower_unwrap_surface_shell(node: node) match sugar_sequence_pair_optional(node: stripped) { Present { value: pair } => if is_empty_conj_root(n: pair.right) { - body_lower_strip_operator_free_levels(node: pair.left) + body_lower_strip_empty_tails(node: pair.left) } else { stripped } @@ -1934,21 +2076,24 @@ fn body_lower_strip_operator_free_levels(node: Node) -> Node { } } -// Whether a tail carries an operator: its first element is one, or is sequence(operator, operand). -fn body_lower_tail_has_binary_operator(tail: Node) -> Bool { - match sugar_sequence_pair_optional(node: tail) { - Absent => false - Present { value: pair } => - match body_lower_operator_token_optional(node: pair.left, binary: true) { - Present { value: _ } => true - Absent => - match sugar_sequence_pair_optional(node: pair.left) { - Present { value: element } => - match body_lower_operator_token_optional(node: element.left, binary: true) { - Present { value: _ } => true - Absent => false - } - Absent => false +// ONE OPERATOR APPLICATION, AS THE PARSE STAMPED IT (v2.compiler.parse parse_stamp_infix_application). +type InfixApplication { + left: Node + operator: Node + right: Node +} + +fn body_lower_infix_application_optional(node: Node) -> Optional { + match body_lower_seq_child(node: node, name: ^grammar_infix_operator_node_projection) { + Absent => optional_absent() + Present { value: operator } => + match body_lower_seq_child(node: node, name: ^grammar_infix_left_node_projection) { + Absent => optional_absent() + Present { value: left } => + match body_lower_seq_child(node: node, name: ^grammar_infix_right_node_projection) { + Absent => optional_absent() + Present { value: right } => + optional_present(value: InfixApplication { left: left, operator: operator, right: right }) } } } @@ -1960,16 +2105,15 @@ fn body_lower_operator_refusal(reason: Symbol, at: Node) -> OperatorExpressionRe // One operand of an operator expression, lowered whole or refused at the operand. fn body_lower_operator_operand(node: Node) -> Outcome { - let operand = body_lower_strip_operator_free_levels(node: node) + let operand = body_lower_strip_empty_tails(node: node) match body_lower_read_operator_expression(node: operand) { OperatorExpressionRead { lowered: lowered } => outcome_accepted(value: lowered) OperatorExpressionRefused { diagnostics: r } => Rejected { diagnostics: r } OperatorExpressionAbsent => - if body_lower_is_core_substrate(node: operand) { - outcome_accepted(value: operand) - } else if body_lower_is_unrecognised_lowered_shape(node: operand) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: operand) } - } else { + match body_term_of(node: operand) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => outcome_accepted(value: lowered) + Accepted { value: _, diagnostics: _ } => match body_lower_try_body_lowered(node: operand) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: Present { value: lowered }, diagnostics: d } => @@ -2014,7 +2158,7 @@ fn body_lower_operator_is_pipe(operator: Node) -> Bool { fn body_lower_piped_fold_optional(acc: Node, operator: Node, right: Node) -> Optional> { if body_lower_operator_is_pipe(operator: operator) { - match body_lower_fold_call_beneath(node: body_lower_strip_operator_free_levels(node: right)) { + match body_lower_fold_call_beneath(node: body_lower_strip_empty_tails(node: right)) { Absent => optional_absent() Present { value: call } => match fold_call_lowering_piped(fold_call: call, receiver: acc) { @@ -2051,160 +2195,67 @@ fn body_lower_fold_call_beneath(node: Node) -> Optional { } } -// Whether a tower tail element is `|> `: the element is sequence(operator, operand). -fn body_lower_tail_element_pipes_into_fold(element: Node) -> Bool { - match sugar_sequence_pair_optional(node: element) { +// A PIPE INTO A FOLD IS READ AT ITS APPLICATION, BEFORE ITS OPERANDS ARE FOLDED. The bottom-up fold +// lowers an operator expression's children first, so the fold call on the right of `|>` would be +// lowered on its own -- with no collection, since the pipe supplies it -- before the application +// could hand it the pipe's input. So an application whose operator is `|>` and whose right operand is +// a fold-family call is read whole, raw, by body_lower_read_operator_expression, whose binary step +// (body_lower_binary_apply) builds the fold encoding with the left operand as the collection. The +// decision is the node's own: one projection lookup and one identity comparison, no walk -- the tree +// states which operands each `|>` binds, so no tower root has to be found first. Every other +// application takes the ordinary bottom-up path unchanged. +fn body_lower_application_pipes_into_fold(node: Node) -> Bool { + match body_lower_infix_application_optional(node: node) { Absent => false - Present { value: e } => - match body_lower_operator_token_optional(node: e.left, binary: true) { - Absent => false - Present { value: operator } => - if body_lower_operator_is_pipe(operator: operator) { - match body_lower_fold_call_beneath(node: body_lower_strip_operator_free_levels(node: e.right)) { + Present { value: app } => + match node_atom_identity_optional(node: app.operator) { + Present { value: id } => + if id == ^dag_token_pipe_gt { + match body_lower_fold_call_beneath(node: body_lower_strip_empty_tails(node: app.right)) { Present { value: _ } => true Absent => false } } else { false } + Absent => false } } } -// Whether a tower tail -- empty, sequence(operator, operand), or a repeat spine -// sequence(sequence(operator, operand), rest) -- pipes into a fold-family call anywhere along it. -fn body_lower_tail_pipes_into_fold(tail: Node) -> Bool { - if is_empty_conj_root(n: tail) { - false - } else if body_lower_tail_element_pipes_into_fold(element: tail) { - true - } else { - match sugar_sequence_pair_optional(node: tail) { - Absent => false - Present { value: pair } => - body_lower_tail_element_pipes_into_fold(element: pair.left) || body_lower_tail_pipes_into_fold(tail: pair.right) - } - } -} - -// Whether a node is the `|>` token: one identity comparison, no operator-list scan. -fn body_lower_node_is_pipe_token(node: Node) -> Bool { - match node_atom_identity_optional(node: node) { - Present { value: id } => id == ^dag_token_pipe_gt - Absent => false - } -} - -// Whether a tail's FIRST element is headed by `|>`: the tail is sequence(operator, operand) or -// sequence(sequence(operator, operand), rest), read from the node's own shape. -fn body_lower_tail_starts_with_pipe(tail: Node) -> Bool { - match sugar_sequence_pair_optional(node: tail) { - Absent => false - Present { value: pair } => - if body_lower_node_is_pipe_token(node: pair.left) { - true - } else { - match sugar_sequence_pair_optional(node: pair.left) { - Absent => false - Present { value: element } => body_lower_node_is_pipe_token(node: element.left) - } - } - } -} - -// THE ROOT OF A PIPE TOWER, DECIDED FROM THE NODE'S OWN SHAPE BY IDENTITY COMPARISONS ALONE. -// body_lower_fold runs at every node, so this guard reads only the node's own sequence pair and -// compares token identities with `|>`: no walk, and no scan of the operator roster (the first -// constant-time cut asked body_lower_operator_token_optional, whose roster fold ran several times -// per node and doubled the native route's context phase -- 918s at base against 1892s, census -// gunbc#12550). The pipe is the outermost level of v2.extdeps.languages.dag -// dag_grammar_binary_expr_expr, so a sequence whose tail starts with `|>` is a pipe tower; a -// repeat-spine node inside that tail, sequence(sequence(`|>`, operand), rest), is excluded by its -// left element starting with `|>`, so the tower is decided once, at its root. -fn body_lower_is_pipe_tower_root(node: Node) -> Bool { - match sugar_sequence_pair_optional(node: node) { - Absent => false - Present { value: pair } => - if body_lower_tail_starts_with_pipe(tail: pair.left) { - false - } else { - body_lower_tail_starts_with_pipe(tail: pair.right) +fn body_lower_read_infix_application(app: InfixApplication) -> OperatorExpressionRead { + match body_lower_operator_token_optional(node: app.operator, binary: true) { + Absent => body_lower_operator_refusal(reason: ^body_lowering_reason_operator_tail_unread, at: app.operator) + Present { value: operator } => + match body_lower_operator_operand(node: app.left) { + Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } + Accepted { value: left, diagnostics: _ } => + match body_lower_binary_apply(acc: left, operator: operator, right: app.right) { + Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } + Accepted { value: applied, diagnostics: _ } => OperatorExpressionRead { lowered: applied } + } } } } -// A TOWER THAT PIPES INTO A FOLD IS READ WHOLE, BEFORE ITS OPERANDS ARE FOLDED. The bottom-up fold -// lowers an operator expression's children first, so the fold call on the right of `|>` would be -// lowered on its own -- with no collection, since the pipe supplies it -- before the tower could hand -// it the pipe's input. So such a tower -- recognised at its ROOT by structure (body_lower_is_pipe_tower_root, -// constant time), whatever production identity the node carries, since the value reader unwraps shells -// before folding; only a root then pays the tail scan below, once per tower -- is read by the -// raw tower reader, whose binary step -// (body_lower_binary_apply) builds the fold encoding with the accumulated left operand as the -// collection. Every other tower takes the ordinary bottom-up path unchanged. -fn body_lower_tower_pipes_into_fold(node: Node) -> Bool { - match sugar_sequence_pair_optional(node: body_lower_strip_operator_free_levels(node: node)) { - Absent => false - Present { value: pair } => body_lower_tail_pipes_into_fold(tail: pair.right) - } -} - -fn body_lower_fold_operator_tail(acc: Node, tail: Node) -> OperatorExpressionRead { - if is_empty_conj_root(n: tail) { - OperatorExpressionRead { lowered: acc } - } else { - match sugar_sequence_pair_optional(node: tail) { - Absent => body_lower_operator_refusal(reason: ^body_lowering_reason_operator_tail_unread, at: tail) - Present { value: pair } => - match body_lower_operator_token_optional(node: pair.left, binary: true) { - Present { value: operator } => - match body_lower_binary_apply(acc: acc, operator: operator, right: pair.right) { - Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } - Accepted { value: applied, diagnostics: _ } => OperatorExpressionRead { lowered: applied } - } - Absent => - match sugar_sequence_pair_optional(node: pair.left) { - Absent => body_lower_operator_refusal(reason: ^body_lowering_reason_operator_tail_unread, at: tail) - Present { value: element } => - match body_lower_operator_token_optional(node: element.left, binary: true) { - Absent => body_lower_operator_refusal(reason: ^body_lowering_reason_operator_tail_unread, at: tail) - Present { value: operator } => - match body_lower_binary_apply(acc: acc, operator: operator, right: element.right) { - Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } - Accepted { value: applied, diagnostics: _ } => - body_lower_fold_operator_tail(acc: applied, tail: pair.right) - } - } - } - } - } - } -} - -// Absent: this node is not an operator expression -- no prefix operator heads it and no operator -// sits in its tower tail. Read: every operand lowered. Refused: an operator was read and a position -// under it was not. +// Absent: this node is not an operator expression -- it is no application and no prefix operator +// heads it. Read: every operand lowered. Refused: an operator was read and a position under it was not. fn body_lower_read_operator_expression(node: Node) -> OperatorExpressionRead { - let stripped = body_lower_strip_operator_free_levels(node: node) - match sugar_sequence_pair_optional(node: stripped) { - Absent => OperatorExpressionAbsent - Present { value: pair } => - match body_lower_operator_token_optional(node: pair.left, binary: false) { - Present { value: operator } => - match body_lower_operator_operand(node: pair.right) { - Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } - Accepted { value: operand, diagnostics: _ } => - OperatorExpressionRead { lowered: lower_unary_prefix(operator: operator, operand: operand) } - } - Absent => - if body_lower_tail_has_binary_operator(tail: pair.right) { - match body_lower_operator_operand(node: pair.left) { - Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } - Accepted { value: left, diagnostics: _ } => - body_lower_fold_operator_tail(acc: left, tail: pair.right) - } - } else { - OperatorExpressionAbsent + let stripped = body_lower_strip_empty_tails(node: node) + match body_lower_infix_application_optional(node: stripped) { + Present { value: app } => body_lower_read_infix_application(app: app) + Absent => + match sugar_sequence_pair_optional(node: stripped) { + Absent => OperatorExpressionAbsent + Present { value: pair } => + match body_lower_operator_token_optional(node: pair.left, binary: false) { + Present { value: operator } => + match body_lower_operator_operand(node: pair.right) { + Rejected { diagnostics: r } => OperatorExpressionRefused { diagnostics: r } + Accepted { value: operand, diagnostics: _ } => + OperatorExpressionRead { lowered: lower_unary_prefix(operator: operator, operand: operand) } + } + Absent => OperatorExpressionAbsent } } } @@ -3272,19 +3323,6 @@ fn body_lower_return_type_from_fn_captured(captured: Node) -> Optional { } } -fn body_lower_find_core_substrate_optional(root: Node) -> Optional { - if body_lower_is_core_substrate(node: root) { - optional_present(value: root) - } else { - fold(root.children, init: Absent, f: fn(acc, e) { - match acc { - Present { value: _ } => acc - Absent => body_lower_find_core_substrate_optional(root: e.target) - } - }) - } -} - // THE FOLD'S ANSWER IS CARRIED, NOT RE-DEMANDED. The reducers leave a lowered node behind // pass-through shells (body_lower_lowered_behind_shells_read), so testing only the ROOT for core // substrate discarded a fold that had already lowered the node; every later strategy in @@ -3307,11 +3345,16 @@ fn body_lower_try_fold_behavior_optional(node: Node) -> Outcome> // every call site then walks the interior. `{` is also unwritable as an // infix operator in body_lower_operator_atom_optional. fn body_lower_try_body_lowered(node: Node) -> Outcome> { - if body_lower_is_core_substrate(node: node) { - outcome_accepted(value: optional_present(value: node)) - } else if body_lower_is_unrecognised_lowered_shape(node: node) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: node) } - } else if body_lower_sequence_left_is_lbrace(captured: node) { + match body_term_of(node: node) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => outcome_accepted(value: optional_present(value: lowered)) + Accepted { value: _, diagnostics: _ } => body_lower_try_raw_body_lowered(node: node) + } +} + +// The strategies over a node the parse stamped, in order; Absent means none of them lowers it. +fn body_lower_try_raw_body_lowered(node: Node) -> Outcome> { + if body_lower_sequence_left_is_lbrace(captured: node) { outcome_accepted(value: optional_absent()) } else { match body_lower_dotted_chain_lowered_optional(node: node) { @@ -3395,38 +3438,40 @@ fn body_lower_reduce_emitted(folded: Node, emitted: Symbol) -> Outcome { } else if emitted == ^dag_surface_binary_expr { match body_lower_try_infix_transform(node: folded) { Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: Present { value: transform }, diagnostics: _ } => outcome_accepted(value: transform) + Accepted { value: Present { value: transform }, diagnostics: _ } => outcome_accepted(value: body_term_seal(node: transform, shape: LoweredOperatorExpression)) Accepted { value: Absent, diagnostics: _ } => body_lower_reduce_passthrough(folded: folded) } } else if emitted == ^dag_surface_postfix_expr { - body_lower_reduce_or_keep(o: body_lower_postfix_expr(shell: folded), folded: folded) + body_lower_reduce_or_keep(o: body_term_sealed_outcome(o: body_lower_postfix_expr(shell: folded), shape: LoweredPostfix), folded: folded) } else if emitted == ^dag_surface_primary_expr { - body_lower_reduce_or_keep(o: body_lower_primary_expr(shell: folded), folded: folded) + body_lower_reduce_or_keep(o: body_term_sealed_outcome(o: body_lower_primary_expr(shell: folded), shape: LoweredPrimary), folded: folded) } else if body_lower_is_pass_through_emitted(emitted: emitted) { body_lower_reduce_passthrough(folded: folded) } else if emitted == ^dag_surface_fn_decl { - body_lower_reduce_or_keep(o: body_lowering_refuse_retained(o: body_lower_fn_decl_to_arrow(shell: folded)), folded: folded) + body_lower_reduce_or_keep(o: body_term_sealed_outcome(o: body_lowering_refuse_retained(o: body_lower_fn_decl_to_arrow(shell: folded)), shape: LoweredFnDeclaration), folded: folded) } else if emitted == ^dag_surface_test_fn_decl { - body_lower_reduce_or_keep(o: body_lowering_refuse_retained(o: body_lower_test_fn_decl_to_member(shell: folded)), folded: folded) + body_lower_reduce_or_keep(o: body_term_sealed_outcome(o: body_lowering_refuse_retained(o: body_lower_test_fn_decl_to_member(shell: folded)), shape: LoweredTestDeclaration), folded: folded) } else if emitted == ^dag_surface_data_decl { - body_lower_reduce_or_keep(o: body_lowering_refuse_retained(o: body_lower_data_decl_to_member(shell: folded)), folded: folded) + body_lower_reduce_or_keep(o: body_term_sealed_outcome(o: body_lowering_refuse_retained(o: body_lower_data_decl_to_member(shell: folded)), shape: LoweredDataDeclaration), folded: folded) } else { outcome_accepted(value: folded) } } fn body_lower_reduce(folded: Node) -> Outcome { - if body_lower_is_core_substrate(node: folded) { - outcome_accepted(value: folded) - } else if dag_resolve_preserve_module_metadata_subtree(node: folded) { - outcome_accepted(value: folded) - } else if body_lower_is_unrecognised_lowered_shape(node: folded) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: folded) } - } else { - match parse_production_emitted_identity_optional(node: folded) { - Absent => outcome_accepted(value: folded) - Present { value: emitted } => body_lower_reduce_emitted(folded: folded, emitted: emitted) - } + match body_term_of(node: folded) { + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => outcome_accepted(value: folded) + Rejected { diagnostics: r } => + if dag_resolve_preserve_module_metadata_subtree(node: folded) { outcome_accepted(value: folded) } else { Rejected { diagnostics: r } } + Accepted { value: _, diagnostics: _ } => + if dag_resolve_preserve_module_metadata_subtree(node: folded) { + outcome_accepted(value: folded) + } else { + match parse_production_emitted_identity_optional(node: folded) { + Absent => outcome_accepted(value: folded) + Present { value: emitted } => body_lower_reduce_emitted(folded: folded, emitted: emitted) + } + } } } @@ -3459,7 +3504,7 @@ fn body_lower_fold_identity_absent(node: Node) -> Outcome { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: lowered_opt, diagnostics: d } => match lowered_opt { - Present { value: lowered } => Accepted { value: lowered, diagnostics: d } + Present { value: lowered } => Accepted { value: body_term_seal(node: lowered, shape: LoweredMatch), diagnostics: d } Absent => body_lower_fold_reduce(node: node) } } @@ -3477,13 +3522,19 @@ fn body_lower_fold_identity_absent(node: Node) -> Outcome { // step argument is still the literal fold_lowering reads; a call that is not a fold, or whose step // is not an inline literal, folds as any other expression. fn body_lower_fold(node: Node) -> Outcome { - if body_lower_is_core_substrate(node: node) { - outcome_accepted(value: node) - } else if body_lower_is_unrecognised_lowered_shape(node: node) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: node) } - } else if body_lower_is_pipe_tower_root(node: node) && body_lower_tower_pipes_into_fold(node: node) { + match body_term_of(node: node) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => outcome_accepted(value: node) + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => outcome_accepted(value: node) + Accepted { value: RawParse { node: _ }, diagnostics: _ } => body_lower_fold_raw(node: node) + } +} + +// The fold over a node the parse stamped: what it lowers to leaves sealed with its producer's arm. +fn body_lower_fold_raw(node: Node) -> Outcome { + if body_lower_is_pipe_tower_root(node: node) && body_lower_tower_pipes_into_fold(node: node) { match body_lower_read_operator_expression(node: node) { - OperatorExpressionRead { lowered: lowered } => outcome_accepted(value: lowered) + OperatorExpressionRead { lowered: lowered } => body_term_sealed_outcome(o: outcome_accepted(value: lowered), shape: LoweredOperatorExpression) OperatorExpressionRefused { diagnostics: r } => Rejected { diagnostics: r } OperatorExpressionAbsent => body_lower_fold_by_identity(node: node) } @@ -3496,10 +3547,10 @@ fn body_lower_fold_by_identity(node: Node) -> Outcome { match parse_production_emitted_identity_optional(node: node) { Present { value: emitted } => if (emitted == ^dag_surface_arrow_lambda) || (emitted == ^dag_surface_fn_literal) { - body_lower_function_value_in_fold(node: node) + body_term_sealed_outcome(o: body_lower_function_value_in_fold(node: node), shape: LoweredFunctionValue) } else { match body_lower_fold_family_call_first(node: node) { - Present { value: lowered } => lowered + Present { value: lowered } => body_term_sealed_outcome(o: lowered, shape: LoweredFoldEncoding) Absent => body_lower_fold_dispatch(node: node, emitted: emitted) } } @@ -3707,15 +3758,15 @@ fn body_lower_fold_family_call_first(node: Node) -> Optional> { fn body_lower_fold_dispatch(node: Node, emitted: Symbol) -> Outcome { if emitted == ^dag_surface_match_expr { - body_lower_match_expr(shell: node) + body_term_sealed_outcome(o: body_lower_match_expr(shell: node), shape: LoweredMatch) } else if emitted == ^dag_surface_if_expr { - body_lower_if_expr(shell: node) + body_term_sealed_outcome(o: body_lower_if_expr(shell: node), shape: LoweredBranch) } else if emitted == ^dag_surface_loop_expr { - body_lower_loop_expr(shell: node) + body_term_sealed_outcome(o: body_lower_loop_expr(shell: node), shape: LoweredLoop) } else if emitted == ^dag_surface_let_expr { - body_lower_let_expr(shell: node) + body_term_sealed_outcome(o: body_lower_let_expr(shell: node), shape: LoweredLet) } else if emitted == ^dag_surface_stmt_seq { - body_lower_stmt_seq(shell: node) + body_term_sealed_outcome(o: body_lower_stmt_seq(shell: node), shape: LoweredStatementSequence) } else if body_lower_is_metadata_preserved_emitted(emitted: emitted) { outcome_accepted(value: node) } else if body_lower_is_deferred_lower_emitted(emitted: emitted) { @@ -3754,18 +3805,14 @@ fn body_lower_body_subtree_lower_from_primary(captured: Node) -> Outcome body_lower_body_subtree_lower(captured: stmt) ParseSubtreeAbsent => - match body_lower_find_core_substrate_passes_unrecognised(root: captured) { - Present { value: passed } => - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: passed) } - Absent => - match body_lower_find_core_substrate_optional(root: captured) { - Present { value: lowered } => - outcome_accepted(value: optional_present(value: lowered)) - Absent => - body_lower_try_body_lowered( - node: body_lower_unwrap_surface_shell(node: captured) - ) - } + match body_lower_lowered_behind_shells_read(node: captured) { + OperandRefLowered { lowered: lowered } => + outcome_accepted(value: optional_present(value: lowered)) + OperandRefRefused { diagnostics: r } => Rejected { diagnostics: r } + OperandRefAbsent => + body_lower_try_body_lowered( + node: body_lower_unwrap_surface_shell(node: captured) + ) } } } @@ -4042,7 +4089,37 @@ fn body_lower_body_after_eq_token(captured: Node) -> Optional { // unsupported_form at the target -- the refusal-forgetting projection of // gunbc.recurring_failure_mode lowering_accessor_collapses_a_sequence_operand, on the reify route. // Only a target neither reader reads is unsupported_form, located at it. +// THE INTERIOR OF A LOWERED VALUE IS LOWERED BY POSITION. The reify pass walks a value a producer +// already built, so a composite under it that the parse did not stamp is that producer's own output, +// not a node of unknown provenance: it is kept as it stands. A stamped composite or a leaf under it +// is still read as the parse wrote it. This reading is confined to the reify pass; everywhere else +// an unstamped, unwrapped composite refuses. +// WHY THIS IS NOT A WIDENED FAILURE ARM (review 75108). body_term_of has exactly one Rejected arm: +// a composite that is neither wrapped nor parse-stamped. The parse stamps every composite it mints, +// and reify's input is the fold's own output (body_lower_reify_arrow_body is reached only from a +// producer holding a body it lowered), so under reify that arm can hold only a producer's internal +// structure. Refusing it was measured, not assumed: propagating the refusal turned three real paths +// red (v2.test.claim.namespace_xl0.reference_conservation the_if_arm_call_argument_is_conserved_holds, +// caret_symbol_payloads_are_conserved_holds, a_list_literal_call_argument_conserves_every_element_holds). +// The reading is grounded in provenance by position, not in a shape list, and that is its rung: +// mitigation, held by the reify call graph rather than by a type. The climb is the stage ceiling +// named above, a sealed BodyTerm term tree in which a producer's interior is itself a term, after +// which this arm deletes and an unplaceable composite refuses here as everywhere else. +// The question is asked of the term authority: a parse production or a leaf is read as the parse +// wrote it; a Lowered term, or a composite body_term_of cannot place, is lowered by position here. +fn body_lower_reify_interior_is_lowered(node: Node) -> Bool { + match body_term_of(node: node) { + Accepted { value: RawParse { node: _ }, diagnostics: _ } => false + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => false + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => true + Rejected { diagnostics: _ } => true + } +} + fn body_lower_reify_operand_read(node: Node) -> Outcome { + if body_lower_reify_interior_is_lowered(node: node) { + outcome_accepted(value: node) + } else { match body_lower_operand_ref_read(node: node) { OperandRefLowered { lowered: op } => outcome_accepted(value: op) OperandRefRefused { diagnostics: r } => Rejected { diagnostics: r } @@ -4056,6 +4133,7 @@ fn body_lower_reify_operand_read(node: Node) -> Outcome { ) } } + } } // The head (positional 0) and a Named edge are kept as authored: the named edge is a @@ -4413,8 +4491,10 @@ fn body_lower_repeated_param_binding_diagnostic(edge: Edge) -> Diagnostic { // returns it: that is the seed's own realization (v1.compiler.emit_rust emits a data item as a // nullary fn and calls it at every reference), and it lets the existing Arrow, product and literal // derivations ground the member with no data-specific infer rule. The data-versus-fn distinction a -// reference site needs is not carried by this shape; it lives in the declaration index -// (v2.std.decl_index data_decl_type_facts), which reads the parse, not this tree. A shell whose +// reference site needs IS carried, by one marker on the Arrow (body_lower_constant_member_arrow, v2.std.node +// ConstantMemberEdge): a reference to the member uses its value. v2.std.decl_index data_decl_type_facts +// still answers the same question for a corpus CENSUS over the parse (a second reader of the fact, declared +// as a frontier until a corpus index over lowered roots exists to read the marker instead). A shell whose // name, declared type or value is not reachable is retained as the counted frontier, exactly as // the fn arm retains, never guessed. // THE INITIALIZER IS ALREADY LOWERED WHEN THIS ARM RUNS. normalize_node_ctx folds a declaration's @@ -4424,12 +4504,10 @@ fn body_lower_repeated_param_binding_diagnostic(edge: Edge) -> Diagnostic { // which retain on shapes they do not recognize (a record literal whose field is a fn literal). // Only an initializer that is not yet substrate goes through that reader. fn body_lower_data_initializer_body(expr: Node) -> Outcome> { - if body_lower_is_core_substrate(node: expr) { - outcome_accepted(value: optional_present(value: expr)) - } else if body_lower_is_unrecognised_lowered_shape(node: expr) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: expr) } - } else { - body_lower_eq_expr_to_body(expr: expr) + match body_term_of(node: expr) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => outcome_accepted(value: optional_present(value: lowered)) + Accepted { value: _, diagnostics: _ } => body_lower_eq_expr_to_body(expr: expr) } } @@ -4465,7 +4543,7 @@ fn body_lower_data_decl_to_member(shell: Node) -> Outcome { ) Present { value: data_name } => let signature = declared_signature(params: [], source: shell) - let arrow = body_lower_callable_arrow( + let arrow = body_lower_constant_member_arrow(arrow: body_lower_callable_arrow( source: shell, domain: signature.domain, codomain: declared_type, @@ -4474,7 +4552,7 @@ fn body_lower_data_decl_to_member(shell: Node) -> Outcome { body: optional_present( value: body_lower_position_function_values(node: reified_body, path: body_lower_unpositioned_path()) ) - ) + )) body_lowering_lowered( o: outcome_with_diagnostics( value: body_lower_fn_decl_named_member_wrap( @@ -4496,13 +4574,25 @@ fn body_lower_data_decl_to_member(shell: Node) -> Outcome { } } +// THE DATA ARROW CARRIES THE ONE FACT THAT DISTINGUISHES IT FROM A NULLARY FN (v2.std.node +// ConstantMemberEdge): a reference to it uses its value. Without the marker the lowered member was +// indistinguishable from `fn x() -> T`, so infer typed a bare reference `x` as the function () -> T +// and every data reference at a declared position refused (a String data row as a concat argument, +// an Int data row as an Int initializer). The fact is kept on the carrier that owns it instead of +// being re-read from the parse. +fn body_lower_constant_member_arrow(arrow: Node) -> Node { + node_rebuild( + n: arrow, + children: list_snoc_item(xs: arrow.children, item: core_edge(marker: ConstantMemberEdge, target: node_synthetic(kind: TypeNode { connective: Conj }, children: []))) + ) +} + // THE DECLARED ORDER, CARRIED ON THE ARROW. Every Arrow built from a declared signature carries one // ^arrow_signature_order_edge (v2.std.arrow_signature signature_order_edge) whose target is // the language's canonical FreeMonoid introduction (lower_list_introduction) of the parameter labels as childless // Atoms, in declared order: arity 0 is the empty introduction, arity 1 a singleton. It is the ONE // place positional binding reads parameter order (v2.std.node arrow_declared_parameter_order). // Resolve does not walk it (v2.compiler.resolve resolve_arrow_node): its atoms are labels, not -// references, so a parameter spelled like a name in scope cannot bind through it. // // THE ONE CALLABLE ARROW. A lambda is surface sugar over the same Arrow a named `fn` declares, so the // child LAYOUT of a callable Arrow -- which children exist, in which order, under which labels -- is @@ -4558,15 +4648,15 @@ fn body_lower_callable_arrow( ) )) } - fn body_lower_fn_decl_arrow(shell: Node, sig: BodyLowerFnSignature, body: Optional) -> Node { + body_lower_fn_decl_named_member_wrap( arrow: body_lower_callable_arrow( source: shell, domain: Edge { label: Positional, target: sig.domain }, codomain: sig.return_type, order_edge: signature_order_edge(order: sig.order, source: shell), - type_param_edges: body_lower_fn_decl_type_params_edges(sig: sig), + type_param_edges: list_append(left: body_lower_fn_decl_type_params_edges(sig: sig), right: body_lower_fn_admit_clause_edges(shell: shell)), body: match body { Absent => optional_absent() Present { value: b } => @@ -4578,6 +4668,317 @@ fn body_lower_fn_decl_arrow(shell: Node, sig: BodyLowerFnSignature, body: Option ) } +// THE ADMIT_CALLERS CARRIER, ONE BUILD PER CLAUSE. The generic dispatch (body_lower_after_children) +// reaches the clause shell BOTTOM-UP, so the clause's OWN production case is where the authored +// encoding is consumed: body_lower_admit_clause_image reads the roster off the raw spine (the reader +// needs the parse encoding) and the image REPLACES the shell, so the raw token subtree never enters +// the tree. The fn declaration's Arrow then finds the image by the shell's own identity (kept in +// body_lower_admit_clause_image) and hangs it under the core marker AdmitCallersEdge — one build, +// one fact, one place (DESIGN section 3). +// +// WHY THE ROSTER IS RE-HUNG AT ALL: a raw grammar-sequence encoding is token atoms, and token atoms +// do not ground, so a raw roster in a subject's own module would refuse the whole fixture at +// infer_grounding_not_derived before any wall could judge it (pinned by probe 2026-10-04) — the same +// reason the where-clause lowering does not carry predicates raw. +// +// FALLBACK, FAIL-CLOSED: if the authored spine does not read, the carrier hangs a MARKER atom at +// the clause's own occurrence instead of a roster. The reader demands a roster conj and REFUSES +// (^infer_reason_admit_callers_entries_unreadable at the clause) — a malformed clause is never a +// shorter roster (DESIGN section 5: a failure arm must refuse, never widen). +// THE CARRIER (v2 body lowering): a fn's authored `admit_callers` clause becomes one AdmitCallersEdge +// on the fn's Arrow, whose target is the clause's lowered roster. The clause is an OPTIONAL ELEMENT of +// the fn-decl production, so it is read off the fn shell's optional-element projection — identified by +// its own production identity (^dag_surface_admit_callers_clause), which distinguishes it from the +// fn header's other optional (the return type). The outer Absent (no such optional) is the only +// silence: a fn without the clause has nothing to judge. Once a clause IS present, every failure to +// read it hangs the clause-fallback marker instead -- the reader refuses it as +// ^infer_reason_admit_callers_entries_unreadable at the clause (DESIGN §5: a failure arm must refuse, +// never widen). A grammar or graft change that reshapes the clause shell switches the wall to +// REFUSING, never off. +fn body_lower_fn_admit_clause_edges(shell: Node) -> List { + match body_lower_admit_clause_shell_optional(shell: shell) { + Absent => [] + Present { value: clause } => + match body_lower_admit_clause_roster_optional(clause: clause) { + Absent => + [core_edge( + marker: AdmitCallersEdge, + target: sugar_type_node(connective: Atom { identity: ^infer_reason_admit_callers_entries_unreadable }, children: [], n: clause) + )] + Present { value: roster } => [core_edge(marker: AdmitCallersEdge, target: roster)] + } + } +} + +// THE FN SHELL'S ADMIT CLAUSE, among its optionals: the optional-element projection whose target's +// production identity is ^dag_surface_admit_callers_clause. +fn body_lower_admit_clause_shell_optional(shell: Node) -> Optional { + match parse_production_captured_child_optional(node: shell) { + Absent => optional_absent() + Present { value: captured } => + match body_lower_find_production_shell_optional(root: captured, emitted: ^dag_surface_admit_callers_clause) { + Absent => optional_absent() + Present { value: clause } => optional_present(value: clause) + } + } +} + +// THE CLAUSE'S ROSTER: the clause shell's named body value (the bracketed entry list), else its +// captured child. +fn body_lower_admit_clause_roster_optional(clause: Node) -> Optional { + match sugar_named_child_optional(root: clause, name: ^admit_callers_clause_body) { + Present { value: roster } => optional_present(value: roster) + Absent => parse_production_captured_child_optional(node: clause) + } +} + +// THE CLAUSE'S LOWERED IMAGE: the shell's own kind and occurrence (so the Arrow finds it), carrying +// the roster as one named value edge. The reused occurrence is legal BECAUSE the shell it came from +// leaves the tree with this replacement, together with the whole raw spine (occurrence wall: a +// minted id on two nodes with different content refuses). The production-identity projection is +// KEPT verbatim, so body_lower_find_production_shell_optional and every graft that recognizes the +// shell still do; only the value encoding is swapped. +fn body_lower_admit_clause_image(clause: Node) -> Node { + let roster = match body_lower_admit_roster_node(clause: clause) { + Absent => sugar_type_node(connective: Atom { identity: ^infer_reason_admit_callers_entries_unreadable }, children: [], n: clause) + Present { value: r } => r + } + node_lowered_from( + source: clause, + kind: clause.kind, + children: list_append( + left: keep_production_projections(children: clause.children), + right: [Edge { label: Authored { name: ^admit_callers_clause_body }, target: roster }] + ) + ) +} + +// The projection edges that are part of the shell's identity (the production-identity projection and +// its peers), kept so the image remains a recognized production shell. +fn keep_production_projections(children: List) -> List { + fold( + xs: children, + init: Empty, + f: fn(acc, e) { + if label_is_projection(e: e) { list_append(left: acc, right: [e]) } else { acc } + } + ) +} + +fn label_is_projection(e: Edge) -> Bool { + match e.label { + Authored { name: name } => + (name == ^grammar_production_identity_node_projection) + || (name == ^grammar_optional_element_node_projection) + Positional => false + StructuralLabel { label: _ } => true + } +} + +fn body_lower_admit_roster_node(clause: Node) -> Optional { + match parse_production_captured_child_optional(node: clause) { + Absent => optional_absent() + Present { value: captured } => + match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: captured)) { + Absent => optional_absent() + Present { value: head_pair } => + match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: head_pair.right)) { + Absent => optional_absent() + Present { value: colon_pair } => + match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: colon_pair.right)) { + Absent => optional_absent() + Present { value: bracket_pair } => + match body_lower_comma_list_items_optional(list_capture: bracket_pair.right) { + Absent => optional_absent() + Present { value: entries } => + body_lower_admit_roster_from_entries(entries: entries, template: bracket_pair.right) + } + } + } + } + } +} + +fn body_lower_admit_roster_from_entries(entries: List, template: Node) -> Optional { + match body_lower_admit_record_spine(entries: entries, template: template) { + Absent => optional_absent() + Present { value: spine } => + optional_present(value: close_lowered_image(root: spine)) + } +} + +// THE CONFORMING ROSTER: the declared sequence-pair spine — one record per entry, under the +// grammar's sequence-projection edges (@grammar_sequence_left_node_projection carries the record, +// @grammar_sequence_right_node_projection carries the rest, the empty conj root ends the spine). +// This is the substrate's standing linked-list-in-a-tree shape, read by the shared +// sugar_sequence_pair_optional; the carrier mints it and adds no second name for it (DESIGN §2). +// Every edge is authored and distinct within its node, so connective_edges_conform accepts it, and +// an empty authored list lowers to the empty conj root without refusing. All built nodes share the +// template's minted occurrence (the template left the tree with the swap); close_lowered_image marks +// every member below the root OccurrencePending so allocation gives each a real id after +// normalization. +fn body_lower_admit_record_spine(entries: List, template: Node) -> Optional { + fold( + xs: entries, + init: optional_present(value: sugar_type_node(connective: Conj, children: [], n: template)), + f: fn(acc, entry) { + match acc { + Absent => optional_absent() + Present { value: rest } => + match body_lower_admit_entry_edge(entry: entry, template: template) { + Absent => optional_absent() + Present { value: record_edge } => + optional_present( + value: sugar_type_node( + connective: Conj, + children: [ + Edge { + label: Authored { name: ^grammar_sequence_left_node_projection }, + target: record_edge.target + }, + Edge { + label: Authored { name: ^grammar_sequence_right_node_projection }, + target: rest + } + ], + n: template + ) + ) + } + } + } + ) +} + +// ONE ENTRY -> ONE RECORD CONJ IN THE SPINE, ALWAYS GROUNDABLE. The entry's call is read through the +// shared readers -- application_head_read for the callee (the canonical Transform-head surface, the +// same accessor every other callee consumer uses) and the authored field edges for the two +// coordinates, whose string literals are read by the language's own +// dag_string_literal_value_symbol_optional. The record carries +// the operator spelling (^decl_ref) and the two lexemes as plain atoms. An entry that READS but is not +// such a call lowers to a marker record the reader refuses at the entry +// (^infer_reason_admit_callers_entry_not_decl_ref) — never dropped, never a raw token subtree. Only a +// spine the reader cannot walk returns Absent, failing the whole roster into the clause fallback's +// refusal. EVERY built node is minted with the roster template's id: close_lowered_image marks them +// all OccurrencePending below the spine root, so allocation gives each a real id and no two built +// nodes ever share one minted identity. +fn body_lower_admit_entry_edge(entry: Node, template: Node) -> Optional { + match body_lower_admit_entry_call_optional(item: entry) { + Absent => optional_absent() + Present { value: call } => + match call.kind { + TypeNode { connective: Conj } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + TypeNode { connective: Atom { identity: _ } } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + TypeNode { connective: Arrow } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + TypeNode { connective: Cardinality } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + TypeNode { connective: Instantiation } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + TypeNode { connective: Disj } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ComputationNode { behavior: Transform } => + match application_head_read(site: call) { + ApplicationHeadFound { head: _, head_identity: AtomHead { identity: id }, operands: _ } => + if symbol_eq(a: id, b: ^decl_ref) { + match body_lower_admit_string_field_edge(call: call, field: ^module_path, template: template) { + Absent => optional_present(value: body_lower_admit_entry_marker(template: template)) + Present { value: mp_edge } => + match body_lower_admit_string_field_edge(call: call, field: ^decl_name, template: template) { + Absent => optional_present(value: body_lower_admit_entry_marker(template: template)) + Present { value: dn_edge } => + optional_present( + value: Edge { + label: Authored { name: ^admit_callers_clause_body }, + target: sugar_type_node( + connective: Conj, + children: [ + Edge { + label: Authored { name: ^decl_ref }, + target: sugar_type_node(connective: Atom { identity: ^decl_ref }, children: [], n: template) + }, + mp_edge, + dn_edge + ], + n: template + ) + } + ) + } + } + } else { + optional_present(value: body_lower_admit_entry_marker(template: template)) + } + ApplicationHeadFound { head: _, head_identity: ArrowHead, operands: _ } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ApplicationHeadRefused { diagnostics: _ } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + NotApplicationHead => + optional_present(value: body_lower_admit_entry_marker(template: template)) + } + ComputationNode { behavior: Loop } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ComputationNode { behavior: Branch } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ComputationNode { behavior: Bind } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ComputationNode { behavior: Match } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + ComputationNode { behavior: Value } => + optional_present(value: body_lower_admit_entry_marker(template: template)) + } + } +} + +// @field -> string literal -> the value lexeme as an interned symbol, read by the language's own +// string-literal reader, hung as the record's authored field edge. Absent when the edge is missing or +// its target is not a string literal. +fn body_lower_admit_string_field_edge(call: Node, field: Symbol, template: Node) -> Optional { + match sugar_named_child_optional(root: call, name: field) { + Absent => optional_absent() + Present { value: literal } => + match dag_string_literal_value_symbol_optional(node: literal) { + Absent => optional_absent() + Present { value: lexeme } => + optional_present( + value: Edge { + label: Authored { name: field }, + target: sugar_type_node(connective: Atom { identity: lexeme }, children: [], n: template) + } + ) + } + } +} + +// THE UNINTERPRETABLE-ENTRY MARKER: a record conj whose single edge names the refusal the reader +// raises at the entry. Minted with the template's id like every other built node; close_lowered_image +// marks it pending and allocation gives it a real id. +fn body_lower_admit_entry_marker(template: Node) -> Edge { + Edge { + label: Authored { name: ^uninterpretable }, + target: sugar_type_node( + connective: Conj, + children: [Edge { + label: Authored { name: ^infer_reason_admit_callers_entry_not_decl_ref }, + target: sugar_type_node(connective: Atom { identity: ^infer_reason_admit_callers_entry_not_decl_ref }, children: [], n: template) + }], + n: template + ) + } +} + +// The entry item is the parse spine of the call expression: nested sequence pairs whose every right +// component is empty (a call has no operator tail), bottoming at the lowered comp:transform. Descend +// the left spine to the leaf; a non-empty tail is not an entry shape this reader admits. +fn body_lower_admit_entry_call_optional(item: Node) -> Optional { + match body_lower_body_subtree_lower_from_binary(captured: item) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: found, diagnostics: _ } => found + } +} + // CENSUS GRADE: THE SIGNATURE, NEVER THE BODY. A module outside the compiled closure is read only for // what resolution asks of it -- declaration paths and payload kinds -- and the symbol-index fill // descends into an Arrow's domain alone. So the census arm builds the full arm's Arrow from the same @@ -4955,11 +5356,17 @@ fn body_lower_value_lowered(node: Node) -> Outcome> { // projection is an unlowered operator tail and is not peeled. fn body_lower_lowered_behind_shells_read(node: Node) -> OperandRefRead { let stripped = body_lower_deep_unwrap_optional(node: node) - if body_lower_is_core_substrate(node: stripped) { - OperandRefLowered { lowered: stripped } - } else if body_lower_is_unrecognised_lowered_shape(node: stripped) { - OperandRefRefused { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: stripped) } - } else if body_lower_node_is_lowered_atom(node: stripped) { + match body_term_of(node: stripped) { + Rejected { diagnostics: r } => OperandRefRefused { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => OperandRefLowered { lowered: lowered } + Accepted { value: _, diagnostics: _ } => body_lower_raw_behind_shells_read(stripped: stripped) + } +} + +// Behind the shells of a node the parse stamped: a token is read as an operand, a production or an +// empty-tailed sequence is peeled one level, anything else is not a lowered value. +fn body_lower_raw_behind_shells_read(stripped: Node) -> OperandRefRead { + if body_lower_node_is_lowered_atom(node: stripped) { body_lower_operand_ref_read(node: stripped) } else { match parse_production_emitted_identity_optional(node: stripped) { @@ -5100,7 +5507,7 @@ fn body_lower_function_value_body(body: FunctionLiteralBody) -> Outcome match body_lower_fn_body_stmt_seq_optional(fn_body: b) { Absent => outcome_accepted(value: optional_absent()) - Present { value: stmt_seq } => body_lower_function_value_lowered_whole(o: body_lower_stmt_seq(shell: stmt_seq)) + Present { value: stmt_seq } => body_lower_function_value_lowered_whole(o: body_term_sealed_outcome(o: body_lower_stmt_seq(shell: stmt_seq), shape: LoweredStatementSequence)) } } FunctionLiteralStatementBody { body: b } => @@ -5859,7 +6266,19 @@ fn body_lower_postfix_projection_step(repeat_elem: Node) -> Optional { // idiom the existing arms still erase (the receipt is on the class row; see DottedChainTailRead). // A brace record suffix (`a.b.C { f: v }`) is a construction, lowered by the chain fold's // PostfixRecord step, and an `as` suffix is a cast step; neither is read by this head reader. +// A HEAD A PRODUCER PASSED THROUGH IS STILL A NAME. The primary producer returns a bare identifier +// sealed as its output; whether `a.b` is one qualified name is decided by what the head IS, so a +// Lowered head is read as the node it holds, exactly as this reader saw it before the seal. fn body_lower_dotted_head_atom_optional(node: Node) -> Optional { + match body_term_of(node: node) { + Accepted { value: Lowered { node: inner, shape: _ }, diagnostics: _ } => body_lower_dotted_head_raw_atom_optional(node: inner) + Accepted { value: RawParse { node: raw }, diagnostics: _ } => body_lower_dotted_head_raw_atom_optional(node: raw) + Accepted { value: RawLeaf { node: leaf }, diagnostics: _ } => body_lower_dotted_head_raw_atom_optional(node: leaf) + Rejected { diagnostics: _ } => optional_absent() + } +} + +fn body_lower_dotted_head_raw_atom_optional(node: Node) -> Optional { match node_atom_identity_optional(node: node) { Present { value: sym } => if dag_lex_token_symbol(sym: sym) { @@ -7177,6 +7596,9 @@ fn body_lower_reify_branch_target(node: Node) -> Outcome { match body_lower_literal_bool_atom_optional(node: node) { Present { value: reified } => outcome_accepted(value: reified) Absent => + if body_lower_reify_interior_is_lowered(node: node) { + outcome_accepted(value: node) + } else { match body_lower_operand_ref_read(node: node) { OperandRefLowered { lowered: op } => outcome_accepted(value: op) OperandRefRefused { diagnostics: r } => Rejected { diagnostics: r } @@ -7187,6 +7609,7 @@ fn body_lower_reify_branch_target(node: Node) -> Outcome { OperandRefAbsent => outcome_accepted(value: node) } } + } } } } @@ -9053,7 +9476,12 @@ fn body_lowering_match_spine_identity_absent_dispatch_holds() -> Bool { match body_lower_fold_identity_absent(node: fixture) { Rejected { diagnostics: _ } => true Accepted { value: dispatched, diagnostics: _ } => - body_lower_is_core_substrate(node: dispatched) || dispatched != reduced + (match body_term_of(node: dispatched) { + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => true + Accepted { value: RawParse { node: _ }, diagnostics: _ } => false + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => false + Rejected { diagnostics: _ } => false + }) || dispatched != reduced } } } @@ -9088,9 +9516,13 @@ fn body_lower_postfix_lowered_primary_optional(captured: Node) -> Optional match sugar_sequence_pair_optional(node: captured) { Absent => Absent Present { value: pair } => - if body_lower_is_core_substrate(node: pair.left) - && is_empty_conj_root(n: body_lower_deep_unwrap_optional(node: pair.right)) { - optional_present(value: pair.left) + if is_empty_conj_root(n: body_lower_deep_unwrap_optional(node: pair.right)) { + match body_term_of(node: pair.left) { + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => optional_present(value: lowered) + Accepted { value: RawParse { node: _ }, diagnostics: _ } => optional_absent() + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => optional_absent() + Rejected { diagnostics: _ } => optional_absent() + } } else { optional_absent() } @@ -9626,11 +10058,10 @@ fn body_lower_primary_expr(shell: Node) -> Outcome { Present { value: inner } => inner Absent => shell } - if body_lower_is_core_substrate(node: captured) { - outcome_accepted(value: captured) - } else if body_lower_is_unrecognised_lowered_shape(node: captured) { - Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: captured) } - } else { + match body_term_of(node: captured) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: Lowered { node: lowered, shape: _ }, diagnostics: _ } => outcome_accepted(value: lowered) + Accepted { value: _, diagnostics: _ } => match body_lower_int_literal_with_magnitude_optional(node: captured) { Present { value: lit } => outcome_accepted(value: lit) Absent => @@ -9728,21 +10159,21 @@ fn body_lower_fn_body_shell(shell: Node) -> Outcome { fn body_lower_forward_row_apply(shell: Node, emitted: Symbol) -> Outcome { if emitted == ^dag_surface_fn_decl { - body_lower_fn_decl_to_arrow(shell: shell) + body_term_sealed_lowering(o: body_lower_fn_decl_to_arrow(shell: shell), shape: LoweredFnDeclaration) } else if emitted == ^dag_surface_test_fn_decl { - body_lower_test_fn_decl_to_member(shell: shell) + body_term_sealed_lowering(o: body_lower_test_fn_decl_to_member(shell: shell), shape: LoweredTestDeclaration) } else if emitted == ^dag_surface_data_decl { - body_lower_data_decl_to_member(shell: shell) + body_term_sealed_lowering(o: body_lower_data_decl_to_member(shell: shell), shape: LoweredDataDeclaration) } else if emitted == ^dag_surface_fn_body { - body_lower_fn_body_shell(shell: shell) + body_term_sealed_lowering(o: body_lower_fn_body_shell(shell: shell), shape: LoweredFnBody) } else if emitted == ^dag_surface_match_expr { - body_lowering_lowered(o: body_lower_match_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_match_expr(shell: shell)), shape: LoweredMatch) } else if emitted == ^dag_surface_if_expr { - body_lowering_lowered(o: body_lower_if_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_if_expr(shell: shell)), shape: LoweredBranch) } else if emitted == ^dag_surface_loop_expr { - body_lowering_lowered(o: body_lower_loop_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_loop_expr(shell: shell)), shape: LoweredLoop) } else if emitted == ^dag_surface_let_expr { - body_lowering_lowered(o: body_lower_let_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_let_expr(shell: shell)), shape: LoweredLet) } else { body_lower_wrapper_retained_shell(shell: shell) } @@ -9877,21 +10308,12 @@ fn body_lower_type_decl_eq_rhs_optional(captured: Node) -> Optional { // time this lowering runs, so it takes the member arm. fn body_lower_eq_type_decl(shell: Node, captured: Node, type_name: Symbol, type_params: DeclTypeParams) -> Outcome { match body_lower_type_decl_eq_rhs_optional(captured: captured) { - Present { value: rhs } => - match rhs.kind { - TypeNode { connective: Disj } => - outcome_accepted(value: body_lower_type_decl_member(shell: shell, type_name: type_name, body: rhs, type_params: type_params)) - TypeNode { connective: Conj } => - if body_lower_node_is_production_shell(n: rhs) { - body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) - } else { - outcome_accepted(value: body_lower_type_decl_member(shell: shell, type_name: type_name, body: rhs, type_params: type_params)) - } - TypeNode { connective: Atom { identity: _ } } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) - TypeNode { connective: Arrow } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) - TypeNode { connective: Cardinality } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) - TypeNode { connective: Instantiation } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) - ComputationNode { behavior: _ } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + Present { value: rhs_term } => + match body_term_wrapper_optional(n: rhs_term) { + Present { value: w } => + body_lower_eq_type_decl_rhs(shell: shell, type_name: type_name, rhs: w.node, type_params: type_params) + Absent => + body_lower_eq_type_decl_rhs(shell: shell, type_name: type_name, rhs: rhs_term, type_params: type_params) } Absent => match body_lower_find_production_shell_optional(root: captured, emitted: ^dag_surface_type_alias_rhs_lead) { @@ -9906,6 +10328,31 @@ fn body_lower_eq_type_decl(shell: Node, captured: Node, type_name: Symbol, type_ } } +// THE TYPE DECLARATION'S RIGHT-HAND SIDE (MQ-5). The rhs is a TYPE EXPRESSION -- raw parse, or a type +// the type lowering built (a coproduct's Disj, a record's Conj), which is not a body term and is never +// sealed -- but a body producer's sealed wrapper can still sit around it. A wrapper is a non-production +// Conj, so reading its kind directly took the record-member arm and read the wrapper's one marker edge +// as a field; body_lower_eq_type_decl unwraps a wrapper and passes everything else through unchanged. +// It does NOT ask body_term_of: a type expression is outside the term vocabulary, and asking refused +// every coproduct declaration (`type RcT = RcA | RcB | RcC`, a Disj; srv1 probe on #12942). +fn body_lower_eq_type_decl_rhs(shell: Node, type_name: Symbol, rhs: Node, type_params: DeclTypeParams) -> Outcome { + match rhs.kind { + TypeNode { connective: Disj } => + outcome_accepted(value: body_lower_type_decl_member(shell: shell, type_name: type_name, body: rhs, type_params: type_params)) + TypeNode { connective: Conj } => + if body_lower_node_is_production_shell(n: rhs) { + body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + } else { + outcome_accepted(value: body_lower_type_decl_member(shell: shell, type_name: type_name, body: rhs, type_params: type_params)) + } + TypeNode { connective: Atom { identity: _ } } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + TypeNode { connective: Arrow } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + TypeNode { connective: Cardinality } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + TypeNode { connective: Instantiation } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + ComputationNode { behavior: _ } => body_lower_alias_type_decl(shell: shell, type_name: type_name, rhs: rhs, type_params: type_params) + } +} + fn body_lower_alias_type_decl(shell: Node, type_name: Symbol, rhs: Node, type_params: DeclTypeParams) -> Outcome { match body_lower_decl_binders(shell: shell, type_params: type_params) { Absent => body_lower_type_decl_uncarried(shell: shell) @@ -10030,8 +10477,8 @@ fn body_lower_type_decl_record_name_optional(shell: Node) -> Optional { // v2.compiler.normalized_tree NormalizedTree type_declaration_kinds. One value per declaration: a record // and a refinement are authored alternatives, so the carrier has no state naming one declaration as // both. A record is what body_lower_type_decl_record_name_optional says; otherwise a where clause -- -// here still its dag_surface_where_refinement_clause production shell, the parse-level form of the arm -// the by-body dispatch finds as a Named edge -- makes a refinement; anything else carries no kind, and +// the same host body_lower_type_decl_by_body finds (body_lower_find_where_host_conj), not a second +// walk that only recognises a production shell -- makes a refinement; anything else carries no kind, and // a plain Conj member without one is undecided downstream, never inferred. type DeclaredTypeKind = DeclaredRecord { name: Symbol } @@ -10045,7 +10492,7 @@ fn body_lower_type_decl_declared_kind_optional(shell: Node) -> Optional Absent Present { value: captured } => - match body_lower_find_production_shell_optional(root: captured, emitted: ^dag_surface_where_refinement_clause) { + match body_lower_find_where_host_conj(root: captured) { Absent => Absent Present { value: _ } => match dag_surface_kw_then_ident_from_captured(captured: captured) { @@ -11665,30 +12112,19 @@ fn body_lower_resource_decl_declared_kind_optional(shell: Node) -> Optional Outcome { - outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_alias_binding_row_not_carried, n: shell)) -} - fn body_lower_production_emitted(shell: Node, emitted: Symbol, fn_body_pass_through: Bool) -> Outcome { if emitted == ^dag_surface_service_decl { - body_lower_service_decl(shell: shell, admit_set_aside: false) + body_term_sealed_lowering(o: body_lower_service_decl(shell: shell, admit_set_aside: false), shape: LoweredServiceDeclaration) } else if emitted == ^dag_surface_resource_decl { - body_lower_resource_decl(shell: shell) + body_term_sealed_lowering(o: body_lower_resource_decl(shell: shell), shape: LoweredResourceDeclaration) } else if emitted == ^dag_surface_type_decl { - body_lowering_lowered(o: body_lower_type_decl(shell: shell)) - } else if emitted == ^dag_surface_alias_decl { - body_lowering_lowered(o: body_lower_alias_decl_refused(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_type_decl(shell: shell)), shape: LoweredTypeDeclaration) } else if body_lower_is_metadata_preserved_emitted(emitted: emitted) { body_lowering_lowered(o: outcome_accepted(value: shell)) } else if emitted == ^dag_surface_type_variant { body_lowering_lowered(o: body_lower_type_variant(shell: shell)) + } else if emitted == ^dag_surface_admit_callers_clause { + body_lowering_lowered(o: outcome_accepted(value: body_lower_admit_clause_image(clause: shell))) } else if body_lower_is_structure_preserved_emitted(emitted: emitted) { body_lowering_lowered(o: outcome_accepted(value: shell)) } else if emitted == ^dag_surface_fn_body && fn_body_pass_through { @@ -11698,11 +12134,11 @@ fn body_lower_production_emitted(shell: Node, emitted: Symbol, fn_body_pass_thro } else if body_producer_forward_is_registered_surface_identity(surface_identity: emitted) { body_lower_forward_row_apply(shell: shell, emitted: emitted) } else if emitted == ^dag_surface_postfix_expr { - body_lowering_lowered(o: body_lower_postfix_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_postfix_expr(shell: shell)), shape: LoweredPostfix) } else if body_lower_is_pass_through_emitted(emitted: emitted) { body_lowering_lowered(o: body_lower_unwrap_pass_through(shell: shell)) } else if emitted == ^dag_surface_primary_expr { - body_lowering_lowered(o: body_lower_primary_expr(shell: shell)) + body_term_sealed_lowering(o: body_lowering_lowered(o: body_lower_primary_expr(shell: shell)), shape: LoweredPrimary) } else { body_lower_wrapper_retained_shell(shell: shell) } @@ -12090,12 +12526,17 @@ fn body_lower_tail_statement_read(stmt: Node) -> Outcome { } fn body_lower_after_children(folded: Node, fn_body_pass_through: Bool) -> Outcome { - if body_lower_is_core_substrate(node: folded) { - body_lowering_lowered(o: outcome_accepted(value: folded)) - } else if dag_resolve_preserve_module_metadata_subtree(node: folded) { + match body_term_of(node: folded) { + Accepted { value: Lowered { node: _, shape: _ }, diagnostics: _ } => body_lowering_lowered(o: outcome_accepted(value: folded)) + Rejected { diagnostics: r } => + if dag_resolve_preserve_module_metadata_subtree(node: folded) { body_lowering_lowered(o: outcome_accepted(value: folded)) } else { Rejected { diagnostics: r } } + Accepted { value: _, diagnostics: _ } => body_lower_after_raw_children(folded: folded, fn_body_pass_through: fn_body_pass_through) + } +} + +fn body_lower_after_raw_children(folded: Node, fn_body_pass_through: Bool) -> Outcome { + if dag_resolve_preserve_module_metadata_subtree(node: folded) { body_lowering_lowered(o: outcome_accepted(value: folded)) - } else if body_lower_is_unrecognised_lowered_shape(node: folded) { - body_lowering_lowered(o: Rejected { diagnostics: body_lower_unrecognised_lowered_shape_diagnostics(node: folded) }) } else { match parse_production_emitted_identity_optional(node: folded) { Absent => body_lowering_lowered(o: outcome_accepted(value: folded)) @@ -12130,3 +12571,4 @@ fn body_lower_finish_for_normalize( ) } + diff --git a/src/v2/compiler/emit_host.dag b/src/v2/compiler/emit_host.dag index fc0076030a5..5152c0d604d 100644 --- a/src/v2/compiler/emit_host.dag +++ b/src/v2/compiler/emit_host.dag @@ -21,8 +21,7 @@ import v2.compiler.eval { eval_context_interpretation, inputs_for_emit_host, inputs_root_only, - run_test_claim, - test_claim_cache_receipt_node + run_test_claim } import extdeps.communication.medium { Medium } import std.algebra { Cons, Empty } @@ -825,7 +824,7 @@ fn run_test_claim_emit_vs_eval_run( verdict: Verdict, RuntimeValue> ) -> TestClaimRun { TestClaimRun { - cache: test_claim_cache_receipt_node(subject: subject), + claim: subject.claim, verdict: verdict } } diff --git a/src/v2/compiler/emit_produced.dag b/src/v2/compiler/emit_produced.dag index 31b35d5feb6..845e1792df7 100644 --- a/src/v2/compiler/emit_produced.dag +++ b/src/v2/compiler/emit_produced.dag @@ -20,7 +20,7 @@ import v2.std.compilers.target_model { FixedToken } import extdeps.communication.medium { Medium } -import v2.std.collection { Absent, Present, list_at_optional, map_get } +import v2.std.collection { Absent, Present, list_at_optional, map_get_checked } import v2.std.algebra { contains, fold_list, FreeMonoid } import std.algebra { Cons, Empty, list_append, list_snoc_item } import v2.std.diagnostic { diff --git a/src/v2/compiler/emit_semantic_decl.dag b/src/v2/compiler/emit_semantic_decl.dag index e499d1d6b51..9e2f55c13c7 100644 --- a/src/v2/compiler/emit_semantic_decl.dag +++ b/src/v2/compiler/emit_semantic_decl.dag @@ -11,7 +11,7 @@ import v2.std.compilers.semantic_decl_emission { target_semantic_enum_surface_from_coproduct, target_semantic_struct_surface_from_record } -import v2.std.collection { Absent, Map, Present, empty_map, map_get } +import v2.std.collection { Absent, Map, Present, empty_map, map_get_checked } import v2.std.qualified_name { declaration_reference_path_optional } import v2.std.compilers.target_model { TargetModel, @@ -218,7 +218,7 @@ fn produced_type_binding_over(target: TargetModel, spellings: Map outcome_accepted(value: target_atom_type_spelling(row: row)) Rejected { diagnostics: _ } => - match map_get(spellings, id) { + match map_get_checked(spellings, id) { Accepted { value: Present { value: _ }, diagnostics: _ } => outcome_accepted(value: id) _ => outcome_rejected(d: emit_type_reference_unrealized_diagnostic(node: type_node)) } diff --git a/src/v2/compiler/fold_lowering.dag b/src/v2/compiler/fold_lowering.dag index 89452cc5f05..840b422f46e 100644 --- a/src/v2/compiler/fold_lowering.dag +++ b/src/v2/compiler/fold_lowering.dag @@ -4,6 +4,7 @@ import std.types { List } import v2.extdeps.languages.dag { dag_call_named_arg_value_optional, dag_call_positional_arg_value_optional, + dag_kernel_value_type_first, dag_lex_token_symbol, parse_production_captured_child_optional, parse_production_emitted_identity_optional @@ -33,6 +34,7 @@ import std.algebra { Empty, collection_all_shape, collection_concat_shape, + collection_count_shape, collection_fold_shape, collection_map_shape, template_accumulator_position, @@ -249,7 +251,8 @@ data collection_roster_rows: List = [ collection_roster_row(t: collection_fold_shape(), decl_name: "collection_fold_shape", form: RosterAuthoredStep), collection_roster_row(t: collection_map_shape(), decl_name: "collection_map_shape", form: RosterCallbackStep { realization: CallbackMapsToCons }), collection_roster_row(t: collection_all_shape(), decl_name: "collection_all_shape", form: RosterCallbackStep { realization: CallbackAllByAnd }), - collection_roster_row(t: collection_concat_shape(), decl_name: "collection_concat_shape", form: RosterFreeMonoidPrimitiveCall) + collection_roster_row(t: collection_concat_shape(), decl_name: "collection_concat_shape", form: RosterFreeMonoidPrimitiveCall), + collection_roster_row(t: collection_count_shape(), decl_name: "collection_count_shape", form: RosterFreeMonoidPrimitiveCall) ] data collection_fold_head: Symbol = symbol_intern_lexeme(lexeme: collection_fold_shape().name) @@ -323,7 +326,16 @@ fn collection_roster_template_type(t: AlgebraTypeTemplate) -> Optional { ReceiverElement => optional_present(value: collection_roster_element_type()) ReceiverKey => optional_absent() ReceiverValue => optional_absent() - NamedTemplate { name: _ } => optional_absent() + NamedTemplate { name: n } => + match dag_kernel_value_type_first(select: fn(row, _binding) { + match row.spelling { + Present { value: spelled } => spelled == n + Absent => false + } + }) { + Present { value: row } => optional_present(value: row.value_type) + Absent => optional_absent() + } ContainerOf { source: _, element: _ } => optional_absent() OptionalOf { inner: _ } => optional_absent() WitnessOf { inner: _ } => optional_absent() diff --git a/src/v2/compiler/materialization_carriers.dag b/src/v2/compiler/materialization_carriers.dag index 38aac4d32d6..ec0e39c16cf 100644 --- a/src/v2/compiler/materialization_carriers.dag +++ b/src/v2/compiler/materialization_carriers.dag @@ -1,59 +1,21 @@ module v2.compiler.materialization_carriers -import std.types { List, String } -import std.cache_identity { - CacheInterfaceId, - ArtifactKindId, - parse_table_artifact_kind, -} -import std.realization { - Materialization, Memoize, Recompute, Share, - RealizedStep, Placement, LocalInProcess, -} -import std.measure { Nano } -import std.realization_schedule { cost_account_predicted_zero } -import std.effects { ReadEffect } -import std.materialization_ladder { - Frame, FrameKind, SharedStateFrame, IsolatedChildrenFrame, UnboundedSiblingsFrame, - FrameDemand, DemandNature, PureComputation, - CacheProvider, CoversAllComputations, - LadderVerdict, - materialization_ladder, materialization_ladder_holds, - ladder_verdict_prescribes, - ladder_verdict_for, - ladder_verdict_is_green, - empty_frame_path, no_cost_floor_exemptions, no_cache_providers, - verdict_is_accepted_single_recompute, - verdict_is_discharged_by, - verdict_is_refused_no_provider, - verdict_is_refused_scope_too_narrow, -} -import extdeps.cache.materialization { - provider_from_catalog, -} -import extdeps.realization.parse_table_memo { parse_table_memo_facts, parse_table_memo_id } -import extdeps.realization.compile_stage_memo { compile_stage_memo_facts, compile_stage_memo_id } +import std.realization { Materialization, Memoize, Recompute, Share } import std.disposition { Disposition, Terminal } -import v2.std.staging { CacheProbe, resolve_probe } -import v2.std.diagnostic { - Accepted, Rejected, Outcome, None, - Diagnostic, Correction, Unavailable, ExternalContractUnknown, - diagnostics_singleton, invariant_port_locus, -} -import v2.std.node { Symbol } -import std.optional { Present, Absent } data v2_compiler_materialization_memo_gate_predicate_contract: Disposition = Terminal { - reason: "Structural coproduct reader over std.realization.Materialization (materialization_allows_memo_store — Memoize => store permitted, Recompute/Share => refused) — closed by the three declared Materialization variants; .dag authority for parse_table_lookup/insert and cached_stage_governed; discriminating corpus: parse_table_memo_governed_witness_test.dag + compiler_materialization_witness_test.dag." -} - -data v2_compiler_materialization_note: String = "The .dag-substrate demonstration lane (duplicate-work-graph-lens-design.md section 10b): ParseTable and cached_stage memoization flows through the governed carrier door — Memoize verdict consumed (non-Memoize refuses store), ContentKeyed law at parse_table_subject (grammar_digest × token_stream_digest × position × production). std.realization.reconcile-as-lookup-path is the following increment (no v2 runtime dispatch yet). Seed-side memo interceptor: extdeps/realization/parse_table_memo.dag parse_table_memo_seed_handler_dissolution_trigger (Scaffold dissolves_to RealizationDispatch on materialization_allows_memo_store; v1_interpreter.rs try_parse_table_memo_dispatch is the seed handler). In-process memo cannot span runs: RefusedScopeTooNarrow at IsolatedChildren LCA; persistent-tier discharge belongs to a store-tier provider row (materialization_store_local class). Collapsing plural sites into one process frame → AuthoredDuplication (rule 1). The parse demand's site is derived per grammar (ParseMemoRecurrence, computed in v2.compiler.parse compute_grammar_first_analysis_given) and carried on the table; the compile-stage demand is still authored. Dissolve-on: the remaining demands derive from demand-engine carriers (docs/plans/demand-engine-program.md M1) and the transport folds into materialize (Half A)." - -// TAIL probe 2026-07-17 (cssl_assemble, smart-newt-39): execution-measured Gate-A-shaped rustc -// refusal (UNRESOLVED_CompilerError on emitter Rc/Optional surface) — NOT materialize-spine. -// Frontier migration_trigger reclassified to ^migrate_when_closure_self_emits_cargo_green (#6775 -// lane). Receipt: TSV deleted 2026-08-16 (operator ruling: delete anything not actively derived). - + reason: "Structural coproduct reader over std.realization.Materialization (materialization_allows_memo_store — Memoize => store permitted, Recompute/Share => refused) — closed by the three declared Materialization variants; discriminating corpus: v2.test.claim.self_host_contract.materialization_carriers_emitted_contract." +} + +// THE COMPILER'S MEMO DEMANDS AND PROVIDERS ARE GONE, AND THIS MODULE KEEPS ONLY THE PREDICATE. +// The parse-table demand was deleted with the packrat parse memo (operator ruling R2, +// 2026-10-08) and the compile-stage demand with its always-Miss memo (roadmap +// native-memory-inert-carriers): its only lookup answered Miss unconditionally, so the governed +// door, its provider, its catalog row and its admission controls described a store that never +// served. The generic ladder obligations those controls exercised -- RefusedScopeTooNarrow on +// an isolated plural site, RefusedNoProvider without a provider, discharge inside the +// provider's scope, single recompute -- stay enrolled on fixture subjects in +// test.claim.materialization_ladder_witness. fn materialization_allows_memo_store(m: Materialization) -> Bool { match m { Memoize => true @@ -61,569 +23,3 @@ fn materialization_allows_memo_store(m: Materialization) -> Bool { Share => false } } - -fn v2_compile_process_frame() -> std.materialization_ladder.Frame { - std.materialization_ladder.Frame { name: "v2-compile-ingest", kind: SharedStateFrame } -} - -fn v2_compile_process_site() -> List { - [v2_compile_process_frame()] -} - -data parse_table_memo_demand_identity: String = "parse-nonterminal-memo" -data compile_stage_memo_demand_identity: String = "compile-pipeline-stage" - -// THE PARSE DEMAND CASE IS DERIVED FROM THE GRAMMAR, NOT AUTHORED. The identity is one -// (nonterminal, position) parse under one grammar and one token stream -- the ContentKeyed -// preimage parse_table_subject already names. Whether that identity is demanded MORE THAN ONCE -// within one parse is a structural fact of the grammar, and ParseMemoRecurrence is its census, -// computed once per grammar by v2.compiler.parse compute_grammar_first_analysis_given. -// -// Why the census and not dependency_lens: a DependencyView is a (source, dependent, kind, -// usage_site) edge over a Node. It records THAT a production references a nonterminal; it -// carries no position, no ordered-choice retry and no nullability, and those three are exactly -// what decide whether one (nonterminal, position) is asked twice. A reference-edge count is the -// "import edge / lookup count" the demand case must not be equated with -- a grammar with a -// nonterminal referenced from forty sites can still ask each (nonterminal, position) once. -// -// The two sources of within-parse recurrence, and why there is no third: -// - a DISCARDING backtrack site: an ordered Choice's left branch, or an Optional / Repeat -// element, whose attempt can fail after demanding a nonterminal. The failed attempt's -// result is thrown away and the continuation resumes at the SAME position, so any -// nonterminal it demanded can be demanded again there. -// - a NULLABLE nonterminal: it can succeed consuming nothing, so the next demand on the same -// successful path starts at the same position and may be the same nonterminal. -// WHY THESE TWO ARE EXHAUSTIVE -- an argument over the constructors, so a new construct is -// checked against it rather than against a list. A second demand of one (N, p) needs two demand -// events for N at the same position p. Take any two such events in one parse's execution and -// ask how the parse got from the first to the second. Either -// (a) the first event's subtree was ABANDONED and execution resumed at a position <= p: that -// needs a construct that attempts a branch and continues after its failure. In GrammarExpr -// only three do -- Choice (tries left, then right from the same position), Optional and -// Repeat (try the element, then continue from the same position). Terminal, LiteralTerminal -// and Nonterminal never resume after failure (they propagate it), and Sequence fails as a -// whole when either side fails, so the resuming point is always an enclosing -// Choice/Optional/Repeat. The abandoned subtree must contain the N demand, hence the -// "attempted branch demands a nonterminal" condition. Or -// (b) both events are on the SURVIVING path: then either one is an ancestor of the other at the -// same position, which is left recursion (refused by grammar_has_left_recursion_given), or -// they are ordered in the token stream and everything demanded between the first N's start -// and the second consumed zero tokens -- which requires the first N itself to consume -// nothing, i.e. N is nullable. -// So (a) is a discarding backtrack site and (b) a nullable nonterminal; there is no third route. -// THE TWO CODE FACTS (a) RESTS ON, named rather than inferred, because they are what a future -// reader has to re-check and what a falsifier is aimed at. Both are in v2.compiler.parse. -// - parse_expr_sequence: when the RIGHT side rejects, the whole sequence returns -// ParseExprRejected. It never re-invokes the left side, so a failure downstream of a -// construct does not re-execute that construct or anything before it. -// - parse_expr_repeat_step: on a rejected element it sets done and KEEPS acc.remaining -- a -// successful iteration is never given back. Repeat is greedy and possessive, so it cannot -// shift the position at which a later nonterminal is demanded by yielding iterations. (It -// also stops on a zero-consumption iteration rather than looping at one position.) -// Without those two, a Choice or Repeat branch containing NO Nonterminal could still cause a -// later nonterminal to be re-demanded at the same position, and the syntactic site test would -// not see it. That is the hole this argument would have. -// -// THE FALSIFIER, stated so this is an argument someone can kill rather than one they must -// accept: either a parse_expr path that re-executes a continuation after a downstream failure, -// or a demand of a nonterminal from inside a Choice / Optional / Repeat branch that contains no -// Nonterminal node. Either one breaks (a) and the no-recurrence arm with it. -// -// WHO ACTUALLY TAKES THE NO-RECURRENCE ARM TODAY: no real grammar. It requires zero discarding -// sites AND zero nullable nonterminals, and the .dag grammar has both, so it derives Memoize -- -// the same value the literal it replaced gave. The direct-computation arm is inhabited only by -// synthetic fixtures (v2.test.provenance.parse_memo_frame_replay linear_root, and the -// one-Terminal grammar in v2.test.claim.compiler_materialization_witness). That is declared -// rather than left to be discovered: the arm is admitted and executed, but its only inhabitants -// are fixtures, and a reader asking which real grammar recomputes should get "none today" from -// here rather than from a search. -// THE OBLIGATION ON A NEW CONSTRUCT: if it can resume after a failed attempt, it is a discarding -// site and expr_parse_demand_fold in v2.compiler.parse must count it; if it can succeed consuming -// nothing, the nullable fixpoint must include it. Parser error recovery (sync tokens) is outside -// this argument: it is assumed to resume strictly later, and a recovery mode that re-enters at -// the failed position is a third route that must be counted before it lands. -// -// Counting a Choice whose left branch token dispatch would never enter is a structural -// over-approximation computed AS the answer (DESIGN section 5's admitted neighbour of the -// absorbing fallback): it can only claim recurrence where none occurs, which costs a store and -// never a wrong tree. -type ParseMemoRecurrence { - discarding_backtrack_sites: Int - nullable_nonterminals: Int -} - -fn parse_memo_recurrence_is_possible(r: ParseMemoRecurrence) -> Bool { - (r.discarding_backtrack_sites > 0) || (r.nullable_nonterminals > 0) -} - -// Within-parse recurrence is a replay whose count the INPUT decides, so the emergent frame is -// UnboundedSiblingsFrame -- it names the input-driven retry at one position, and it sits BELOW the -// process frame: within-parse sharing is not cross-run reuse, and a run-spanning demand is a -// different site (parse_table_memo_plural_demands below), refused, not discharged, by the -// in-process provider. -fn parse_backtrack_replay_frame() -> std.materialization_ladder.Frame { - std.materialization_ladder.Frame { - name: "parse-backtrack-replay", - kind: UnboundedSiblingsFrame - } -} - -fn parse_table_memo_site_for(r: ParseMemoRecurrence) -> List { - if parse_memo_recurrence_is_possible(r: r) { - [v2_compile_process_frame(), parse_backtrack_replay_frame()] - } else { - v2_compile_process_site() - } -} - -fn parse_table_memo_demand_for(r: ParseMemoRecurrence) -> FrameDemand { - FrameDemand { - identity: parse_table_memo_demand_identity, - site: parse_table_memo_site_for(r: r), - nature: PureComputation - } -} - -fn parse_table_memo_verdicts_for(r: ParseMemoRecurrence) -> List { - materialization_ladder( - demands: [parse_table_memo_demand_for(r: r)], - providers: v2_compiler_cache_providers(), - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -// ONCE PER GRAMMAR, ADMITTED AND THEN CARRIED. prepare_grammar asks this once, on the census the -// grammar's own analysis derived, and a refused admission refuses the prepare; every ParseTable -// built from that analysis carries the admitted value, so parse_table_lookup and parse_table_insert -// read it and never re-run the ladder. It goes through admitted_prescription_in -- THE SAME DOOR -// BODY the compile-stage demand uses -- so the green-only test and the preserved refusal are one -// rule over two verdict lists, not a rule and a copy of it. -// -// WHICH ARMS THIS DOOR CAN REACH TODAY, stated deliberately rather than discovered: the ladder is -// run here over exactly [parse_table_memo_demand_for(r)], so it always rules on this identity and -// the Absent arm is unreachable from the parse path -- a refusal here would have to be a REFUSED -// verdict, not a missing one. With the in-process provider both census outcomes are green: -// recurrence Discharged (Memoize) and no recurrence AcceptedSingleRecompute (Recompute). So no -// live parse behaviour moves; what changes is that an identity the ladder refuses now refuses the -// prepare instead of quietly recomputing. -fn parse_table_memo_materialization_for(r: ParseMemoRecurrence) -> Outcome { - admitted_prescription_in( - verdicts: parse_table_memo_verdicts_for(r: r), - identity: parse_table_memo_demand_identity, - port: ^parse_table_memo_demand_port - ) -} - -data parse_memo_no_recurrence: ParseMemoRecurrence = ParseMemoRecurrence { - discarding_backtrack_sites: 0, - nullable_nonterminals: 0 -} - -fn compile_stage_memo_demand() -> FrameDemand { - FrameDemand { - identity: compile_stage_memo_demand_identity, - site: v2_compile_process_site(), - nature: PureComputation - } -} - -// The parse demand is not in this list: its site depends on the grammar, so it is ruled per -// grammar by parse_table_memo_verdicts_for rather than once here for a grammar nobody named. -fn v2_compiler_materialization_demands() -> List { - [compile_stage_memo_demand()] -} - -fn parse_table_memo_provider() -> CacheProvider { - provider_from_catalog( - facts: parse_table_memo_facts, - scope: v2_compile_process_site(), - coverage: CoversAllComputations - ) -} - -fn compile_stage_memo_provider() -> CacheProvider { - provider_from_catalog( - facts: compile_stage_memo_facts, - scope: v2_compile_process_site(), - coverage: CoversAllComputations - ) -} - -fn v2_compiler_cache_providers() -> List { - [parse_table_memo_provider(), compile_stage_memo_provider()] -} - -fn parse_table_memo_provider_id() -> CacheInterfaceId { - extdeps.realization.parse_table_memo.parse_table_memo_id -} - -fn compile_stage_memo_provider_id() -> CacheInterfaceId { - extdeps.realization.compile_stage_memo.compile_stage_memo_id -} - -fn parse_table_memo_artifact_kind() -> ArtifactKindId { - parse_table_artifact_kind -} - -fn v2_compiler_materialization_verdicts() -> List { - materialization_ladder( - demands: v2_compiler_materialization_demands(), - providers: v2_compiler_cache_providers(), - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -fn v2_compiler_materialization_ladder_holds() -> Bool { - materialization_ladder_holds( - demands: v2_compiler_materialization_demands(), - providers: v2_compiler_cache_providers(), - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -// DERIVED, NOT DECLARED. These two returned the literal Memoize while the ladder standing -// beside them in this same module ruled on the same two identities, and nothing compared the -// two answers -- one witness pinned the literal, another pinned the verdict, and both were -// green while they disagreed. That is the fork this module exists to prevent (DESIGN section -// 3), so the prescription is now read from the verdict for this demand's own identity. -// -// Absent -- no verdict for this identity -- is the fail-closed arm: a demand the ladder never -// ruled on has no admission to consume, so the store is not permitted. It is deliberately NOT -// the same answer as a ruled Recompute, which is why ladder_verdict_for hands back the VERDICT -// and leaves the reading to this door instead of flattening it into a prescription. -// A REFUSED VERDICT IS NOT AN ADMISSION, and this is the correction review 69770 asked for. -// std.materialization_ladder.ladder_verdict_prescribes answers what materialization each verdict -// IMPLIES, and for five of the six Refused arms it answers Memoize -- deliberately, because a -// refusal is an UNMET OBLIGATION and the standing ruling is that it is not a licence to delete -// the store. That table is right for what it is. What it is NOT is a door's admission, and this -// module is what turned it into one: read as permission, "the obligation is unmet" became -// "permission granted", which is DESIGN section 5's widen -- a failure arm must refuse. -// -// So admission is green-or-nothing. The obligation, the provider and the store all remain -- -// nothing here deletes them, and the refusal verdict stays enrolled and readable -- but an -// unmet obligation does not open the door. Today the parse-table demand is Discharged, so no -// live behaviour changes; what changes is that it is admitted BECAUSE it is green rather than -// because every Refused arm happened to answer Memoize too. -// A REFUSAL IS PRESERVED, NOT RE-ENCODED AS Recompute, and this is the correction the delta -// review asked for. Green-or-nothing was first written as "green or Recompute", which prevented -// a refused verdict from opening the store and then handed back a perfectly valid operational -// materialization -- so the caller continued, unmemoized, with the obligation still unmet and -// nothing anywhere saying so. DESIGN section 2 is explicit that a demand whose identity finds no -// provider of sufficient scope, coverage, key and retention is refused WITH A TYPED CAUSE, and -// section 5 is explicit that a failure arm refuses rather than widening. Recompute-as-fallback -// widened: the answer was operationally fine and the deficit it stood on became unobservable, -// which is exactly the absorbing fallback -- the signal that would have made the missing provider -// rank for fixing is destroyed by the arm that papers over it. It was also inconsistent with this -// package's own Share frontier, which rules that silently treating Share as recomputation is -// wrong and that the door must refuse with a typed diagnostic; RefusedNoProvider is the stronger -// case, since the ladder has already said the required realization is unavailable. -// So the door answers Outcome: green admits its prescription, and every other -// result -- refused, non-green, or an identity the ladder never ruled on -- is Rejected with a -// located diagnostic. Absent is refused for the same reason and not a weaker one: a demand with -// no ruling has no admission to consume, and the port symbol each named demand passes is what -// keeps the two refusals distinguishable at their own sites. -// ONE BODY, and the verdict list is a parameter so that a control can exercise THE DOOR rather -// than a copy of it. Review 69791 caught the previous shape: refused_scope_prescription had this -// body inlined with only the verdict source swapped, so deleting the green test HERE -- the -// production door, which parse_table_memo_materialization_for and compile_stage_memo_materialization -// consume -- left the control that exists to catch that deletion green. A control evaluating its -// own copy of the property it guards cannot fail, which is the forged control DESIGN section -// 4b(1) refuses: the discriminating RED has to run on the real acceptance path. -// That parameter is also what makes this ONE ADMISSION RULE over whichever verdict list ruled the -// identity: the compile-stage demand is ruled once in v2_compiler_materialization_verdicts, the -// parse demand once per grammar in parse_table_memo_verdicts_for, and both doors are this body. -fn materialization_obligation_unmet_diagnostic(port: Symbol) -> Diagnostic { - Diagnostic { - reason: ^materialization_obligation_unmet, - at: invariant_port_locus(port: port, invariant: ^materialization_obligation_unmet), - correction: Unavailable { reason: ExternalContractUnknown } - } -} - -fn admitted_prescription_in( - verdicts: List, - identity: String, - port: Symbol -) -> Outcome { - match ladder_verdict_for(verdicts: verdicts, identity: identity) { - Absent => - Rejected { - diagnostics: diagnostics_singleton(d: materialization_obligation_unmet_diagnostic(port: port)) - } - Present { value: chosen } => - if ladder_verdict_is_green(v: chosen) { - Accepted { value: ladder_verdict_prescribes(v: chosen), diagnostics: None } - } else { - Rejected { - diagnostics: diagnostics_singleton(d: materialization_obligation_unmet_diagnostic(port: port)) - } - } - } -} - -fn admitted_prescription(identity: String, port: Symbol) -> Outcome { - admitted_prescription_in( - verdicts: v2_compiler_materialization_verdicts(), - identity: identity, - port: port - ) -} - -fn compile_stage_memo_materialization() -> Outcome { - admitted_prescription( - identity: compile_stage_memo_demand_identity, - port: ^compile_stage_memo_demand_port - ) -} - -type CachedStageRealization { - provider_id: CacheInterfaceId - materialization: Materialization -} - -// THE REALIZATION CARRIES AN ADMITTED MATERIALIZATION OR IT DOES NOT EXIST. Constructing a -// CachedStageRealization out of a refused admission would put the refusal back inside a value -// the stage spine consumes as though it had been admitted, so the refusal is answered here, -// at the constructor, and compile_ingest_staging returns it as the stage outcome it already is. -fn cached_stage_realization() -> Outcome { - match compile_stage_memo_materialization() { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: m, diagnostics: residue } => - Accepted { - value: CachedStageRealization { - provider_id: compile_stage_memo_provider_id(), - materialization: m, - }, - diagnostics: residue - } - } -} - -fn cached_stage_governed( - realization: CachedStageRealization, - lookup: fn(A) -> CacheProbe, - stage: fn(A) -> Outcome -) -> fn(A) -> Outcome { - fn(x) { - match materialization_allows_memo_store(m: realization.materialization) { - true => v2.std.staging.resolve_probe(probe: lookup(x), stage: stage, x: x) - false => stage(x) - } - } -} - -fn realized_step_for(m: Materialization) -> RealizedStep { - RealizedStep { - shape: ReadEffect, - placement: LocalInProcess, - materialization: m, - cost: cost_account_predicted_zero() - } -} - -fn parse_table_realized_step(r: ParseMemoRecurrence) -> Outcome> { - match parse_table_memo_materialization_for(r: r) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: m, diagnostics: residue } => - Accepted { value: realized_step_for(m: m), diagnostics: residue } - } -} - -fn compile_stage_realized_step() -> Outcome> { - match compile_stage_memo_materialization() { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: m, diagnostics: residue } => - Accepted { value: realized_step_for(m: m), diagnostics: residue } - } -} - -// ACCOUNTING CANNOT REFUSE, so the old projection_ok pair is gone rather than -// rewritten: it asked whether classifying a cited row had failed, which is now -// unaskable by type. What is still worth asserting is that the provider carries -// the catalog's retention UNCHANGED — the property the pre-verdicting mapping -// actually broke when it discarded placement retention and minted SpacePacked. -fn parse_table_memo_retention_preserved() -> Bool { - parse_table_memo_provider().retention == parse_table_memo_facts.placement.retention -} - -fn compile_stage_memo_retention_preserved() -> Bool { - compile_stage_memo_provider().retention == compile_stage_memo_facts.placement.retention -} - -// The two arms the parse demand can reach from the grammar, each read off the verdict rather -// than off the prescription, so a verdict that changed arm while keeping the same Materialization -// (a refusal also prescribes Memoize) cannot pass for a discharge. -fn parse_table_memo_verdict_is_discharged_for(r: ParseMemoRecurrence) -> Bool { - parse_table_memo_verdicts_for(r: r) - |> fold(init: false, f: (acc, v) => - acc || verdict_is_discharged_by( - v: v, - identity: parse_table_memo_demand_identity, - provider_id: extdeps.realization.parse_table_memo.parse_table_memo_id - ) - ) -} - -fn parse_table_memo_verdict_is_single_recompute_for(r: ParseMemoRecurrence) -> Bool { - parse_table_memo_verdicts_for(r: r) - |> fold(init: false, f: (acc, v) => - acc || verdict_is_accepted_single_recompute(v: v, identity: parse_table_memo_demand_identity) - ) -} - -// A REFUSED ADMISSION IS A Rejected OUTCOME, and these controls now discriminate that rather -// than the weaker property they used to. Asking only whether the store was permitted could not -// tell a refusal apart from an admitted Recompute -- both answer false -- which is precisely how -// the collapsed arm survived: the control that existed to catch it agreed with it. -fn materialization_admission_is_refused(a: Outcome) -> Bool { - match a { - Rejected { diagnostics: _ } => true - Accepted { value: _, diagnostics: _ } => false - } -} - -fn materialization_admission_admits_store(a: Outcome) -> Bool { - match a { - Rejected { diagnostics: _ } => false - Accepted { value: m, diagnostics: _ } => materialization_allows_memo_store(m: m) - } -} - -// The discriminating RED for the fail-closed arm: an identity the ladder never ruled on has no -// admitted decision, so the door must refuse rather than inherit the permissive answer. -// Deleting the Absent arm of admitted_prescription_in makes this admit. -data unruled_demand_identity: String = "identity-the-ladder-never-ruled-on" - -fn unruled_identity_prescription() -> Outcome { - admitted_prescription( - identity: unruled_demand_identity, - port: ^parse_table_memo_demand_port - ) -} - -// THE RED REVIEW 69770 ASKED FOR, now at the strength the delta review asked for: a REFUSED -// identity must not be ADMITTED AT ALL. The plural parse-table demands refuse with -// RefusedScopeTooNarrow against the very provider that discharges the single-site demand -- -// same identity, same provider, refused on scope rather than absence, which is exactly an arm -// ladder_verdict_prescribes answers Memoize for. It runs through admitted_prescription_in, THE -// DOOR'S OWN BODY, so dropping the green test there makes this admit and the control dies with -// it; re-encoding the refusal as Recompute makes it admit too, which the old store-only form -// could not see. -fn refused_scope_prescription() -> Outcome { - admitted_prescription_in( - verdicts: parse_table_memo_plural_verdicts(providers: v2_compiler_cache_providers()), - identity: parse_table_memo_demand_identity, - port: ^parse_table_memo_demand_port - ) -} - -fn refused_scope_is_not_admitted() -> Bool { - materialization_admission_is_refused(a: refused_scope_prescription()) -} - -// The positive control that keeps the pair honest: the SAME fold over the SAME provider admits -// the recurrent-grammar demand. Without this, refused_scope_is_not_admitted would also pass if -// admitted_prescription_in simply never admitted anything. -data parse_memo_one_discarding_site: ParseMemoRecurrence = ParseMemoRecurrence { - discarding_backtrack_sites: 1, - nullable_nonterminals: 0 -} - -fn discharged_scope_admits_store() -> Bool { - materialization_admission_admits_store( - a: parse_table_memo_materialization_for(r: parse_memo_one_discarding_site) - ) -} - -fn unruled_identity_is_not_admitted() -> Bool { - materialization_admission_is_refused(a: unruled_identity_prescription()) -} - -fn compile_stage_memo_verdict_is_single_recompute() -> Bool { - v2_compiler_materialization_verdicts() - |> fold(init: false, f: (acc, v) => - acc || verdict_is_accepted_single_recompute(v: v, identity: compile_stage_memo_demand_identity) - ) -} - -fn parse_table_carrier_grounded_on_catalog() -> Bool { - (parse_table_memo_artifact_kind() == parse_table_artifact_kind) - && (parse_table_memo_provider_id() == extdeps.realization.parse_table_memo.parse_table_memo_id) -} - -fn v2_compile_workflow_frame() -> std.materialization_ladder.Frame { - std.materialization_ladder.Frame { name: "v2-compile-workflow", kind: IsolatedChildrenFrame } -} - -fn v2_compile_run_frame(name: String) -> std.materialization_ladder.Frame { - std.materialization_ladder.Frame { name: name, kind: SharedStateFrame } -} - -fn parse_table_memo_plural_demands() -> List { - [ - FrameDemand { - identity: parse_table_memo_demand_identity, - site: [ - v2_compile_workflow_frame(), - v2_compile_run_frame(name: "compile-run-a"), - v2_compile_process_frame() - ], - nature: PureComputation - }, - FrameDemand { - identity: parse_table_memo_demand_identity, - site: [ - v2_compile_workflow_frame(), - v2_compile_run_frame(name: "compile-run-b"), - v2_compile_process_frame() - ], - nature: PureComputation - } - ] -} - -fn parse_table_memo_plural_verdicts(providers: List) -> List { - materialization_ladder( - demands: parse_table_memo_plural_demands(), - providers: providers, - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -fn parse_table_memo_plural_holds_with_provider() -> Bool { - materialization_ladder_holds( - demands: parse_table_memo_plural_demands(), - providers: v2_compiler_cache_providers(), - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -fn parse_table_memo_plural_holds_without_provider() -> Bool { - materialization_ladder_holds( - demands: parse_table_memo_plural_demands(), - providers: no_cache_providers, - cost_floor_exempt: no_cost_floor_exemptions - ) -} - -fn parse_table_memo_plural_refused_without_provider_count() -> Int { - parse_table_memo_plural_verdicts(providers: no_cache_providers) - |> fold(init: 0, f: (acc, v) => - if verdict_is_refused_no_provider(v: v, identity: parse_table_memo_demand_identity) { - acc + 1 - } else { - acc - } - ) -} - -fn parse_table_memo_plural_scope_too_narrow_count() -> Int { - parse_table_memo_plural_verdicts(providers: v2_compiler_cache_providers()) - |> fold(init: 0, f: (acc, v) => - if verdict_is_refused_scope_too_narrow(v: v, provider_id: extdeps.realization.parse_table_memo.parse_table_memo_id) { - acc + 1 - } else { - acc - } - ) -} diff --git a/src/v2/compiler/namespace_graft.dag b/src/v2/compiler/namespace_graft.dag index 6a189e8b0c9..111e05205c5 100644 --- a/src/v2/compiler/namespace_graft.dag +++ b/src/v2/compiler/namespace_graft.dag @@ -193,7 +193,8 @@ fn namespace_graft_grammar_expr_nonterminal_productions(expr: GrammarExpr) -> Li expect: fn(xs, _reason) { xs }, refuse_on_match: fn(xs, _reason) { xs }, after_line_break: fn(xs) { xs }, - not_predicate: fn(_xs) { [] } + not_predicate: fn(_xs) { [] }, + infix_table: fn(operand, _guard, _operators) { operand } } ) } @@ -408,6 +409,14 @@ fn namespace_graft_disposition_from_named_opt(named: Edge?) -> NamespaceGraftUni // v2.test.claim.type_param_binder_frame and v2.test.claim.body_lowering.plain_type_decl_lowering held. // A type_decl shell that did arrive would fall to UnitResidual and be refused by normalize's retention // admission, never named from a shell. +// AN `alias` DECLARATION DISSOLVES, AS AN IMPORT DOES. Its meaning is a binding row (binding name -> +// target path), captured from the parse tree at normalize (v2.compiler.normalize +// import_binding_capture) and bound by the symbol index fill at the declaring module's position, +// so references resolve THROUGH it by unique-on-chain lookup (docs/plans/namespace-resolution-design.md +// section 13). Grafting it as a member named by its binding made the name resolve to its own grafted +// node instead of its target -- a silent wrong answer (gunbc.recurring_failure_mode +// alias_declaration_binding_row_not_carried), refused as an interim by gunbc#13375 and carried by this arm; the discriminating control is v2.test.parse.alias_decl_parse +// alias_decl_fill_lexical_lookup_holds. fn namespace_graft_unit_disposition(unit: Node, id: Symbol) -> NamespaceGraftUnitDisposition { match id == ^dag_surface_import_decl { true => UnitDissolved @@ -422,7 +431,7 @@ fn namespace_graft_unit_disposition(unit: Node, id: Symbol) -> NamespaceGraftUni true => namespace_graft_disposition_from_named_opt(named: namespace_graft_test_fn_named_edge(unit: unit)) false => match id == ^dag_surface_alias_decl { - true => namespace_graft_disposition_from_named_opt(named: namespace_graft_kw_ident_named_edge(unit: unit)) + true => UnitDissolved false => UnitResidual } } diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index 0cea14bba87..7d310f27f05 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -1,6 +1,7 @@ module v2.compiler.normalized_tree import v2.compiler.body_lowering_fold { + body_term_erase_tree, BodyLoweredTree, DeclaredTypeKind, DeclaredTypeModifiers, @@ -26,6 +27,7 @@ import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.algebra { fold_list } import v2.std.node { Edge, Node, Symbol, content_hash } import v2.std.declaration_marker { TestMarkerChannel, test_marker_channel_empty } +import v2.std.member_declaration_kind { DeclaredMember } import v2.std.namespace_alias { AliasBindingRow, ModuleBindingSource } // A NormalizedTree is the ADMITTED product of normalize: a root whose own lowering left no wrapper @@ -59,8 +61,17 @@ type NormalizedTree sole_constructor { import_bindings: FreeMonoid, type_declaration_kinds: FreeMonoid, type_declaration_modifiers: FreeMonoid, + member_declarations: NormalizedMemberDeclarations, } +// WHETHER THE TOP-LEVEL MEMBERS' AUTHORED KINDS WERE READ. normalize reads them from the parse +// (member_declaration_capture); a hand-built root has no parse, and an empty list would say "this module +// declares no members" about a module nothing was read for. So the unread arm is a value, never an empty +// list (v2.std.member_declaration_kind MemberKindChannel carries the same distinction forward). +type NormalizedMemberDeclarations + = MemberDeclarationsNotRead + | MemberDeclarationsRead { members: FreeMonoid } + // THE RETENTION DOOR (MQ-4). A shell the body lowering could not lower is a typed variant of its result // (v2.compiler.body_lowering_fold WrapperRetention), gathered by v2.compiler.normalize into the // BodyLoweredTree it hands here. This is where that list is CONSUMED: an empty list releases the @@ -88,7 +99,7 @@ type NormalizedTree sole_constructor { // BodyWrapperRetained and drops it -- never through a silent `diagnostics: _`. fn admit_retention_free(tree: BodyLoweredTree, diagnostics: Diagnostics) -> Outcome { match tree.retained { - Empty => Accepted { value: tree.node, diagnostics: diagnostics } + Empty => Accepted { value: body_term_erase_tree(n: tree.node), diagnostics: diagnostics } Cons { head: first, tail: _ } => Rejected { diagnostics: rejected_with_pending( @@ -207,13 +218,27 @@ fn admit_normalized_tree( test_markers: test_markers, import_bindings: import_bindings, type_declaration_kinds: type_declaration_kinds, - type_declaration_modifiers: type_declaration_modifiers + type_declaration_modifiers: type_declaration_modifiers, + member_declarations: MemberDeclarationsNotRead }, diagnostics: diagnostics } } } +// The one place a read member roster joins a tree, so admit_normalized_tree keeps its signature for the +// hand-built callers that hold no parse. +fn normalized_tree_with_member_declarations(tree: NormalizedTree, members: FreeMonoid) -> NormalizedTree { + NormalizedTree { + root: tree.root, + test_markers: tree.test_markers, + import_bindings: tree.import_bindings, + type_declaration_kinds: tree.type_declaration_kinds, + type_declaration_modifiers: tree.type_declaration_modifiers, + member_declarations: MemberDeclarationsRead { members: members } + } +} + fn normalized_tree_record_declarations(t: NormalizedTree) -> FreeMonoid { declared_record_names(kinds: t.type_declaration_kinds) } @@ -356,7 +381,8 @@ fn admit_unmarked_normalized_roots( test_markers: test_marker_channel_empty(), import_bindings: no_import_bindings(), type_declaration_kinds: no_type_declaration_kinds(), - type_declaration_modifiers: no_type_declaration_modifiers() + type_declaration_modifiers: no_type_declaration_modifiers(), + member_declarations: MemberDeclarationsNotRead } ) } diff --git a/src/v2/compiler/occurrence_role.dag b/src/v2/compiler/occurrence_role.dag index 2e6b189dcc9..76bf86eae67 100644 --- a/src/v2/compiler/occurrence_role.dag +++ b/src/v2/compiler/occurrence_role.dag @@ -293,7 +293,8 @@ fn production_holds_name_terminal(expr: GrammarExpr) -> Bool { expect: fn(a, _reason) { a }, refuse_on_match: fn(a, _reason) { a }, after_line_break: fn(a) { a }, - not_predicate: fn(_a) { false } + not_predicate: fn(_a) { false }, + infix_table: fn(a, _guard, _operators) { a } }) } diff --git a/src/v2/compiler/parse_acceptance_census.dag b/src/v2/compiler/parse_acceptance_census.dag index 381815c019f..923baf9c9eb 100644 --- a/src/v2/compiler/parse_acceptance_census.dag +++ b/src/v2/compiler/parse_acceptance_census.dag @@ -19,7 +19,7 @@ import v2.std.diagnostic { WholeFile, diagnostics_fatal } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import v2.std.node { symbol_intern_lexeme, symbol_lexeme } import v2.std.text { String } @@ -52,11 +52,11 @@ fn parse_acceptance_census_line(path: String) -> String { fn parse_acceptance_of_text(text: String, path: String) -> String { match dag_prepared_grammar() { Rejected { diagnostics: _ } => "grammar-unprepared" - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => match tokenize(text: text, file: symbol_intern_lexeme(lexeme: path), rules: dag_lex()) { Rejected { diagnostics: d } => "refused " + parse_acceptance_fatal_text(d: d) Accepted { value: stream, diagnostics: _ } => - match parse_module_prepared(tokens: stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: stream, prepared: prepared) { Accepted { value: _, diagnostics: _ } => "accepted" Rejected { diagnostics: d } => "refused " + parse_acceptance_fatal_text(d: d) } @@ -81,7 +81,7 @@ fn parse_acceptance_locus_text(at: Locus) -> String { fn parse_acceptance_extent_text(extent: v2.std.diagnostic.Extent) -> String { match extent { - ByteRange { start: s, end: e } => integer_int_to_decimal_string(value: s) + ".." + integer_int_to_decimal_string(value: e) + ByteRange { start: s, end: e } => int_to_decimal_string(value: s) + ".." + int_to_decimal_string(value: e) WholeFile => "whole-file" } } diff --git a/src/v2/compiler/parse_diagnostic.dag b/src/v2/compiler/parse_diagnostic.dag deleted file mode 100644 index da5e52d6d99..00000000000 --- a/src/v2/compiler/parse_diagnostic.dag +++ /dev/null @@ -1,172 +0,0 @@ -module v2.compiler.parse_diagnostic - -import std.types { List } -import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } -import v2.std.algebra { - Cons, - Empty, - HeadAbsent, - HeadFound, - TailAbsent, - TailFound, - fold_list, - list_head, - list_tail -} -import v2.std.diagnostic { - Diagnostic, - ExternalContractUnknown, - Locus, - NonEmptyDiagnostics, - Unavailable, - invariant_port_locus, - port_refusal_locus -} -import v2.std.grammar_choice_ambiguity { - GrammarChoiceAmbiguityRow, - NonEmptyGrammarChoiceAmbiguityRows -} -import std.optional { Optional, Present, optional_absent, optional_present, Absent } - -// ParseDiagnosticEvidence is parse-stage evidence orthogonal to the universal Diagnostic carrier. Grammar-choice overlap rows live here — never on Correction, Locus, or Diagnostic itself. -type ParseDiagnosticEvidence - = NoParseDiagnosticEvidence - | GrammarChoiceAmbiguityEvidence { row: GrammarChoiceAmbiguityRow } - -type ParseStageDiagnostic { - diagnostic: Diagnostic - evidence: ParseDiagnosticEvidence -} - -type NonEmptyParseStageDiagnostics { - head: ParseStageDiagnostic - tail: FreeMonoid -} - -fn parse_overlap_residue_stage_diagnostic_at( - at: Locus, - row: GrammarChoiceAmbiguityRow -) -> ParseStageDiagnostic { - ParseStageDiagnostic { - diagnostic: Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: port_refusal_locus( - at: at, - port: ^parse_stage_locus_port, - invariant: ^parse_grammar_choice_overlap_residue - ), - correction: Unavailable { reason: ExternalContractUnknown } - }, - evidence: GrammarChoiceAmbiguityEvidence { row: row } - } -} - -fn parse_overlap_residue_stage_diagnostic(row: GrammarChoiceAmbiguityRow) -> ParseStageDiagnostic { - parse_overlap_residue_stage_diagnostic_at( - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_grammar_choice_overlap_residue), - row: row - ) -} - -fn non_empty_parse_stage_diagnostics_from_overlap_roster( - at: Locus, - roster: List -) -> Optional { - match roster { - Empty => optional_absent() - Cons { head: first, tail: rest } => - optional_present( - value: NonEmptyParseStageDiagnostics { - head: parse_overlap_residue_stage_diagnostic_at(at: at, row: first), - tail: fold_list( - xs: rest, - empty: Empty, - cons: fn(acc, row) { - list_snoc_item( - xs: acc, - item: parse_overlap_residue_stage_diagnostic_at(at: at, row: row) - ) - } - ) - } - ) - } -} - -fn parse_stage_diagnostic_from_diagnostic_only(d: Diagnostic) -> ParseStageDiagnostic { - ParseStageDiagnostic { - diagnostic: d, - evidence: NoParseDiagnosticEvidence - } -} - -fn parse_stage_diagnostic_core(d: ParseStageDiagnostic) -> Diagnostic { - d.diagnostic -} - -fn parse_stage_evidence_row_optional(d: ParseStageDiagnostic) -> Optional { - match d.evidence { - GrammarChoiceAmbiguityEvidence { row: row } => optional_present(value: row) - NoParseDiagnosticEvidence => optional_absent() - } -} - -fn non_empty_diagnostics_from_parse_stage(d: NonEmptyParseStageDiagnostics) -> NonEmptyDiagnostics { - NonEmptyDiagnostics { - head: parse_stage_diagnostic_core(d: d.head), - tail: fold_list( - xs: d.tail, - empty: Empty, - cons: fn(acc, item) { - list_snoc_item(xs: acc, item: parse_stage_diagnostic_core(d: item)) - } - ) - } -} - -fn grammar_choice_ambiguity_rows_append_parse_stage( - acc: FreeMonoid, - d: ParseStageDiagnostic -) -> FreeMonoid { - match parse_stage_evidence_row_optional(d: d) { - Present { value: row } => list_snoc_item(xs: acc, item: row) - Absent => acc - } -} - -fn grammar_choice_ambiguity_rows_from_non_empty_parse_stage( - d: NonEmptyParseStageDiagnostics -) -> FreeMonoid { - let head_rows = grammar_choice_ambiguity_rows_append_parse_stage(acc: Empty, d: d.head) - fold_list( - xs: d.tail, - empty: head_rows, - cons: fn(acc, item) { - grammar_choice_ambiguity_rows_append_parse_stage(acc: acc, d: item) - } - ) -} - -fn non_empty_grammar_choice_ambiguity_rows_from_parse_stage( - d: NonEmptyParseStageDiagnostics -) -> Optional { - let rows = grammar_choice_ambiguity_rows_from_non_empty_parse_stage(d: d) - match list_head(xs: rows) { - HeadFound { value: head } => - optional_present( - value: NonEmptyGrammarChoiceAmbiguityRows { - head: head, - tail: list_tail_or_empty_rows(xs: rows) - } - ) - HeadAbsent => optional_absent() - } -} - -fn list_tail_or_empty_rows(xs: FreeMonoid) -> FreeMonoid { - match list_tail(xs: xs) { - TailAbsent => Empty - TailFound { tail: rest } => rest - } -} - diff --git a/src/v2/compiler/program_assembly.dag b/src/v2/compiler/program_assembly.dag index f0e276f9805..1be835fe10f 100644 --- a/src/v2/compiler/program_assembly.dag +++ b/src/v2/compiler/program_assembly.dag @@ -19,11 +19,7 @@ import v2.compiler.tokenize { PreparedLexRules, prepare_lex_rules, tokenize, tok import v2.compiler.parse { ParseArtifact, parse_module_prepared_seeded, - parse_module_prepared_measured_seeded, - ParseProductionMeasured, PreparedGrammar, - parse_memo_stats_empty, - parse_module_prepared_measured, prepare_grammar } import v2.std.compilers.lexing { TokenStream, token_stream_remaining_count } @@ -189,7 +185,7 @@ fn program_assembly_phase_tokenize( } } -// THE SEEDED MEASURED PHASE, AND THE MARK READ BACK OUT AS A SCALAR. These two exist for the +// THE SEEDED PARSE PHASE, AND THE MARK READ BACK OUT AS A SCALAR. These two exist for the // emitted `SourceRootEvalDriver`, which does not call the composed per-file fold: it open-codes the // sequence so a clock can sit between the phases, which is admitted debt filed at // `gunbc.recurring_failure_mode` `realization_respells_a_modeled_folds_sequencing_to_instrument_it`. @@ -198,9 +194,8 @@ fn program_assembly_phase_tokenize( // driver silently keeps the per-file reset while the modeled fold no longer has it. // // THE REJECTED-INPUT ARM REPRODUCES bind_outcome EXACTLY, and is spelled out rather than reused -// because bind_outcome cannot carry a second value out of the bound function: a Rejected token -// stream passes its diagnostics through untouched and reports empty accounting, because no parse -// ran -- an answer, not a missing measurement. The Accepted arm merges the incoming diagnostics +// as the phase body the driver times: a Rejected token stream passes its diagnostics through +// untouched, because no parse ran. The Accepted arm merges the incoming diagnostics // through bind_outcome_accepted, which is what bind_outcome itself calls. (That reasoning arrived // here with the unseeded twin this phase replaced; the twin is deleted, so the reasoning moves to // the survivor rather than being deleted with it.) @@ -218,69 +213,43 @@ fn program_assembly_phase_tokenize( // exactly while nothing retains a refused parse's node loci; the precondition and the carrier that // would break it are named on `v2.compiler.compile` `native_test_context_absorb`, which is where // the reuse actually happens. -fn program_assembly_phase_parse_measured_seeded( +fn program_assembly_phase_parse_seeded( tokens: Outcome, prepared: PreparedGrammar, - residue: Diagnostics, seed: OccurrenceIdAllocator -) -> ParseProductionMeasured { +) -> Outcome { match tokens { - Rejected { diagnostics: d } => - ParseProductionMeasured { - outcome: Rejected { diagnostics: d }, - memo: parse_memo_stats_empty() - } + Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: token_stream, diagnostics: od } => - let measured = parse_module_prepared_measured_seeded( - tokens: token_stream, - prepared: prepared, - validation_residue: residue, - seed: seed + bind_outcome_accepted( + od: od, + inner: parse_module_prepared_seeded( + tokens: token_stream, + prepared: prepared, + seed: seed + ) ) - ParseProductionMeasured { - outcome: bind_outcome_accepted(od: od, inner: measured.outcome), - memo: measured.memo - } } } -// THE MARK AS A SCALAR, for the same reason the three memo counters above are read out one at a -// time: the driver prints scalars, and a record does not cross that boundary as one. This is what -// makes the emitted loop's threading OBSERVABLE from outside the binary -- the per-file line carries -// the mark it was handed and the mark it reached, so a run over several files shows one sequence -// continuing rather than a claim that it does. +// THE MARK AS A SCALAR: the driver prints scalars, and a record does not cross that boundary as +// one. This is what makes the emitted loop's threading OBSERVABLE from outside the binary -- the +// per-file line carries the mark it was handed and the mark it reached, so a run over several +// files shows one sequence continuing rather than a claim that it does. fn program_assembly_allocator_next(alloc: OccurrenceIdAllocator) -> Int { alloc.next_id } -fn program_assembly_measured_alloc( - measured: ParseProductionMeasured, +fn program_assembly_parsed_alloc( + parsed: Outcome, seed: OccurrenceIdAllocator ) -> OccurrenceIdAllocator { - match measured.outcome { + match parsed { Rejected { diagnostics: _ } => seed Accepted { value: artifact, diagnostics: _ } => artifact.alloc } } -// The three counters, read out one at a time because the driver prints scalars and a record does -// not cross that boundary as one. -fn program_assembly_memo_hits(measured: ParseProductionMeasured) -> Int { - measured.memo.hits -} - -fn program_assembly_memo_misses(measured: ParseProductionMeasured) -> Int { - measured.memo.misses -} - -fn program_assembly_memo_lookup_calls(measured: ParseProductionMeasured) -> Int { - measured.memo.lookup_calls -} - -fn program_assembly_measured_outcome(measured: ParseProductionMeasured) -> Outcome { - measured.outcome -} - // THE NORMALIZE PHASE ADVANCES THE INGEST'S ALLOCATOR, and returns it beside the outcome so a loop // over the ingest seeds the next file's parse from it: lowering allocates every image member an id // (v2.compiler.normalize normalize_scoped), and the next file's authored ids must start above those. @@ -380,7 +349,6 @@ fn program_assembly_read_to_normalized_root_seeded( read: DagSourceReadWitness, lex: PreparedLexRules, prepared: PreparedGrammar, - residue: Diagnostics, seed: OccurrenceIdAllocator, scope: Fnv1a64Structural ) -> ProgramAssemblySeededReadResult { @@ -390,7 +358,6 @@ fn program_assembly_read_to_normalized_root_seeded( parse_module_prepared_seeded( tokens: token_stream, prepared: prepared, - validation_residue: residue, seed: seed ) } @@ -485,7 +452,6 @@ fn program_assembly_fold_step( read: DagSourceReadWitness, lex: PreparedLexRules, prepared: PreparedGrammar, - residue: Diagnostics, scope: Fnv1a64Structural ) -> ProgramAssemblyLocatedFoldState { match state { @@ -496,7 +462,6 @@ fn program_assembly_fold_step( read: read, lex: lex, prepared: prepared, - residue: residue, seed: seed, scope: scope ) @@ -574,7 +539,7 @@ fn program_assembly_ingest_scope(ingest: SourceRootIngest) -> SourceRefListDiges source_ref_list_digest_build(refs: fold_list( xs: ingest, empty: Empty, - cons: fn(acc, read) { list_snoc_item(xs: acc, item: source_ref_for_storage_path(path: read.artifact.file_path)) } + cons: fn(acc, read) { list_snoc_item(xs: acc, item: source_ref_for_storage_path(path: read.artifact.file_path, source_root: read.source_root)) } )) } @@ -595,7 +560,7 @@ fn program_assembly_fold_ingest_located( match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => ProgramAssemblyLocatedFoldFailed { diagnostics: d, spans: span_index_empty() } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => let lex = prepare_lex_rules(rules: lm.lex) match program_assembly_ingest_scope(ingest: ingest) { SourceRefListDigestNonStructuralRefused { path: _ } => @@ -614,7 +579,7 @@ fn program_assembly_fold_ingest_located( advisories: None }, cons: fn(state, read) { - program_assembly_fold_step(state: state, read: read, lex: lex, prepared: prepared, residue: residue, scope: scope) + program_assembly_fold_step(state: state, read: read, lex: lex, prepared: prepared, scope: scope) } ) } @@ -698,8 +663,7 @@ fn assemble_program_from_module_roots( // its file exactly when the program did NOT assemble, and `Outcome`'s Rejected arm carries no value // -- so an index returned as part of the accepted program is an index that has evaporated precisely // where it was wanted. This carrier holds both arms' worth: the span index the fold recorded before -// it stopped, and the outcome itself. It is the same stance `ParseProductionMeasured` already takes -// for memo counters one layer down, for the same reason. +// it stopped, and the outcome itself. // // A REFUSED ASSEMBLY STILL CARRIES THE SPANS OF EVERY FILE IT GOT THROUGH, which is the honest // answer rather than a convenient one: the fold is fail-closed and stops at the first refusing read, diff --git a/src/v2/compiler/reference_conservation.dag b/src/v2/compiler/reference_conservation.dag index 729c6f0708b..95b19b8e776 100644 --- a/src/v2/compiler/reference_conservation.dag +++ b/src/v2/compiler/reference_conservation.dag @@ -5,7 +5,7 @@ import std.types { List } import v2.compiler.parse { ParseArtifact, PreparedGrammar } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.program_assembly { - program_assembly_phase_parse_measured_seeded, + program_assembly_phase_parse_seeded, program_assembly_phase_tokenize } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } @@ -47,7 +47,8 @@ import std.optional { Absent, Optional, Present, optional_absent, optional_prese import v2.std.lens_verdict { lens_verdict_diagnostic_located_text_in_spans } import v2.std.provenance { SpanIndex, span_index_empty, span_index_resolve_textual_locus } import v2.std.text { String } -import v2.std.integer { Int, integer_int_to_decimal_string } +import v2.std.integer { Int } +import std.integer { int_to_decimal_string } import v2.std.witness { Holds, Violates } import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import std.occurrence_identity { @@ -1125,15 +1126,13 @@ type ReferenceConservationSubject { fn reference_conservation_subject_of_read( read: DagSourceReadWitness, lex: PreparedLexRules, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> ReferenceConservationSubject { - let parsed = program_assembly_phase_parse_measured_seeded( + let parsed = program_assembly_phase_parse_seeded( tokens: program_assembly_phase_tokenize(read: read, lex: lex), prepared: prepared, - residue: residue, seed: occurrence_id_allocator_initial() - ).outcome + ) ReferenceConservationSubject { path: read.artifact.file_path, parsed: parsed, @@ -1253,11 +1252,10 @@ fn reference_conservation_of_read( read: DagSourceReadWitness, lm: LanguageModel, lex: PreparedLexRules, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> ReferenceConservationReport { reference_conservation_of_subject( - subject: reference_conservation_subject_of_read(read: read, lex: lex, prepared: prepared, residue: residue), + subject: reference_conservation_subject_of_read(read: read, lex: lex, prepared: prepared), token_classes: declared_token_classes(lm: lm) ) } @@ -1265,8 +1263,7 @@ fn reference_conservation_of_read( fn reference_conservation_of_ingest( ingest: SourceRootIngest, lm: LanguageModel, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> FreeMonoid { let lex = prepare_lex_rules(rules: lm.lex) fold_list( @@ -1275,7 +1272,7 @@ fn reference_conservation_of_ingest( cons: fn(acc, read) { list_snoc_item( xs: acc, - item: reference_conservation_of_read(read: read, lm: lm, lex: lex, prepared: prepared, residue: residue) + item: reference_conservation_of_read(read: read, lm: lm, lex: lex, prepared: prepared) ) } ) @@ -1301,7 +1298,7 @@ fn authored_atom_extent_text(atom: AuthoredAtom) -> String { Textual { file: _, extent: extent } => match extent { ByteRange { start: s, end: e } => - integer_int_to_decimal_string(value: s) + "-" + integer_int_to_decimal_string(value: e) + int_to_decimal_string(value: s) + "-" + int_to_decimal_string(value: e) WholeFile => "whole-file" } NodeLocus { anchor: _ } => "unlocated" @@ -1352,7 +1349,7 @@ fn reference_conservation_refusal_text(report: ReferenceConservationReport) -> S // the accepted subject carried, in chain order: the census's per-path record of what an accepted // module's front end attached without refusing. fn reference_conservation_report_lines(report: ReferenceConservationReport) -> FreeMonoid { - let summary = "module " + report.path + " authored=" + integer_int_to_decimal_string(value: report.authored) + " conserved=" + integer_int_to_decimal_string(value: report.conserved) + " locus_erased=" + integer_int_to_decimal_string(value: report.locus_erased) + " role_excluded=" + integer_int_to_decimal_string(value: report.role_excluded) + " header_channel=" + integer_int_to_decimal_string(value: report.header_channel) + " role_not_yet_read=" + integer_int_to_decimal_string(value: report.role_not_yet_read) + " role_reader_refused=" + integer_int_to_decimal_string(value: report.role_reader_refused) + " refused=" + integer_int_to_decimal_string(value: report.refused) + " import_channel=" + integer_int_to_decimal_string(value: report.import_channel) + " dropped=" + integer_int_to_decimal_string(value: length(xs: report.dropped)) + " unmeasured=" + integer_int_to_decimal_string(value: length(xs: report.unmeasured)) + " refusal=" + reference_conservation_refusal_text(report: report) + let summary = "module " + report.path + " authored=" + int_to_decimal_string(value: report.authored) + " conserved=" + int_to_decimal_string(value: report.conserved) + " locus_erased=" + int_to_decimal_string(value: report.locus_erased) + " role_excluded=" + int_to_decimal_string(value: report.role_excluded) + " header_channel=" + int_to_decimal_string(value: report.header_channel) + " role_not_yet_read=" + int_to_decimal_string(value: report.role_not_yet_read) + " role_reader_refused=" + int_to_decimal_string(value: report.role_reader_refused) + " refused=" + int_to_decimal_string(value: report.refused) + " import_channel=" + int_to_decimal_string(value: report.import_channel) + " dropped=" + int_to_decimal_string(value: length(xs: report.dropped)) + " unmeasured=" + int_to_decimal_string(value: length(xs: report.unmeasured)) + " refusal=" + reference_conservation_refusal_text(report: report) let with_dropped = fold_list( xs: report.dropped, empty: Cons { head: summary, tail: Empty }, diff --git a/src/v2/compiler/reference_conservation_admission.dag b/src/v2/compiler/reference_conservation_admission.dag index cd1ddf8acd8..bf4ba337c72 100644 --- a/src/v2/compiler/reference_conservation_admission.dag +++ b/src/v2/compiler/reference_conservation_admission.dag @@ -130,20 +130,13 @@ fn conservation_read_of(path: String, text: String) -> DagSourceReadWitness { } } -// THE GRAMMAR'S VALIDATION RESIDUE IS NOT CARRIED INTO A FIXTURE'S OUTCOME. It is a fact about the -// prepared grammar, reported where the grammar is prepared (dag_prepared_grammar's own Accepted -// diagnostics), and threaded into a module's parse it stands at the HEAD of every refusal, so a -// fixture asserting its refusal's cause would read the grammar's residue instead of its own. The -// parse is the same prepared parse either way; only that one module-independent diagnostic is not -// attached. fn conservation_subject_of_text(path: String, text: String) -> ReferenceConservationSubject { match dag_prepared_grammar() { - Accepted { value: prepared, diagnostics: _grammar_residue } => + Accepted { value: prepared, diagnostics: _ } => reference_conservation_subject_of_read( read: conservation_read_of(path: path, text: text), lex: prepare_lex_rules(rules: dag_language_model().lex), - prepared: prepared, - residue: None + prepared: prepared ) Rejected { diagnostics: d } => ReferenceConservationSubject { path: path, parsed: Rejected { diagnostics: d }, normalized: Rejected { diagnostics: d } } diff --git a/src/v2/compiler/reference_conservation_census.dag b/src/v2/compiler/reference_conservation_census.dag index 595475a43f5..c904be0c724 100644 --- a/src/v2/compiler/reference_conservation_census.dag +++ b/src/v2/compiler/reference_conservation_census.dag @@ -48,7 +48,7 @@ import v2.std.collection { empty_map, map_insert, map_lookup } import std.optional { Absent, Present } import std.bytes { bytes_octets, utf8_encode_bytes } import gunbc.namespace_step0_subject_collector { Step0SubjectCollected, Step0SubjectCollectionRefused, collect_step0_subject_vector_at } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import std.algebra { Cons, Empty, FreeMonoid, list_snoc_item } import v2.std.diagnostic { Accepted, Rejected, diagnostics_fatal_reason } import v2.std.text { String } @@ -120,14 +120,13 @@ fn sample_path_parts(path: String) -> SamplePathParts { fn sample_path_measured(path: String, text: String) -> SamplePathObservation { match dag_prepared_grammar() { Rejected { diagnostics: _ } => SamplePathGrammarUnprepared { path: path } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => SamplePathMeasured { report: reference_conservation_of_read( read: conservation_read_of(path: path, text: text), lm: dag_language_model(), lex: prepare_lex_rules(rules: dag_language_model().lex), - prepared: prepared, - residue: residue + prepared: prepared ) } } @@ -243,9 +242,9 @@ fn changed_scope_row_lines(rows: FreeMonoid) -> String { fn changed_scope_verdict_line(observation: ChangedScopeConservationObservation) -> String { match observation { NoBodyLoweringChange { rows: _ } => "observation no-body-lowering-change" - ChangedScopeObservedConserved { rows: _, in_scope: n } => "observation conserved in_scope=" + integer_int_to_decimal_string(value: n) + ChangedScopeObservedConserved { rows: _, in_scope: n } => "observation conserved in_scope=" + int_to_decimal_string(value: n) ChangedScopeObservedDropped { rows: _, in_scope: n, refused: k } => - "observation dropped in_scope=" + integer_int_to_decimal_string(value: n) + " refused=" + integer_int_to_decimal_string(value: k) + "observation dropped in_scope=" + int_to_decimal_string(value: n) + " refused=" + int_to_decimal_string(value: k) } } @@ -441,7 +440,6 @@ data reference_conservation_stratified_sample_paths: List = [ "dag/gunbc/repo/repo_self_warning_policy.dag", "dag/gunbc/roadmap/roadmap_acceptance_history_projection.dag", "dag/gunbc/roadmap/roadmap_publish_admission.dag", - "dag/gunbc/rung_drop/admit_callers_discarded_on_the_native_route.dag", "dag/gunbc/rung_drop/dashboard_merge_ready_semantic_admissibility.dag", "dag/gunbc/rung_drop/floor_cut_effect_gates.dag", "dag/gunbc/rung_drop/lens_enforcement_censuses.dag", @@ -811,7 +809,7 @@ fn reference_conservation_sample_by_rule_at_pin_exit() -> ProcessExit { if length(xs: missing) == 0 && length(xs: extra) == 0 && length(xs: derived) == length(xs: pinned) { ExitSuccess } else { - exit_failure(reason: "the rule at " + reference_conservation_stratified_sample_revision + " selects " + integer_int_to_decimal_string(value: length(xs: derived)) + " paths; the pin lists " + integer_int_to_decimal_string(value: length(xs: pinned)) + "; rule-only " + integer_int_to_decimal_string(value: length(xs: missing)) + fold_list(xs: missing, empty: "", cons: fn(a, p) { a + " " + p }) + "; pin-only " + integer_int_to_decimal_string(value: length(xs: extra)) + fold_list(xs: extra, empty: "", cons: fn(a, p) { a + " " + p })) + exit_failure(reason: "the rule at " + reference_conservation_stratified_sample_revision + " selects " + int_to_decimal_string(value: length(xs: derived)) + " paths; the pin lists " + int_to_decimal_string(value: length(xs: pinned)) + "; rule-only " + int_to_decimal_string(value: length(xs: missing)) + fold_list(xs: missing, empty: "", cons: fn(a, p) { a + " " + p }) + "; pin-only " + int_to_decimal_string(value: length(xs: extra)) + fold_list(xs: extra, empty: "", cons: fn(a, p) { a + " " + p })) } } } diff --git a/src/v2/compiler/self_host/closure_emission.dag b/src/v2/compiler/self_host/closure_emission.dag index 1f14d07ab0d..c2b2c1b2f01 100644 --- a/src/v2/compiler/self_host/closure_emission.dag +++ b/src/v2/compiler/self_host/closure_emission.dag @@ -1,5 +1,6 @@ module v2.compiler.self_host.closure_emission +import v2.std.member_declaration_kind { no_member_declaration_kinds } import v2.compiler.tokenize { PreparedLexRules, prepare_lex_rules } import std.algebra { Cons, Empty, FreeMonoid } import std.occurrence_identity { OccurrenceIdAllocator, occurrence_id_allocator_initial, scoped_occurrence_allocator } @@ -62,6 +63,7 @@ import v2.std.diagnostic { bind_outcome, diagnostics_fatal, diagnostics_fatal_reason, + diagnostics_singleton, non_empty_diagnostics_append, node_locus, outcome_accepted, @@ -77,7 +79,7 @@ import v2.std.resolution_policy { default_name_resolution_policy } import v2.std.symbol_index { SymbolIndex, empty_symbol_index } import v2.std.grammar { ParseTree } import v2.std.integer { Int } -import std.optional { Absent, Optional, Present } +import std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.text { String } import v2.compiler.refinement_discharge { infer_and_discharge } @@ -193,8 +195,7 @@ fn closure_ingest_step( state: ClosureIngestState, read: DagSourceReadWitness, lex: PreparedLexRules, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> ClosureIngestState { let acc = state.ingest match bind_outcome( @@ -203,7 +204,6 @@ fn closure_ingest_step( parse_module_prepared_seeded( tokens: tokens, prepared: prepared, - validation_residue: residue, seed: state.alloc ) } @@ -218,22 +218,23 @@ fn closure_ingest_step( bind_outcome( o: qualified_name_from_module_node(root: census.root), f: fn(module_qn) { - if closure_ingest_declares(files: acc.files, member_module: module_qn) { - outcome_rejected(d: closure_emission_diagnostic( - reason: ^closure_census_duplicate_module, - at: census.root - )) - } else { - outcome_accepted(value: ClosureIngest { - files: list_snoc_item(xs: acc.files, item: ClosureIngestFile { - module: module_qn, - parse_tree: artifact.tree, - census: census, - source_root: read.source_root - }), - index: source_root_index_insert(index: acc.index, module: module_qn, root: read.source_root), - alloc: artifact.alloc - }) + match closure_ingest_first_declaring(files: acc.files, member_module: module_qn) { + Present { value: first } => + closure_census_duplicate_module_outcome( + first: first, + second_root: census.root + ) + Absent => + outcome_accepted(value: ClosureIngest { + files: list_snoc_item(xs: acc.files, item: ClosureIngestFile { + module: module_qn, + parse_tree: artifact.tree, + census: census, + source_root: read.source_root + }), + index: source_root_index_insert(index: acc.index, module: module_qn, root: read.source_root), + alloc: artifact.alloc + }) } } ) @@ -258,7 +259,7 @@ type ClosureCensus fn closure_ingest(ingest: SourceRootIngest, lm: LanguageModel) -> ClosureCensus { match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => ClosureGrammarUnprepared { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => let lex = prepare_lex_rules(rules: lm.lex) ClosureCensusRead { state: fold_list( @@ -270,7 +271,7 @@ fn closure_ingest(ingest: SourceRootIngest, lm: LanguageModel) -> ClosureCensus spans: span_index_empty() }, cons: fn(state, read) { - closure_ingest_step(state: state, read: read, lex: lex, prepared: prepared, residue: residue) + closure_ingest_step(state: state, read: read, lex: lex, prepared: prepared) } ) } @@ -309,8 +310,43 @@ fn closure_census_index(files: FreeMonoid) -> SymbolIndex { // would refuse a duplicate too (resolve_duplicate_module_name), but each member is resolved in a // context holding only itself, so that wall cannot fire here; the census is where the whole // population is in view, so the wall lives there (review of gunbc#12218 at 242b7af). -fn closure_ingest_declares(files: FreeMonoid, member_module: QualifiedName) -> Bool { - fold_list(xs: files, empty: false, cons: fn(found, f) { found || f.module == member_module }) +fn closure_ingest_first_declaring( + files: FreeMonoid, + member_module: QualifiedName +) -> Optional { + fold_list(xs: files, empty: optional_absent(), cons: fn(found, f) { + match found { + Present { value: _ } => found + Absent => + if f.module == member_module { + optional_present(value: f) + } else { + found + } + } + }) +} + +// BOTH FILES, because a collision is a two-place fact. Locating only the second file's module +// node left the first unnamed, and a later resolve diagnostic at an innocent token was then +// read as the cause. +fn closure_census_duplicate_module_diagnostics(first: Node, second: Node) -> NonEmptyDiagnostics { + non_empty_diagnostics_append( + a: diagnostics_singleton(d: closure_emission_diagnostic(reason: ^closure_census_duplicate_module, at: first)), + b: diagnostics_singleton(d: closure_emission_diagnostic(reason: ^closure_census_duplicate_module, at: second)) + ) +} + +fn closure_census_duplicate_module_outcome( + first: ClosureIngestFile, + second_root: Node +) -> Outcome { + Rejected { + diagnostics: closure_census_duplicate_module_diagnostics( + first: first.census.root, + second: second_root + ) + } } // EXACTLY ONE FILE, OR A TYPED REFUSAL. The census makes more than one unreachable, and it is refused @@ -349,7 +385,7 @@ fn closure_resolve_member( policy: default_name_resolution_policy() )), admission: Admission { subject: ResolutionSubject { name: member_module }, imports: Empty } - ).walk, symbol_index: symbol_index, closure_declarations: empty_symbol_index(), unavailable_providers: []) + ).walk, symbol_index: symbol_index, closure_declarations: empty_symbol_index(), unavailable_providers: [], member_kinds: no_member_declaration_kinds()) } ) } diff --git a/src/v2/compiler/self_host/frontier_probe.dag b/src/v2/compiler/self_host/frontier_probe.dag index de7282c9366..14c97cb77d7 100644 --- a/src/v2/compiler/self_host/frontier_probe.dag +++ b/src/v2/compiler/self_host/frontier_probe.dag @@ -15,9 +15,6 @@ import v2.compiler.name_resolve { resolve_diagnostics, validate_module_roots } -import v2.compiler.parse { - overlap_residue_stage_from_parse_grammar -} import v2.compiler.program_assembly { assemble_program_from_ingest, program_assembly_fold_ingest, @@ -716,9 +713,6 @@ fn frontier_probe_emit_from_populated_ingest( d: d, ingest: ingest, admission: admission - ), - overlap_residue_stage: overlap_residue_stage_from_parse_grammar( - grammar: dag_language_model().grammar ) ) Accepted { value: _, diagnostics: _ } => diff --git a/src/v2/compiler/self_host/frontier_probe_assemble_detail.dag b/src/v2/compiler/self_host/frontier_probe_assemble_detail.dag index ddd8027c298..8198177f506 100644 --- a/src/v2/compiler/self_host/frontier_probe_assemble_detail.dag +++ b/src/v2/compiler/self_host/frontier_probe_assemble_detail.dag @@ -1,27 +1,7 @@ module v2.compiler.self_host.frontier_probe_assemble_detail -import std.algebra { FreeMonoid, list_snoc_item } -import v2.std.algebra { - Empty, - HeadAbsent, - HeadFound, - TailAbsent, - TailFound, - fold_list, - is_empty, - list_head, - list_tail -} -import v2.compiler.parse_diagnostic { - NoParseDiagnosticEvidence, - NonEmptyParseStageDiagnostics, - ParseStageDiagnostic, - grammar_choice_ambiguity_rows_from_non_empty_parse_stage, - non_empty_grammar_choice_ambiguity_rows_from_parse_stage, - parse_overlap_residue_stage_diagnostic_at, - parse_stage_diagnostic_from_diagnostic_only -} -import v2.std.grammar_choice_ambiguity { GrammarChoiceAmbiguityRow } +import std.algebra { list_snoc_item } +import v2.std.algebra { Empty, fold_list } import v2.compiler.self_host.frontier_probe_types { AssembleMissingModule, AssembleRejectionCause, @@ -29,7 +9,6 @@ import v2.compiler.self_host.frontier_probe_types { FrontierProbeAssembleDetail, FrontierProbeDetail, FrontierProbeReceipt, - GrammarChoiceOverlap, MissingModuleIdentityUnavailable, NonEmptySymbolChain, OtherAssembleRefusal, @@ -38,23 +17,10 @@ import v2.compiler.self_host.frontier_probe_types { ProbeStageAssemble } import v2.std.diagnostic { Diagnostic, NonEmptyDiagnostics, diagnostics_fatal, diagnostics_fatal_reason } -import std.optional { Absent, Optional, Present, optional_absent, optional_present } +import std.optional { Absent, Optional, Present, optional_absent } import v2.std.qualified_name { QualifiedName } import v2.std.text { String } -fn non_empty_symbol_chain_from_non_empty_parse_stage( - d: NonEmptyParseStageDiagnostics -) -> NonEmptySymbolChain { - NonEmptySymbolChain { - head: d.head.diagnostic.reason, - tail: fold_list( - xs: d.tail, - empty: Empty, - cons: fn(acc, item) { list_snoc_item(xs: acc, item: item.diagnostic.reason) } - ) - } -} - fn non_empty_symbol_chain_from_non_empty_diagnostics( d: NonEmptyDiagnostics ) -> NonEmptySymbolChain { @@ -68,192 +34,20 @@ fn non_empty_symbol_chain_from_non_empty_diagnostics( } } +// diagnostic_module_root_missing_qn_optional always returns Absent: no diagnostic carries the +// module root it failed to find, so resolve_module_not_found assemble causes classify as +// MissingModuleIdentityUnavailable rather than AssembleMissingModule { requested }. +// FrontierProbeDetail.MissingModule (#7762) carries the structural QualifiedName on receipt.detail +// when frontier_probe_detail_from_diagnostic binds it. Trigger: a diagnostic evidence carrier for +// ModuleRootMissing (one that does not reintroduce the diagnostic.dag ↔ qualified_name import +// cycle); then extract it here and map Present -> AssembleMissingModule { requested }. fn diagnostic_module_root_missing_qn_optional(d: Diagnostic) -> Optional { optional_absent() } -// diagnostic_module_root_missing_qn_optional is deliberately unavailable in this -// module — it always returns Absent, so resolve_module_not_found assemble causes -// classify as MissingModuleIdentityUnavailable rather than AssembleMissingModule { -// requested }. FrontierProbeDetail.MissingModule (#7762) carries the structural -// QualifiedName on receipt.detail when frontier_probe_detail_from_diagnostic binds -// it. Parse-stage AssembleMissingModule awaits ModuleRootMissingEvidence on -// ParseDiagnosticEvidence without reintroducing the diagnostic.dag ↔ -// qualified_name import cycle. Dissolve trigger: wire parse-stage diagnostics to -// carry that evidence; then replace optional_absent() with extraction from -// ParseStageDiagnostic.evidence and map Present -> AssembleMissingModule { -// requested }. - -// THE CAUSE IS THE FATAL DIAGNOSTIC, THE CHAIN'S LAST. An advisory prepended to the refusal (the -// grammar overlap residue heads every module's chain) is not the cause, so the chain is classified -// by its fatal element and the residue no longer needs a masking arm of its own: its rows ride on -// the detail's overlap_residue_stage. A refusal whose fatal element is itself the overlap residue -// carries its rows as GrammarChoiceOverlap. -fn assemble_rejection_cause_from_parse_stage( - d: NonEmptyParseStageDiagnostics -) -> AssembleRejectionCause { - let fatal = parse_stage_fatal(d: d) - if fatal.diagnostic.reason == ^resolve_module_not_found { - match diagnostic_module_root_missing_qn_optional(d: fatal.diagnostic) { - Present { value: requested } => AssembleMissingModule { requested: requested } - Absent => MissingModuleIdentityUnavailable - } - } else if fatal.diagnostic.reason == ^parse_grammar_choice_overlap_residue { - match non_empty_grammar_choice_ambiguity_rows_from_parse_stage(d: d) { - Present { value: rows } => GrammarChoiceOverlap { rows: rows } - Absent => OtherAssembleRefusal { reason: fatal.diagnostic.reason } - } - } else { - OtherAssembleRefusal { reason: fatal.diagnostic.reason } - } -} - -fn parse_stage_fatal(d: NonEmptyParseStageDiagnostics) -> ParseStageDiagnostic { - fold_list(xs: d.tail, empty: d.head, cons: fn(acc, item) { item }) -} - -fn diagnostic_reason_is_overlap_residue(d: Diagnostic) -> Bool { - d.reason == ^parse_grammar_choice_overlap_residue -} - -fn all_overlap_residue_diagnostic_list(xs: FreeMonoid) -> Bool { - fold_list( - xs: xs, - empty: true, - cons: fn(acc, diag) { acc && diagnostic_reason_is_overlap_residue(d: diag) } - ) -} - -fn all_overlap_residue_non_empty_diagnostics(d: NonEmptyDiagnostics) -> Bool { - diagnostic_reason_is_overlap_residue(d: d.head) - && all_overlap_residue_diagnostic_list(xs: d.tail) -} - -// parse_stage_head_from_overlap_row_queue_and_diagnostic and -// parse_stage_tail_from_overlap_row_queue_and_diagnostics pair plain -// NonEmptyDiagnostics overlap residues against a GrammarChoiceAmbiguityRow queue -// from overlap_residue_stage. Both sides derive from the same ambiguity_roster in -// grammar_validate_and_analyze today — count mismatch is unreachable in-tree. -// HeadAbsent arms silently narrow to NoParseDiagnosticEvidence -// (empty-observation narrow at this boundary if parallel representation diverges). -// Dissolve trigger: carry overlap evidence only on parse-stage diagnostics and -// delete this queue-merge path; then refuse on evidence deficit instead of narrowing. - -type OverlapQueueMergeAcc { - stages: FreeMonoid - row_queue: FreeMonoid -} - -fn overlap_row_queue_tail_after_pop( - row_queue: FreeMonoid -) -> FreeMonoid { - match list_tail(xs: row_queue) { - TailAbsent => Empty - TailFound { tail: rest } => rest - } -} - -fn overlap_queue_merge_acc_step(acc: OverlapQueueMergeAcc, diag: Diagnostic) -> OverlapQueueMergeAcc { - if diagnostic_reason_is_overlap_residue(d: diag) { - match list_head(xs: acc.row_queue) { - HeadFound { value: row } => - OverlapQueueMergeAcc { - stages: list_snoc_item( - xs: acc.stages, - item: parse_overlap_residue_stage_diagnostic_at(at: diag.at, row: row) - ), - row_queue: overlap_row_queue_tail_after_pop(row_queue: acc.row_queue) - } - HeadAbsent => - OverlapQueueMergeAcc { - stages: list_snoc_item( - xs: acc.stages, - item: parse_stage_diagnostic_from_diagnostic_only(d: diag) - ), - row_queue: acc.row_queue - } - } - } else { - OverlapQueueMergeAcc { - stages: list_snoc_item( - xs: acc.stages, - item: parse_stage_diagnostic_from_diagnostic_only(d: diag) - ), - row_queue: acc.row_queue - } - } -} - -fn parse_stage_tail_from_overlap_row_queue_and_diagnostics( - tail: FreeMonoid, - row_queue: FreeMonoid -) -> FreeMonoid { - fold_list( - xs: tail, - empty: OverlapQueueMergeAcc { stages: Empty, row_queue: row_queue }, - cons: fn(acc, diag) { overlap_queue_merge_acc_step(acc: acc, diag: diag) } - ).stages -} - -fn parse_stage_head_from_overlap_row_queue_and_diagnostic( - diag: Diagnostic, - row_queue: FreeMonoid -) -> ParseStageDiagnostic { - if diagnostic_reason_is_overlap_residue(d: diag) { - match list_head(xs: row_queue) { - HeadFound { value: row } => - parse_overlap_residue_stage_diagnostic_at(at: diag.at, row: row) - HeadAbsent => parse_stage_diagnostic_from_diagnostic_only(d: diag) - } - } else { - parse_stage_diagnostic_from_diagnostic_only(d: diag) - } -} - -fn overlap_row_queue_after_overlap_head( - diag: Diagnostic, - row_queue: FreeMonoid -) -> FreeMonoid { - if diagnostic_reason_is_overlap_residue(d: diag) { - overlap_row_queue_tail_after_pop(row_queue: row_queue) - } else { - row_queue - } -} - -fn non_empty_parse_stage_diagnostics_from_rejection_and_overlap_stage( - d: NonEmptyDiagnostics, - overlap_stage: NonEmptyParseStageDiagnostics -) -> NonEmptyParseStageDiagnostics { - let row_queue = grammar_choice_ambiguity_rows_from_non_empty_parse_stage(d: overlap_stage) - let head = parse_stage_head_from_overlap_row_queue_and_diagnostic( - diag: d.head, - row_queue: row_queue - ) - let row_queue_after_head = overlap_row_queue_after_overlap_head(diag: d.head, row_queue: row_queue) - NonEmptyParseStageDiagnostics { - head: head, - tail: parse_stage_tail_from_overlap_row_queue_and_diagnostics( - tail: d.tail, - row_queue: row_queue_after_head - ) - } -} - -fn non_empty_parse_stage_diagnostics_merged( - d: NonEmptyDiagnostics, - overlap_stage: NonEmptyParseStageDiagnostics -) -> NonEmptyParseStageDiagnostics { - if all_overlap_residue_non_empty_diagnostics(d: d) { - overlap_stage - } else { - non_empty_parse_stage_diagnostics_from_rejection_and_overlap_stage( - d: d, - overlap_stage: overlap_stage - ) - } -} - +// THE CAUSE IS THE FATAL DIAGNOSTIC, THE CHAIN'S LAST (advisories step 2, gunbc#13103). An advisory +// rejected_with_pending prepended is not the cause, so the cause, its location and the receipt's +// located reason are all read from the fatal element; the chain keeps the whole ordered list. fn assemble_rejection_cause_from_diagnostics( d: NonEmptyDiagnostics ) -> AssembleRejectionCause { @@ -268,16 +62,6 @@ fn assemble_rejection_cause_from_diagnostics( } } -fn frontier_probe_assemble_detail_from_parse_stage( - d: NonEmptyParseStageDiagnostics -) -> FrontierProbeAssembleDetail { - AssembleRejectionDetail { - rejection_chain: non_empty_symbol_chain_from_non_empty_parse_stage(d: d), - cause: assemble_rejection_cause_from_parse_stage(d: d), - at: frontier_probe_cause_location_from_locus(at: parse_stage_fatal(d: d).diagnostic.at) - } -} - fn frontier_probe_assemble_detail_from_rejection( d: NonEmptyDiagnostics ) -> FrontierProbeAssembleDetail { @@ -288,39 +72,17 @@ fn frontier_probe_assemble_detail_from_rejection( } } -fn frontier_probe_assemble_detail_from_rejection_with_overlap_stage( - d: NonEmptyDiagnostics, - overlap_residue_stage: Optional -) -> FrontierProbeAssembleDetail { - match overlap_residue_stage { - Absent => frontier_probe_assemble_detail_from_rejection(d: d) - Present { value: stage } => - AssembleRejectionDetail { - rejection_chain: non_empty_symbol_chain_from_non_empty_diagnostics(d: d), - cause: assemble_rejection_cause_from_parse_stage( - d: non_empty_parse_stage_diagnostics_merged(d: d, overlap_stage: stage) - ), - at: frontier_probe_cause_location_from_locus(at: diagnostics_fatal(d: d).at) - } - } -} - fn frontier_probe_receipt_from_assemble_rejection( module_path: String, diagnostics: NonEmptyDiagnostics, - detail: FrontierProbeDetail, - overlap_residue_stage: Optional + detail: FrontierProbeDetail ) -> FrontierProbeReceipt { - let located_reason = diagnostics_fatal_reason(d: diagnostics) FrontierProbeReceipt { module_path: module_path, - blocker_class: frontier_classify_rejection_reason(reason: located_reason), + blocker_class: frontier_classify_rejection_reason(reason: diagnostics_fatal_reason(d: diagnostics)), located_stage: ProbeStageAssemble, - located_reason: located_reason, + located_reason: diagnostics_fatal_reason(d: diagnostics), detail: detail, - assemble_detail: frontier_probe_assemble_detail_from_rejection_with_overlap_stage( - d: diagnostics, - overlap_residue_stage: overlap_residue_stage - ) + assemble_detail: frontier_probe_assemble_detail_from_rejection(d: diagnostics) } } diff --git a/src/v2/compiler/self_host/frontier_probe_semantic_family_receipt.dag b/src/v2/compiler/self_host/frontier_probe_semantic_family_receipt.dag deleted file mode 100644 index 760764d7145..00000000000 --- a/src/v2/compiler/self_host/frontier_probe_semantic_family_receipt.dag +++ /dev/null @@ -1,241 +0,0 @@ -module v2.compiler.self_host.frontier_probe_semantic_family_receipt - -import std.types { List } -import std.content_hash { Fnv1a64Structural } -import v2.std.algebra { fold_list } -import v2.std.integer { Int } -import v2.std.node { Symbol } -import v2.std.text { String } - -// SH-D D1: what the five semantic-pipeline shards MEASURE at the v2 direct Rust door, as executed -// observations rather than expectations. Each row is one shard, and the two dispositions are not -// two flavours of the same answer: a shard whose closure fits the manifest transport produces a -// detailed cause, and a shard whose closure exceeds the inline row cap produces nothing at all — -// the manifest emits zero rows and an empty ingest, so there is no observation to record. Recording -// the second as a measurement with absent fields would make the cap look like a property of the -// module. The rows carry no verdict about the frontier roster; frontier.dag is the program lead's -// authority and this module supplies it evidence, never a correction. - -// Provenance of these observations, so a reader can re-derive rather than trust them: source_commit -// 2bc11e0cd53, survey/probe binaries built from that tree, closures produced by -// discover_source_root_ingest against source roots src/v2 and dag, receipts read through -// frontier_probe_emit_from_ingest. The measured shards were read on UNPATCHED main for -// materialization_carriers, and for use_site_verdict with the annotation-channel hunk of gunbc#8149 -// (commit fa2576a9) applied locally, because on unpatched main that shard's closure refuses at the -// lexer before reaching the layer under study. Which side each row was read on is carried per row, -// not assumed. - -type ShardObservationSide - = ObservedOnMain - | ObservedWithAnnotationChannelHunk { upstream_commit: String } - -type ShardDoorDisposition - = DetailedCauseObserved { - located_reason: Symbol, - chain_length: Int, - symbols_differing_from_head: Int, - first_differing_symbol: String - } - | ClosureExceedsManifestCap { read_count: Int, cap: Int } - -type SemanticFamilyShardRow { - module_path: String - closure_reads: Int - side: ShardObservationSide - disposition: ShardDoorDisposition -} - -data shard_use_site_verdict: SemanticFamilyShardRow = SemanticFamilyShardRow { - module_path: "src/v2/compiler/use_site_verdict.dag", - closure_reads: 15, - side: ObservedWithAnnotationChannelHunk { upstream_commit: "fa2576a9" }, - disposition: DetailedCauseObserved { - located_reason: ^parse_grammar_choice_overlap_residue, - chain_length: 126, - symbols_differing_from_head: 120, - first_differing_symbol: "body_lowering_reason_wrapper_retained_emitted" - } -} - -data shard_materialization_carriers: SemanticFamilyShardRow = SemanticFamilyShardRow { - module_path: "src/v2/compiler/materialization_carriers.dag", - closure_reads: 48, - side: ObservedOnMain, - disposition: DetailedCauseObserved { - located_reason: ^parse_grammar_choice_overlap_residue, - chain_length: 11, - symbols_differing_from_head: 5, - first_differing_symbol: "body_lowering_reason_wrapper_retained_emitted" - } -} - -data shard_03_resolve: SemanticFamilyShardRow = SemanticFamilyShardRow { - module_path: "src/v2/compiler/03_resolve.dag", - closure_reads: 71, - side: ObservedOnMain, - disposition: ClosureExceedsManifestCap { read_count: 71, cap: 64 } -} - -data shard_04_infer: SemanticFamilyShardRow = SemanticFamilyShardRow { - module_path: "src/v2/compiler/04_infer.dag", - closure_reads: 75, - side: ObservedOnMain, - disposition: ClosureExceedsManifestCap { read_count: 75, cap: 64 } -} - -data shard_03_name_resolve: SemanticFamilyShardRow = SemanticFamilyShardRow { - module_path: "src/v2/compiler/03_name_resolve.dag", - closure_reads: 143, - side: ObservedOnMain, - disposition: ClosureExceedsManifestCap { read_count: 143, cap: 64 } -} - -data semantic_family_shard_rows: List = [ - shard_use_site_verdict, - shard_materialization_carriers, - shard_03_resolve, - shard_04_infer, - shard_03_name_resolve -] - -// CORRECTION (2026-08-13, vivid-bat-377 executed finding, mechanism re-verified here): the sentence -// this row used to carry — that the recorded reason is CONFIRMED at reason grain — is WITHDRAWN, -// and it was withdrawn for a stronger reason than being unproven. -// ^parse_grammar_choice_overlap_residue is not a per-module cause at all. v2.compiler.02_parse -// grammar_choice_ambiguity_roster_from_analysis folds over root.productions ALONE — no module, no -// source text, no closure is an input — so the roster is grammar-global and constant, and -// rejected_with_pending PREPENDS it to any later rejection. The head of a parse failure is -// therefore that residue for ANY module under the dag grammar, unconditionally. Agreement between a -// receipt and a frontier row on that symbol is agreement with a constant: it discriminates nothing, -// so it can neither confirm nor disconfirm the row. A population that is everything is not -// evidence. -// -// WHAT SURVIVES UNCHANGED, and why the distinction is not special pleading: every figure in the -// rows above is a TAIL read. first_differing_symbol is by construction the first chain symbol -// DIFFERING from the head, so the constant prefix is exactly what it skips; chain_length and -// symbols_differing_from_head are counts over the whole chain. The body-lowering finding was -// reached by going past the head, which is why it survived a defect that invalidated the head -// reading. What changed is the standing of the recorded ROW, not the standing of the measurement. - -// THE FINDING, at the grain the evidence supports. Both measurable shards refuse with a head of -// ^parse_grammar_choice_overlap_residue — a grammar-global constant that says nothing about either -// module (see the correction note above; an earlier revision of this row read that agreement as -// confirming the frontier rows, which it cannot). The cause in both was recorded under an arm since -// deleted (the probe now classifies by the fatal element), and it meant that constant stood in -// front of a further chain. Reading those chains: -// use_site_verdict carries 126 symbols of which 120 differ from the head, materialization_carriers -// carries 11 of which 5 differ, and in BOTH the first differing symbol is -// body_lowering_reason_wrapper_retained_emitted. So the recorded reason is neither confirmed nor -// terminal, and what the chains DO show is unaffected by that: the dominant refusal underneath is -// body-lowering wrapper-retained residue, whose named prerequisite is the general body producer. -// What this does NOT establish: that body lowering is the LAST layer. A chain read one symbol deep -// answers what the mask hides, not what that in turn hides, and nothing here has looked beneath the -// second layer. - -// The three shards above 64 reads were UNOBSERVED at commit 2bc11e0 because the inline-text carrier -// capped at MANIFEST_INLINE_LIST_MAX. Capless closure transport (SourceRef rows, read-through -// ingest) removes that transport ceiling; a fresh frontier_probe_survey at the current head is -// required to replace ClosureExceedsManifestCap dispositions with detailed causes. This note -// records the pre-capless measurement only. - -// OBSERVED DRIFT, reported and not acted on: the frontier roster records closure_reads 9 for -// use_site_verdict and 35 for materialization_carriers; discover_source_root_ingest measures 15 and -// 48 at this commit. The rows are the program lead's authority and are not edited from here. Stated -// precisely, because the two are different claims: what is established is that the closure sizes -// differ from the recorded ones at this commit, NOT that either row's blocker classification is -// wrong. - -fn shard_count() -> Int { - fold_list(xs: semantic_family_shard_rows, empty: 0, cons: fn(n, _r) { n + 1 }) -} - -fn measured_shard_count() -> Int { - fold_list( - xs: semantic_family_shard_rows, - empty: 0, - cons: fn(n, r) { - match r.disposition { - DetailedCauseObserved { - located_reason: _, - chain_length: _, - symbols_differing_from_head: _, - first_differing_symbol: _ - } => n + 1 - ClosureExceedsManifestCap { read_count: _, cap: _ } => n - } - } - ) -} - -fn capped_shard_count() -> Int { - fold_list( - xs: semantic_family_shard_rows, - empty: 0, - cons: fn(n, r) { - match r.disposition { - ClosureExceedsManifestCap { read_count: _, cap: _ } => n + 1 - DetailedCauseObserved { - located_reason: _, - chain_length: _, - symbols_differing_from_head: _, - first_differing_symbol: _ - } => n - } - } - ) -} - -fn shards_naming_body_lowering_beneath_the_mask() -> Int { - fold_list( - xs: semantic_family_shard_rows, - empty: 0, - cons: fn(n, r) { - match r.disposition { - DetailedCauseObserved { - located_reason: _, - chain_length: _, - symbols_differing_from_head: _, - first_differing_symbol: s - } => - if s == "body_lowering_reason_wrapper_retained_emitted" { n + 1 } else { n } - ClosureExceedsManifestCap { read_count: _, cap: _ } => n - } - } - ) -} - -fn every_capped_shard_exceeds_its_own_cap() -> Bool { - fold_list( - xs: semantic_family_shard_rows, - empty: true, - cons: fn(ok, r) { - match r.disposition { - ClosureExceedsManifestCap { read_count: reads, cap: cap } => ok && (reads > cap) - DetailedCauseObserved { - located_reason: _, - chain_length: _, - symbols_differing_from_head: _, - first_differing_symbol: _ - } => ok - } - } - ) -} - -fn every_measured_chain_is_non_uniform() -> Bool { - fold_list( - xs: semantic_family_shard_rows, - empty: true, - cons: fn(ok, r) { - match r.disposition { - DetailedCauseObserved { - located_reason: _, - chain_length: len, - symbols_differing_from_head: differing, - first_differing_symbol: _ - } => ok && (differing > 0) && (differing < len) - ClosureExceedsManifestCap { read_count: _, cap: _ } => ok - } - } - ) -} diff --git a/src/v2/compiler/self_host/frontier_probe_types.dag b/src/v2/compiler/self_host/frontier_probe_types.dag index 3446f0755e5..cfd0f945398 100644 --- a/src/v2/compiler/self_host/frontier_probe_types.dag +++ b/src/v2/compiler/self_host/frontier_probe_types.dag @@ -32,10 +32,6 @@ import std.content_hash { } import std.types { CommitSha, GitRef, List } import v2.std.algebra { fold_list } -import v2.std.grammar_choice_ambiguity { - GrammarChoiceAmbiguityRow, - NonEmptyGrammarChoiceAmbiguityRows -} import v2.std.integer { Int } import v2.std.diagnostic { DeclarationLocus, Extent, InvariantPortLocus, Locus, NodeLocus, SourcePortLocus, Textual } import v2.std.node { Symbol, symbol_lexeme } @@ -112,8 +108,7 @@ type NonEmptySymbolChain { } type AssembleRejectionCause - = GrammarChoiceOverlap { rows: NonEmptyGrammarChoiceAmbiguityRows } - | AssembleMissingModule { requested: QualifiedName } + = AssembleMissingModule { requested: QualifiedName } | MissingModuleIdentityUnavailable | OtherAssembleRefusal { reason: Symbol } @@ -163,14 +158,11 @@ type FrontierProbeAssembleDetail at: FrontierProbeCauseLocation } -// AssembleRejectionDetail binds only parse-stage-carried evidence — never a global grammar roster. -// GrammarChoiceOverlap rows are extracted from ParseDiagnosticEvidence on the parse-stage -// diagnostic carrier. The cause is read from the chain's FATAL element: an overlap residue that -// heads a chain only rode along, and the refusal is classified by what refused. -// AssembleMissingModule in assemble_detail awaits parse-stage -// ModuleRootMissingEvidence; FrontierProbeDetail.MissingModule (#7762) is the structural -// resolve_module_not_found authority on detail. MissingModuleIdentityUnavailable remains when -// assemble cause cannot bind parse-stage evidence. Emit-stage receipts use NoFrontierProbeDetail. +// AssembleRejectionDetail binds only what the rejection's own diagnostics carry -- never a global +// grammar roster. AssembleMissingModule awaits a ModuleRootMissing evidence carrier on the +// diagnostic; FrontierProbeDetail.MissingModule (#7762) is the structural resolve_module_not_found +// authority on detail. MissingModuleIdentityUnavailable remains when the assemble cause cannot bind +// that evidence. Emit-stage receipts use NoFrontierProbeDetail. type FrontierProbeReceipt { module_path: String diff --git a/src/v2/compiler/source_authority.dag b/src/v2/compiler/source_authority.dag index 4f449781656..c209616c85b 100644 --- a/src/v2/compiler/source_authority.dag +++ b/src/v2/compiler/source_authority.dag @@ -25,12 +25,11 @@ import v2.extdeps.languages.dag { import v2.std.compilers.lexing { TokenStream } import v2.std.provenance { span_index_empty } import std.algebra { Empty, FreeMonoid, list_snoc_item } -import v2.std.algebra { fold_list, length } +import v2.std.algebra { HeadAbsent, HeadFound, fold_list, list_head, skip } import v2.std.artifact { Artifact, NodeArtifactProvenance, SourceFile } import v2.std.diagnostic { Accepted, Diagnostic, - Diagnostics, ExternalContractUnknown, NonEmptyDiagnostics, Outcome, @@ -483,18 +482,36 @@ fn source_ref_path(ref: SourceRef) -> String { ref.path } -fn source_root_for_storage_path(path: String) -> SourceRootRef { - if starts_with(s: path, prefix: "src/") { - V2Tree +// Tree identity is the --source-root argument that discovered the file, never a substring of +// the file's storage path. SourceRootRef has two inhabitants (the dag tree and the src/v2 +// tree); an argument names one of those directories or is unrecognized. A parent of both, a +// copied single-root dump, or any other spelling refuses rather than defaulting to DagTree. +type SourceRootArgumentAdmission + = SourceRootArgumentAdmitted { tree: SourceRootRef } + | SourceRootArgumentUnrecognized { argument: String } + +fn admit_source_root_argument(argument: String) -> SourceRootArgumentAdmission { + if (argument == "src/v2") + || (argument == "./src/v2") + || (argument == "src/v2/") + || argument.ends_with(suffix: "/src/v2") + || argument.ends_with(suffix: "/src/v2/") { + SourceRootArgumentAdmitted { tree: V2Tree } + } else if (argument == "dag") + || (argument == "./dag") + || (argument == "dag/") + || argument.ends_with(suffix: "/dag") + || argument.ends_with(suffix: "/dag/") { + SourceRootArgumentAdmitted { tree: DagTree } } else { - DagTree + SourceRootArgumentUnrecognized { argument: argument } } } -fn source_ref_for_storage_path(path: String) -> SourceRef { +fn source_ref_for_storage_path(path: String, source_root: SourceRootRef) -> SourceRef { SourceRef { path: path, - source_root: source_root_for_storage_path(path: path), + source_root: source_root, content_hash: content_hash_of_value(value: "selection-unpinned" as NonEmptyStr) } } @@ -791,8 +808,7 @@ fn module_storage_binding_from_parse_artifact( fn module_storage_binding_from_read_prepared( source_read: DagSourceReadWitness, lm: LanguageModel, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> Outcome { match source_read.artifact.kind { SourceFile => @@ -806,8 +822,7 @@ fn module_storage_binding_from_read_prepared( bind_outcome( o: parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ), f: fn(artifact) { module_storage_binding_from_parse_artifact( @@ -839,12 +854,11 @@ fn module_storage_binding_from_read_with_model( ) -> Outcome { match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => module_storage_binding_from_read_prepared( source_read: source_read, lm: lm, - prepared: prepared, - residue: residue + prepared: prepared ) } } @@ -900,8 +914,7 @@ fn module_storage_fold_step( state: ModuleStorageFoldState, read: DagSourceReadWitness, lm: LanguageModel, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> ModuleStorageFoldState { match state { ModuleStorageFoldFailed { diagnostics: d } => @@ -910,8 +923,7 @@ fn module_storage_fold_step( match module_storage_binding_from_read_prepared( source_read: read, lm: lm, - prepared: prepared, - residue: residue + prepared: prepared ) { Rejected { diagnostics: d } => ModuleStorageFoldFailed { diagnostics: d } @@ -929,8 +941,7 @@ fn module_storage_fold_step( fn module_storage_bindings_from_ingest_prepared( ingest: SourceRootIngest, lm: LanguageModel, - prepared: PreparedGrammar, - residue: Diagnostics + prepared: PreparedGrammar ) -> Outcome { match fold_list( xs: ingest, @@ -940,8 +951,7 @@ fn module_storage_bindings_from_ingest_prepared( state: state, read: read, lm: lm, - prepared: prepared, - residue: residue + prepared: prepared ) } ) { @@ -959,12 +969,11 @@ fn module_storage_bindings_from_ingest_with_model( _ => match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => module_storage_bindings_from_ingest_prepared( ingest: ingest, lm: lm, - prepared: prepared, - residue: residue + prepared: prepared ) } } @@ -1555,13 +1564,37 @@ fn source_authority_round_trip( ) } -// The native filesystem fold supplies observed bytes; provenance is constructed at the same -// authority as every other source witness. Neither rendered main owns this conversion. -fn source_root_ingest_from_files(files: List) -> SourceRootIngest { - files |> map(file => DagSourceReadWitness { - source: Medium { carried: file.content, fidelity: Lossless }, - artifact: Artifact { kind: SourceFile, id: file.path, file_path: file.path }, - compilation_unit: file.path, - source_root: source_root_for_storage_path(path: file.path) - }) +// The native filesystem fold supplies observed bytes and the discovering --source-root +// argument on each file. Tree identity is admitted from that argument. An unrecognized +// argument refuses; the file path is never classified. +type SourceRootIngestFromFiles + = SourceRootIngestFromFilesBuilt { ingest: SourceRootIngest } + | SourceRootIngestFromFilesUnrecognizedRoot { argument: String, path: String } + +fn source_root_ingest_from_files_go( + pending: List, + acc: SourceRootIngest +) -> SourceRootIngestFromFiles { + match list_head(xs: pending) { + HeadAbsent => SourceRootIngestFromFilesBuilt { ingest: acc } + HeadFound { value: file } => + match admit_source_root_argument(argument: file.origin) { + SourceRootArgumentAdmitted { tree: tree } => + source_root_ingest_from_files_go( + pending: skip(xs: pending, n: 1), + acc: list_snoc_item(xs: acc, item: DagSourceReadWitness { + source: Medium { carried: file.content, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: file.path, file_path: file.path }, + compilation_unit: file.path, + source_root: tree + }) + ) + SourceRootArgumentUnrecognized { argument: argument } => + SourceRootIngestFromFilesUnrecognizedRoot { argument: argument, path: file.path } + } + } +} + +fn source_root_ingest_from_files(files: List) -> SourceRootIngestFromFiles { + source_root_ingest_from_files_go(pending: files, acc: Empty) } diff --git a/src/v2/compiler/source_authority_read.dag b/src/v2/compiler/source_authority_read.dag index de4d8bc9f96..4a4b6adda13 100644 --- a/src/v2/compiler/source_authority_read.dag +++ b/src/v2/compiler/source_authority_read.dag @@ -15,10 +15,9 @@ import std.content_hash { import v2.compiler.source_authority { DagSourceReadWitness, SourceRef, - SourceRootIngest, - source_ref_for_storage_path, - source_root_for_storage_path + SourceRootIngest } +import v2.std.cross_tree.import_model { SourceRootRef } import extdeps.communication.medium { Lossless, Medium } import v2.std.algebra { fold_list } import v2.std.artifact { Artifact, SourceFile } @@ -58,11 +57,11 @@ import v2.std.text { String } // witness is minted, a missing or unreadable path refuses as a read failure rather than a // misclassified parse error, and both refusals carry the observed digest. Nothing here widens on // failure. -fn source_ref_for_observed_storage_path(path: String) -> SourceRef { +fn source_ref_for_observed_storage_path(path: String, source_root: SourceRootRef) -> SourceRef { let read = Filesystem.Read(path: path) SourceRef { path: path, - source_root: source_root_for_storage_path(path: path), + source_root: source_root, content_hash: if read.success { dag_source_content_hash_for_text(text: read.content) } else { @@ -79,11 +78,11 @@ data source_ref_read_refused: Symbol = ^source_ref_read_refused data source_ref_content_hash_mismatch: Symbol = ^source_ref_content_hash_mismatch -fn source_ref_read_failed_diagnostic(ref: SourceRef, error: String) -> NonEmptyDiagnostics { +fn source_ref_read_failed_diagnostic(path: String, error: String) -> NonEmptyDiagnostics { NonEmptyDiagnostics { head: Diagnostic { reason: source_ref_read_refused, - at: textual_whole_file_locus(file: symbol_intern_lexeme(lexeme: ref.path)), + at: textual_whole_file_locus(file: symbol_intern_lexeme(lexeme: path)), correction: Unavailable { reason: ExternalContractUnknown } }, tail: [ @@ -184,7 +183,7 @@ fn source_ref_read_result_to_outcome(result: SourceRefReadResult) -> Outcome Rejected { - diagnostics: source_ref_read_failed_diagnostic(ref: ref, error: error) + diagnostics: source_ref_read_failed_diagnostic(path: ref.path, error: error) } SourceRefReadHashMismatch { ref: ref, expected: _, observed: observed } => Rejected { @@ -244,7 +243,7 @@ fn source_root_ingest_from_source_refs(refs: List) -> Outcome Rejected { diagnostics: source_ref_read_failed_diagnostic( - ref: source_ref_for_storage_path(path: "source-root-ingest-build-impossible"), + path: "source-root-ingest-build-impossible", error: "internal: read-through accepted without ingest row" ) } diff --git a/src/v2/compiler/symbol_index_fill.dag b/src/v2/compiler/symbol_index_fill.dag index bb6c5edc684..b3083372244 100644 --- a/src/v2/compiler/symbol_index_fill.dag +++ b/src/v2/compiler/symbol_index_fill.dag @@ -122,7 +122,14 @@ fn symbol_index_mark_declared_binders(index: SymbolIndex, qualified_path: Qualif } // A labeled child declares a binding at path.segment and its members below it. -fn symbol_index_fill_declared_child(index: SymbolIndex, path: QualifiedName, segment: Symbol, edge: Edge) -> SymbolIndex { +fn symbol_index_fill_declared_child( + index: SymbolIndex, + path: QualifiedName, + segment: Symbol, + edge: Edge, + counts: Map, + module_qn: QualifiedName +) -> SymbolIndex { let child_path = qualified_name_snoc(qn: path, segment: segment) let declared = symbol_index_declared(target: symbol_index_binder_type_or_target(target: edge.target)) let with_binding = symbol_index_mark_namespace_body_if( @@ -143,7 +150,9 @@ fn symbol_index_fill_declared_child(index: SymbolIndex, path: QualifiedName, seg symbol_index_fill_containment_node( index: with_binding, path: child_path, - node: member + node: member, + counts: counts, + module_qn: module_qn ) Absent => with_binding } @@ -179,7 +188,9 @@ fn symbol_index_mark_namespace_body_if(index: SymbolIndex, qualified_path: Quali fn symbol_index_fill_containment_edge( index: SymbolIndex, path: QualifiedName, - edge: Edge + edge: Edge, + counts: Map, + module_qn: QualifiedName ) -> SymbolIndex { match edge.label { StructuralLabel { label: _ } => @@ -187,28 +198,34 @@ fn symbol_index_fill_containment_edge( symbol_index_fill_containment_node( index: index, path: path, - node: edge.target + node: edge.target, + counts: counts, + module_qn: module_qn ) } else { - symbol_index_fill_declared_child(index: index, path: path, segment: label_sort_key(label: edge.label), edge: edge) + symbol_index_fill_declared_child(index: index, path: path, segment: label_sort_key(label: edge.label), edge: edge, counts: counts, module_qn: module_qn) } Authored { name: sym } => if parse_tree_projection_edge(name: sym) { symbol_index_fill_containment_node( index: index, path: path, - node: edge.target + node: edge.target, + counts: counts, + module_qn: module_qn ) } else if sym == realization_root_label() { index } else { - symbol_index_fill_declared_child(index: index, path: path, segment: sym, edge: edge) + symbol_index_fill_declared_child(index: index, path: path, segment: sym, edge: edge, counts: counts, module_qn: module_qn) } Positional => symbol_index_fill_containment_node( index: index, path: path, - node: edge.target + node: edge.target, + counts: counts, + module_qn: module_qn ) } } @@ -224,10 +241,12 @@ fn symbol_index_fill_containment_edge( fn symbol_index_fill_containment_children( index: SymbolIndex, path: QualifiedName, - node: Node + node: Node, + counts: Map, + module_qn: QualifiedName ) -> SymbolIndex { fold(node.children, init: index, f: fn(acc, edge) { - symbol_index_fill_containment_edge(index: acc, path: path, edge: edge) + symbol_index_fill_containment_edge(index: acc, path: path, edge: edge, counts: counts, module_qn: module_qn) }) } @@ -236,21 +255,30 @@ fn symbol_index_fill_containment_children( // Conj arm: there, namespace_graft_spine_segment_edge_optional read a one-binder domain whose binder is // a record (an operation's ^operation_input, a fn of one record-typed parameter) as a dotted-name // segment, and indexed the record's fields at the Arrow's path with the binder's level erased. +// A UNIQUE MODULE-LEVEL VARIANT SPELLING RESOLVES THROUGH THE ALIAS, AND THE ALIAS IS FILED AS A +// BINDING TO ITS DECLARING PATH (the repair the receipt +// a_binding_position_recorded_as_a_declaring_identity names): for a uniquely-named variant the +// walk below files the binding in the same pass where the variant's declared path is in hand, so +// symbol_index_declaring_path_of answers the variant's declaring path at the alias and the +// variant's own row answers itself -- one authority, no parallel carrier map. (A second ENTRY over +// the same arm node was the double-entry defect that row describes.) fn symbol_index_fill_containment_node( index: SymbolIndex, path: QualifiedName, - node: Node + node: Node, + counts: Map, + module_qn: QualifiedName ) -> SymbolIndex { match node.kind { TypeNode { connective: Conj } => match namespace_graft_spine_body_optional(node: node) { Present { value: body } => fold(body.children, init: index, f: fn(acc, edge) { - symbol_index_fill_containment_edge(index: acc, path: path, edge: edge) + symbol_index_fill_containment_edge(index: acc, path: path, edge: edge, counts: counts, module_qn: module_qn) }) Absent => fold(node.children, init: index, f: fn(acc, edge) { - symbol_index_fill_containment_edge(index: acc, path: path, edge: edge) + symbol_index_fill_containment_edge(index: acc, path: path, edge: edge, counts: counts, module_qn: module_qn) }) } TypeNode { connective: Disj } => @@ -268,11 +296,33 @@ fn symbol_index_fill_containment_node( qualified_path: variant_path, kind: VariantArmPayload ) - symbol_index_fill_containment_node( - index: with_variant, - path: variant_path, - node: edge.target - ) + if disj_variant_count_of(counts: counts, sym: sym) == 1 { + let alias_path = qualified_name_snoc(qn: module_qn, segment: sym) + symbol_index_fill_containment_node( + index: symbol_index_mark_declared_payload( + index: symbol_index_bind_at( + index: with_variant, + binding_path: alias_path, + declaring_path: variant_path, + resolved: edge.target + ), + qualified_path: alias_path, + kind: VariantArmPayload + ), + path: variant_path, + node: edge.target, + counts: counts, + module_qn: module_qn + ) + } else { + symbol_index_fill_containment_node( + index: with_variant, + path: variant_path, + node: edge.target, + counts: counts, + module_qn: module_qn + ) + } Positional => acc } }) @@ -282,26 +332,26 @@ fn symbol_index_fill_containment_node( match domain.kind { TypeNode { connective: Conj } => fold(domain.children, init: index, f: fn(acc, edge) { - symbol_index_fill_containment_edge(index: acc, path: path, edge: edge) + symbol_index_fill_containment_edge(index: acc, path: path, edge: edge, counts: counts, module_qn: module_qn) }) - TypeNode { connective: Atom { identity: _ } } => symbol_index_fill_containment_node(index: index, path: path, node: domain) - TypeNode { connective: Disj } => symbol_index_fill_containment_node(index: index, path: path, node: domain) - TypeNode { connective: Arrow } => symbol_index_fill_containment_node(index: index, path: path, node: domain) - TypeNode { connective: Cardinality } => symbol_index_fill_containment_node(index: index, path: path, node: domain) - TypeNode { connective: Instantiation } => symbol_index_fill_containment_node(index: index, path: path, node: domain) - ComputationNode { behavior: _ } => symbol_index_fill_containment_node(index: index, path: path, node: domain) + TypeNode { connective: Atom { identity: _ } } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) + TypeNode { connective: Disj } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) + TypeNode { connective: Arrow } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) + TypeNode { connective: Cardinality } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) + TypeNode { connective: Instantiation } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) + ComputationNode { behavior: _ } => symbol_index_fill_containment_node(index: index, path: path, node: domain, counts: counts, module_qn: module_qn) } FirstPositionalAbsent => index } - TypeNode { connective: Atom { identity: _ } } => symbol_index_fill_containment_children(index: index, path: path, node: node) - TypeNode { connective: Cardinality } => symbol_index_fill_containment_children(index: index, path: path, node: node) - TypeNode { connective: Instantiation } => symbol_index_fill_containment_children(index: index, path: path, node: node) - ComputationNode { behavior: _ } => symbol_index_fill_containment_children(index: index, path: path, node: node) + TypeNode { connective: Atom { identity: _ } } => symbol_index_fill_containment_children(index: index, path: path, node: node, counts: counts, module_qn: module_qn) + TypeNode { connective: Cardinality } => symbol_index_fill_containment_children(index: index, path: path, node: node, counts: counts, module_qn: module_qn) + TypeNode { connective: Instantiation } => symbol_index_fill_containment_children(index: index, path: path, node: node, counts: counts, module_qn: module_qn) + ComputationNode { behavior: _ } => symbol_index_fill_containment_children(index: index, path: path, node: node, counts: counts, module_qn: module_qn) } } fn disj_variant_count_of(counts: Map, sym: Symbol) -> Int { - match v2.std.collection.map_get(counts, sym) { + match v2.std.collection.map_get_checked(counts, sym) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: n } => n Absent => 0 @@ -459,50 +509,6 @@ fn symbol_index_fill_alias_bindings( pending: symbol_index_alias_rows_of(module_qn: module_qn, root: root) ) } - -fn symbol_index_fill_unique_variant_aliases( - index: SymbolIndex, - module_qn: QualifiedName, - root: Node -) -> SymbolIndex { - let counts = disj_variant_counts_in_module(root: root) - fold( - node_subtree_nodes(root: root), - init: index, - f: fn(acc, n) { - match n.kind { - TypeNode { connective: Disj } => - fold(n.children, init: acc, f: fn(a2, e) { - match e.label { - StructuralLabel { label: _ } => a2 - Authored { name: sym } => - if disj_variant_count_of(counts: counts, sym: sym) == 1 { - symbol_index_mark_declared_payload( - index: symbol_index_insert( - index: a2, - qualified_path: qualified_name_snoc(qn: module_qn, segment: sym), - resolved: e.target - ), - qualified_path: qualified_name_snoc(qn: module_qn, segment: sym), - kind: VariantArmPayload - ) - } else { - a2 - } - Positional => a2 - } - }) - TypeNode { connective: Atom { identity: _ } } => acc - TypeNode { connective: Conj } => acc - TypeNode { connective: Arrow } => acc - TypeNode { connective: Cardinality } => acc - TypeNode { connective: Instantiation } => acc - ComputationNode { behavior: _ } => acc - } - } - ) -} - // PASS A -- WHAT THIS ROOT DECLARES. Every path here is rooted at this module's own qualified name, // so no other root can write it and the result does not depend on when this root is visited. // A RECORD IS MARKED FROM THE CARRIER, AND BY THE ARM THAT ALREADY DECIDED THE MODULE'S NAME. @@ -529,14 +535,13 @@ fn symbol_index_fill_module_declarations( symbol_index_fill_resource_marks( index: fold_list( xs: record_declarations, - empty: symbol_index_fill_unique_variant_aliases( - index: symbol_index_fill_containment_node( - index: symbol_index_mark_module_root(index: index, qualified_path: module_qn), - path: module_qn, - node: root - ), - module_qn: module_qn, - root: root + empty: symbol_index_fill_containment_node( + index: symbol_index_mark_module_root(index: index, qualified_path: module_qn), + path: module_qn, + node: root, + counts: disj_variant_counts_in_module(root: root), + module_qn: module_qn + ), cons: fn(acc, name) { symbol_index_mark_declared_payload( @@ -553,6 +558,8 @@ fn symbol_index_fill_module_declarations( } } + + // A RESOURCE IS MARKED FROM THE CARRIER, AS A RECORD IS, by the module's own name: a lowered resource // has a service's Conj shape, so the containment walk cannot tell it apart (v2.compiler.body_lowering_fold // DeclaredResource). Its path is the containment path the walk already inserted. diff --git a/src/v2/compiler/target_serialize.dag b/src/v2/compiler/target_serialize.dag index 08db8c2d549..1b820fc3cd7 100644 --- a/src/v2/compiler/target_serialize.dag +++ b/src/v2/compiler/target_serialize.dag @@ -144,7 +144,7 @@ import v2.std.compilers.target_model { } import v2.std.collection { list_at_optional, - map_get + map_get_checked } import std.optional { Absent, @@ -913,7 +913,7 @@ fn serialize_relation_row_nested_bounded( unit: Symbol, tokens: List ) -> Outcome { - match map_get(target.binding_spellings, unit) { + match map_get_checked(target.binding_spellings, unit) { Accepted { value: Present { value: unit_spelling }, diagnostics: _ } => bind_outcome( o: serialize_relation_row_tokens_bounded(remaining: remaining, target: target, lex: lex, tokens: tokens), diff --git a/src/v2/extdeps/languages/bash_orch_if.dag b/src/v2/extdeps/languages/bash_orch_if.dag index fd13ee3e2da..681aeb91104 100644 --- a/src/v2/extdeps/languages/bash_orch_if.dag +++ b/src/v2/extdeps/languages/bash_orch_if.dag @@ -11,6 +11,10 @@ import v2.std.compilers.target_model { } import v2.std.collection { Map, empty_map, map_insert } import v2.std.diagnostic { + Accepted, + Rejected, + NonEmptyDiagnostics, + diagnostics_reason_lexemes, Diagnostic, ExternalContractUnknown, Outcome, @@ -1043,28 +1047,36 @@ fn bash_nearest_ancestor_locate_assign_command(spec: NearestAncestorContaining) ) } -fn bash_nearest_ancestor_locate_cond_command(spec: NearestAncestorContaining) -> String { - match bash_path_predicate_target_model( - predicate: nearest_ancestor_while_predicate(spec: spec) - ) { +// A LOWERED LEAF'S TEXT, OR ITS LOCATED REFUSAL -- NEVER THE EMPTY STRING. Run.command is a +// String-typed seam, so a refusal cannot cross it as data; the text that crosses instead is a marker +// that is not valid shell (so the committed-workflow drift gate goes red) and names the producing +// symbol and the refusal's reasons. It used to be "", which said nothing about what refused, and +// left every consumer to remember to test for it. +fn bash_orch_if_leaf_text(producer: String, outcome: Outcome) -> String { + match outcome { Accepted { value: source, diagnostics: d } => match d { None => source.authority_source_text - Some { diagnostics: _ } => "" + Some { diagnostics: ds } => bash_orch_if_leaf_refusal(producer: producer, diagnostics: ds) } - Rejected { diagnostics: _ } => "" + Rejected { diagnostics: ds } => bash_orch_if_leaf_refusal(producer: producer, diagnostics: ds) } } +fn bash_orch_if_leaf_refusal(producer: String, diagnostics: NonEmptyDiagnostics) -> String { + concat(concat(concat("__GUNBC_PATH_INTENT_LOWERING_REFUSED__ ", producer), concat(": refused (", diagnostics_reason_lexemes(diagnostics: diagnostics))), ")") +} + +fn bash_nearest_ancestor_locate_cond_command(spec: NearestAncestorContaining) -> String { + bash_orch_if_leaf_text( + producer: "v2.extdeps.languages.bash_orch_if bash_nearest_ancestor_locate_cond_command", + outcome: bash_path_predicate_target_model(predicate: nearest_ancestor_while_predicate(spec: spec)), + ) +} + fn bash_nearest_ancestor_locate_body_command(spec: NearestAncestorContaining) -> String { - match bash_dirname_ascent_target_model( - ascent: nearest_ancestor_while_ascent(spec: spec) - ) { - Accepted { value: source, diagnostics: d } => - match d { - None => source.authority_source_text - Some { diagnostics: _ } => "" - } - Rejected { diagnostics: _ } => "" - } + bash_orch_if_leaf_text( + producer: "v2.extdeps.languages.bash_orch_if bash_nearest_ancestor_locate_body_command", + outcome: bash_dirname_ascent_target_model(ascent: nearest_ancestor_while_ascent(spec: spec)), + ) } diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index e4b4bcb6d73..af7d2a0fe38 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -66,6 +66,11 @@ import v2.std.grammar { grammar_expr_expect, grammar_expr_not_predicate, grammar_expr_refuse_on_match, + grammar_expr_infix_table, + InfixAssociativity, + InfixLeft, + InfixNonAssociative, + InfixOperatorRow, derive_grammar_relation_row, derive_grammar_relation_row_node, formal_production_for_lhs_exact, @@ -215,20 +220,8 @@ import v2.std.diagnostic { import std.algebra { Empty, list_append, list_snoc_item } import std.decl_ref { decl_ref } import std.dissolution { DissolutionCondition, retires_dissolution } -import v2.std.integer { - decimal_digits_to_magnitude, - integer_decimal_magnitude_node, - integer_decimal_magnitude_node_to_int, - integer_int_to_decimal_string, - integer_int_type_node, - integer_string_to_decimal_digits_optional, - DecimalMagnitude, - DecimalNonZero, - DecimalNonZeroMagnitude, - Int, - NonZeroD1, - NonZeroD2 -} +import v2.std.integer { decimal_digits_to_magnitude, integer_decimal_magnitude_node, integer_decimal_magnitude_node_to_int, integer_int_type_node, integer_string_to_decimal_digits_optional, DecimalMagnitude, DecimalNonZero, DecimalNonZeroMagnitude, Int, NonZeroD1, NonZeroD2 } +import std.integer { int_to_decimal_string } import v2.std.text { String } import extdeps.numeric.base16 { base16_digit_rows } import std.unicode.types { unicode_scalar } @@ -1493,11 +1486,11 @@ fn dag_grammar_let_sugar_expr() -> GrammarExpr { // // THE TRAILING `match_expr`/`if_expr`/`loop_expr` ALTERNATIVES WERE UNREACHABLE, AND ARE REMOVED. // They are reachable exactly where `binary_expr` fails and they succeed. But `binary_expr` derives -// each of them as a complete operand -- pipe -> or -> and -> equality -> comparison -> additive -> -// multiplicative -> unary -> postfix -> primary -> primary_expr_core -> match_expr / if_expr / -// loop_expr, with every operator tail a `Repeat` or `Optional` that admits zero -- so wherever one -// of them matches at a position, `binary_expr` has already matched at least that much there and the -// ordered choice never tries them. Kept, they read as a second route to block forms that no input +// each of them as a complete operand -- its operand is unary -> postfix -> primary -> +// primary_expr_core -> match_expr / if_expr / loop_expr, and an operand with no operator after it is +// a complete binary_expr (v2.std.grammar InfixTable) -- so wherever one of them matches at a +// position, `binary_expr` has already matched at least that much there and the ordered choice never +// tries them. Kept, they read as a second route to block forms that no input // can take, and as a second row emit could select backward. // // Evidence: `v2.test.parse.block_expr_as_binary_operand_parse` -- two arms red before this reorder @@ -1567,71 +1560,51 @@ fn dag_grammar_expr_expr() -> GrammarExpr { ) } -fn dag_grammar_multiplicative_op_choice() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_star), - right: dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_slash), - right: dag_grammar_terminal(token_class: ^dag_token_percent) - ) - ) -} - -fn dag_grammar_additive_op_choice() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_plus), - right: dag_grammar_terminal(token_class: ^dag_token_minus) - ) +// THE BINARY OPERATORS, ONE ROW EACH, WITH THE PRECEDENCE AS A BINDING POWER (higher binds tighter): +// pipe, then `||`, `&&`, equality, comparison, additive, multiplicative. Equality and comparison do +// not chain -- `a < b < c` refuses with NonAssociativeOperatorChain -- and every other row folds +// left. `|>` binds LOOSEST: `a + b |> f` is `(a + b) |> f` (sharp-raven-357 ruling 2026-10-05; the +// seed's tightest `|>`, dag/extdeps/languages/dag/syntax.dag dag_operators, is a seed defect under +// DESIGN section 7, a declared divergence rather than a reading v2 owes). One row per token class, so +// selecting a row is a lookup and no ordered alternative exists for the overlap roster to inspect +// (v2.compiler.parse grammar_infix_tables_well_formed refuses a class named twice). +fn dag_infix_row(token_class: Symbol, binding_power: Int, associativity: InfixAssociativity) -> InfixOperatorRow { + InfixOperatorRow { token_class: token_class, binding_power: binding_power, associativity: associativity } } -fn dag_grammar_comparison_op_choice() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_gte), - right: dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_lte), - right: dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_lt), - right: dag_grammar_terminal(token_class: ^dag_token_gt) - ) - ) - ) -} - -fn dag_grammar_equality_op_choice() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_terminal(token_class: ^dag_token_eq_eq), - right: dag_grammar_terminal(token_class: ^dag_token_neq) - ) -} - -fn dag_grammar_multiplicative_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_nonterminal(production: ^dag_production_unary_expr), - right: dag_grammar_repeat( - element: dag_grammar_sequence( - left: dag_grammar_multiplicative_op_choice(), - right: dag_grammar_nonterminal(production: ^dag_production_unary_expr) - ) - ) - ) +fn dag_infix_operators() -> List { + [ + dag_infix_row(token_class: ^dag_token_pipe_gt, binding_power: 1, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_or_or, binding_power: 2, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_and_and, binding_power: 3, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_eq_eq, binding_power: 4, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_neq, binding_power: 4, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_gte, binding_power: 5, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_lte, binding_power: 5, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_lt, binding_power: 5, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_gt, binding_power: 5, associativity: InfixNonAssociative), + dag_infix_row(token_class: ^dag_token_plus, binding_power: 6, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_minus, binding_power: 6, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_star, binding_power: 7, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_slash, binding_power: 7, associativity: InfixLeft), + dag_infix_row(token_class: ^dag_token_percent, binding_power: 7, associativity: InfixLeft) + ] } // A `-` THAT BEGINS A LINE AFTER A COMPLETE OPERAND REFUSES. `-` is both infix (subtraction) and // prefix (negation), and this grammar carries no statement terminator, so `x` then a line `-1` could // be `x - 1` or two statements; neither reading is safe to pick silently. The seed refuses it // (v1.compiler.parse is_ambiguous_prefix_infix_newline_boundary, whose dual-role set is exactly -// `-`), and so does this row: where a `-` preceded by a line break (v2.std.grammar AfterLineBreak, -// read from the stream's layout) stands at an additive continuation, the parse refuses at the `-` -// with NewlineBeforeDualRoleOperator. A same-line `-` is subtraction. Every expression reaches this -// continuation, so the rule holds for every statement form -- including a block-headed statement, -// which is an operand since block forms are primaries of binary_expr (#12817). -// THE REFUSAL IS A GUARD, NOT AN ALTERNATIVE. Offered as a choice alternative beside subtraction, -// both arms began with `-` and only their order kept them apart -- the overlap the zero-ambiguity -// ruling (2026-10-02) forbids, inlined once per expansion of this helper. As the not-predicate of a -// refusing match it sits in SEQUENCE before the operator: it consumes nothing, it raises the -// refusal where the line-break `-` stands (a refusal survives the predicate, v2.compiler.parse -// parse_expr_not_predicate), and elsewhere it admits, so the continuation is one route and no -// choice remains here to overlap. +// `-`), and so does this table: it is the continuation guard of dag_grammar_binary_expr_expr, run at +// every continuation point after a complete operand, so where a `-` preceded by a line break +// (v2.std.grammar AfterLineBreak, read from the stream's layout) stands there, the parse refuses at +// the `-` with NewlineBeforeDualRoleOperator. A same-line `-` is subtraction. Every expression reaches +// a continuation point, so the rule holds for every statement form -- including a block-headed +// statement, which is an operand since block forms are primaries of binary_expr (#12817). +// THE REFUSAL IS A GUARD, NOT AN ALTERNATIVE. As the not-predicate of a refusing match it consumes +// nothing, it raises the refusal where the line-break `-` stands (a refusal survives the predicate, +// v2.compiler.parse parse_expr_not_predicate), and elsewhere it admits, so no choice exists here to +// overlap (zero-ambiguity ruling 2026-10-02). fn dag_grammar_additive_newline_minus_guard() -> GrammarExpr { grammar_expr_not_predicate( element: grammar_expr_refuse_on_match( @@ -1641,83 +1614,15 @@ fn dag_grammar_additive_newline_minus_guard() -> GrammarExpr { ) } -fn dag_grammar_additive_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_multiplicative_expr_helper(), - right: dag_grammar_repeat( - element: dag_grammar_sequence( - left: dag_grammar_additive_newline_minus_guard(), - right: dag_grammar_sequence( - left: dag_grammar_additive_op_choice(), - right: dag_grammar_multiplicative_expr_helper() - ) - ) - ) - ) -} - -fn dag_grammar_comparison_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_additive_expr_helper(), - right: dag_grammar_optional( - element: dag_grammar_sequence( - left: dag_grammar_comparison_op_choice(), - right: dag_grammar_additive_expr_helper() - ) - ) - ) -} - -fn dag_grammar_equality_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_comparison_expr_helper(), - right: dag_grammar_optional( - element: dag_grammar_sequence( - left: dag_grammar_equality_op_choice(), - right: dag_grammar_comparison_expr_helper() - ) - ) - ) -} - -fn dag_grammar_and_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_equality_expr_helper(), - right: dag_grammar_repeat( - element: dag_grammar_sequence( - left: dag_grammar_terminal(token_class: ^dag_token_and_and), - right: dag_grammar_equality_expr_helper() - ) - ) - ) -} - -fn dag_grammar_or_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_and_expr_helper(), - right: dag_grammar_repeat( - element: dag_grammar_sequence( - left: dag_grammar_terminal(token_class: ^dag_token_or_or), - right: dag_grammar_and_expr_helper() - ) - ) - ) -} - -fn dag_grammar_pipe_expr_helper() -> GrammarExpr { - dag_grammar_sequence( - left: dag_grammar_or_expr_helper(), - right: dag_grammar_repeat( - element: dag_grammar_sequence( - left: dag_grammar_terminal(token_class: ^dag_token_pipe_gt), - right: dag_grammar_or_expr_helper() - ) - ) - ) -} - +// ONE TREE NODE PER OPERATOR APPLICATION (v2.std.grammar InfixTable; +// docs/plans/precedence-climbing-tree-contract.md). An operand with no operator after it is captured +// as itself; `a + b * c` is one application of `+` whose right operand is one application of `*`. fn dag_grammar_binary_expr_expr() -> GrammarExpr { - dag_grammar_pipe_expr_helper() + grammar_expr_infix_table( + operand: dag_grammar_nonterminal(production: ^dag_production_unary_expr), + continuation_guard: dag_grammar_additive_newline_minus_guard(), + operators: dag_infix_operators() + ) } fn dag_grammar_unary_expr_expr() -> GrammarExpr { @@ -2481,9 +2386,11 @@ fn dag_grammar_data_decl_expr() -> GrammarExpr { // A trailing comma before `]` is admitted, as dag_grammar_comma_list_expr admits it and as v1 reads // the list; without it every multi-line caller list written with one refused its whole module. // `admit_callers` is matched as an exact-word terminal and stays an ordinary identifier everywhere -// else. The clause is consumed and then DISCARDED when the fn lowers to an Arrow: nothing on the -// native route checks caller admission, and the stage that does adds its Arrow carrier together -// with that consumer (gunbc.rung_drop admit_callers_discarded_on_the_native_route). +// else. The clause is consumed when the fn lowers to an Arrow: the lowering mints the authored +// roster onto the Arrow under the AdmitCallersEdge core marker, and v2.compiler.infer +// infer_caller_admission_diags judges every call against it (a caller outside the list refuses, +// never admits). The former drop gunbc.rung_drop.admit_callers_discarded_on_the_native_route is +// retired by that carrier-plus-consumer restoration. // `uses net: Network, fs: Filesystem` after a fn's return clause declares the resources the fn // requires, as v1 02_parse parse_uses_clause reads it: one or more comma-separated entries, each a // name, a type and an optional parenthesised config. `uses` is matched as an exact-word terminal and @@ -3113,28 +3020,67 @@ fn dag_grammar_resource_decl_expr() -> GrammarExpr { ) } -fn dag_grammar_top_level_item_expr() -> GrammarExpr { - dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_import_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_alias_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_type_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_data_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_fn_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_test_fn_decl), - right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_service_decl), - right: dag_grammar_nonterminal(production: ^dag_production_resource_decl) - ) - ) - ) - ) - ) +// THE TOP-LEVEL DECLARATION ROWS: one list is the authority for which productions inhabit a +// module as items, for the choice the module grammar repeats, and for which emitted identities +// are declaration members rather than record binders. +fn dag_grammar_top_level_item_productions() -> List { + [ + dag_grammar_production( + name: ^dag_production_import_decl, + expression: dag_grammar_import_decl_expr(), + emitted: ^dag_surface_import_decl + ), + dag_grammar_production( + name: ^dag_production_alias_decl, + expression: dag_grammar_alias_decl_expr(), + emitted: ^dag_surface_alias_decl + ), + dag_grammar_production( + name: ^dag_production_type_decl, + expression: dag_grammar_type_decl_expr(), + emitted: ^dag_surface_type_decl + ), + dag_grammar_production( + name: ^dag_production_data_decl, + expression: dag_grammar_data_decl_expr(), + emitted: ^dag_surface_data_decl + ), + dag_grammar_production( + name: ^dag_production_fn_decl, + expression: dag_grammar_fn_decl_expr(), + emitted: ^dag_surface_fn_decl + ), + dag_grammar_production( + name: ^dag_production_test_fn_decl, + expression: dag_grammar_test_fn_decl_expr(), + emitted: ^dag_surface_test_fn_decl + ), + dag_grammar_production( + name: ^dag_production_service_decl, + expression: dag_grammar_service_decl_expr(), + emitted: ^dag_surface_service_decl + ), + dag_grammar_production( + name: ^dag_production_resource_decl, + expression: dag_grammar_resource_decl_expr(), + emitted: ^dag_surface_resource_decl ) + ] +} + +fn dag_grammar_choice_of_production_names(names: List) -> GrammarExpr { + match names { + Cons { head: first, tail: rest } => + fold(rest, init: dag_grammar_nonterminal(production: first), f: fn(acc, name) { + dag_grammar_choice(left: acc, right: dag_grammar_nonterminal(production: name)) + }) + Empty => dag_grammar_nonterminal(production: ^dag_production_top_level_item) + } +} + +fn dag_grammar_top_level_item_expr() -> GrammarExpr { + dag_grammar_choice_of_production_names( + names: list_map(xs: dag_grammar_top_level_item_productions(), f: fn(p) { p.name }) ) } @@ -3368,46 +3314,6 @@ fn dag_grammar_root() -> GrammarRoot { expression: dag_grammar_top_level_item_expr(), emitted: ^dag_surface_top_level_item ) - let import_decl = dag_grammar_production( - name: ^dag_production_import_decl, - expression: dag_grammar_import_decl_expr(), - emitted: ^dag_surface_import_decl - ) - let alias_decl = dag_grammar_production( - name: ^dag_production_alias_decl, - expression: dag_grammar_alias_decl_expr(), - emitted: ^dag_surface_alias_decl - ) - let type_decl = dag_grammar_production( - name: ^dag_production_type_decl, - expression: dag_grammar_type_decl_expr(), - emitted: ^dag_surface_type_decl - ) - let data_decl = dag_grammar_production( - name: ^dag_production_data_decl, - expression: dag_grammar_data_decl_expr(), - emitted: ^dag_surface_data_decl - ) - let fn_decl = dag_grammar_production( - name: ^dag_production_fn_decl, - expression: dag_grammar_fn_decl_expr(), - emitted: ^dag_surface_fn_decl - ) - let test_fn_decl = dag_grammar_production( - name: ^dag_production_test_fn_decl, - expression: dag_grammar_test_fn_decl_expr(), - emitted: ^dag_surface_test_fn_decl - ) - let service_decl = dag_grammar_production( - name: ^dag_production_service_decl, - expression: dag_grammar_service_decl_expr(), - emitted: ^dag_surface_service_decl - ) - let resource_decl = dag_grammar_production( - name: ^dag_production_resource_decl, - expression: dag_grammar_resource_decl_expr(), - emitted: ^dag_surface_resource_decl - ) let resource_body_entry = dag_grammar_production( name: ^dag_production_resource_body_entry, expression: dag_grammar_resource_body_entry_expr(), @@ -3823,19 +3729,12 @@ fn dag_grammar_root() -> GrammarRoot { ) GrammarRoot { start: ^dag_production_module, - productions: [ - module_prod, - module_header, - top_level_item, - import_decl, - alias_decl, - type_decl, - data_decl, - fn_decl, - test_fn_decl, - service_decl, + productions: list_append( + left: [module_prod, module_header, top_level_item], + right: list_append( + left: dag_grammar_top_level_item_productions(), + right: [ service_body_entry, - resource_decl, resource_body_entry, resource_kind, resource_mode, @@ -3917,7 +3816,9 @@ fn dag_grammar_root() -> GrammarRoot { if_expr, fn_literal, arrow_lambda - ], + ] + ) + ), sync_tokens: dag_sync_tokens() } } @@ -4930,10 +4831,9 @@ fn dag_node_is_float_literal_atom(node: Node) -> Bool { // Single authority for eval literal-vs-binding dispatch (v2_eval_is_literal_shaped_node projects // this). Fixture pick literals admit both token_class (^dag_pick_lit_*) and branded binding -// (^dag_binding_pick_lit_*) rows — same ingest surface, two spellings. Complement bool literals -// (^dag_complement_lit_*) are logic-ingest tokens used as scrutinee/body literals in complement -// fixtures. dissolve-on: general body producer lands grammar-owned literal classification and this -// roster folds into it. +// (^dag_binding_pick_lit_*) rows — same ingest surface, two spellings. Complement/meet/join +// fixtures use the same true/false atoms as match (dag_token_kw_true/false). dissolve-on: general +// body producer lands grammar-owned literal classification and this roster folds into it. fn dag_symbol_is_fixture_pick_literal(id: Symbol) -> Bool { (id == ^dag_pick_lit_one) @@ -4943,7 +4843,7 @@ fn dag_symbol_is_fixture_pick_literal(id: Symbol) -> Bool { } fn dag_symbol_is_complement_bool_literal(id: Symbol) -> Bool { - (id == ^dag_complement_lit_true) || (id == ^dag_complement_lit_false) + (id == ^dag_token_kw_true) || (id == ^dag_token_kw_false) } fn dag_symbol_is_fixture_record_literal_atom(id: Symbol) -> Bool { @@ -5924,11 +5824,11 @@ fn dag_complement_match_body( dag_match_body_from_arms( scrutinee: scrutinee, true_arm: dag_match_arm_node( - pattern_lit: ^dag_complement_lit_true, + pattern_lit: ^dag_token_kw_true, body_lit: true_body_lit ), false_arm: dag_match_arm_node( - pattern_lit: ^dag_complement_lit_false, + pattern_lit: ^dag_token_kw_false, body_lit: false_body_lit ) ) @@ -5951,16 +5851,16 @@ fn dag_complement_arrow_domain_node() -> Node { fn dag_complement_body_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), - true_body_lit: ^dag_complement_lit_false, - false_body_lit: ^dag_complement_lit_true + true_body_lit: ^dag_token_kw_false, + false_body_lit: ^dag_token_kw_true ) } fn dag_complement_body_swapped_arms_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), - true_body_lit: ^dag_complement_lit_true, - false_body_lit: ^dag_complement_lit_false + true_body_lit: ^dag_token_kw_true, + false_body_lit: ^dag_token_kw_false ) } @@ -5982,9 +5882,9 @@ fn dag_complement_arrow_with_body_swapped_arms_node() -> Node { fn dag_complement_eval_subgraph_node() -> Node { dag_complement_match_body( - scrutinee: dag_type_atom_node(identity: ^dag_complement_lit_true), - true_body_lit: ^dag_complement_lit_false, - false_body_lit: ^dag_complement_lit_true + scrutinee: dag_type_atom_node(identity: ^dag_token_kw_true), + true_body_lit: ^dag_token_kw_false, + false_body_lit: ^dag_token_kw_true ) } @@ -6016,14 +5916,14 @@ fn dag_meet_body_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), true_body_lit: ^dag_binding_param_b, - false_body_lit: ^dag_complement_lit_false + false_body_lit: ^dag_token_kw_false ) } fn dag_meet_body_swapped_arms_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), - true_body_lit: ^dag_complement_lit_false, + true_body_lit: ^dag_token_kw_false, false_body_lit: ^dag_binding_param_b ) } @@ -6031,7 +5931,7 @@ fn dag_meet_body_swapped_arms_node() -> Node { fn dag_join_body_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), - true_body_lit: ^dag_complement_lit_true, + true_body_lit: ^dag_token_kw_true, false_body_lit: ^dag_binding_param_b ) } @@ -6040,23 +5940,23 @@ fn dag_join_body_swapped_arms_node() -> Node { dag_complement_match_body( scrutinee: dag_complement_param_scrutinee_node(), true_body_lit: ^dag_binding_param_b, - false_body_lit: ^dag_complement_lit_true + false_body_lit: ^dag_token_kw_true ) } fn dag_meet_eval_subgraph_node() -> Node { dag_complement_match_body( - scrutinee: dag_type_atom_node(identity: ^dag_complement_lit_true), - true_body_lit: ^dag_complement_lit_true, - false_body_lit: ^dag_complement_lit_false + scrutinee: dag_type_atom_node(identity: ^dag_token_kw_true), + true_body_lit: ^dag_token_kw_true, + false_body_lit: ^dag_token_kw_false ) } fn dag_join_eval_subgraph_node() -> Node { dag_complement_match_body( - scrutinee: dag_type_atom_node(identity: ^dag_complement_lit_true), - true_body_lit: ^dag_complement_lit_true, - false_body_lit: ^dag_complement_lit_false + scrutinee: dag_type_atom_node(identity: ^dag_token_kw_true), + true_body_lit: ^dag_token_kw_true, + false_body_lit: ^dag_token_kw_false ) } @@ -6774,6 +6674,36 @@ fn parse_production_emitted_identity_optional(node: Node) -> Optional { } } +fn dag_surface_identity_is_top_level_item(id: Symbol) -> Bool { + fold_list( + xs: dag_grammar_top_level_item_productions(), + empty: false, + cons: fn(acc, production) { + acc || match node_atom_identity_optional(node: production.emitted) { + Present { value: emitted } => emitted == id + Absent => false + } + } + ) +} + +// Parse identities that inhabit a module as Authored members and are not record binders: +// the module surface and graft-body stamp as the only explicit extras, plus every emitted +// identity of dag_grammar_top_level_item_productions. Identity only — a missing stamp is +// false, never a Conj-shape guess (DESIGN §5: refuse the member, never widen to the container). +fn dag_surface_identity_is_module_member(id: Symbol) -> Bool { + (id == ^dag_surface_module) + || (id == ^namespace_graft_module_body) + || dag_surface_identity_is_top_level_item(id: id) +} + +fn dag_node_is_module_member_surface(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => dag_surface_identity_is_module_member(id: id) + Absent => false + } +} + fn dag_node_is_fn_decl(node: Node) -> Bool { match parse_production_emitted_identity_optional(node: node) { Present { value: id } => id == ^dag_surface_fn_decl @@ -7135,18 +7065,17 @@ fn parse_qn_segment_with(sym_node: Node, root: Node, seg: fn(Node, Symbol) -> } } -fn parse_qn_repeat_suffix_ident_with(sequence_capture: Node, root: Node, seg: fn(Node, Symbol) -> T) -> Outcome { - match find_named_child( - root: sequence_capture, - name: ^grammar_sequence_right_node_projection - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: ident_node, diagnostics: _ } => - parse_qn_segment_with(sym_node: ident_node, root: root, seg: seg) - } -} - -fn parse_qn_repeat_suffixes_with(repeat_capture: Node, root: Node, seg: fn(Node, Symbol) -> T) -> Outcome> { +// ONE REPEAT-CAPTURE SPINE, by the rule above: one walker for the encoding, per-item readers on +// top of it. THE ENCODING: a repeat capture is either an empty conj (no items) or a conj whose +// left projection is the element sequence and whose right projection is the next capture, and each +// element sequence's right projection is the item. The spine refuses when a projection the +// encoding promises is missing -- a malformed spine is not an empty list -- and the item reader +// owns every refusal about what an item is. +fn parse_repeat_capture_items_with( + repeat_capture: Node, + root: Node, + item: fn(Node, Node) -> Outcome +) -> Outcome> { if is_empty_conj_root(n: repeat_capture) { outcome_accepted(value: Empty) } else { @@ -7156,25 +7085,32 @@ fn parse_qn_repeat_suffixes_with(repeat_capture: Node, root: Node, seg: fn(No ) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } Accepted { value: head_sequence, diagnostics: _ } => - match parse_qn_repeat_suffix_ident_with(sequence_capture: head_sequence, root: root, seg: seg) { + match find_named_child( + root: head_sequence, + name: ^grammar_sequence_right_node_projection + ) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: item, diagnostics: _ } => - match find_named_child( - root: repeat_capture, - name: ^grammar_sequence_right_node_projection - ) { + Accepted { value: item_node, diagnostics: _ } => + match item(item_node, root) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tail, diagnostics: _ } => - match parse_qn_repeat_suffixes_with(repeat_capture: tail, root: root, seg: seg) { + Accepted { value: item_value, diagnostics: _ } => + match find_named_child( + root: repeat_capture, + name: ^grammar_sequence_right_node_projection + ) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: suffixes, diagnostics: tail_diag } => - outcome_with_diagnostics( - value: list_append( - left: Cons { head: item, tail: Empty }, - right: suffixes - ), - diagnostics: tail_diag - ) + Accepted { value: tail, diagnostics: _ } => + match parse_repeat_capture_items_with(repeat_capture: tail, root: root, item: item) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: suffixes, diagnostics: tail_diag } => + outcome_with_diagnostics( + value: list_append( + left: Cons { head: item_value, tail: Empty }, + right: suffixes + ), + diagnostics: tail_diag + ) + } } } } @@ -7182,6 +7118,14 @@ fn parse_qn_repeat_suffixes_with(repeat_capture: Node, root: Node, seg: fn(No } } +fn parse_qn_repeat_suffixes_with(repeat_capture: Node, root: Node, seg: fn(Node, Symbol) -> T) -> Outcome> { + parse_repeat_capture_items_with( + repeat_capture: repeat_capture, + root: root, + item: fn(n, r) { parse_qn_segment_with(sym_node: n, root: r, seg: seg) } + ) +} + fn parse_qualified_name_segments_from_capture_with(qn_capture: Node, root: Node, seg: fn(Node, Symbol) -> T) -> Outcome> { match find_named_child(root: qn_capture, name: ^grammar_sequence_left_node_projection) { Rejected { diagnostics: diags } => Rejected { diagnostics: diags } @@ -7610,6 +7554,60 @@ fn namespace_graft_node_is_conj(node: Node) -> Bool { // would re-create the one-Named-Conj level the fill misreads. data namespace_graft_module_body_identity: Symbol = ^namespace_graft_module_body +fn dag_identity_is_module_surface(id: Symbol) -> Bool { + (id == ^dag_surface_module) || (id == namespace_graft_module_body_identity) +} + +// A MODULE SURFACE is identified by what the node IS: parse/emit production identity +// dag_surface_module, the graft body's producer stamp, or a synthetic Atom whose +// identity is dag_surface_module. A header child edge is not identity (review 77909). +fn dag_node_is_module_surface(node: Node) -> Bool { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => dag_identity_is_module_surface(id: id) + Absent => + match node_atom_identity_optional(node: node) { + Present { value: id } => dag_identity_is_module_surface(id: id) + Absent => false + } + } +} + +// THE RECORD-FIELD CARRIER PREDICATE the unit-modeling and lifecycle-carrier lenses share. +// A Conj is a record-field carrier unless THIS NODE is a module surface. A sibling that +// happens to be a module surface does not opt the whole Conj out (DESIGN §5: refuse the +// member, never widen to the container). Other connectives are not record payloads; Disj +// arm payloads are Conjs and are judged when those Conjs are visited. +fn dag_node_is_record_field_carrier(node: Node) -> Bool { + namespace_graft_node_is_conj(node: node) && !dag_node_is_module_surface(node: node) +} + +fn dag_authored_name_is_parse_production_projection(name: Symbol) -> Bool { + (name == ^grammar_production_identity_node_projection) || + (name == ^grammar_production_captured_node_projection) +} + +// Stamp edges on a MODULE SURFACE only. Same spelling on an ordinary record is a user field +// and must still be judged (DESIGN §5: refuse, never widen). +fn dag_edge_is_module_surface_parse_projection(carrier: Node, name: Symbol) -> Bool { + dag_node_is_module_surface(node: carrier) && + dag_authored_name_is_parse_production_projection(name: name) +} + +// Per-edge skip by identity only (DESIGN §5: refuse, never widen). A non-binder record +// field that is an Atom or Disj must still be member_not_a_binder. fn-decl is the +// production identity dag_surface_fn_decl (unchanged meaning for other callers). +// test-fn identity lives only here, not in dag_node_is_fn_decl. +fn dag_node_is_namespace_member_target(node: Node) -> Bool { + if dag_node_is_module_surface(node: node) || dag_node_is_fn_decl(node: node) { + true + } else { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => id == ^dag_surface_test_fn_decl + Absent => false + } + } +} + // The one constructor of the marker node: emitted by namespace_graft_build_body_conj. // It is a surface identity atom, not a type denotation, so infer grounds it with the // grammar authority's surface-production type (dag_grammar_production_member_declared_type) @@ -7863,7 +7861,7 @@ fn dag_emit_class_terminal_spelling(lex: LexRules, token_class: Symbol, captured Present { value: _ } => bind_outcome( o: dag_int_literal_magnitude_int_from_node(node: captured), - f: fn(v) { outcome_accepted(value: integer_int_to_decimal_string(value: v)) } + f: fn(v) { outcome_accepted(value: int_to_decimal_string(value: v)) } ) } } else if token_class == ^dag_token_string_literal { diff --git a/src/v2/extdeps/languages/rust.dag b/src/v2/extdeps/languages/rust.dag index 231a33e6917..ab47b50ee95 100644 --- a/src/v2/extdeps/languages/rust.dag +++ b/src/v2/extdeps/languages/rust.dag @@ -315,6 +315,7 @@ import v2.std.node { TypeNode, Instantiation, } +import v2.std.node_query { binder_edge } import v2.std.logic { bool_node } import std.algebra { Empty, @@ -4962,8 +4963,14 @@ fn rust_sg2_conj_source_type_node() -> Node { Node { kind: TypeNode { connective: Conj }, children: [ - rust_named_edge(name: ^rust_binding_sg2_x, target: rust_sg2_atom_node(binding: ^rust_binding_sg2_x)), - rust_named_edge(name: ^rust_binding_sg2_y, target: rust_sg2_atom_node(binding: ^rust_binding_sg2_y)) + binder_edge( + name: ^rust_binding_sg2_x, + type_node: rust_sg2_atom_node(binding: ^rust_binding_sg2_x) + ), + binder_edge( + name: ^rust_binding_sg2_y, + type_node: rust_sg2_atom_node(binding: ^rust_binding_sg2_y) + ) ], occurrence_id: OccurrenceSynthetic } diff --git a/src/v2/extdeps/languages/rust_test_fixtures.dag b/src/v2/extdeps/languages/rust_test_fixtures.dag index d39a8ce8804..85c0d8b1909 100644 --- a/src/v2/extdeps/languages/rust_test_fixtures.dag +++ b/src/v2/extdeps/languages/rust_test_fixtures.dag @@ -8,6 +8,7 @@ import std.algebra { Cons, Empty } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.extdeps.languages.dag { dag_bind_arrow_with_body_fixture } +import v2.std.arrow_signature { declared_signature } import v2.extdeps.languages.rust { rust_add_declared_inhabitants_root, @@ -1522,13 +1523,15 @@ fn rust_variant_construct_translation_rules_node() -> Node { } fn rust_variant_construct_binding_spellings() -> Map { - let m0 = map_insert(empty_map(), ^dag_binding_fn_one_or_two, "one_or_two") - let m1 = map_insert(m0, ^dag_binding_rec_lit_nine, "9") - let m2 = map_insert(m1, ^dag_binding_pick_lit_one, "1") - let m3 = map_insert(m2, ^dag_binding_pick_lit_two, "2") - let m4 = map_insert(m3, ^dag_binding_rec_v, "v") - let m5 = map_insert(m4, ^dag_binding_type_variant_holds, "Holds") - map_insert(m5, ^dag_binding_field_variant_value, "value") + let m0 = rust_binding_spellings() + let m1 = map_insert(m0, ^dag_binding_fn_one_or_two, "one_or_two") + let m2 = map_insert(m1, ^dag_binding_rec_lit_nine, "9") + let m3 = map_insert(m2, ^dag_binding_pick_lit_one, "1") + let m4 = map_insert(m3, ^dag_binding_pick_lit_two, "2") + let m5 = map_insert(m4, ^dag_binding_rec_v, "v") + let m6 = map_insert(m5, ^dag_binding_type_variant_holds, "Holds") + let m7 = map_insert(m6, ^dag_binding_field_variant_value, "value") + map_insert(m7, ^dag_binding_type_variant_true, "True") } fn rust_variant_construct_additional_lex_rules() -> List { @@ -1542,8 +1545,11 @@ fn rust_variant_construct_additional_lex_rules() -> List { fn rust_variant_construct_lex_rules() -> LexRuleSet { LexRuleSet { rules: list_append( - left: rust_fn_add_lex_rules().rules, - right: rust_variant_construct_additional_lex_rules() + left: list_append( + left: rust_fn_add_lex_rules().rules, + right: rust_variant_construct_additional_lex_rules() + ), + right: rust_value_expression_family_lex_rules() ), modes: [] } @@ -1667,9 +1673,9 @@ fn rust_variant_construct_target_model_staging() -> TargetModel { } } -data rust_logic_complement_source_text: String = "fn complement(a: Bool) -> Bool { match (a) { True => False, False => True } }" +data rust_logic_complement_source_text: String = "fn complement(a: bool) -> bool { match (a) { true => false, false => true } }" -data rust_logic_complement_swapped_arms_source_text: String = "fn complement(a: Bool) -> Bool { match (a) { True => True, False => False } }" +data rust_logic_complement_swapped_arms_source_text: String = "fn complement(a: bool) -> bool { match (a) { true => true, false => false } }" fn rust_logic_complement_signature_with_open() -> List { [ @@ -1696,16 +1702,16 @@ fn rust_logic_complement_body_tokens() -> List { [ FixedToken { token_class: ^rust_token_unwired_match }, FixedToken { token_class: ^rust_token_lparen }, - FixedToken { token_class: ^dag_binding_param_a }, + BoundToken { token_class: ^rust_token_ident, binding: ^dag_binding_param_a }, FixedToken { token_class: ^rust_token_rparen }, FixedToken { token_class: ^rust_token_lbrace }, - FixedToken { token_class: ^dag_complement_lit_true }, + FixedToken { token_class: ^dag_token_kw_true }, FixedToken { token_class: ^rust_token_unwired_fat_arrow }, - FixedToken { token_class: ^dag_complement_lit_false }, + FixedToken { token_class: ^dag_token_kw_false }, FixedToken { token_class: ^rust_token_comma }, - FixedToken { token_class: ^dag_complement_lit_false }, + FixedToken { token_class: ^dag_token_kw_false }, FixedToken { token_class: ^rust_token_unwired_fat_arrow }, - FixedToken { token_class: ^dag_complement_lit_true }, + FixedToken { token_class: ^dag_token_kw_true }, FixedToken { token_class: ^rust_token_rbrace } ] } @@ -1795,26 +1801,29 @@ fn rust_logic_complement_translation_rules_node() -> Node { } fn rust_logic_complement_binding_spellings() -> Map { - let m0 = map_insert(empty_map(), ^dag_binding_fn_complement, "complement") - map_insert(m0, ^dag_binding_param_a, "a") + let m0 = rust_binding_spellings() + let m1 = map_insert(m0, ^dag_binding_fn_complement, "complement") + map_insert(m1, ^dag_binding_param_a, "a") } fn rust_logic_complement_additional_lex_rules() -> List { [ rust_lex_rule(token_class: ^rust_token_unwired_match, text: "match "), rust_lex_rule(token_class: ^rust_token_unwired_fat_arrow, text: " => "), - rust_lex_rule(token_class: ^rust_token_ty_bool, text: "Bool"), - rust_lex_rule(token_class: ^dag_binding_param_a, text: "a"), - rust_lex_rule(token_class: ^dag_complement_lit_true, text: "True"), - rust_lex_rule(token_class: ^dag_complement_lit_false, text: "False") + rust_lex_rule(token_class: ^rust_token_ty_bool, text: "bool"), + rust_lex_rule(token_class: ^dag_token_kw_true, text: "true"), + rust_lex_rule(token_class: ^dag_token_kw_false, text: "false") ] } fn rust_logic_complement_lex_rules() -> LexRuleSet { LexRuleSet { rules: list_append( - left: rust_fn_add_lex_rules().rules, - right: rust_logic_complement_additional_lex_rules() + left: list_append( + left: rust_fn_add_lex_rules().rules, + right: rust_logic_complement_additional_lex_rules() + ), + right: rust_value_expression_family_lex_rules() ), modes: [] } @@ -1883,7 +1892,7 @@ fn rust_logic_complement_target_model_bundle_core() -> Node { } } -data rust_logic_complement_emit_host_main_suffix: String = "\n\nuse Bool::*;\n\nenum Bool { True, False }\n\nfn main() { let v = complement(True); let d: i32 = match v { False => 0, True => 1 }; let bytes = [0u8, (d & 0xff) as u8, ((d >> 8) & 0xff) as u8, ((d >> 16) & 0xff) as u8, ((d >> 24) & 0xff) as u8]; let _ = std::io::Write::write_all(&mut std::io::stdout(), &bytes); }\n" +data rust_logic_complement_emit_host_main_suffix: String = "\n\nfn main() { let v = complement(true); let d: i32 = match v { false => 0, true => 1 }; let bytes = [0u8, (d & 0xff) as u8, ((d >> 8) & 0xff) as u8, ((d >> 16) & 0xff) as u8, ((d >> 24) & 0xff) as u8]; let _ = std::io::Write::write_all(&mut std::io::stdout(), &bytes); }\n" fn rust_logic_complement_host_transport_descriptor() -> HostTransportDescriptor { HostTransportDescriptor { @@ -2031,11 +2040,10 @@ fn rust_bool_fn_additional_lex_rules() -> List { [ rust_lex_rule(token_class: ^rust_token_unwired_match, text: "match "), rust_lex_rule(token_class: ^rust_token_unwired_fat_arrow, text: " => "), - rust_lex_rule(token_class: ^rust_token_ty_bool, text: "Bool"), - rust_lex_rule(token_class: ^dag_binding_param_a, text: "a"), - rust_lex_rule(token_class: ^dag_binding_param_b, text: "b"), - rust_lex_rule(token_class: ^dag_complement_lit_true, text: "True"), - rust_lex_rule(token_class: ^dag_complement_lit_false, text: "False") + rust_lex_rule(token_class: ^rust_token_ty_bool, text: "bool"), + rust_lex_rule(token_class: ^dag_token_kw_true, text: "true"), + rust_lex_rule(token_class: ^dag_token_kw_false, text: "false"), + rust_lex_rule(token_class: ^dag_binding_param_b, text: "b") ] } @@ -2043,8 +2051,11 @@ fn rust_bool_fn_lex() -> LexRules { ModeledLexRules { root: LexRuleSet { rules: list_append( - left: rust_fn_add_lex_rules().rules, - right: rust_bool_fn_additional_lex_rules() + left: list_append( + left: rust_fn_add_lex_rules().rules, + right: rust_bool_fn_additional_lex_rules() + ), + right: rust_value_expression_family_lex_rules() ), modes: [] } @@ -2172,9 +2183,10 @@ fn rust_bool_fn_target_model( signature_with_open: signature_with_open, body_tokens: body_tokens ) - let m0 = map_insert(empty_map(), fn_binding, fn_spelling) - let m1 = map_insert(m0, ^dag_binding_param_a, "a") - let binding_spellings = map_insert(m1, ^dag_binding_param_b, "b") + let m0 = rust_binding_spellings() + let m1 = map_insert(m0, fn_binding, fn_spelling) + let m2 = map_insert(m1, ^dag_binding_param_a, "a") + let binding_spellings = map_insert(m2, ^dag_binding_param_b, "b") TargetModel { produced_decl_support: ProducedDeclUnwired, bundle: Node { @@ -2222,25 +2234,25 @@ fn rust_bool_binop_body_tokens(true_body: Symbol, false_body: Symbol) -> List TargetModel { rust_bool_fn_target_model( @@ -2250,7 +2262,7 @@ fn rust_logic_meet_target_model_staging() -> TargetModel { signature_with_open: rust_bool_binop_signature_with_open(fn_binding: ^dag_binding_fn_meet), body_tokens: rust_bool_binop_body_tokens( true_body: ^dag_binding_param_b, - false_body: ^dag_complement_lit_false + false_body: ^dag_token_kw_false ), production_sym: ^rust_production_fn_logic_meet, relation_sym: ^rust_relation_rule_fn_logic_meet, @@ -2260,11 +2272,11 @@ fn rust_logic_meet_target_model_staging() -> TargetModel { ) } -data rust_logic_join_source_text: String = "fn join(a: Bool, b: Bool) -> Bool { match (a) { True => True, False => b } }" +data rust_logic_join_source_text: String = "fn join(a: bool, b: bool) -> bool { match (a) { true => true, false => b } }" -data rust_logic_join_swapped_arms_source_text: String = "fn join(a: Bool, b: Bool) -> Bool { match (a) { True => b, False => True } }" +data rust_logic_join_swapped_arms_source_text: String = "fn join(a: bool, b: bool) -> bool { match (a) { true => b, false => true } }" -data rust_logic_join_emit_host_main_suffix: String = "\n\nuse Bool::*;\n\nenum Bool { True, False }\n\nfn main() { let v = join(True, False); let d: i32 = match v { False => 0, True => 1 }; let bytes = [0u8, (d & 0xff) as u8, ((d >> 8) & 0xff) as u8, ((d >> 16) & 0xff) as u8, ((d >> 24) & 0xff) as u8]; let _ = std::io::Write::write_all(&mut std::io::stdout(), &bytes); }\n" +data rust_logic_join_emit_host_main_suffix: String = "\n\nfn main() { let v = join(true, false); let d: i32 = match v { false => 0, true => 1 }; let bytes = [0u8, (d & 0xff) as u8, ((d >> 8) & 0xff) as u8, ((d >> 16) & 0xff) as u8, ((d >> 24) & 0xff) as u8]; let _ = std::io::Write::write_all(&mut std::io::stdout(), &bytes); }\n" fn rust_logic_join_target_model_staging() -> TargetModel { rust_bool_fn_target_model( @@ -2273,7 +2285,7 @@ fn rust_logic_join_target_model_staging() -> TargetModel { source_text: rust_logic_join_source_text, signature_with_open: rust_bool_binop_signature_with_open(fn_binding: ^dag_binding_fn_join), body_tokens: rust_bool_binop_body_tokens( - true_body: ^dag_complement_lit_true, + true_body: ^dag_token_kw_true, false_body: ^dag_binding_param_b ), production_sym: ^rust_production_fn_logic_join, @@ -2284,21 +2296,21 @@ fn rust_logic_join_target_model_staging() -> TargetModel { ) } -data rust_logic_family_prelude: String = "\n\nuse Bool::*;\n\nenum Bool { True, False }" +data rust_logic_family_prelude: String = "" data rust_logic_family_member_meet: RustEmitHostFamilyMember = RustEmitHostFamilyMember { member_id: "meet", - result_expression: "match meet(True, True) { False => 0, True => 1 }" + result_expression: "match meet(true, true) { false => 0, true => 1 }" } data rust_logic_family_member_join: RustEmitHostFamilyMember = RustEmitHostFamilyMember { member_id: "join", - result_expression: "match join(True, False) { False => 0, True => 1 }" + result_expression: "match join(true, false) { false => 0, true => 1 }" } data rust_logic_family_member_complement: RustEmitHostFamilyMember = RustEmitHostFamilyMember { member_id: "complement", - result_expression: "match complement(True) { False => 0, True => 1 }" + result_expression: "match complement(true) { false => 0, true => 1 }" } fn rust_logic_family_dispatch_members() -> List { @@ -2402,7 +2414,7 @@ fn rust_logic_selected_bundle_target_model_staging() -> TargetModel { data rust_logic_family_member_meet_zero: RustEmitHostFamilyMember = RustEmitHostFamilyMember { member_id: "meet", - result_expression: "match meet(True, False) { False => 0, True => 1 }" + result_expression: "match meet(true, false) { false => 0, true => 1 }" } fn rust_logic_family_dispatch_members_meet_zero() -> List { @@ -3429,10 +3441,12 @@ fn rust_arrow_with_body_node( return_type_binding: Symbol, body: Node ) -> Node { + let domain = rust_nullary_arrow_domain_conj_node() + let signature = declared_signature(params: domain.children, source: domain) Node { kind: TypeNode { connective: Arrow }, children: [ - Edge { label: Positional, target: rust_nullary_arrow_domain_conj_node() }, + signature.domain, Edge { label: Positional, target: rust_type_atom_node(identity: return_type_binding) @@ -3441,6 +3455,7 @@ fn rust_arrow_with_body_node( label: Positional, target: rust_type_atom_node(identity: return_type_binding) }, + signature.order, Edge { label: core_edge_label(marker: ArrowBodyEdge), target: body } ], occurrence_id: OccurrenceSynthetic diff --git a/src/v2/extdeps/runtimes/v2_effect_io_pure.dag b/src/v2/extdeps/runtimes/v2_effect_io_pure.dag index a33056d75c6..a31f3441d67 100644 --- a/src/v2/extdeps/runtimes/v2_effect_io_pure.dag +++ b/src/v2/extdeps/runtimes/v2_effect_io_pure.dag @@ -9,7 +9,7 @@ import v2.std.collection { Present, empty_map, list_nth, - map_get, + map_get_checked, map_insert } import v2.std.diagnostic { @@ -202,7 +202,7 @@ fn effect_io_pure_store_lookup( locator: EffectIoResourceLocator ) -> Outcome { if effect_io_pure_backends_match(left: locator.backend, right: store.backend) { - match map_get(m: store.entries, key: locator.identity) { + match map_get_checked(m: store.entries, key: locator.identity) { Accepted { value: opt, diagnostics: d } => match opt { Present { value: payload } => diff --git a/src/v2/lens/affected_set.dag b/src/v2/lens/affected_set.dag index c6ad9e219f5..38272ddaaee 100644 --- a/src/v2/lens/affected_set.dag +++ b/src/v2/lens/affected_set.dag @@ -4,7 +4,7 @@ import std.occurrence_identity { OccurrenceSynthetic } import v2.lens.common.construction_justification { ConstructionJustification, WallAfterGrounding } import std.disposition { SingleAuthority } -import v2.std.collection { Absent, Map, Present, list_at_optional, map_get, map_insert } +import v2.std.collection { Absent, Map, Present, list_at_optional, map_get_checked, map_insert } import v2.std.algebra { TailAbsent, TailFound, contains, length, list_tail } import std.algebra { list_append, list_snoc_item } import v2.compiler.infer { InferredTree } @@ -590,7 +590,7 @@ fn mark_excluded(frontier: ReExecFrontier, key: ExclusionKey, proof: Propagation FailClosed { reason: e } => FailClosed { reason: e } ChangedSubgraphFrontier { boundary_exclusions: boundary_exclusions, decisions: decisions } => - match map_get(decisions, key) { + match map_get_checked(decisions, key) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: decision } => match decision { diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index c327036bade..3f68231473e 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -61,7 +61,8 @@ import v2.std.node { import v2.std.diagnostic { Accepted, Rejected } import v2.std.node_query { find_arrow_body_child, find_named_child } import v2.std.witness { Holds, Violates } -import v2.std.integer { Int, integer_int_to_decimal_string } +import v2.std.integer { Int } +import std.integer { int_to_decimal_string } import v2.lens.common.source_analysis_standing { LanguageGrammarRejected, LanguageGrammarUnavailable, @@ -259,8 +260,8 @@ fn census_tsv_line_for_path(path: String) -> String { concat( "\tscanned\t", concat( - integer_int_to_decimal_string(value: s), - concat("\t", integer_int_to_decimal_string(value: r)) + int_to_decimal_string(value: s), + concat("\t", int_to_decimal_string(value: r)) ) ) ) diff --git a/src/v2/lens/fact_density.dag b/src/v2/lens/fact_density.dag index 67e201b7d23..d8b273dacc3 100644 --- a/src/v2/lens/fact_density.dag +++ b/src/v2/lens/fact_density.dag @@ -2,6 +2,7 @@ module v2.lens.fact_density import std.nat { Nat, Succ, Zero } import v2.std.node { Authored, StructuralLabel } +import v2.extdeps.languages.dag { dag_node_is_module_surface } import v2.lens.common.construction_justification { ConstructionJustification } import v2.std.witness { Holds } @@ -61,10 +62,14 @@ fn connective_spec_fact(c: Connective, named_facts: Nat) -> SourceSpecReadFact { } fn carrier_spec_fact(carrier: Node) -> SourceSpecReadFact { - match carrier.kind { - ComputationNode { behavior: _ } => NotATypeCarrier - TypeNode { connective: c } => - connective_spec_fact(c: c, named_facts: named_fact_count(children: carrier.children)) + if dag_node_is_module_surface(node: carrier) { + NotATypeCarrier + } else { + match carrier.kind { + ComputationNode { behavior: _ } => NotATypeCarrier + TypeNode { connective: c } => + connective_spec_fact(c: c, named_facts: named_fact_count(children: carrier.children)) + } } } // Node-local predicate (DESIGN 6 bare-minimum-cost): carrier_spec_fact reads only the node's direct diff --git a/src/v2/lens/grounding.dag b/src/v2/lens/grounding.dag index 2a59efd6f44..ae49484c88e 100644 --- a/src/v2/lens/grounding.dag +++ b/src/v2/lens/grounding.dag @@ -10,6 +10,12 @@ import v2.lens.common.construction_justification { } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.disposition { SingleAuthority } +import v2.std.node { Node, symbol_lexeme } +import v2.std.concept_index { ConceptByName, ConceptDecl, ConceptStruct, FieldRef, QualifiedConcept, build_concept_name_index, concept_from_decl, enumerate_concepts_from_decls } +import v2.std.layer { LayerPrefix, LayerPrefixCompiler, LayerPrefixExtdeps, LayerPrefixStd, LayerPrefixWorkflow, layer_prefix_from_dotted_qualified_name } +import v2.std.node_query { coproduct_arm_list_from_node, coproduct_arm_payload_pair_from_arm, node_is_coproduct } +import v2.std.algebra { fold_list, is_prefix_of } +import gunbc.ci_layer_roots { witness_layer_roots } data construction_justification: ConstructionJustification = ConstructionJustification { class: WallAfterGrounding { dissolves_to: SingleAuthority } @@ -151,7 +157,7 @@ fn sibling_variant_names(node: Node) -> String { } else { fold(coproduct_arm_list_from_node(node: node), init: "", f: fn(acc, arm) { let pair = coproduct_arm_payload_pair_from_arm(arm: arm) - let piece = "{pair.label}" + let piece = symbol_lexeme(sym: pair.label) if acc == "" { piece } else { concat(acc, concat(", ", piece)) } }) } @@ -163,7 +169,7 @@ fn sibling_has_unrecognized_variant(node: Node) -> Bool { } else { fold(coproduct_arm_list_from_node(node: node), init: false, f: fn(acc, arm) { let pair = coproduct_arm_payload_pair_from_arm(arm: arm) - acc || name_has_unrecognized_prefix(name: "{pair.label}") + acc || name_has_unrecognized_prefix(name: symbol_lexeme(sym: pair.label)) }) } } @@ -174,7 +180,7 @@ fn sibling_decode_optional_present(node: Node) -> Bool { } else { fold(coproduct_arm_list_from_node(node: node), init: false, f: fn(acc, arm) { let pair = coproduct_arm_payload_pair_from_arm(arm: arm) - acc || str_starts_with(s: "{pair.payload_type_name}", prefix: "Optional") + acc || str_starts_with(s: symbol_lexeme(sym: pair.payload_type_name), prefix: "Optional") }) } } diff --git a/src/v2/lens/lifecycle_carrier.dag b/src/v2/lens/lifecycle_carrier.dag index 230468660b2..fba28b5bfd5 100644 --- a/src/v2/lens/lifecycle_carrier.dag +++ b/src/v2/lens/lifecycle_carrier.dag @@ -14,17 +14,20 @@ import v2.std.diagnostic { } import v2.std.witness { Holds, Witness } import v2.std.node { - ComputationNode, Edge, Authored, Node, Positional, Symbol, - TypeNode, symbol_lexeme, StructuralLabel, } import v2.std.node_query { edge_field_type_name, member_edge_type_node, member_not_a_binder_diagnostic } +import v2.extdeps.languages.dag { + dag_edge_is_module_surface_parse_projection, + dag_node_is_namespace_member_target, + dag_node_is_record_field_carrier +} import std.optional { Absent, Optional, @@ -115,14 +118,23 @@ fn lifecycle_carrier_diagnostic(carrier: Node) -> Diagnostic { fn field_lifecycle_violation(carrier: Node, edge: Edge) -> Optional { match edge.label { Authored { name: nm } => - match member_edge_type_node(e: edge) { - Absent => optional_present(value: member_not_a_binder_diagnostic(e: edge)) - Present { value: t } => - if type_is_bare_string(target: t) && name_is_lifecycle_field(name: nm) { - optional_present(value: lifecycle_carrier_diagnostic(carrier: carrier)) - } else { - optional_absent() - } + if dag_edge_is_module_surface_parse_projection(carrier: carrier, name: nm) { + optional_absent() + } else { + match member_edge_type_node(e: edge) { + Absent => + if dag_node_is_namespace_member_target(node: edge.target) { + optional_absent() + } else { + optional_present(value: member_not_a_binder_diagnostic(e: edge)) + } + Present { value: t } => + if type_is_bare_string(target: t) && name_is_lifecycle_field(name: nm) { + optional_present(value: lifecycle_carrier_diagnostic(carrier: carrier)) + } else { + optional_absent() + } + } } StructuralLabel { label: _ } => optional_absent() Positional => optional_absent() @@ -130,12 +142,12 @@ fn field_lifecycle_violation(carrier: Node, edge: Edge) -> Optional } fn carrier_first_lifecycle_violation(carrier: Node) -> Optional { - match carrier.kind { - TypeNode { connective: _ } => - fold(carrier.children, init: optional_absent(), f: fn(acc, edge) { - optional_prefer_first_present(left: acc, right: field_lifecycle_violation(carrier: carrier, edge: edge)) - }) - ComputationNode { behavior: _ } => Absent + if dag_node_is_record_field_carrier(node: carrier) { + fold(carrier.children, init: optional_absent(), f: fn(acc, edge) { + optional_prefer_first_present(left: acc, right: field_lifecycle_violation(carrier: carrier, edge: edge)) + }) + } else { + optional_absent() } } diff --git a/src/v2/lens/live_read_classification.dag b/src/v2/lens/live_read_classification.dag index 6b124425cd0..8ea773fca33 100644 --- a/src/v2/lens/live_read_classification.dag +++ b/src/v2/lens/live_read_classification.dag @@ -30,7 +30,7 @@ import v2.lens.module_graph { module_declaration_facts_live, path_matches_touched } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import v2.std.live_read { LiveReadUnestablished } import v2.std.text { string_eq } @@ -547,7 +547,7 @@ fn live_read_selection_of(classification: LiveReadClassification) -> LiveReadSel LiveReadSelectionRefused { cause: concat( "G2 call walk met applications it could not read, so no classification was established: ", - integer_int_to_decimal_string(value: length(xs: r)) + int_to_decimal_string(value: length(xs: r)) ) } LocalRead => LiveReadSelectionClassified { classification: classification } diff --git a/src/v2/lens/mandatory_tag.dag b/src/v2/lens/mandatory_tag.dag index 1109ea06f9c..25f70a88c6c 100644 --- a/src/v2/lens/mandatory_tag.dag +++ b/src/v2/lens/mandatory_tag.dag @@ -25,10 +25,36 @@ import v2.std.diagnostic { node_locus, outcome_accepted } -import v2.std.node { Atom, Edge, Node, NodeFold, Symbol, TypeNode, fold_node } +import v2.std.node { + ArrowBodyEdge, + Atom, + Authored, + ComputationNode, + StructuralLabel, + Edge, + Node, + NodeFold, + Positional, + Symbol, + TypeNode, + core_edge_label, + fold_node, + node_is_arrow +} import v2.std.node_query { find_named_child } import v2.std.qualified_name { QualifiedName, qualified_name_to_dotted_string } import v2.std.text { String } +import v2.compiler.namespace_graft { namespace_graft_module_body_optional } +import v2.std.member_declaration_kind { + DeclaredDataMember, + DeclaredFnMember, + MemberKindChannel, + MemberKindDeclared, + MemberKindNotDeclared, + MemberKindProvenanceUnavailable, + member_kind_lookup, + no_member_declaration_kinds +} import v2.std.witness { Holds, Witness } import v2.std.grammar { node_atom_identity_optional } import v2.extdeps.languages.dag { @@ -52,6 +78,11 @@ import v2.extdeps.languages.dag { // same-path shadow module is unwritable upstream of this gate. Grain: root - one rooted whole-tree // walk locating dag_surface_module wrappers; per-module work is a bounded read of one small // declaration subtree, never a per-node subtree re-walk (the required_lens_grain_note O(n^2) bar). +// Door grain (validate_then_compile / compile_source_root_ingest_with_admission) still +// refuses a trivial module at member_not_a_binder on the unmarked QN spine. That is +// not claimed green here (DESIGN §4b(1); review 77921). find_witness_reason_no_candidate +// is infer, before lenses, and is not inhabitance (DESIGN §3). The member-kind channel +// that would classify the spine is gunbc#13513. // Corpus grain: the same fold is consumed at parse grain by the live witness in // src/v2/test/claim/long/mandatory_tag_extdeps_corpus_test.dag over dag/extdeps/** via // filesystem_list_dag_files + filesystem_read + the v2 grammar - the Node-tree projection that @@ -90,13 +121,12 @@ import v2.extdeps.languages.dag { // non-external, the old NonExternalAnchorScheme refusal). The tagged declaration's value subtree is // scanned for vocabulary atoms: any non-accepted atom refuses; none found refuses IF the value // region structurally carries its expression content (dag_surface_expr productions present - -// parse-shaped and emit-shaped trees), and is out of representation IF the region carries no -// expression productions (03_normalize currently collapses data-decl value internals to bare token -// markers - the general-body-producer frontier, docs/plans/general-body-producer-design.md). The -// out-of-representation arm contributes no finding at THIS grain because the fact is absent from -// the carrier, not because the check widened - the corpus witness enforces the same rule at parse -// grain, where value content is always present, so scheme enforcement stays whole-corpus while the -// normalize-grain representation matures. +// parse-shaped and emit-shaped trees) and when the declaration is a lowered Arrow member, whose +// initializer body is its value content. A tree whose declaration carries neither is out of +// representation and contributes no finding, because the fact is absent from the carrier, not +// because the check widened. Since normalize lowers a data declaration to a named Arrow +// (body_lower_data_decl_to_member) the normalized and inferred grains carry the value, so scheme +// enforcement holds there too (see the lowered carrier shape beside mandatory_tag_decl_type_node). type MandatoryTagRegion { region_dotted_prefix: String @@ -299,18 +329,83 @@ fn mandatory_tag_decl_name(d: Node) -> MandatoryTagDeclName { } } +// THE LOWERED CARRIER SHAPE. Normalize lowers a `data` declaration to a named nullary Arrow member +// (v2.compiler.body_lowering_fold body_lower_data_decl_to_member, 2026-09-24, #12197): the +// dag_surface_data_decl production is consumed and the declaration stands as a Conj member whose +// Authored edge carries the declared name and targets an Arrow whose domain is the first Positional +// child, whose codomain is the second (body_lower_callable_arrow), and whose initializer is the +// ArrowBodyEdge target. The root lens grain runs over the inferred tree, so this is the shape the +// compile door actually hands this gate; a reader of only the parse and emit shapes finds no +// declaration and reports every well-tagged module as missing its tag. +type MandatoryTagPositionalScan { + seen: Int + second: Optional +} + +fn mandatory_tag_lowered_codomain(d: Node) -> Optional { + let scan = fold(d.children, init: MandatoryTagPositionalScan { seen: 0, second: optional_absent() }, f: fn(acc, e) { + match e.label { + Positional => + MandatoryTagPositionalScan { + seen: acc.seen + 1, + second: if acc.seen == 1 { optional_present(value: e.target) } else { acc.second } + } + Authored { name: _ } => acc + StructuralLabel { label: _ } => acc + } + }) + scan.second +} + +fn mandatory_tag_lowered_body(d: Node) -> Optional { + fold(d.children, init: optional_absent(), f: fn(acc, e) { + if e.label == core_edge_label(marker: ArrowBodyEdge) { optional_present(value: e.target) } else { acc } + }) +} + +// A DIRECT top-level member of the module body: an Authored edge on the grafted module body (never a +// descendant of a member, so a nested Arrow-valued member of the same name is not it) whose target is an +// Arrow. That a lowered Arrow is a DATA declaration is not readable here -- a zero-argument fn lowers to the +// same shape -- so this finds the candidate and the member-kind channel decides what it is. +fn mandatory_tag_lowered_member(module_node: Node, required_name: Symbol) -> Optional { + match namespace_graft_module_body_optional(root: module_node) { + Absent => optional_absent() + Present { value: body } => + fold(body.children, init: optional_absent(), f: fn(acc, e) { + match e.label { + Authored { name: nm } => + if (nm == required_name) && node_is_arrow(n: e.target) { + optional_present(value: e.target) + } else { + acc + } + StructuralLabel { label: _ } => acc + Positional => acc + } + }) + } +} + fn mandatory_tag_decl_type_node(d: Node) -> Optional { + if node_is_arrow(n: d) { + mandatory_tag_lowered_codomain(d: d) + } else { match find_named_child(root: d, name: ^dag_surface_data_decl_type) { Accepted { value: ty, diagnostics: _ } => optional_present(value: ty) Rejected { diagnostics: _ } => mandatory_tag_parse_type_node(d: d) } + } } fn mandatory_tag_decl_value_node(d: Node) -> Optional { + if node_is_arrow(n: d) { + mandatory_tag_lowered_body(d: d) + } else { match find_named_child(root: d, name: ^dag_surface_data_decl_expr) { Accepted { value: v, diagnostics: _ } => optional_present(value: v) Rejected { diagnostics: _ } => mandatory_tag_parse_value_node(d: d) } + } } type MandatoryTagDeclScan { @@ -355,6 +450,14 @@ fn mandatory_tag_missing_decl_diagnostic(module_node: Node) -> Diagnostic { } } +fn mandatory_tag_member_provenance_unavailable_diagnostic(module_node: Node) -> Diagnostic { + Diagnostic { + reason: ^mandatory_tag_member_provenance_unavailable, + at: node_locus(node: module_node), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + fn mandatory_tag_projection_gated_diagnostic(module_node: Node) -> Diagnostic { Diagnostic { reason: ^mandatory_tag_name_projection_gated, @@ -422,7 +525,7 @@ fn mandatory_tag_value_findings(d: Node, region: MandatoryTagRegion) -> List - if mandatory_tag_subtree_has_production(n: v, want: ^dag_surface_expr) { + if node_is_arrow(n: d) || mandatory_tag_subtree_has_production(n: v, want: ^dag_surface_expr) { [mandatory_tag_value_unreadable_diagnostic(d: d)] } else { [] @@ -441,10 +544,45 @@ fn mandatory_tag_value_findings(d: Node, region: MandatoryTagRegion) -> List List { + match member_kind_lookup(channel: channel, module: dotted, name: region.required_decl_name) { + MemberKindProvenanceUnavailable => [mandatory_tag_member_provenance_unavailable_diagnostic(module_node: module_node)] + MemberKindNotDeclared => [mandatory_tag_missing_decl_diagnostic(module_node: module_node)] + MemberKindDeclared { kind: kind } => + match kind { + DeclaredFnMember => [mandatory_tag_missing_decl_diagnostic(module_node: module_node)] + DeclaredDataMember => + match mandatory_tag_lowered_codomain(d: candidate) { + Absent => [mandatory_tag_type_mismatch_diagnostic(d: candidate)] + Present { value: _ } => + match mandatory_tag_lowered_body(d: candidate) { + Absent => [mandatory_tag_value_unreadable_diagnostic(d: candidate)] + Present { value: _ } => + concat( + mandatory_tag_type_findings(d: candidate, region: region), + mandatory_tag_value_findings(d: candidate, region: region) + ) + } + } + } + } +} + fn mandatory_tag_region_module_findings( region: MandatoryTagRegion, module_node: Node, - dotted: String + dotted: String, + channel: MemberKindChannel ) -> List { if mandatory_tag_region_applies(region: region, dotted: dotted) { let scan = mandatory_tag_scan_decls( @@ -459,7 +597,17 @@ fn mandatory_tag_region_module_findings( ) Absent => if scan.projection_gated_count == 0 { - [mandatory_tag_missing_decl_diagnostic(module_node: module_node)] + match mandatory_tag_lowered_member(module_node: module_node, required_name: region.required_decl_name) { + Absent => [mandatory_tag_missing_decl_diagnostic(module_node: module_node)] + Present { value: candidate } => + mandatory_tag_lowered_candidate_findings( + region: region, + module_node: module_node, + dotted: dotted, + candidate: candidate, + channel: channel + ) + } } else { [mandatory_tag_projection_gated_diagnostic(module_node: module_node)] } @@ -469,7 +617,7 @@ fn mandatory_tag_region_module_findings( } } -fn mandatory_tag_module_findings(n: Node) -> List { +fn mandatory_tag_module_findings(n: Node, channel: MemberKindChannel) -> List { if mandatory_tag_node_production_is(n: n, want: ^dag_surface_module) { match qualified_name_from_module_node(root: n) { Rejected { diagnostics: _ } => [] @@ -481,7 +629,8 @@ fn mandatory_tag_module_findings(n: Node) -> List { mandatory_tag_region_module_findings( region: region, module_node: n, - dotted: dotted + dotted: dotted, + channel: channel ) ) }) @@ -496,22 +645,32 @@ fn mandatory_tag_module_findings(n: Node) -> List { // malformed module headers upstream of every root lens, so the nameless-wrapper arm is unreachable // on a tree that reached the lens roster; keying region membership on a fabricated name would be // the fabricated-plausible-output trap the arm avoids. -fn mandatory_tag_findings(root: Node) -> List { +fn mandatory_tag_findings_with_provenance(root: Node, channel: MemberKindChannel) -> List { fold_node( n: root, algebra: NodeFold { - init: fn(n0) { mandatory_tag_module_findings(n: n0) }, + init: fn(n0) { mandatory_tag_module_findings(n: n0, channel: channel) }, step: fn(acc, _edge, child) { concat(acc, child) } } ) } +// The parse/emit grain: no provenance is read, so a lowered candidate refuses as provenance-unavailable. +fn mandatory_tag_findings(root: Node) -> List { + mandatory_tag_findings_with_provenance(root: root, channel: no_member_declaration_kinds()) +} + fn mandatory_tag_findings_count(root: Node) -> Int { length(xs: mandatory_tag_findings(root: root)) } fn mandatory_tag_compile_gate(node: Node) -> Outcome> { - match mandatory_tag_findings(root: node) { + mandatory_tag_compile_gate_with_provenance(node: node, channel: no_member_declaration_kinds()) +} + +// The compile door's entry: the channel is the member-kind provenance ResolvedTree carries for this subject. +fn mandatory_tag_compile_gate_with_provenance(node: Node, channel: MemberKindChannel) -> Outcome> { + match mandatory_tag_findings_with_provenance(root: node, channel: channel) { Cons { head: first, tail: rest } => Rejected { diagnostics: NonEmptyDiagnostics { head: first, tail: rest } diff --git a/src/v2/lens/manufactured_dependency_census.dag b/src/v2/lens/manufactured_dependency_census.dag index 050063031e3..3b917ef15b5 100644 --- a/src/v2/lens/manufactured_dependency_census.dag +++ b/src/v2/lens/manufactured_dependency_census.dag @@ -103,7 +103,7 @@ fn import_targets_by_path(edges: List) -> Map) -> Bool any(xs: roots, predicate: fn(root) { starts_with(s: path, prefix: concat(root, "/")) }) } -fn source_refs_from_declared(declared: List) -> List { +fn path_is_under_pool_root(path: String, root: String) -> Bool { + path == root || starts_with(s: path, prefix: concat(root, "/")) +} + +fn discovering_pool_root(path: String, pool_roots: List) -> Optional { + fold_list( + xs: pool_roots, + empty: Absent, + cons: fn(acc, root) { + match acc { + Present { value: found } => Present { value: found } + Absent => match path_is_under_pool_root(path: path, root: root) { + true => Present { value: root } + false => Absent + } + } + } + ) +} + +fn source_refs_from_declared( + declared: List, + pool_roots: List +) -> Outcome> { fold_list( xs: declared, - empty: no_source_refs, + empty: outcome_accepted(value: no_source_refs), cons: fn(acc, fact) { - list_snoc_item(xs: acc, item: source_ref_for_storage_path(path: fact.path)) + bind_outcome( + o: acc, + f: fn(refs) { + match discovering_pool_root(path: fact.path, pool_roots: pool_roots) { + Absent => + outcome_rejected( + d: Diagnostic { + reason: ^declared_module_pool_root_unknown, + at: Textual { file: ^declared_module_pool_root_unknown, extent: WholeFile }, + correction: Unavailable { reason: ExternalContractUnknown } + } + ) + Present { value: origin } => + match admit_source_root_argument(argument: origin) { + SourceRootArgumentAdmitted { tree: tree } => + outcome_accepted( + value: list_snoc_item( + xs: refs, + item: source_ref_for_storage_path(path: fact.path, source_root: tree) + ) + ) + SourceRootArgumentUnrecognized { argument: _ } => + outcome_rejected( + d: Diagnostic { + reason: ^declared_module_source_root_undetermined, + at: Textual { file: ^declared_module_source_root_undetermined, extent: WholeFile }, + correction: Unavailable { reason: ExternalContractUnknown } + } + ) + } + } + } + ) } ) } @@ -375,15 +433,21 @@ fn edges_from_provenance_owners( } fn load_reference_derived_pool( - declared: List + declared: List, + pool_roots: List ) -> Outcome { bind_outcome( - o: source_root_ingest_from_source_refs(refs: source_refs_from_declared(declared: declared)), - f: fn(ingest) { + o: source_refs_from_declared(declared: declared, pool_roots: pool_roots), + f: fn(refs) { bind_outcome( - o: located_roots_from_source_root_ingest(ingest: ingest), - f: fn(items) { - outcome_accepted(value: reference_derived_pool_from_located_roots(items: items)) + o: source_root_ingest_from_source_refs(refs: refs), + f: fn(ingest) { + bind_outcome( + o: located_roots_from_source_root_ingest(ingest: ingest), + f: fn(items) { + outcome_accepted(value: reference_derived_pool_from_located_roots(items: items)) + } + ) } ) } @@ -599,7 +663,7 @@ fn reference_derived_resolution_facts( let declared = module_declaration_facts_live(pool_roots: pool_roots) |> filter(fact => !reference_derived_path_excluded(path: fact.path, exclude_substrings: exclude_substrings)) bind_outcome( - o: load_reference_derived_pool(declared: declared), + o: load_reference_derived_pool(declared: declared, pool_roots: pool_roots), f: fn(pool) { bind_outcome( o: fold_list( @@ -642,7 +706,7 @@ type ImportClosureBfsState { } fn map_lookup_where_rejected_unreachable(m: Map, key: K) -> Optional { - match v2.std.collection.map_get(m: m, key: key) { + match v2.std.collection.map_get_checked(m: m, key: key) { Accepted { value: v, diagnostics: _ } => v Rejected { diagnostics: _ } => Absent } diff --git a/src/v2/lens/reference_derived_residency_reading.dag b/src/v2/lens/reference_derived_residency_reading.dag index 8f6fb06ca19..57a30b9abe7 100644 --- a/src/v2/lens/reference_derived_residency_reading.dag +++ b/src/v2/lens/reference_derived_residency_reading.dag @@ -20,6 +20,7 @@ import std.decl_ref { DeclarationRef } import v2.std.algebra { length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.extdeps.languages.dag { dag_node_is_module_member_surface } import v2.std.node_query { member_edge_type_node } import v2.std.node { Arrow, @@ -168,7 +169,9 @@ fn decl_first_unrecognised_atom(decl: Node) -> Optional { } // A Named member that is not a binder is read first: the carrier declares no type for it, and that -// refuses the whole reading rather than letting the remaining fields decide it. +// refuses the whole reading rather than letting the remaining fields decide it. Identity-stamped +// module or declaration members (v2.extdeps.languages.dag dag_node_is_module_member_surface) are +// skipped per target; a hand-built field on the same Conj is still refused. fn decl_first_member_not_a_binder(decl: Node) -> Optional { match decl.kind { TypeNode { connective: _ } => @@ -180,9 +183,13 @@ fn decl_first_member_not_a_binder(decl: Node) -> Optional { Positional => acc StructuralLabel { label: _ } => acc Authored { name: nm } => - match member_edge_type_node(e: edge) { - Present { value: _ } => acc - Absent => optional_present(value: nm) + if dag_node_is_module_member_surface(node: edge.target) { + acc + } else { + match member_edge_type_node(e: edge) { + Present { value: _ } => acc + Absent => optional_present(value: nm) + } } } } diff --git a/src/v2/lens/text_string_importer_census.dag b/src/v2/lens/text_string_importer_census.dag index 1e835bd51ed..2e6f27b229c 100644 --- a/src/v2/lens/text_string_importer_census.dag +++ b/src/v2/lens/text_string_importer_census.dag @@ -27,7 +27,8 @@ import v2.std.provenance { SpanIndex, span_index_empty, node_occurrence_id_optio import std.optional { Optional } import v2.std.diagnostic { Accepted, Rejected, Textual, ByteRange, WholeFile } import v2.std.algebra { contains, fold_list, length, list_reverse } -import v2.std.integer { Int, integer_int_to_decimal_string } +import v2.std.integer { Int } +import std.integer { int_to_decimal_string } import v2.std.collection { Map, empty_map, map_insert, map_lookup } import std.optional { Absent, Present } import std.algebra { Cons, Empty, FreeMonoid } @@ -62,7 +63,7 @@ import std.primitives { // THE CENSUS OF `import v2.std.text { String }` IMPORTERS, AT SITE GRAIN (gunbc#12760, decision A; // method ruled by neat-boar-16 2026-09-30). v1 binds a bare `String` in these modules to the KERNEL -// host text whatever the import says (`v1.compiler.infer` `overlay_skips_kernel_name`); v2 binds it +// host text whatever the import says (`v1.compiler.infer_env` `overlay_skips_kernel_name`); v2 binds it // to the structural `FreeMonoid` the import names. The fork resolves at the importers: // HOST-USING and MIXED importers drop `String` from the import, STRUCTURAL-USING importers keep it // as `gunbc.recurring_failure_mode` `carrier_by_spelling`'s declared v1/v2 divergence. This lens @@ -1412,7 +1413,7 @@ fn site_tokens(k: SiteKind) -> String { } fn int_text(n: Int) -> String { - integer_int_to_decimal_string(value: n) + int_to_decimal_string(value: n) } // One module's lines: an `M` row carrying the verdict and the counts (host, structural, OTHER, diff --git a/src/v2/lens/unit_modeling.dag b/src/v2/lens/unit_modeling.dag index 38533e64004..74fbc3d3f04 100644 --- a/src/v2/lens/unit_modeling.dag +++ b/src/v2/lens/unit_modeling.dag @@ -7,6 +7,11 @@ import std.disposition { SingleAuthority } import v2.std.witness { Holds } import v2.std.node_query { edge_field_type_name, member_edge_type_node, member_not_a_binder_diagnostic } +import v2.extdeps.languages.dag { + dag_edge_is_module_surface_parse_projection, + dag_node_is_namespace_member_target, + dag_node_is_record_field_carrier +} import v2.std.diagnostic { node_locus, outcome_accepted, outcome_rejected } import std.optional { optional_absent, optional_present, optional_prefer_first_present } @@ -60,7 +65,7 @@ fn unit_field_token_map() -> Map { } fn field_name_has_known_carrier(name: Symbol) -> Bool { - match v2.std.collection.map_get(m: unit_field_token_map(), key: name) { + match v2.std.collection.map_get_checked(m: unit_field_token_map(), key: name) { Accepted { value: Present { value: _ } } => true Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -81,7 +86,7 @@ fn exemption_marker_set() -> Map { } fn name_is_exempt(name: Symbol) -> Bool { - match v2.std.collection.map_get(m: exemption_marker_set(), key: name) { + match v2.std.collection.map_get_checked(m: exemption_marker_set(), key: name) { Accepted { value: Present { value: _ } } => true Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -95,8 +100,16 @@ fn type_is_flat_scalar(target: Node) -> Bool { fn field_unit_violation(carrier: Node, edge: Edge) -> Optional { match edge.label { Authored { name: nm } => + if dag_edge_is_module_surface_parse_projection(carrier: carrier, name: nm) { + optional_absent() + } else { match member_edge_type_node(e: edge) { - Absent => optional_present(value: member_not_a_binder_diagnostic(e: edge)) + Absent => + if dag_node_is_namespace_member_target(node: edge.target) { + optional_absent() + } else { + optional_present(value: member_not_a_binder_diagnostic(e: edge)) + } Present { value: t } => if type_is_flat_scalar(target: t) { if name_is_exempt(name: nm) { @@ -108,18 +121,19 @@ fn field_unit_violation(carrier: Node, edge: Edge) -> Optional { optional_absent() } } + } StructuralLabel { label: _ } => optional_absent() Positional => optional_absent() } } fn carrier_first_unit_violation(carrier: Node) -> Optional { - match carrier.kind { - TypeNode { connective: _ } => - 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 + if dag_node_is_record_field_carrier(node: carrier) { + fold(carrier.children, init: optional_absent(), f: fn(acc, edge) { + optional_prefer_first_present(left: acc, right: field_unit_violation(carrier: carrier, edge: edge)) + }) + } else { + optional_absent() } } diff --git a/src/v2/std/anonymous_binder.dag b/src/v2/std/anonymous_binder.dag index a15505809b3..e0f5d78fa77 100644 --- a/src/v2/std/anonymous_binder.dag +++ b/src/v2/std/anonymous_binder.dag @@ -3,7 +3,8 @@ module v2.std.anonymous_binder import std.types { List } import std.algebra { Cons, Empty } import v2.std.algebra { list_reverse } -import v2.std.integer { Int, integer_int_to_decimal_string } +import v2.std.integer { Int } +import std.integer { int_to_decimal_string } import v2.std.node { Edge, Authored, Positional, Symbol, symbol_intern_lexeme, symbol_lexeme, StructuralLabel } import v2.std.node_query { binder_relabelled, pattern_wildcard_name } @@ -34,7 +35,7 @@ fn anonymous_param_label_prefix() -> String { // minted slots. fn anonymous_param_label(slot: Int) -> Symbol { symbol_intern_lexeme( - lexeme: anonymous_param_label_prefix() + integer_int_to_decimal_string(value: slot) + ">" + lexeme: anonymous_param_label_prefix() + int_to_decimal_string(value: slot) + ">" ) } @@ -59,7 +60,7 @@ fn fresh_type_variable_prefix() -> String { fn fresh_type_variable(path: List, label: Symbol) -> Symbol { let p = fold(path, init: "", f: fn(acc, i) { - if acc == "" { integer_int_to_decimal_string(value: i) } else { acc + "." + integer_int_to_decimal_string(value: i) } + if acc == "" { int_to_decimal_string(value: i) } else { acc + "." + int_to_decimal_string(value: i) } }) symbol_intern_lexeme(lexeme: fresh_type_variable_prefix() + p + ":" + symbol_lexeme(sym: label) + ">") } diff --git a/src/v2/std/collection.dag b/src/v2/std/collection.dag index 16d531b476d..4d3fa33ed2c 100644 --- a/src/v2/std/collection.dag +++ b/src/v2/std/collection.dag @@ -63,7 +63,7 @@ fn map_insert(m: Map, key: K, value: V) -> Map { // third state in which the map fails to say. So the total form is the primitive and the // Outcome-wrapped form below is a projection of it, not the other way round. // -// The direction matters at the consumer. A caller reaching for `map_get` must match a +// The direction matters at the consumer. A caller reaching for `map_get_checked` must match a // `Rejected` arm that no producer can construct, and the only arms available to it are the // caller's own domain answers — so the unreachable arm gets bound to whichever domain answer is // nearest, and a substrate failure would report as a domain fact. Exposing the total lookup @@ -85,7 +85,7 @@ fn map_contains_key(m: Map, key: K) -> Bool { } } -fn map_get(m: Map, key: K) -> Outcome> { +fn map_get_checked(m: Map, key: K) -> Outcome> { outcome_accepted(value: map_lookup(m: m, key: key)) } fn list_nth(xs: List, wanted: Int, absent: Diagnostic) -> Witness { diff --git a/src/v2/std/compilers/body_lowering.dag b/src/v2/std/compilers/body_lowering.dag index 87af912e6b0..2d3b822acca 100644 --- a/src/v2/std/compilers/body_lowering.dag +++ b/src/v2/std/compilers/body_lowering.dag @@ -1,6 +1,7 @@ module v2.std.compilers.body_lowering import std.types { List } +import v2.std.compilers.lowered_shape { LoweredShape } import v2.std.type_binder { edge_is_cast_target, type_annotation_edge } import std.algebra { Cons, Empty, FreeMonoid, list_append, list_snoc_item } import v2.std.algebra { contains, fold_list } @@ -703,3 +704,16 @@ fn branch_view(site: Node) -> Outcome { ComputationNode { behavior: Match } => view_refusal(reason: ^body_view_not_branch, at: site) } } + + +// A NODE IN THE FOLD'S TREE IS ONE OF THREE THINGS, EACH IDENTIFIED POSITIVELY. Raw parse is a +// composite the parse stage stamped (it carries a grammar projection edge); a raw leaf is an atom or +// the childless conj the parse emits as a repeat or optional terminator, and is its own arm so it can +// never stand in for a composite; lowered output is a node a producer wrapped. Anything else is not a +// term and refuses where it is read (body_term_of). The leaf arm is the honest limit of today's +// occurrence model: a leaf carries no projection edge and inherits its source occurrence, so its +// provenance is not on it; it has no interior, so no part of it can be re-read for the whole. +type BodyTerm + = RawParse { node: Node } + | RawLeaf { node: Node } + | Lowered { node: Node, shape: LoweredShape } diff --git a/src/v2/std/compilers/cli_surface.dag b/src/v2/std/compilers/cli_surface.dag index 331d1b57358..7968467b85d 100644 --- a/src/v2/std/compilers/cli_surface.dag +++ b/src/v2/std/compilers/cli_surface.dag @@ -12,7 +12,7 @@ import v2.std.compilers.target_model { LexRules, lex_rules_literal_for_class } -import v2.std.collection { map_get } +import v2.std.collection { map_get_checked } import v2.std.diagnostic { Accepted, Diagnostic, @@ -262,7 +262,7 @@ fn required_bound_spelling( binding_spellings: Map, binding: Symbol ) -> Outcome { - match map_get(binding_spellings, binding) { + match map_get_checked(binding_spellings, binding) { Rejected { diagnostics: ds } => Rejected { diagnostics: ds } Accepted { value: found, diagnostics: pending } => match found { diff --git a/src/v2/std/compilers/compilation_unit.dag b/src/v2/std/compilers/compilation_unit.dag index 03e542e5b56..cd9284edfd0 100644 --- a/src/v2/std/compilers/compilation_unit.dag +++ b/src/v2/std/compilers/compilation_unit.dag @@ -9,7 +9,7 @@ import std.disposition { SingleAuthority } import v2.lens.common.construction_justification { ConstructionJustification, WallNow } import v2.std.algebra { any, fold_list, length } import v2.std.collection { - map_get + map_get_checked } import std.optional { Absent, @@ -210,7 +210,7 @@ fn module_unit_id_outcome( assignment: Map, module: ModuleId ) -> Outcome> { - match map_get(assignment, module) { + match map_get_checked(assignment, module) { Accepted { value: opt, diagnostics: _ } => outcome_accepted(value: opt) Rejected { diagnostics: d } => Rejected { diagnostics: d } } @@ -220,7 +220,7 @@ fn module_unit_id( assignment: Map, module: ModuleId ) -> Optional { - match map_get(assignment, module) { + match map_get_checked(assignment, module) { Accepted { value: opt, diagnostics: _ } => opt Rejected { diagnostics: _ } => Absent } @@ -341,7 +341,7 @@ fn module_export_declarations_outcome( module_dag: ModuleDag, module: ModuleId ) -> Outcome> { - match map_get(module_dag.exports, module) { + match map_get_checked(module_dag.exports, module) { Accepted { value: opt, diagnostics: _ } => outcome_accepted(value: match opt { Present { value: nodes } => nodes @@ -355,7 +355,7 @@ fn module_type_definition_rows_outcome( module_dag: ModuleDag, module: ModuleId ) -> Outcome> { - match map_get(module_dag.type_definitions, module) { + match map_get_checked(module_dag.type_definitions, module) { Accepted { value: opt, diagnostics: _ } => outcome_accepted(value: match opt { Present { value: rows } => rows @@ -373,7 +373,7 @@ fn module_type_definition_rows_outcome( // then discarded both values, so the closure below it re-established a fact its predecessor already // had (DESIGN section 6b). With the rows carried, no map lookup runs inside the closure at all: // the swallowing arm has no site to exist at rather than being refused at one, and the repeated -// map_get is gone with it. +// map_get_checked is gone with it. fn resolve_module_interface( module_dag: ModuleDag, module: ModuleId diff --git a/src/v2/std/compilers/lexing.dag b/src/v2/std/compilers/lexing.dag index 39782008475..4a5ca5bbc6f 100644 --- a/src/v2/std/compilers/lexing.dag +++ b/src/v2/std/compilers/lexing.dag @@ -20,9 +20,7 @@ import std.optional { optional_absent, optional_present } -import v2.std.node { Atom, Conj, Node, Symbol, TypeNode, combine_hash, symbol_identity_digest } -import std.content_hash { Fnv1a64Structural } -import v2.std.integer { integer_int_content_digest } +import v2.std.node { Atom, Conj, Node, Symbol, TypeNode } import v2.std.node_shape { stamp_node_shape_synthetic, type_shape } import v2.std.text { Char, String } import std.unicode.char_class { unicode_char_code_point } @@ -231,9 +229,10 @@ fn phrase_to_source_text(p: LexicalPhrase) -> String { } // TOKEN STARTS STRICTLY INCREASE ALONG A STREAM. A token's start is its stream position: the parse -// keys its memo and decides repeat progress by it (v2.compiler.parse parse_current_position), and -// refuses a stream that breaks the order at its entry (parse_token_start_order_violation, -// TokenStartsNotIncreasing) rather than serve one position's answer for another. +// identifies a stream position and decides repeat progress by it (v2.compiler.parse +// parse_current_position), and refuses a stream that breaks the order at its entry +// (parse_token_start_order_violation, TokenStartsNotIncreasing) rather than take one position's +// progress for another's. type Token { class: Symbol lexeme: Lexeme @@ -254,13 +253,10 @@ type Token { // (v2.compiler.tokenize lex_walk_step), the one place the source is read, through the two builders // below, so there is no second pass over the source and a layout question is a lookup. A line feed // counts when it is in a gap -- in trivia or an annotation between tokens -- never inside a token (a -// multi-line string). `digest` summarizes exactly the recorded entries, folded as each is recorded, so -// the parse memo key covers layout without a lookup per token. A stream built from tokens alone -// (fixtures) carries no layout, and a layout question on it answers `LineBreakUnknownWithoutSource` +// multi-line string). A stream built from tokens alone (fixtures) carries no layout, and a layout question on it answers `LineBreakUnknownWithoutSource` // -- never an assumed single line. type StreamLayout { line_break_before: Map - digest: Fnv1a64Structural } type TokenStream { @@ -292,14 +288,13 @@ type LineBreakAnswer | LineBreakUnknownWithoutSource fn stream_layout_empty() -> StreamLayout { - StreamLayout { line_break_before: empty_map(), digest: symbol_identity_digest(sym: ^stream_layout_digest_nil) } + StreamLayout { line_break_before: empty_map() } } // The token starting at octet `token_start` has a line break in its gap, which began at `gap_start`. fn stream_layout_with_break(layout: StreamLayout, token_start: Int, gap_start: Int) -> StreamLayout { StreamLayout { - line_break_before: map_insert(m: layout.line_break_before, key: token_start, value: gap_start), - digest: combine_hash(a: layout.digest, b: combine_hash(a: integer_int_content_digest(value: token_start), b: integer_int_content_digest(value: gap_start))) + line_break_before: map_insert(m: layout.line_break_before, key: token_start, value: gap_start) } } diff --git a/src/v2/std/compilers/lowered_shape.dag b/src/v2/std/compilers/lowered_shape.dag new file mode 100644 index 00000000000..75788b3f712 --- /dev/null +++ b/src/v2/std/compilers/lowered_shape.dag @@ -0,0 +1,41 @@ +module v2.std.compilers.lowered_shape + +// WHICH PRODUCER LOWERED A NODE IS A FACT OF ITS CONSTRUCTION, NOT A READING OF ITS SHAPE (MQ-5, +// neat-boar-16 ruling 2026-09-30, option A; realized as the operator-approved interim B, msg_63aee66e). +// Body lowering is a bottom-up fold over a tree that holds raw parse and lowered output at once, and +// nothing on a Node says which is which, so every reader used to ask a hand-kept shape allowlist +// (body_lower_is_core_substrate) and re-read anything it missed as raw parse. Each producer now +// declares what it built: its result leaves the producer inside the stage's Lowered wrapper, carrying +// one of these arms as a constant the producer writes, and a reader matches the term +// (v2.compiler.body_lowering_fold body_term_of) instead of classifying the node. The arms name the +// producer family that built the value. +// WHO READS THE ARM, STATED AS IT IS (review 76884; DESIGN section 3c). In this change the SEAL is +// consumed -- body_term_of keys Lowered versus RawParse/RawLeaf on its presence -- and the arm it +// carries is not: every reader takes a Lowered term whole (shape: _). That is a declared frontier, +// not a consumer. Its named consumer is the stage ceiling body_lowering_fold states beside the seal: +// a sealed BodyTerm term tree that normalize consumes by dispatching on the producer arm instead of +// re-reading node shapes (MQ-6). TRIGGER: that change lands its first normalize match over +// LoweredShape. If it does not, this arm collapses to a unit seal -- the discriminator alone is what +// this change needs -- rather than staying as data nothing reads. +// It sits in its own module, below v2.std.node, because the seal on the Lowered wrapper's edge +// carries it (v2.std.node BodyTermLoweredSeal): the seal holds the closed shape itself, so no +// symbol round-trip, roster list or unknown-shape arm exists to drift from this declaration. +type LoweredShape + = LoweredOperatorExpression + | LoweredFunctionValue + | LoweredFoldEncoding + | LoweredMatch + | LoweredBranch + | LoweredLoop + | LoweredLet + | LoweredStatementSequence + | LoweredPostfix + | LoweredPrimary + | LoweredFnDeclaration + | LoweredTestDeclaration + | LoweredDataDeclaration + | LoweredFnBody + | LoweredTypeDeclaration + | LoweredServiceDeclaration + | LoweredResourceDeclaration + | LoweredOperand diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index 563b81853cc..114a000f091 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -38,7 +38,7 @@ import v2.std.collection { Map, empty_map, list_at_optional, - map_get, + map_get_checked, map_insert, map_lookup } @@ -215,10 +215,8 @@ import v2.std.host_transport { TargetEmitHostRuntimeRow, configured_emit_host_runtime_row_from_target_row } -import v2.std.integer { - integer_decimal_magnitude_node_to_int, - integer_int_to_decimal_string -} +import v2.std.integer { integer_decimal_magnitude_node_to_int } +import std.integer { int_to_decimal_string } import v2.std.fold_assembly { node_list_from_node } @@ -1343,7 +1341,7 @@ fn bound_spelling_from_map( binding_spellings: Map, binding: Symbol ) -> Outcome { - match map_get(binding_spellings, binding) { + match map_get_checked(binding_spellings, binding) { Accepted { value: opt, diagnostics: pending } => match opt { Present { value: spelling } => Accepted { value: spelling, diagnostics: pending } Absent => @@ -1544,7 +1542,7 @@ fn fixed_token_spelling_from_model( bind_outcome( o: lex_rules_literal_for_class(rules: target.lex, token_class: token_class), f: fn(spelling) { - match map_get(target.token_class_emit_transforms, token_class) { + match map_get_checked(target.token_class_emit_transforms, token_class) { Accepted { value: Present { value: transform }, diagnostics: d } => bind_outcome( o: apply_emit_spelling_transform(transform: transform, token_class: token_class, spelling: spelling), @@ -1566,7 +1564,7 @@ fn bound_token_spelling_from_model( bind_outcome( o: target_binding_spelling_lookup(target: target, binding: binding), f: fn(spelling) { - match map_get(target.token_class_emit_transforms, token_class) { + match map_get_checked(target.token_class_emit_transforms, token_class) { Accepted { value: Present { value: transform }, diagnostics: d } => bind_outcome( o: apply_emit_spelling_transform(transform: transform, token_class: token_class, spelling: spelling), @@ -1581,7 +1579,7 @@ fn bound_token_spelling_from_model( } fn layout_or_atom_in(transforms: Map, token_class: Symbol, spelling: String) -> Outcome { - match map_get(transforms, token_class) { + match map_get_checked(transforms, token_class) { Accepted { value: Present { value: transform }, diagnostics: d } => match transform { EmitLineBreaks { before: b, after: a } => @@ -5658,7 +5656,7 @@ fn target_value_expr_effect_apply_callee( shape: TargetEffectApplyShape, expr: TargetValueExpression ) -> Outcome { - match map_get( + match map_get_checked( m: fold_list( xs: shape.callees, empty: empty_map(), @@ -8125,7 +8123,7 @@ fn target_value_expr_match_arm_tokens( } } -data target_value_expr_int_bool_octet_channel_dissolution_note: String = "Staging bridge only: inline scrutinee `x != 0` decodes Int wire to bool at the match seam. Ground on the modeled octet↔bool codec authority once inhabited ({0,1} domain, not general nonzero) — do not let this bridge harden into a second decode nickname. interim bindings: ^dag_binding_octet_zero/^dag_binding_octet_one spell 0/1 until codec rows land. scaffold-widen: infer half emits infer_match_int_scrutinee_bool_literal_scaffold diagnostic on Int-scrutinee bool-literal match (countable). paired-scaffold: infer half = infer_match_int_scrutinee_bool_literal_body_note (04_infer.dag) — dissolve both halves together on octet↔bool codec inhabitation." +data target_value_expr_int_bool_octet_channel_dissolution_note: String = "Emit half deleted (DESIGN §3/§4b(4)): after the Bool de-fork, BindingRef bool-param match is not an Int octet wire, and the predicate that selected `x != 0` plus 0/1 arm bodies was constantly false, so the match-seam production was uprooted rather than left as a dead parallel route. Remaining paired half is infer: infer_match_int_scrutinee_bool_literal_scaffold on Int-scrutinee bool-literal match (countable). Dissolve that diagnostic on octet↔bool codec inhabitation ({0,1} domain, not general nonzero) — v2.compiler.infer infer_match_int_scrutinee_bool_literal_body_note." fn target_value_expr_bool_literal_value_from_atom(node: Node) -> Optional { match node.kind { @@ -8256,217 +8254,6 @@ fn target_value_expr_is_source_bool_literal_atom( } } -fn target_value_expr_match_arms_are_int_bool_octet_channel( - arms: List -) -> Bool { - if is_empty(xs: arms) { - false - } else { - !any( - xs: arms, - predicate: fn(arm) { - arm.pattern.kind != TargetValueExprBoolLiteral - || arm.body.kind != TargetValueExprBoolLiteral - } - ) - } -} - -fn target_value_expr_match_is_int_bool_octet_channel( - scrutinee: TargetValueExpression, - arms: List -) -> Bool { - match scrutinee.kind { - TargetValueExprBindingRef => - target_value_expr_match_arms_are_int_bool_octet_channel(arms: arms) - TargetValueExprSymbolIdentity => false - TargetValueExprSymbolToOwnedString => false - TargetValueExprBoolLiteral => false - TargetValueExprCharUnicodeScalar => false - TargetValueExprStringLiteral => false - TargetValueExprIntLiteral => false - TargetValueExprReferenceRcNew => false - TargetValueExprReferenceBoxNew => false - TargetValueExprPrimitiveApply => false - TargetValueExprEffectApply => false - TargetValueExprCallableApply => false - TargetValueExprClosure => false - TargetValueExprConditional => false - TargetValueExprBindLet => false - TargetValueExprLoop => false - TargetValueExprRecordConstruct => false - TargetValueExprFieldAccess => false - TargetValueExprMatch => false - } -} - -fn target_value_expr_bool_literal_octet_body_tokens( - expr: TargetValueExpression, - projection: TargetValueExpressionProjection -) -> Outcome> { - match expr.kind { - TargetValueExprBoolLiteral => - match target_value_expression_bool_literal_value_from_wire(wire: expr.node) { - Present { value: false } => - outcome_accepted( - Cons { - head: target_value_expression_binding_ref_token( - binding: ^dag_binding_octet_zero, - projection: projection - ), - tail: Empty - } - ) - Present { value: true } => - outcome_accepted( - Cons { - head: target_value_expression_binding_ref_token( - binding: ^dag_binding_octet_one, - projection: projection - ), - tail: Empty - } - ) - Absent => - outcome_rejected( - target_value_expr_diagnostic( - reason: ^target_value_expr_reason_transform_shape_invalid, - node: expr.node - ) - ) - } - TargetValueExprSymbolIdentity => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprSymbolToOwnedString => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprCharUnicodeScalar => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprStringLiteral => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprIntLiteral => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprReferenceRcNew => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprReferenceBoxNew => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprBindingRef => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprPrimitiveApply => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprEffectApply => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprCallableApply => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprClosure => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprConditional => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprBindLet => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprLoop => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprRecordConstruct => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprFieldAccess => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - TargetValueExprMatch => target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: expr) - } -} - -fn target_value_expr_bool_literal_octet_body_tokens_otherwise(expr: TargetValueExpression) -> Outcome> { - outcome_rejected( - target_value_expr_diagnostic( - reason: ^target_value_expr_reason_transform_shape_invalid, - node: expr.node - ) - ) -} - -fn target_value_expr_int_bool_octet_scrutinee_carrier_tokens( - scrutinee_tokens: List, - catalog: Node, - projection: TargetValueExpressionProjection -) -> Outcome> { - bind_outcome( - o: target_operator_realization_catalog_lookup( - catalog: catalog, - canonical_op: canonical_operation_op_ne() - ), - f: fn(ne_realization) { - match ne_realization.shape { - InfixToken { token: ne_token } => - outcome_accepted( - concrete_syntax_tokens_extend( - acc: concrete_syntax_tokens_extend( - acc: scrutinee_tokens, - more: Cons { - head: FixedToken { token_class: ne_token }, - tail: Empty - } - ), - more: Cons { - head: target_value_expression_binding_ref_token( - binding: ^dag_binding_octet_zero, - projection: projection - ), - tail: Empty - } - ) - ) - PrefixToken { token: _ } => target_value_expr_int_bool_octet_scrutinee_carrier_tokens_otherwise(catalog: catalog) - CallIdent { ident: _ } => target_value_expr_int_bool_octet_scrutinee_carrier_tokens_otherwise(catalog: catalog) - MethodIdent { ident: _ } => target_value_expr_int_bool_octet_scrutinee_carrier_tokens_otherwise(catalog: catalog) - OperandUnchanged => target_value_expr_int_bool_octet_scrutinee_carrier_tokens_otherwise(catalog: catalog) - } - } - ) -} - -fn target_value_expr_int_bool_octet_scrutinee_carrier_tokens_otherwise(catalog: Node) -> Outcome> { - outcome_rejected( - target_operator_realization_catalog_miss_diagnostic(catalog: catalog) - ) -} - -fn target_value_expr_match_arm_tokens_int_bool_octet_channel( - arms: List, - projection: TargetValueExpressionProjection, - catalog: Node -) -> Outcome> { - match fold( - arms, - init: Accepted { value: Empty, diagnostics: None }, - f: fn(acc, arm) { - match acc { - Rejected { diagnostics: r } => acc - Accepted { value: prior_tokens, diagnostics: d } => - bind_outcome( - o: target_value_expression_to_concrete_tokens( - expr: arm.pattern, - projection: projection, - catalog: catalog - ), - f: fn(pattern_tokens) { - bind_outcome( - o: target_value_expr_bool_literal_octet_body_tokens( - expr: arm.body, - projection: projection - ), - f: fn(body_tokens) { - let arm_chunk = concrete_syntax_tokens_extend( - acc: concrete_syntax_tokens_snoc_fixed( - acc: pattern_tokens, - token_class: projection.match_form.fat_arrow - ), - more: body_tokens - ) - if is_empty(xs: prior_tokens) { - outcome_accepted(arm_chunk) - } else { - outcome_accepted( - concrete_syntax_tokens_extend( - acc: concrete_syntax_tokens_snoc_fixed( - acc: prior_tokens, - token_class: projection.match_form.arm_separator - ), - more: arm_chunk - ) - ) - } - } - ) - } - ) - } - } - ) { - Accepted { value: tokens, diagnostics: d } => Accepted { value: tokens, diagnostics: d } - Rejected { diagnostics: r } => Rejected { diagnostics: r } - } -} fn target_value_expression_match_to_concrete_tokens( expr: TargetValueExpression, @@ -8509,53 +8296,22 @@ fn target_value_expression_match_to_concrete_tokens_inner( catalog: catalog ), f: fn(scrutinee_tokens) { - if target_value_expr_match_is_int_bool_octet_channel( - scrutinee: scrutinee, - arms: arms - ) { - bind_outcome( - o: target_value_expr_int_bool_octet_scrutinee_carrier_tokens( - scrutinee_tokens: scrutinee_tokens, - catalog: catalog, - projection: projection - ), - f: fn(carrier_scrutinee_tokens) { - bind_outcome( - o: target_value_expr_match_arm_tokens_int_bool_octet_channel( - arms: arms, - projection: projection, - catalog: catalog - ), - f: fn(arm_tokens) { - outcome_accepted( - target_value_expression_match_concrete_token_shell( - scrutinee_tokens: carrier_scrutinee_tokens, - arm_tokens: arm_tokens, - projection: projection - ) - ) - } - ) - } - ) - } else { - bind_outcome( - o: target_value_expr_match_arm_tokens( - arms: arms, - projection: projection, - catalog: catalog - ), - f: fn(arm_tokens) { - outcome_accepted( - target_value_expression_match_concrete_token_shell( - scrutinee_tokens: scrutinee_tokens, - arm_tokens: arm_tokens, - projection: projection - ) + bind_outcome( + o: target_value_expr_match_arm_tokens( + arms: arms, + projection: projection, + catalog: catalog + ), + f: fn(arm_tokens) { + outcome_accepted( + target_value_expression_match_concrete_token_shell( + scrutinee_tokens: scrutinee_tokens, + arm_tokens: arm_tokens, + projection: projection ) - } - ) - } + ) + } + ) } ) } @@ -15378,7 +15134,7 @@ fn target_value_expr_project_int_literal_atom(node: Node) -> Outcome outcome_accepted( target_value_expression_int_literal( - lexeme: integer_int_to_decimal_string(value: magnitude), + lexeme: int_to_decimal_string(value: magnitude), source: node ) ) @@ -15402,6 +15158,10 @@ fn target_value_expr_binding_ref_for_literal_atom(id: Symbol) -> Optional Outcome String { match c { @@ -78,7 +78,7 @@ fn demand_blocked_cause_text(c: DemandBlockedCause) -> String { BlockedWaitingForCapacity => "ready, but no lease was offered for its requirements" BlockedLookupPending => "a materialization lookup for this demand has not answered" BlockedNoDemand => "nothing demanded this judgment" - BlockedKeyConflict => "two distinct subjects derived one demand key" + BlockedKeyConflict { stored: _, requested: _ } => "one demand key was requested under two different natures, so it names two demands" } } @@ -170,7 +170,7 @@ fn demand_state_is_cycle_blocked(s: DemandState) -> Bool { BlockedWaitingForCapacity => false BlockedLookupPending => false BlockedNoDemand => false - BlockedKeyConflict => false + BlockedKeyConflict { stored: _, requested: _ } => false } DemandWaiting => false DemandReady => false @@ -183,10 +183,82 @@ fn demand_state_is_cycle_blocked(s: DemandState) -> Bool { } } +// A KEY CONFLICT IS ALSO A VERDICT FOR THE WHOLE RUN. The key was requested under two different +// natures, so it does not name one demand and no later lifecycle step can make it name one again: +// readiness, seal and settlement each carry it rather than re-deriving over it. +fn demand_state_is_key_conflict(s: DemandState) -> Bool { + match s { + DemandBlocked { cause: c } => + match c { + BlockedKeyConflict { stored: _, requested: _ } => true + BlockedByCycle { members: _ } => false + BlockedByMissingEdge { prerequisite: _ } => false + BlockedByRefusedPrerequisite { prerequisite: _ } => false + BlockedWaitingForCapacity => false + BlockedLookupPending => false + BlockedNoDemand => false + } + DemandWaiting => false + DemandReady => false + DemandLeasePending => false + DemandRunning => false + DemandSuspended => false + DemandCommitPending => false + DemandAvailable { value: _ } => false + DemandRefused => false + } +} + +// A dependent reads a key-conflicted prerequisite as refused: its value can no longer be trusted to +// answer the demand the dependent named, so waiting on it would be a stall with no cause. +fn demand_state_refuses_dependents(s: DemandState) -> Bool { + demand_state_is_refused(s: s) || demand_state_is_key_conflict(s: s) +} + fn demand_state_is_settled(s: DemandState) -> Bool { demand_state_is_available(s: s) || demand_state_is_refused(s: s) } +fn demand_state_is_blocked(s: DemandState) -> Bool { + match s { + DemandBlocked { cause: _ } => true + DemandWaiting => false + DemandReady => false + DemandLeasePending => false + DemandRunning => false + DemandSuspended => false + DemandCommitPending => false + DemandAvailable { value: _ } => false + DemandRefused => false + } +} + +// A VERDICT THAT OUTLIVES THE PRODUCTION UNDER IT. A production may finish after its own key was found +// conflicted or after a prerequisite it read refused; its report is counted, but its result answers no +// demand that still stands, so it does not replace the verdict. +fn demand_state_stands_over_a_report(s: DemandState) -> Bool { + match s { + DemandBlocked { cause: c } => + match c { + BlockedKeyConflict { stored: _, requested: _ } => true + BlockedByRefusedPrerequisite { prerequisite: _ } => true + BlockedByCycle { members: _ } => false + BlockedByMissingEdge { prerequisite: _ } => false + BlockedWaitingForCapacity => false + BlockedLookupPending => false + BlockedNoDemand => false + } + DemandWaiting => false + DemandReady => false + DemandLeasePending => false + DemandRunning => false + DemandSuspended => false + DemandCommitPending => false + DemandAvailable { value: _ } => false + DemandRefused => false + } +} + // ═══════════════════════════════════════════════════════════════════════════════════════════ // THE ENTRY: ONE PRODUCTION, MANY ATTACHMENTS // ═══════════════════════════════════════════════════════════════════════════════════════════ @@ -202,6 +274,7 @@ type DemandEntry { state: DemandState attachments: Int productions: Int + seat_held: Bool } fn demand_entry_new(identity: DemandIdentity, nature: DemandNature) -> DemandEntry { @@ -210,7 +283,8 @@ fn demand_entry_new(identity: DemandIdentity, nature: DemandNature) -> Demand nature: nature, state: DemandWaiting, attachments: 0, - productions: 0 + productions: 0, + seat_held: false } } @@ -284,13 +358,18 @@ fn demand_entry_state(entry: DemandEntry) -> DemandState { entry.state } +fn demand_entry_holds_seat(entry: DemandEntry) -> Bool { + entry.seat_held +} + fn demand_entry_with_state(entry: DemandEntry, state: DemandState) -> DemandEntry { DemandEntry { identity: entry.identity, nature: entry.nature, state: state, attachments: entry.attachments, - productions: entry.productions + productions: entry.productions, + seat_held: entry.seat_held } } @@ -300,7 +379,21 @@ fn demand_entry_produced(entry: DemandEntry, state: DemandState) -> Dem nature: entry.nature, state: state, attachments: entry.attachments, - productions: entry.productions + 1 + productions: entry.productions + 1, + seat_held: entry.seat_held + } +} + +// A SETTLEMENT IS THE PRODUCER'S REPORT, AND ONLY THE PRODUCER MOVES THE SEAT. The seat is held exactly +// while the producer last reported running, whatever verdict the demand's state carries meanwhile. +fn demand_entry_settled(entry: DemandEntry, state: DemandState, reported: DemandState) -> DemandEntry { + DemandEntry { + identity: entry.identity, + nature: entry.nature, + state: state, + attachments: entry.attachments, + productions: entry.productions + 1, + seat_held: demand_state_is_running(s: reported) } } @@ -310,7 +403,8 @@ fn demand_entry_attached(entry: DemandEntry) -> DemandEntry { nature: entry.nature, state: entry.state, attachments: entry.attachments + 1, - productions: entry.productions + productions: entry.productions, + seat_held: entry.seat_held } } @@ -408,7 +502,11 @@ fn demand_engine_request( ) -> DemandEngine { match demand_engine_entry(e: e, id: id) { Present { value: existing } => - if demand_nature_admits_sharing(nature: existing.nature) { + if demand_state_is_key_conflict(s: existing.state) { + e + } else if !nature_eq(a: existing.nature, b: nature) { + demand_engine_key_conflict(e: e, existing: existing, requested: nature) + } else if demand_nature_admits_sharing(nature: existing.nature) { DemandEngine { entries: map_insert( m: e.entries, @@ -463,6 +561,51 @@ fn demand_engine_request( } } +// ONE KEY, TWO NATURES. Sharing is decided by the nature, so an entry may answer a request only when +// the request declares the SAME complete nature: comparing either side's sharing permission alone +// would attach a fresh effect to a pure entry in one order and a pure request to a fresh entry in the +// other, and Pure and Idempotent both share while being different contracts. The comparison is the +// ladder's own nature_eq, not a second one written here. The entry leaves the ready order, its direct +// dependents are re-derived (they now read a refused prerequisite), and the conflict stands for the +// rest of the run -- including over a result that was already available, which is no longer a +// trustworthy answer for either demand. A production already running keeps its seat: the conflict is a +// verdict about the key, not the end of the work, and the seat is released when that production reports. +fn demand_engine_key_conflict( + e: DemandEngine, + existing: DemandEntry, + requested: DemandNature +) -> DemandEngine { + let id = existing.identity + let conflict: DemandState = DemandBlocked { + cause: BlockedKeyConflict { stored: existing.nature, requested: requested } + } + let marked = DemandEngine { + entries: map_insert(m: e.entries, key: id, value: demand_entry_with_state(entry: existing, state: conflict)), + entry_order: e.entry_order, + relations: e.relations, + dependents_of: e.dependents_of, + prerequisites_of: e.prerequisites_of, + ready: demand_ready_remove(ready: e.ready, id: id), + in_flight: e.in_flight, + completion_order: e.completion_order, + readiness_computations: e.readiness_computations, + entries_visited: e.entries_visited + 1 + } + let direct = demand_engine_dependents(e: marked, id: id) + DemandEngine { + entries: demand_engine_reevaluate_only(e: marked, ids: direct), + entry_order: marked.entry_order, + relations: marked.relations, + dependents_of: marked.dependents_of, + prerequisites_of: marked.prerequisites_of, + ready: demand_ready_sync_many(e: marked, ready: marked.ready, ids: direct), + in_flight: marked.in_flight, + completion_order: marked.completion_order, + readiness_computations: marked.readiness_computations + length(xs: direct), + entries_visited: marked.entries_visited + length(xs: direct) + } +} + fn demand_engine_adjacency_push( m: Map>, key: DemandIdentity, @@ -558,6 +701,10 @@ fn demand_in_cycle(e: DemandEngine, id: DemandIdentity) -> Bool { // // A SETTLED DEMAND KEEPS ITS OWN STATE. Recomputing readiness for something already available // would discard the value it carries. +// +// A DEMAND WHOSE PRODUCTION HOLDS A SEAT KEEPS ITS LIFECYCLE STATE unless a prerequisite now blocks it. +// Re-deriving it to ready would offer a second admission for work already in flight; a block is a +// verdict on the demand and lands, while the seat stays with the production until it reports. fn demand_readiness(e: DemandEngine, id: DemandIdentity) -> DemandState { match demand_engine_entry(e: e, id: id) { Absent => DemandBlocked { cause: BlockedNoDemand } @@ -566,40 +713,47 @@ fn demand_readiness(e: DemandEngine, id: DemandIdentity) -> DemandState entry.state } else if demand_state_is_cycle_blocked(s: entry.state) { entry.state + } else if demand_state_is_key_conflict(s: entry.state) { + entry.state } else { - let prereqs = demand_engine_prerequisites(e: e, id: id) - let missing = filter(xs: prereqs, predicate: fn(p) { !demand_engine_has(e: e, id: p) }) - let refused = filter( - xs: prereqs, - predicate: fn(p) { - match demand_engine_entry(e: e, id: p) { - Absent => false - Present { value: pe } => demand_state_is_refused(s: pe.state) + let derived = demand_readiness_from_prerequisites(e: e, id: id) + if demand_entry_holds_seat(entry: entry) && !demand_state_is_blocked(s: derived) { entry.state } else { derived } + } + } +} + +fn demand_readiness_from_prerequisites(e: DemandEngine, id: DemandIdentity) -> DemandState { + let prereqs = demand_engine_prerequisites(e: e, id: id) + let missing = filter(xs: prereqs, predicate: fn(p) { !demand_engine_has(e: e, id: p) }) + let refused = filter( + xs: prereqs, + predicate: fn(p) { + match demand_engine_entry(e: e, id: p) { + Absent => false + Present { value: pe } => demand_state_refuses_dependents(s: pe.state) + } + } + ) + match missing { + Cons { head: m, tail: _ } => DemandBlocked { cause: BlockedByMissingEdge { prerequisite: m } } + Empty => + match refused { + Cons { head: r, tail: _ } => + DemandBlocked { cause: BlockedByRefusedPrerequisite { prerequisite: r } } + Empty => + if for_all( + xs: prereqs, + predicate: fn(p) { + match demand_engine_entry(e: e, id: p) { + Absent => false + Present { value: pe } => demand_state_is_available(s: pe.state) + } } + ) { + DemandReady + } else { + DemandWaiting } - ) - match missing { - Cons { head: m, tail: _ } => DemandBlocked { cause: BlockedByMissingEdge { prerequisite: m } } - Empty => - match refused { - Cons { head: r, tail: _ } => - DemandBlocked { cause: BlockedByRefusedPrerequisite { prerequisite: r } } - Empty => - if for_all( - xs: prereqs, - predicate: fn(p) { - match demand_engine_entry(e: e, id: p) { - Absent => false - Present { value: pe } => demand_state_is_available(s: pe.state) - } - } - ) { - DemandReady - } else { - DemandWaiting - } - } - } } } } @@ -618,13 +772,14 @@ fn demand_engine_seal(e: DemandEngine) -> DemandEngine { entries: demand_engine_entries_mapped( e: e, f: fn(entry) { - if demand_in_cycle(e: e, id: entry.identity) { + if demand_in_cycle(e: e, id: entry.identity) && !demand_state_is_key_conflict(s: entry.state) { DemandEntry { identity: entry.identity, nature: entry.nature, state: DemandBlocked { cause: BlockedByCycle { members: [entry.identity] } }, attachments: entry.attachments, - productions: entry.productions + productions: entry.productions, + seat_held: entry.seat_held } } else { entry @@ -676,7 +831,8 @@ fn demand_engine_reevaluated_states(e: DemandEngine) -> DemandEngine { nature: entry.nature, state: demand_readiness(e: e, id: entry.identity), attachments: entry.attachments, - productions: entry.productions + productions: entry.productions, + seat_held: entry.seat_held } } ), @@ -761,14 +917,19 @@ fn demand_ready_sync( // no demand is ever observably in flight, so the state has nothing to mark; with more than one, the // admission must mark it and settlement must clear it, and the delta below is what clears it. // -// IT STAYS DERIVED FROM STATE RATHER THAN HARDCODED for the same reason. Returning 0 with a comment -// would be correct today and a fail-open the moment that producer lands -- admitting past the seat -// count because the count was a literal -- and the delta costs nothing while the frontier stands. -fn demand_in_flight_delta(before: DemandState, after: DemandState) -> Int { - if demand_state_is_running(s: after) { - if demand_state_is_running(s: before) { 0 } else { 1 } +// IT STAYS DERIVED RATHER THAN HARDCODED for the same reason. Returning 0 with a comment would be +// correct today and a fail-open the moment that producer lands -- admitting past the seat count because +// the count was a literal -- and the delta costs nothing while the frontier stands. +// +// IT IS DERIVED FROM THE ENTRY'S SEAT, NOT FROM ITS STATE. A verdict (a key conflict, a refused +// prerequisite) can replace a running demand's state while its production is still underway; reading +// the seat off the state would release it at the verdict and lose it at the report. Only the producer's +// own report moves the seat (demand_entry_settled). +fn demand_in_flight_delta(before: Bool, after: Bool) -> Int { + if after { + if before { 0 } else { 1 } } else { - if demand_state_is_running(s: before) { 0 - 1 } else { 0 } + if before { 0 - 1 } else { 0 } } } @@ -791,7 +952,7 @@ fn demand_ready_rebuild(e: DemandEngine) -> List { } fn demand_in_flight_rebuild(e: DemandEngine) -> Int { - length(xs: filter(xs: demand_engine_all_entries(e: e), predicate: fn(entry) { demand_state_is_running(s: entry.state) })) + length(xs: filter(xs: demand_engine_all_entries(e: e), predicate: fn(entry) { demand_entry_holds_seat(entry: entry) })) } // ═══════════════════════════════════════════════════════════════════════════════════════════ @@ -846,6 +1007,10 @@ fn demand_engine_next(e: DemandEngine, offer: DemandLeaseOffer) -> DemandA // bound checkable from outside, since a rescan would reach the same final states. The completion order is recorded rather than derived afterwards because it is the // receipt the A/B cutover compares: two runs of one closure must complete in one order, and an // order reconstructed from final states has already lost it. +// +// A production that completes after its key was found conflicted, or after a prerequisite it read +// refused, still counts as produced and releases its seat, but its result does not replace the verdict +// (demand_state_stands_over_a_report). fn demand_engine_settle( e: DemandEngine, id: DemandIdentity, @@ -855,14 +1020,19 @@ fn demand_engine_settle( Absent => DemandWaiting Present { value: prior } => demand_entry_state(entry: prior) } + let held = match map_lookup(m: e.entries, key: id) { + Absent => false + Present { value: prior } => demand_entry_holds_seat(entry: prior) + } + let landed = if demand_state_stands_over_a_report(s: before) { before } else { state } let settled = DemandEngine { - entries: demand_engine_settle_entry(e: e, id: id, state: state), + entries: demand_engine_settle_entry(e: e, id: id, state: landed, reported: state), entry_order: e.entry_order, relations: e.relations, dependents_of: e.dependents_of, prerequisites_of: e.prerequisites_of, - ready: demand_ready_sync(ready: e.ready, id: id, state: state), - in_flight: e.in_flight + demand_in_flight_delta(before: before, after: state), + ready: demand_ready_sync(ready: e.ready, id: id, state: landed), + in_flight: e.in_flight + demand_in_flight_delta(before: held, after: demand_state_is_running(s: state)), completion_order: list_snoc_item(xs: e.completion_order, item: id), readiness_computations: e.readiness_computations, entries_visited: e.entries_visited + 1 @@ -889,12 +1059,13 @@ fn demand_engine_settle( fn demand_engine_settle_entry( e: DemandEngine, id: DemandIdentity, - state: DemandState + state: DemandState, + reported: DemandState ) -> Map> { match map_lookup(m: e.entries, key: id) { Absent => e.entries Present { value: entry } => - map_insert(m: e.entries, key: id, value: demand_entry_produced(entry: entry, state: state)) + map_insert(m: e.entries, key: id, value: demand_entry_settled(entry: entry, state: state, reported: reported)) } } diff --git a/src/v2/std/diagnostic.dag b/src/v2/std/diagnostic.dag index 8956d54c4c7..64221d2dd4b 100644 --- a/src/v2/std/diagnostic.dag +++ b/src/v2/std/diagnostic.dag @@ -4,7 +4,7 @@ import std.types { List } import v2.std.algebra { bag_eq, fold_list } import std.algebra { FreeMonoid, Monoid, Semigroup, list_append, list_snoc_item, freemonoid_empty } import std.dissolution { DissolutionCondition, unbound_dissolution } -import v2.std.node { Node, Symbol } +import v2.std.node { Node, Symbol, symbol_lexeme } type Extent = WholeFile @@ -56,6 +56,12 @@ type NonEmptyDiagnostics { data non_empty_diagnostics_dissolve_trigger: DissolutionCondition = unbound_dissolution(description: "migrate_when_host_emit_names_free_semigroup — NonEmptyDiagnostics is FreeSemigroup by shape (one required Diagnostic plus a list of Diagnostic). It is not an Option alias: that host-emit mapping is Diagnostics -> Option via v1.compiler.emit_rust render_rust_diagnostics_carrier_applied, which hardcodes the type name NonEmptyDiagnostics as the Option inner. Dissolve into std.algebra FreeSemigroup when that renderer names FreeSemigroup rather than this monomorph; until then a second pair carrier remains.") +// A refusal's reasons by name, in order. A producer whose consumer seam is String-typed cannot hand +// the refusal on as data, so the text it hands on instead must still name what refused and why. +fn diagnostics_reason_lexemes(diagnostics: NonEmptyDiagnostics) -> String { + join(map(concat([diagnostics.head], diagnostics.tail), d => symbol_lexeme(sym: d.reason)), ", ") +} + type Diagnostics = None | Some { diagnostics: NonEmptyDiagnostics } diff --git a/src/v2/std/generic_instantiation.dag b/src/v2/std/generic_instantiation.dag index 53b4b848fe5..f795fd4bce1 100644 --- a/src/v2/std/generic_instantiation.dag +++ b/src/v2/std/generic_instantiation.dag @@ -24,7 +24,7 @@ fn generic_substitution_bind_once( name: String, concrete: Node, ) -> Outcome { - match v2.std.collection.map_get(subst, name) { + match v2.std.collection.map_get_checked(subst, name) { Accepted { value: Absent, diagnostics: _ } => outcome_accepted(value: map_insert(subst, name, concrete)) Accepted { value: Present { value: bound }, diagnostics: _ } => diff --git a/src/v2/std/grammar.dag b/src/v2/std/grammar.dag index 4508ed40a68..eb45528c11d 100644 --- a/src/v2/std/grammar.dag +++ b/src/v2/std/grammar.dag @@ -15,7 +15,6 @@ import v2.std.collection { } import std.optional { Absent, - Optional, Present, optional_absent, optional_present @@ -53,6 +52,7 @@ import v2.std.node { Symbol, TypeNode, StructuralLabel, + symbol_intern_lexeme, } import v2.std.node_shape { stamp_node_shape_synthetic, type_shape } import v2.std.compilers.target_model { @@ -64,6 +64,7 @@ import v2.std.compilers.target_model { concrete_syntax_token_to_node, } import v2.std.text { String } +import std.integer { int_to_decimal_string } type FormalTerminal { identity: Symbol @@ -74,7 +75,7 @@ type FormalNonterminal { } type FormalGrammarSymbol - = FormalTerminalOccurrence { terminal: FormalTerminal, binding: Optional } + = FormalTerminalOccurrence { terminal: FormalTerminal, binding: std.optional.Optional } | FormalNonterminalOccurrence { nonterminal: FormalNonterminal } | FormalNestedNonterminalOccurrence { nonterminal: FormalNonterminal, unit: Symbol } @@ -241,6 +242,116 @@ type GrammarExpr | RefuseOnMatch { element: GrammarExpr, reason: ParseRefusalReason } | AfterLineBreak { element: GrammarExpr } | NotPredicate { element: GrammarExpr } + | InfixTable { operand: GrammarExpr, continuation_guard: GrammarExpr, operators: List } + +// AN OPERATOR TABLE, READ BY PRECEDENCE CLIMBING: ONE TREE NODE PER OPERATOR APPLICATION. +// `InfixTable { operand, continuation_guard, operators }` matches an operand followed by any number +// of `operator operand` continuations, and states WHICH operands each operator binds by the rows' +// binding powers rather than by nesting one grammar level per precedence. A tower of levels -- +// `level = sequence(next_level, repeat(sequence(op, next_level)))`, once per precedence -- states +// the same language, but every operand then walks every level and the parse stamps an empty +// continuation at each, so the tree carries one node per LEVEL per operand that no reader wants and +// the lowering had to strip again (docs/plans/precedence-climbing-tree-contract.md). Here an operand +// with no operator after it is captured as itself, and each operator consumed is ONE application +// node with three projections (grammar_infix_left / _operator / _right). +// +// The rows are an index, never an ordered alternative: each row names ONE token class, and grammar +// validation refuses a table that names a class twice (v2.compiler.parse +// grammar_infix_tables_well_formed), so selecting a row is a lookup and no choice exists to overlap. +// `continuation_guard` is a zero-width predicate run at every continuation point before the +// operator is read -- the place a layout refusal such as the dag newline-minus guard stands. +// A higher binding_power binds tighter. InfixLeft folds `a - b - c` as `(a - b) - c`; +// InfixNonAssociative refuses a second operator of the same binding power +// (NonAssociativeOperatorChain), so `a < b < c` is describable source no accepted tree contains. +type InfixAssociativity = InfixLeft | InfixNonAssociative + +type InfixOperatorRow { + token_class: Symbol + binding_power: Int + associativity: InfixAssociativity +} + +fn grammar_expr_infix_table( + operand: GrammarExpr, + continuation_guard: GrammarExpr, + operators: List +) -> GrammarExpr { + InfixTable { operand: operand, continuation_guard: continuation_guard, operators: operators } +} + +fn grammar_infix_row_lookup(rows: List, token_class: Symbol) -> Optional { + match list_head(xs: rows) { + HeadAbsent => optional_absent() + HeadFound { value: row } => + if grammar_infix_row_class(row: row) == token_class { + optional_present(value: row) + } else { + match list_tail(xs: rows) { + TailAbsent => optional_absent() + TailFound { tail: rest } => grammar_infix_row_lookup(rows: rest, token_class: token_class) + } + } + } +} + +// Projectors at a typed parameter (gunbc.recurring_failure_mode.list_head_bound_record_element_stays_t). +fn grammar_infix_row_class(row: InfixOperatorRow) -> Symbol { row.token_class } +fn grammar_infix_row_binding_power(row: InfixOperatorRow) -> Int { row.binding_power } + +// The minimum binding power an operand on each side of an application of `row` must carry: a +// left-associative left operand may repeat the row's own power, nothing else may. +fn grammar_infix_left_min_power(row: InfixOperatorRow) -> Int { + match row.associativity { + InfixLeft => row.binding_power + InfixNonAssociative => row.binding_power + 1 + } +} + +fn grammar_infix_right_min_power(row: InfixOperatorRow) -> Int { + row.binding_power + 1 +} + +fn grammar_infix_operator_choice(rows: List) -> Optional { + match list_head(xs: rows) { + HeadAbsent => optional_absent() + HeadFound { value: row } => + let here = grammar_expr_terminal(token_class: grammar_infix_row_class(row: row)) + match list_tail(xs: rows) { + TailAbsent => optional_present(value: here) + TailFound { tail: rest } => + match grammar_infix_operator_choice(rows: rest) { + Absent => optional_present(value: here) + Present { value: others } => optional_present(value: Choice { left: here, right: others }) + } + } + } +} + +// THE STRING LANGUAGE A TABLE ACCEPTS, AS AN ORDINARY EXPRESSION, for the analyses that read only +// the language and never the tree: nullability, FIRST and k-token lookahead. Precedence decides +// which operands an operator binds, never which token strings are admitted, so the language is +// `operand (guard operator operand)*` -- an over-approximation by exactly the non-associative chains +// the table refuses, which is the conservative side for an overlap proof. It is DERIVED from the +// table and never parsed, emitted or stamped: the tree contract is the table's own. +fn grammar_infix_table_language( + operand: GrammarExpr, + continuation_guard: GrammarExpr, + operators: List +) -> GrammarExpr { + match grammar_infix_operator_choice(rows: operators) { + Absent => operand + Present { value: ops } => + Sequence { + left: operand, + right: Repeat { + element: Sequence { + left: continuation_guard, + right: Sequence { left: ops, right: operand } + } + } + } + } +} // A PRODUCTION WHOSE MATCH IS A REFUSAL. `RefuseOnMatch { element, reason }` parses `element` and, // where it MATCHES, refuses the whole parse with `reason`, located at the first token of the match: @@ -286,7 +397,7 @@ fn grammar_expr_expect(element: GrammarExpr, reason: ParseRefusalReason) -> Gram } fn grammar_expr_optional(element: GrammarExpr) -> GrammarExpr { - Optional { element: element } + v2.std.grammar.Optional { element: element } } fn grammar_expr_terminal(token_class: Symbol) -> GrammarExpr { @@ -337,6 +448,7 @@ type GrammarExprFold { refuse_on_match: fn(R, ParseRefusalReason) -> R after_line_break: fn(R) -> R not_predicate: fn(R) -> R + infix_table: fn(R, R, List) -> R } fn fold_grammar_expr(expr: GrammarExpr, algebra: GrammarExprFold) -> R { @@ -356,7 +468,7 @@ fn fold_grammar_expr(expr: GrammarExpr, algebra: GrammarExprFold) -> R { fold_grammar_expr(expr: left, algebra: algebra), fold_grammar_expr(expr: right, algebra: algebra) ) - Optional { element: element } => + v2.std.grammar.Optional { element: element } => algebra.optional(fold_grammar_expr(expr: element, algebra: algebra)) Repeat { element: element } => algebra.repeat(fold_grammar_expr(expr: element, algebra: algebra)) @@ -368,6 +480,12 @@ fn fold_grammar_expr(expr: GrammarExpr, algebra: GrammarExprFold) -> R { algebra.after_line_break(fold_grammar_expr(expr: element, algebra: algebra)) NotPredicate { element: element } => algebra.not_predicate(fold_grammar_expr(expr: element, algebra: algebra)) + InfixTable { operand: operand, continuation_guard: guard, operators: operators } => + algebra.infix_table( + fold_grammar_expr(expr: operand, algebra: algebra), + fold_grammar_expr(expr: guard, algebra: algebra), + operators + ) } } @@ -540,7 +658,7 @@ fn formal_terminal_to_node(terminal: FormalTerminal) -> Node { } } -fn formal_terminal_occurrence_to_node(terminal: FormalTerminal, binding: Optional) -> Node { +fn formal_terminal_occurrence_to_node(terminal: FormalTerminal, binding: std.optional.Optional) -> Node { match binding { Present { value: binding } => Node { @@ -1149,7 +1267,7 @@ fn grammar_nested_block_edge(unit: Symbol, edges: List) -> Edge { } } -fn grammar_terminal_occurrence_to_token_edge(terminal: FormalTerminal, binding: Optional) -> Edge { +fn grammar_terminal_occurrence_to_token_edge(terminal: FormalTerminal, binding: std.optional.Optional) -> Edge { match binding { Present { value: binding } => Edge { @@ -1634,7 +1752,7 @@ fn formal_symbol_list_insert(xs: List, item: Symbol) -> List { // does slot i want" by rescanning the rhs from the start for every i made resolving one node of N // slots O(N^2) -- the term under every long grammar row (an N-item orchestration sequence // measured superlinear through it). -fn formal_production_slot_nonterminals(production: FormalProduction) -> List> { +fn formal_production_slot_nonterminals(production: FormalProduction) -> List> { fold_list( xs: production.rhs, empty: Empty, @@ -1655,7 +1773,7 @@ fn formal_production_slot_nonterminals(production: FormalProduction) -> List>>) -> List { +fn formal_slot_cursor_heads_asked(cursors: List>>) -> List { fold_list( xs: cursors, empty: Empty, @@ -1669,7 +1787,7 @@ fn formal_slot_cursor_heads_asked(cursors: List>>) -> List ) } -fn formal_slot_cursors_advanced(cursors: List>>) -> List>> { +fn formal_slot_cursors_advanced(cursors: List>>) -> List>> { fold_list( xs: cursors, empty: Empty, @@ -1678,7 +1796,7 @@ fn formal_slot_cursors_advanced(cursors: List>>) -> List>> + cursors: List>> slots: List } @@ -2443,11 +2561,61 @@ fn grammar_expr_to_node(expr: GrammarExpr) -> Node { grammar_expr_node(children: [ grammar_named_edge(name: ^grammar_not_predicate_element_node_projection, target: element_node) ]) + }, + infix_table: fn(operand_node, guard_node, operators) { + grammar_expr_node(children: [ + grammar_named_edge(name: ^grammar_infix_table_operand_node_projection, target: operand_node), + grammar_named_edge(name: ^grammar_infix_table_guard_node_projection, target: guard_node), + grammar_named_edge( + name: ^grammar_infix_table_operators_node_projection, + target: grammar_infix_rows_node(rows: operators) + ) + ]) } } ) } +fn grammar_infix_associativity_symbol(associativity: InfixAssociativity) -> Symbol { + match associativity { + InfixLeft => ^grammar_infix_left_associative + InfixNonAssociative => ^grammar_infix_non_associative + } +} + +// The rows as a right-folded spine, as a repeat's capture is: a Conj's labels are unique, so a list +// cannot be one Conj with a repeated label. +fn grammar_infix_rows_node(rows: List) -> Node { + match list_head(xs: rows) { + HeadAbsent => grammar_expr_node(children: []) + HeadFound { value: row } => + grammar_expr_node(children: [ + grammar_named_edge(name: ^grammar_sequence_left_node_projection, target: grammar_infix_row_node(row: row)), + grammar_named_edge( + name: ^grammar_sequence_right_node_projection, + target: match list_tail(xs: rows) { + TailAbsent => grammar_expr_node(children: []) + TailFound { tail: rest } => grammar_infix_rows_node(rows: rest) + } + ) + ]) + } +} + +fn grammar_infix_row_node(row: InfixOperatorRow) -> Node { + grammar_expr_node(children: [ + grammar_named_edge(name: ^grammar_terminal_token_class_node_projection, target: grammar_atom(identity: row.token_class)), + grammar_named_edge( + name: ^grammar_infix_binding_power_node_projection, + target: grammar_atom(identity: symbol_intern_lexeme(lexeme: int_to_decimal_string(value: row.binding_power))) + ), + grammar_named_edge( + name: ^grammar_infix_associativity_node_projection, + target: grammar_atom(identity: grammar_infix_associativity_symbol(associativity: row.associativity)) + ) + ]) +} + fn grammar_production_to_node(production: GrammarProduction) -> Node { Node { kind: TypeNode { @@ -3524,12 +3692,77 @@ fn grammar_emit_expr(index: GrammarEmitIndex, expr: GrammarExpr, node: Node) -> Nonterminal { production: p } => grammar_emit_production_capture(index: index, expected: p, node: node) Sequence { left: l, right: r } => grammar_emit_sequence(index: index, left: l, right: r, node: node) Choice { left: l, right: r } => grammar_emit_choice(index: index, left: l, right: r, node: node) - Optional { element: e } => grammar_emit_optional(index: index, element: e, node: node) + v2.std.grammar.Optional { element: e } => grammar_emit_optional(index: index, element: e, node: node) Repeat { element: e } => grammar_emit_repeat(index: index, element: e, node: node) Expect { element: e, reason: _ } => grammar_emit_expr(index: index, expr: e, node: node) RefuseOnMatch { element: _, reason: _ } => grammar_emit_mismatch(reason: ^grammar_emit_refuse_on_match_derives_no_tree, at: node) AfterLineBreak { element: e } => grammar_emit_after_line_break(index: index, element: e, node: node) NotPredicate { element: _ } => GrammarEmitWalked { terminals: [] } + InfixTable { operand: operand, continuation_guard: _, operators: operators } => + grammar_emit_infix(index: index, operand: operand, operators: operators, node: node, min_power: 0) + } +} + +// THE INVERSE OF v2.compiler.parse parse_expr_infix_table. A node carrying the operator projection is +// one application: its left side, the operator's terminal, its right side. Any other node is an +// operand. An application whose operator no row names, or whose binding power is below what its +// position admits -- `a - (b - c)` stamped without its parentheses, or a non-associative operator +// directly under another of the same power -- REFUSES: emitting it would write text that reads back +// as a different tree, and a parse tree never needs a parenthesis inserted, because a grouping is an +// operand of its own. The continuation guard is a predicate, so it emits nothing. +fn grammar_emit_infix( + index: GrammarEmitIndex, + operand: GrammarExpr, + operators: List, + node: Node, + min_power: Int +) -> GrammarEmitWalk { + match named_child_lookup(root: node, name: ^grammar_infix_operator_node_projection) { + NamedChildMissing => grammar_emit_expr(index: index, expr: operand, node: node) + NamedChildAmbiguous => grammar_emit_projection_ambiguous(at: node) + NamedChildFound { target: op } => + match node_atom_identity_optional(node: op) { + Absent => GrammarEmitRefused { diagnostic: grammar_emit_diagnostic(reason: ^grammar_emit_infix_operator_unknown, at: op) } + Present { value: token_class } => + match grammar_infix_row_lookup(rows: operators, token_class: token_class) { + Absent => GrammarEmitRefused { diagnostic: grammar_emit_diagnostic(reason: ^grammar_emit_infix_operator_unknown, at: op) } + Present { value: row } => + if grammar_infix_row_binding_power(row: row) < min_power { + GrammarEmitRefused { diagnostic: grammar_emit_diagnostic(reason: ^grammar_emit_infix_binding_violated, at: node) } + } else { + grammar_emit_infix_application(index: index, operand: operand, operators: operators, node: node, op: op, row: row) + } + } + } + } +} + +fn grammar_emit_infix_application( + index: GrammarEmitIndex, + operand: GrammarExpr, + operators: List, + node: Node, + op: Node, + row: InfixOperatorRow +) -> GrammarEmitWalk { + match named_child_lookup(root: node, name: ^grammar_infix_left_node_projection) { + NamedChildMissing => GrammarEmitRefused { diagnostic: grammar_emit_diagnostic(reason: ^grammar_emit_infix_shape_mismatch, at: node) } + NamedChildAmbiguous => grammar_emit_projection_ambiguous(at: node) + NamedChildFound { target: left } => + match named_child_lookup(root: node, name: ^grammar_infix_right_node_projection) { + NamedChildMissing => GrammarEmitRefused { diagnostic: grammar_emit_diagnostic(reason: ^grammar_emit_infix_shape_mismatch, at: node) } + NamedChildAmbiguous => grammar_emit_projection_ambiguous(at: node) + NamedChildFound { target: right } => + match grammar_emit_infix(index: index, operand: operand, operators: operators, node: left, min_power: grammar_infix_left_min_power(row: row)) { + GrammarEmitWalked { terminals: l } => + grammar_emit_concat( + left: list_snoc_item(xs: l, item: GrammarEmitClassTerminal { token_class: grammar_infix_row_class(row: row), captured: op }), + right: grammar_emit_infix(index: index, operand: operand, operators: operators, node: right, min_power: grammar_infix_right_min_power(row: row)) + ) + GrammarEmitArmMismatch { diagnostic: d } => GrammarEmitArmMismatch { diagnostic: d } + GrammarEmitRefused { diagnostic: d } => GrammarEmitRefused { diagnostic: d } + } + } } } diff --git a/src/v2/std/grammar_choice_ambiguity.dag b/src/v2/std/grammar_choice_ambiguity.dag index bc9e1fcc2e7..495064ae759 100644 --- a/src/v2/std/grammar_choice_ambiguity.dag +++ b/src/v2/std/grammar_choice_ambiguity.dag @@ -1,13 +1,11 @@ module v2.std.grammar_choice_ambiguity -import std.dissolution { DissolutionCondition, dissolution_description, unbound_dissolution } - import std.algebra { FreeMonoid } import v2.std.node { Symbol } // GrammarChoiceAmbiguityRow is the grammar-analysis authority for FIRST-set choice overlap facts. -// Compiler parse stage and frontier probe receipts import this module — not v2.compiler.parse — so -// probe instrumentation does not pull the parse implementation into generic receipt consumers. +// The parse stage's validation refuses on any row, so a row is a fact about a grammar a caller +// supplied, never a residue an accepted grammar carries. type GrammarChoiceAmbiguityRow { production: Symbol @@ -16,10 +14,3 @@ type GrammarChoiceAmbiguityRow { overlap_terminals: FreeMonoid both_nullable: Bool } - -type NonEmptyGrammarChoiceAmbiguityRows { - head: GrammarChoiceAmbiguityRow - tail: FreeMonoid -} - -data non_empty_grammar_choice_ambiguity_rows_dissolve_trigger: DissolutionCondition = unbound_dissolution(description: "cut_over_overlap_row_chain — NonEmptyGrammarChoiceAmbiguityRows is FreeSemigroup by shape. std.algebra FreeSemigroup is already in the tree. Dissolve when v2.compiler.parse_diagnostic non_empty_grammar_choice_ambiguity_rows_from_parse_stage (and the three literals in v2.test.claim.self_host.frontier_probe_overlap_detail_test) construct FreeSemigroup, then delete this type. The previous event name migrate_when_free_semigroup_available has already fired.") diff --git a/src/v2/std/grounding.dag b/src/v2/std/grounding.dag index d403e540b7f..e7fb593e4c3 100644 --- a/src/v2/std/grounding.dag +++ b/src/v2/std/grounding.dag @@ -76,7 +76,7 @@ fn insert_per_language_fact_bundle_entry( key: PerLanguageFactBundleKey, fact_value: Node ) -> Outcome { - match v2.std.collection.map_get(m: registry.by_key, key: key) { + match v2.std.collection.map_get_checked(m: registry.by_key, key: key) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: _ } => outcome_rejected(d: per_language_fact_bundle_duplicate_key_diagnostic()) @@ -110,7 +110,7 @@ fn per_language_fact_bundle_insert_subject_axis( target: TargetModel, fact_axis: ModelCorePrimitiveFactAxis ) -> Outcome { - match v2.std.collection.map_get( + match v2.std.collection.map_get_checked( m: registry.by_key, key: per_language_fact_bundle_subject_axis_key( subject_carrier: subject_carrier, diff --git a/src/v2/std/inhabitance.dag b/src/v2/std/inhabitance.dag index fd0c0135184..001b215407a 100644 --- a/src/v2/std/inhabitance.dag +++ b/src/v2/std/inhabitance.dag @@ -86,12 +86,17 @@ fn type_variable_instance_lookup( }) } +// A match's arms against its scrutinee's coproduct: `declared` is the scrutinee's declared +// coproduct and `produced` is the arms' covered carrier, so the one relation decides coverage -- +// exhaustive exactly when the arms structurally equal the whole coproduct +// (gunbc.recurring_failure_mode native_infer_accepts_only_bool_and_int_match_scrutinees). type DeclaredTypePosition = PositionDirectCallArgument | PositionDeclaredReturn | PositionRecordField { record: Node } | PositionLetAnnotation { bound: Node } | PositionBinderDefault { default: Node } + | PositionMatchCoverage { match_node: Node, scrutinee: Node } // type_variables are the declaring Arrow's type parameters (v2.std.type_binder type_param_names): // the only atoms in `declared` that name no type, and so the only thing that keeps an @@ -142,6 +147,10 @@ fn position_record_field(record: Node) -> DeclaredTypePosition { PositionRecordField { record: record } } +fn position_match_coverage(match_node: Node, scrutinee: Node) -> DeclaredTypePosition { + PositionMatchCoverage { match_node: match_node, scrutinee: scrutinee } +} + // An annotated let (`let x: T = e`) is a declared position like the others, not a cast. Its // obligation is located at the authored annotation (the declared type the author wrote, as a // declared return is located at the body it judges), and the position names the bound value e @@ -161,6 +170,10 @@ fn inhabitance_no_candidate_binder_default_reason() -> Symbol { ^infer_reason_default_does_not_inhabit_binder_type } +fn inhabitance_no_candidate_match_coverage_reason() -> Symbol { + ^infer_reason_match_arms_do_not_cover_scrutinee +} + fn inhabitance_position_symbol(position: DeclaredTypePosition) -> Symbol { match position { PositionDirectCallArgument => ^position_direct_call_argument @@ -168,6 +181,7 @@ fn inhabitance_position_symbol(position: DeclaredTypePosition) -> Symbol { PositionRecordField { record: _ } => ^position_record_field PositionLetAnnotation { bound: _ } => ^position_let_annotation PositionBinderDefault { default: _ } => ^position_binder_default + PositionMatchCoverage { match_node: _, scrutinee: _ } => ^position_match_coverage } } @@ -398,6 +412,7 @@ fn inhabitance_position_context_edges(position: DeclaredTypePosition) -> List [Edge { label: Authored { name: ^inhabitance_record }, target: record }] PositionLetAnnotation { bound: bound } => [Edge { label: Authored { name: ^inhabitance_bound_value }, target: bound }] PositionBinderDefault { default: default } => [Edge { label: Authored { name: ^inhabitance_default_value }, target: default }] + PositionMatchCoverage { match_node: _, scrutinee: scrutinee } => [Edge { label: Authored { name: ^inhabitance_scrutinee }, target: scrutinee }] } } @@ -408,6 +423,7 @@ fn inhabitance_no_candidate_reason(position: DeclaredTypePosition) -> Symbol { PositionRecordField { record: _ } => inhabitance_no_candidate_record_field_reason() PositionLetAnnotation { bound: _ } => inhabitance_no_candidate_let_annotation_reason() PositionBinderDefault { default: _ } => inhabitance_no_candidate_binder_default_reason() + PositionMatchCoverage { match_node: _, scrutinee: _ } => inhabitance_no_candidate_match_coverage_reason() } } diff --git a/src/v2/std/integer.dag b/src/v2/std/integer.dag index b1764d4b420..61488f4f00a 100644 --- a/src/v2/std/integer.dag +++ b/src/v2/std/integer.dag @@ -18,12 +18,12 @@ import std.optional { import v2.std.diagnostic { Correction, Diagnostic, ExternalContractUnknown, Locus, NoCorrectionReason, Outcome, Accepted, None, Rejected, diagnostics_singleton, node_locus, outcome_accepted, outcome_rejected, invariant_port_locus, port_refusal_locus, Unavailable, AmbiguousIntent } import v2.std.node { Arrow, Atom, Cardinality, ComputationNode, Conj, Disj, Edge, Instantiation, Authored, Node, Symbol, TypeNode, is_empty_conj_root } import v2.std.node_query { find_named_child } -import std.integer { Signedness, Signed, Unsigned } +import std.integer { Signedness, Signed, Unsigned, decimal_digit_of_units, int_to_decimal_string } import std.content_hash { Fnv1a64Structural, content_hash_atom, content_hash_tagged_structural } import std.types { NonEmptyStr, List } import v2.std.machine { Byte } import std.machine_constraints { Compose, MachineWidth, PointerWidth } -import std.nat { Nat, Zero, Succ, NatQuotient, nat_div_rem_by_succ } +import std.nat { Nat, Zero, Succ, NatQuotient, nat_div_rem_by_succ, DecimalDigit, D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, decimal_digit_value } import v2.std.text { Char, CharAbsent, CharFound, String, string_head, string_tail } import std.bit { Word64 } type Int = GroupCompletion @@ -59,18 +59,6 @@ type IntegerRangeEndpoint = IntegerRangeInclusiveMin | IntegerRangeInclusiveMax -type DecimalDigit - = D0 - | D1 - | D2 - | D3 - | D4 - | D5 - | D6 - | D7 - | D8 - | D9 - type NonZeroDecimalDigit = NonZeroD1 | NonZeroD2 @@ -117,39 +105,10 @@ fn integer_signed_i32_le_decode_invalid_diagnostic() -> Diagnostic { } } -fn decimal_digit_value(d: DecimalDigit) -> Int { - match d { - D0 => 0 - D1 => 1 - D2 => 2 - D3 => 3 - D4 => 4 - D5 => 5 - D6 => 6 - D7 => 7 - D8 => 8 - D9 => 9 - } -} - -data decimal_digit_roster: List = [D0, D1, D2, D3, D4, D5, D6, D7, D8, D9] - data nonzero_decimal_digit_roster: List = [ NonZeroD1, NonZeroD2, NonZeroD3, NonZeroD4, NonZeroD5, NonZeroD6, NonZeroD7, NonZeroD8, NonZeroD9 ] -fn decimal_digit_of_units(value: Int) -> DecimalDigit { - if value >= 10 { - decimal_digit_of_units(value: value - 10) - } else { - fold_list( - xs: decimal_digit_roster, - empty: D0, - cons: fn(acc, d) { if decimal_digit_value(d: d) == value { d } else { acc } } - ) - } -} - fn nonzero_decimal_digit_of_positive_units(value: Int) -> NonZeroDecimalDigit { fold_list( xs: nonzero_decimal_digit_roster, @@ -1409,101 +1368,6 @@ fn integer_signed_i32_le_bytes_to_int(bytes: List) -> Outcome { } } -// ONE GLYPH TABLE for the ten decimal digits, keyed on the DecimalDigit coproduct this module -// already owns; both the Peano and the machine-integer renderings below reach it through their -// own digit selection rather than each carrying a second copy of the ten glyphs. -fn decimal_digit_glyph(d: DecimalDigit) -> String { - match d { - D0 => "0" - D1 => "1" - D2 => "2" - D3 => "3" - D4 => "4" - D5 => "5" - D6 => "6" - D7 => "7" - D8 => "8" - D9 => "9" - } -} - -// The Peano value of each decimal digit. Every arm is a kernel integer literal at a -// Nat return boundary: the literal elaborates through the declared homomorphism -// (gunbc.structural_realization_bindings literal_homomorphism_rows) to its Zero/Succ image -- -// this function is the return-position exemplar of std.literal_elaboration. -fn decimal_digit_nat(d: DecimalDigit) -> Nat { - match d { - D0 => 0 - D1 => 1 - D2 => 2 - D3 => 3 - D4 => 4 - D5 => 5 - D6 => 6 - D7 => 7 - D8 => 8 - D9 => 9 - } -} - -// A Peano digit (a Nat below ten) to its glyph: select the DecimalDigit whose Peano value equals -// the digit by STRUCTURAL equality (std.operator_realization StructuralEquality on Nat), -// then read the one glyph table. A digit outside the roster (ten or more) is not a decimal digit -// and this function is not its authority: the caller obtains digits from nat_div_rem_by_succ by -// ten, whose remainder is below ten by construction, so the fold's D0 seed is never the answer -// for an in-domain input. -fn integer_nat_decimal_digit_char(digit: Nat) -> String { - decimal_digit_glyph(d: fold_list( - xs: decimal_digit_roster, - empty: D0, - cons: fn(acc, d) { if decimal_digit_nat(d: d) == digit { d } else { acc } } - )) -} - -// PEANO NAT TO DECIMAL TEXT over the DECLARED structural operations: division by ten is -// nat_div_rem_by_succ with divisor predecessor nine (the divisor Succ { prev: 9 } is nonzero by -// construction, so no zero-divisor arm exists to be handled), and the recursion descends on the -// quotient. The literal 9 elaborates at the argument boundary (std.literal_elaboration). No host -// operator is applied to a Nat here: the previous body spelled `value == 0`, `value < 10`, -// `value / 10` and `value - (rest * 10)` on Rc, which rustc refused (E0308 x10, E0369) and -// which the operator realization authority now refuses at emission by typed diagnostic instead. -fn integer_nat_to_decimal_string(value: Nat) -> String { - let q = nat_div_rem_by_succ(a: value, divisor_prev: 9) - match q.quotient { - Zero => integer_nat_decimal_digit_char(digit: q.remainder) - Succ { prev: _ } => - concat( - integer_nat_to_decimal_string(value: q.quotient), - integer_nat_decimal_digit_char(digit: q.remainder) - ) - } -} - -// MACHINE INTEGER TO DECIMAL TEXT. Int realizes as the host integer, so host division and -// remainder are its declared operations (std.operator_realization HostOperator); the digit glyph -// comes from the same one table. This is the formatter the two Int-valued callers that previously -// passed an Int into the Nat renderer's parameter (v2.std.native_agreement octet_display, -// v2.workflow.realization_sweep sweep_histogram_tsv) actually asked for -- the interpreter's -// numeric grounding made that mismatch invisible there, and the emitted closure refused it -// (E0308 Rc vs integer). -fn integer_int_to_decimal_string(value: Int) -> String { - if value < 0 { - concat("-", integer_int_magnitude_to_decimal_string(value: 0 - value)) - } else { - integer_int_magnitude_to_decimal_string(value: value) - } -} - -fn integer_int_magnitude_to_decimal_string(value: Int) -> String { - let rest = value / 10 - let digit = decimal_digit_glyph(d: decimal_digit_of_units(value: value - (rest * 10))) - if rest == 0 { - digit - } else { - concat(integer_int_magnitude_to_decimal_string(value: rest), digit) - } -} - // THE CONTENT IDENTITY OF AN Int, at cost proportional to its digits. The digests already in the // corpus that take an integer are unary -- v2.std.node byte_limb_hash_peano_digest (one // combine_hash per unit, one shared tag for every value outside 0..255) and the byte-offset cache @@ -1513,6 +1377,6 @@ fn integer_int_magnitude_to_decimal_string(value: Int) -> String { fn integer_int_content_digest(value: Int) -> Fnv1a64Structural { content_hash_tagged_structural( tag: "integer-int-decimal", - payload: content_hash_atom(value: integer_int_to_decimal_string(value: value) as NonEmptyStr) + payload: content_hash_atom(value: int_to_decimal_string(value: value) as NonEmptyStr) ) } diff --git a/src/v2/std/lens_verdict.dag b/src/v2/std/lens_verdict.dag index 6b7002fa82e..984237da1ec 100644 --- a/src/v2/std/lens_verdict.dag +++ b/src/v2/std/lens_verdict.dag @@ -17,7 +17,7 @@ import v2.std.diagnostic { WholeFile, textual_whole_file_diagnostic, } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import extdeps.languages.json.emit { quote_json_string } import v2.std.node { Node, Symbol, symbol_intern_lexeme, symbol_lexeme } import v2.std.grammar { node_atom_identity_optional } @@ -83,8 +83,8 @@ fn lens_verdict_extent_text(e: Extent) -> String { WholeFile => "(whole file)" ByteRange { start: st, end: en } => concat( - concat("bytes ", integer_int_to_decimal_string(value: st)), - concat("..", integer_int_to_decimal_string(value: en)) + concat("bytes ", int_to_decimal_string(value: st)), + concat("..", int_to_decimal_string(value: en)) ) } } @@ -123,11 +123,11 @@ fn lens_verdict_node_occurrence_text(node: Node) -> String { match node.occurrence_id { OccurrenceSynthetic => concat("")) OccurrenceMinted { id: id } => - concat(""))) + concat(""))) OccurrenceProjected { id: id, caused_by: _ } => - concat(""))) + concat(""))) OccurrencePending { caused_by: cause } => - concat(""))) + concat(""))) } } diff --git a/src/v2/std/member_declaration_kind.dag b/src/v2/std/member_declaration_kind.dag new file mode 100644 index 00000000000..cd332a3a74b --- /dev/null +++ b/src/v2/std/member_declaration_kind.dag @@ -0,0 +1,105 @@ +module v2.std.member_declaration_kind + +import std.types { List } +import std.algebra { FreeMonoid } +import v2.std.algebra { fold_list } +import v2.std.collection { Map, empty_map, map_contains_key, map_insert } +import v2.std.integer { Int } +import v2.std.node { Symbol } +import v2.std.qualified_name { QualifiedName, qualified_name_to_dotted_string } +import v2.std.text { String } + +// WHICH KIND OF DECLARATION AUTHORED EACH TOP-LEVEL MEMBER, carried because lowering erases it. +// Normalize lowers a `data` declaration to a named nullary Arrow member +// (v2.compiler.body_lowering_fold body_lower_data_decl_to_member) whose shape is, by design, +// identical to a zero-argument `fn` lowered by body_lower_fn_decl_to_arrow: both are a Conj member +// whose Authored edge names an Arrow with an empty domain, a codomain and a body. After lowering no +// reader of the Node can tell `data x: T = v` from `fn x() -> T { v }`, so a reader that must hold +// the first and refuse the second has nothing to read, and classifying a generic Arrow as a data +// declaration is the impersonation hole. The fact is read where it still exists -- on the parse, at +// the one capture site of v2.compiler.normalize -- exactly as type_declaration_kinds is, sealed on +// NormalizedTree beside the root rather than as a structural child edge (which would change the +// content hash of every data declaration), and handed forward by the caller that holds it. +type MemberDeclarationKind + = DeclaredDataMember + | DeclaredFnMember + +// A top-level member by its authored name and kind, as read from ONE module's parse. Unqualified: +// the module that declares it is the NormalizedTree it rides on. +type DeclaredMember { + name: Symbol + kind: MemberDeclarationKind +} + +// A member by its DECLARING path -- the module's qualified name and the member's own name, never a +// bare spelling, because .dag member names are corpus-global (the way symbol_index_fill qualifies). +type QualifiedDeclaredMember { + module: QualifiedName + name: Symbol + kind: MemberDeclarationKind +} + +// THE CHANNEL A LATER-GRAIN CONSUMER RECEIVES. It names the modules it COVERS, because an empty member +// list is ambiguous between "this module declares no such member" and "nothing was read": the first is +// a fact, the second is the absence of one, and a consumer that reads the second as the first refuses +// every module (a false absence) while one that reads it as "fine" accepts every module (a fabricated +// pass). MemberKindsUnavailable is the explicit second arm, so a constructor site that holds no parse +// says so by value (no_member_declaration_kinds), like no_type_declaration_kinds. +type MemberKindChannel + = MemberKindsUnavailable + | MemberKindsCaptured { covered: List, members: List } + +fn no_member_declaration_kinds() -> MemberKindChannel { + MemberKindsUnavailable +} + +type MemberKindLookup + = MemberKindProvenanceUnavailable + | MemberKindNotDeclared + | MemberKindDeclared { kind: MemberDeclarationKind } + +fn member_kind_module_is(module: QualifiedName, wanted: String) -> Bool { + qualified_name_to_dotted_string(qn: module) == wanted +} + +// The kind the channel records for `name` declared at the top level of the module whose dotted name is +// `module`. Unavailable when the channel was never read OR does not cover that module: both mean the +// provenance is not there, and neither may answer "not declared". +fn member_kind_lookup(channel: MemberKindChannel, module: String, name: Symbol) -> MemberKindLookup { + match channel { + MemberKindsUnavailable => MemberKindProvenanceUnavailable + MemberKindsCaptured { covered: covered, members: members } => + if fold(covered, init: false, f: fn(acc, m) { acc || member_kind_module_is(module: m, wanted: module) }) { + fold(members, init: MemberKindNotDeclared, f: fn(acc, m) { + if member_kind_module_is(module: m.module, wanted: module) && (m.name == name) { + MemberKindDeclared { kind: m.kind } + } else { + acc + } + }) + } else { + MemberKindProvenanceUnavailable + } + } +} + +// Two members of one module sharing a name is a duplicate declaration whatever their kinds (a data and a fn of +// one name conflict as surely as two of one kind); the channel refuses it, never first-wins. ONE PASS: the names +// seen so far ride a map, so each member costs one membership probe instead of a rescan of the list. +type MemberNamesSeen { + seen: Map + duplicate: Bool +} + +fn declared_members_have_duplicate(members: FreeMonoid) -> Bool { + fold_list( + xs: members, + empty: MemberNamesSeen { seen: empty_map(), duplicate: false }, + cons: fn(acc, m) { + MemberNamesSeen { + seen: map_insert(m: acc.seen, key: m.name, value: true), + duplicate: acc.duplicate || map_contains_key(m: acc.seen, key: m.name) + } + } + ).duplicate +} diff --git a/src/v2/std/native_agreement.dag b/src/v2/std/native_agreement.dag index 093573a73b1..1d3719b26b4 100644 --- a/src/v2/std/native_agreement.dag +++ b/src/v2/std/native_agreement.dag @@ -10,7 +10,8 @@ import std.optional { Present, Absent } -import v2.std.integer { integer_int_to_decimal_string, integer_u8_from_byte } +import v2.std.integer { integer_u8_from_byte } +import std.integer { int_to_decimal_string } import v2.std.runtime { RuntimePrimitive, RuntimePrimitiveValue, RuntimeValue } import v2.std.text { String } import std.algebra { Cons, Empty } @@ -31,7 +32,7 @@ type NativeAgreementDivergence { // std cannot import compiler back). fn octet_display(octet: Int) -> String { - integer_int_to_decimal_string(value: octet) + int_to_decimal_string(value: octet) } // THE OCTET IS AN Int AND THE LIST HOLDS Byte. `list_at_optional` over a `List` yields diff --git a/src/v2/std/node.dag b/src/v2/std/node.dag index 28e9f79bdeb..27c64435ae5 100644 --- a/src/v2/std/node.dag +++ b/src/v2/std/node.dag @@ -1,5 +1,6 @@ module v2.std.node +import v2.std.compilers.lowered_shape { LoweredShape } import std.types { List } import std.algebra { Cons, Empty, list_snoc_item } import std.optional { Present } @@ -157,6 +158,19 @@ type StructuralEdgeLabel // in the scope enclosing the match, so no pattern binder reaches the rest // (v2.compiler.resolve resolve_match_arm_admitted, its one consumer). It adds no behavior kind; // a marked Bind anywhere else is a malformed tree. +// +// ConstantMemberEdge: A NULLARY ARROW THAT IS A `data` CONSTANT. A named edge (targeting an empty +// Conj, as MatchArmScopeExitEdge is) on the Arrow body lowering builds for `data x: T = e` +// (v2.compiler.body_lowering_fold body_lower_data_decl_to_member, its one producer). The Arrow is the +// same member a nullary `fn x() -> T` lowers to; the marker is the one fact that differs: a reference +// to a marked Arrow USES ITS VALUE, so it is typed at the Arrow's declared codomain T +// (v2.compiler.infer infer_declaration_reference_facts_indexed) and evaluates to the value of the +// nullary application (v2.compiler.eval), while a reference to an unmarked Arrow denotes the function. +// It adds no behavior kind. WHAT HOLDS IT, stated at its rung: its one producer marks only the Arrow +// of a `data` declaration (nullary by construction), and the reader answers Absent for the marker on a +// non-Arrow (v2.test.claim.compiler.data_reference_value drv_a_marked_non_arrow_is_not_a_constant). No +// wall refuses the marker on a NON-nullary Arrow: that holds only by the single producer, so a second +// producer must add that wall rather than rely on this note. type CoreEdgeLabel = ArrowBodyEdge | ArrowSignatureOrderEdge @@ -182,10 +196,34 @@ type CoreEdgeLabel | BinderTypeEdge | BinderDefaultEdge | ArrowResourceRequirementsEdge + | AdmitCallersEdge | LexicalReferenceEdge | MapLiteralEntryEdge | MapLiteralEntriesEdge + | BodyTermLoweredEdge { seal: BodyTermLoweredSeal } | MatchArmScopeExitEdge + | ConstantMemberEdge + +// THE LOWERED TERM'S SEAL (MQ-5). Body lowering wraps each producer's result in a node whose one edge +// carries this marker, and readers take a node under it as lowered output, whole +// (v2.compiler.body_lowering_fold body_term_of). The marker carries a sole_constructor record, so the +// only way to write the label is through body_term_lowered_seal, and that mint admits one caller: the +// body-lowering mint. A lookalike built anywhere else does not compile -- the record has no other +// constructor and the mint refuses the call. The record is not on the codec's closed marker list +// (core_edge_labels), so no wire form decodes to it either. On the native route admit_callers is +// discarded (gunbc.rung_drop admit_callers_discarded_on_the_native_route), so there the record's +// sole construction site is the whole wall. +type BodyTermLoweredSeal sole_constructor { + shape: LoweredShape, +} + +fn body_term_lowered_seal(shape: LoweredShape) -> BodyTermLoweredSeal + admit_callers: [ + decl_ref(module_path: "v2.compiler.body_lowering_fold", decl_name: "body_term_seal") + ] +{ + BodyTermLoweredSeal { shape: shape } +} fn core_edge_label(marker: CoreEdgeLabel) -> EdgeLabel { StructuralLabel { label: CoreMarker { marker: marker } } @@ -246,10 +284,12 @@ fn core_edge_labels() -> List { BinderTypeEdge, BinderDefaultEdge, ArrowResourceRequirementsEdge, + AdmitCallersEdge, LexicalReferenceEdge, MapLiteralEntryEdge, MapLiteralEntriesEdge, MatchArmScopeExitEdge, + ConstantMemberEdge, ] } @@ -281,10 +321,13 @@ fn core_edge_label_canonical_symbol(marker: CoreEdgeLabel) -> Symbol { BinderTypeEdge => ^core_binder_type_edge BinderDefaultEdge => ^core_binder_default_edge ArrowResourceRequirementsEdge => ^core_arrow_resource_requirements_edge + AdmitCallersEdge => ^core_admit_callers_edge LexicalReferenceEdge => ^core_lexical_reference_edge MapLiteralEntryEdge => ^core_map_literal_entry_edge MapLiteralEntriesEdge => ^core_map_literal_entries_edge + BodyTermLoweredEdge { seal: _ } => ^core_body_term_lowered_edge MatchArmScopeExitEdge => ^core_match_arm_scope_exit_edge + ConstantMemberEdge => ^core_constant_member_edge } } type Edge { @@ -309,6 +352,16 @@ fn node_with_occurrence_id( Node { kind: kind, children: children, occurrence_id: occurrence_id } } +// Whether the node is an Arrow type node -- the one connective question every reader of a lowered callable or +// nullary-member shape asks. Single accessor: readers import this rather than re-matching the coproduct. +fn node_is_arrow(n: Node) -> Bool { + match n.kind { + TypeNode { connective: Arrow } => true + TypeNode { connective: _ } => false + ComputationNode { behavior: _ } => false + } +} + fn node_rebuild(n: Node, children: List) -> Node { node_with_occurrence_id( kind: n.kind, @@ -828,16 +881,43 @@ data arrow_signature_order_label: EdgeLabel = StructuralLabel { label: CoreMarke // (v2.std.symbol_index symbol_index_declares_resource_at) or that repeats one. Like the other two it is // metadata to every value reader: v2.compiler.infer and v2.std.type_binder node_inferred_subtree_nodes // skip every arrow_named_edge_is_contract edge. + +// THE DECLARED PERMITTED-CALLERS EDGE, restored with its consumer for the former drop +// gunbc.rung_drop.admit_callers_discarded_on_the_native_route. A fn Arrow built from a signature that declared an +// admit_callers clause carries the clause's STRUCTURED ROSTER under the core marker AdmitCallersEdge +// (v2.compiler body_lowering_fold body_lower_fn_admit_clause_edges: the carrier consumes the authored +// spine at lowering and re-hangs each entry as a positional record conj), and the consumer reads the +// declared permitted callers off it (v2.compiler.infer infer_caller_admission_diags). It is NOT a +// contract edge: a contract edge's target is a claim the Arrow makes in a ruled vocabulary +// (std.effects EffectClaim, std.execution_mode ExecutionMode), while this edge's target is the +// roster the consumer judges. Like a contract edge it is optional, appears at most once, and is +// never a binder. +// +// THE FOURTH, ConstantMemberEdge: what a `data` Arrow promises about its USE -- a reference to it is the +// value of its nullary application, not the function. Its target is an empty Conj and carries no +// vocabulary, so v2.std.arrow_contract has nothing to check on it; like the other three it is metadata to +// every value reader, and it is read only by v2.std.node_query arrow_constant_member_codomain. fn arrow_named_edge_is_contract(e: Edge) -> Bool { edge_is_core(e: e, marker: ArrowEffectClaimsEdge) || edge_is_core(e: e, marker: ArrowExecutionModeClaimEdge) || edge_is_core(e: e, marker: ArrowResourceRequirementsEdge) + || edge_is_core(e: e, marker: ConstantMemberEdge) } +// THE ONE METADATA FACT FOR ARROW NAMED EDGES: this edge's target is metadata to every value +// reader -- never a value the Arrow is built from, never a binder. The single predicate every value +// reader that walks an Arrow consumes (v2.compiler.infer's gather fold and product evidence, +// v2.compiler.translate's grounding gate, v2.std.type_binder node_inferred_subtree_nodes) and every +// count of an Arrow's binders reads (arrow_named_edge_is_non_binder here and v2.std.type_binder +// arrow_named_labels_conform), so no reader keeps its own list of metadata labels. DESIGN section 3: +// each fact lives in exactly one place. +fn arrow_named_edge_is_metadata(e: Edge) -> Bool { + arrow_named_edge_is_contract(e: e) || edge_is_core(e: e, marker: AdmitCallersEdge) +} -// The Arrow named edges that are not binders: its body, its declared order and its contract edges. -// The one predicate every count of an Arrow's binders reads (here and v2.std.type_binder -// arrow_named_labels_conform), so they agree on what a binder is. +// The Arrow named edges that are not binders: its body, its declared order, and the metadata edges. +// The one predicate every count of an Arrow's binders +// reads (here and v2.std.type_binder arrow_named_labels_conform), so they agree on what a binder is. fn arrow_named_edge_is_non_binder(e: Edge) -> Bool { match e.label { Positional => false @@ -845,7 +925,7 @@ fn arrow_named_edge_is_non_binder(e: Edge) -> Bool { StructuralLabel { label: _ } => edge_is_core(e: e, marker: ArrowBodyEdge) || edge_is_core(e: e, marker: ArrowSignatureOrderEdge) - || arrow_named_edge_is_contract(e: e) + || arrow_named_edge_is_metadata(e: e) } } @@ -876,6 +956,7 @@ fn arrow_signature_edges_conform(children: List) -> Bool { && (core_occurrences(marker: ArrowEffectClaimsEdge, children: children) <= 1) && (core_occurrences(marker: ArrowExecutionModeClaimEdge, children: children) <= 1) && (core_occurrences(marker: ArrowResourceRequirementsEdge, children: children) <= 1) + && (core_occurrences(marker: AdmitCallersEdge, children: children) <= 1) && fold(children, init: true, f: fn(acc, e) { match e.label { Positional => acc @@ -1416,12 +1497,18 @@ fn classify_arrow_body_form(target: Node) -> ArrowBodyForm { // not hold still refuses with the same reason, now raised at the USE by eval_type_node_atom's own arm, where // it means what it says. What disappears is a refusal based solely on the body's FORM; what remains is the // refusal based on a missing binding, which is the only one that was ever a fact about the program. +// +// DeclarationReferenceBody IS ADMITTED ON THE SAME REASONING. A fn whose body is a reference to a `data` +// constant (`fn f() -> Int { k }`) refused here by FORM before eval reached the reference, so the one +// route that evaluates a constant (v2.compiler.eval eval_constant_member_body, keyed on v2.std.node +// ConstantMemberEdge) could never run. A declaration reference that is not a constant still refuses at +// the USE, with ^eval_rejected_runtime_binding_lookup_miss raised by eval_type_node_atom's own arm. fn arrow_body_admits_eval_entry(form: ArrowBodyForm) -> Bool { match form { UnsupportedArrowBody => false DirectAtomBody => true RecordConstructBody => false - DeclarationReferenceBody => false + DeclarationReferenceBody => true ParameterReferenceBody => true LexicalReferenceBody => true FunctionValueBody => false diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index fcc060cf91c..ba12200fd02 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -20,6 +20,7 @@ import std.types { Map, List } import v2.std.node { Instantiation, BinderTypeEdge, + ConstantMemberEdge, BinderDefaultEdge, core_edge, Cardinality, @@ -867,6 +868,27 @@ fn nullary_inhabitant_by_discriminant( } } +// THE VALUE TYPE A REFERENCE TO A `data` CONSTANT DENOTES: the declared codomain of an Arrow that +// carries v2.std.node ConstantMemberEdge, or Absent for any other node (an unmarked Arrow, including a +// nullary fn, denotes its function). The one reader of the marker; infer types the reference by it and +// eval applies the constant by it, so the two cannot disagree on which Arrows are constants. +fn arrow_constant_member_codomain(arrow: Node) -> Optional { + match arrow.kind { + TypeNode { connective: Arrow } => + if fold(arrow.children, init: false, f: fn(acc, e) { acc || edge_is_core(e: e, marker: ConstantMemberEdge) }) { + list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) + } else { + Absent + } + TypeNode { connective: Atom { identity: _ } } => Absent + TypeNode { connective: Conj } => Absent + TypeNode { connective: Disj } => Absent + TypeNode { connective: v2.std.node.Cardinality } => Absent + TypeNode { connective: Instantiation } => Absent + ComputationNode { behavior: _ } => Absent + } +} + fn node_positional_child_targets(node: Node) -> List { fold(node.children, init: Empty, f: fn(acc, e) { match e.label { @@ -978,7 +1000,14 @@ fn named_child_lookup(root: Node, name: Symbol) -> NamedChildLookup { TypeNode { connective: Arrow } => NamedChildMissing TypeNode { connective: Cardinality } => NamedChildMissing TypeNode { connective: Instantiation } => NamedChildMissing - ComputationNode { behavior: _ } => NamedChildMissing + ComputationNode { behavior: _ } => + fold(root.children, init: NamedChildMissing, f: fn(acc, e) { + match e.label { + Positional => acc + StructuralLabel { label: _ } => acc + Authored { name: edge_name } => child_lookup_step(acc: acc, hit: edge_name == name, target: e.target) + } + }) } } diff --git a/src/v2/std/parse_refusal_reason.dag b/src/v2/std/parse_refusal_reason.dag index c6cf80eaddc..d8fbb2e9c62 100644 --- a/src/v2/std/parse_refusal_reason.dag +++ b/src/v2/std/parse_refusal_reason.dag @@ -37,11 +37,17 @@ import v2.std.node { Symbol } // (`x.module`, `Rec { module: e }`). Located at the keyword. // TokenStartsNotIncreasing -- the token stream handed to the parse does not have strictly // increasing token starts. A stream position IS its token's -// start (v2.compiler.parse parse_current_position keys the memo -// and decides repeat progress), so two tokens at one start would -// be one position and the parse would silently serve one's -// answer for the other. Located at the first token whose start +// start (v2.compiler.parse parse_current_position identifies a +// stream position and decides repeat progress by it), so two +// tokens at one start would be one position and the parse would +// silently take one's progress for the other's. Located at the first token whose start // does not exceed its predecessor's. +// NonAssociativeOperatorChain -- a non-associative infix operator (an equality or a comparison) +// follows an application of an operator of the same binding +// power (`a < b < c`, `a == b != c`). Neither grouping is the +// one the author meant by rule, so it refuses rather than pick: +// parenthesize, or join the comparisons with `&&`. Located at +// the second operator (v2.std.grammar InfixTable). type ParseRefusalReason = FnExpressionBodyMissingAfterEq | DataValueMissingAfterEq @@ -52,6 +58,7 @@ type ParseRefusalReason = | DeclarationMarkerNotTest | KeywordAsValueName | TokenStartsNotIncreasing + | NonAssociativeOperatorChain // The diagnostic identity each reason refuses under (`v2.std.diagnostic` `Diagnostic.reason`). fn parse_refusal_reason_symbol(reason: ParseRefusalReason) -> Symbol { @@ -65,5 +72,6 @@ fn parse_refusal_reason_symbol(reason: ParseRefusalReason) -> Symbol { DeclarationMarkerNotTest => ^parse_declaration_marker_not_test KeywordAsValueName => ^parse_keyword_as_value_name TokenStartsNotIncreasing => ^parse_token_starts_not_increasing + NonAssociativeOperatorChain => ^parse_non_associative_operator_chain } } diff --git a/src/v2/std/provenance.dag b/src/v2/std/provenance.dag index f2f1b9c42f6..68e3b3e953b 100644 --- a/src/v2/std/provenance.dag +++ b/src/v2/std/provenance.dag @@ -72,22 +72,6 @@ fn span_index_merge(base: SpanIndex, incoming: SpanIndex) -> SpanIndex { } } -// ONE ENTRY OF span_index_merge, for a caller that knows WHICH incoming ids it needs. The merge -// rule is unchanged -- the base's entry wins, an absent id takes the incoming entry -- but the -// cost is one lookup pair instead of a walk over every incoming key. The parser's memo hit is the -// consumer: its incoming index carries every entry minted before the memoized frame started, so -// the whole-key merge paid the size of the file so far on every hit. -fn span_index_adopt(base: SpanIndex, incoming: SpanIndex, id: OccurrenceId) -> SpanIndex { - match base.entries.lookup(id) { - Present { value: _ } => base - Absent => - match incoming.entries.lookup(id) { - Present { value: event } => SpanIndex { entries: map_insert(m: base.entries, key: id, value: event) } - Absent => base - } - } -} - fn span_index_origin_chain_exhausted_diagnostic() -> Diagnostic { Diagnostic { reason: ^span_index_origin_chain_exhausted, diff --git a/src/v2/std/staging.dag b/src/v2/std/staging.dag index fdce7920be1..065c897b5a4 100644 --- a/src/v2/std/staging.dag +++ b/src/v2/std/staging.dag @@ -1,6 +1,6 @@ module v2.std.staging -import v2.std.diagnostic { Outcome, bind_outcome, outcome_accepted } +import v2.std.diagnostic { Outcome, bind_outcome } fn then_outcome( f: fn(A) -> Outcome, @@ -8,27 +8,3 @@ fn then_outcome( ) -> fn(A) -> Outcome { fn(x) { bind_outcome(o: f(x), f: g) } } - -type CacheProbe - = Miss - | Hit { value: B } - -fn resolve_probe( - probe: CacheProbe, - stage: fn(A) -> Outcome, - x: A -) -> Outcome { - match probe { - Hit { value: hit } => outcome_accepted(value: hit) - Miss => stage(x) - } -} - -fn cached_stage( - lookup: fn(A) -> CacheProbe, - stage: fn(A) -> Outcome -) -> fn(A) -> Outcome { - fn(x) { - resolve_probe(probe: lookup(x), stage: stage, x: x) - } -} diff --git a/src/v2/std/symbol_index.dag b/src/v2/std/symbol_index.dag index 6526f9c933d..2e76e8b8d04 100644 --- a/src/v2/std/symbol_index.dag +++ b/src/v2/std/symbol_index.dag @@ -15,7 +15,7 @@ import v2.std.algebra { import v2.std.collection { Map, empty_map, - map_get, + map_get_checked, map_insert, map_contains_key, map_lookup @@ -170,7 +170,7 @@ fn empty_symbol_index() -> SymbolIndex { // a two-binder refusal back to one binder the first time this split was written. `entries` is where // a declaration is stored; `symbol_index_lookup` is what a reference is entitled to be told. fn symbol_index_entry_at(index: SymbolIndex, qualified_path: QualifiedName) -> Optional { - match v2.std.collection.map_get(index.entries, qualified_path) { + match v2.std.collection.map_get_checked(index.entries, qualified_path) { Accepted { value: opt, diagnostics: _ } => opt Rejected { diagnostics: _ } => optional_absent() } @@ -210,7 +210,7 @@ fn symbol_index_track_global_bare( match qualified_name_last_segment(qn: qualified_path) { Absent => index Present { value: leaf } => - match v2.std.collection.map_get(index.global_bare, leaf) { + match v2.std.collection.map_get_checked(index.global_bare, leaf) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: GlobalBareAmbiguous { paths: paths } } => SymbolIndex { @@ -332,9 +332,9 @@ fn symbol_index_mark_declared_payload(index: SymbolIndex, qualified_path: Qualif } // WHAT THE PATH DENOTES, WITH NO FAILURE ARM TO WIDEN (review 70226). The first cut read this -// through map_get, whose Outcome carries a Rejected arm, and answered binder there -- which matches +// through map_get_checked, whose Outcome carries a Rejected arm, and answered binder there -- which matches // every value, so a lookup that could not answer would have widened the arm silently, the fail-open -// shape DESIGN section 5 forbids. map_lookup is the total read underneath map_get (which is +// shape DESIGN section 5 forbids. map_lookup is the total read underneath map_get_checked (which is // `outcome_accepted(map_lookup(..))`, so its refusal arm is unreachable by construction): the kind, // or Absent for a path that denotes no declared payload. The caller matches on it. fn symbol_index_declared_payload_at(index: SymbolIndex, qualified_path: QualifiedName) -> Optional { @@ -431,7 +431,7 @@ fn symbol_index_declared_binders_at(index: SymbolIndex, qualified_path: Qualifie } fn symbol_index_global_unique_lookup(index: SymbolIndex, name: Symbol) -> GlobalBareLookup { - match v2.std.collection.map_get(index.global_bare, name) { + match v2.std.collection.map_get_checked(index.global_bare, name) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: GlobalBareUnique { node: n, path: p } } => GlobalBareHit { node: n, path: p } Present { value: GlobalBareAmbiguous { paths: paths } } => GlobalBareLookupAmbiguous { paths: paths } @@ -564,7 +564,7 @@ fn symbol_index_bound_declarings( index: SymbolIndex, binding_path: QualifiedName ) -> FreeMonoid { - match v2.std.collection.map_get(index.bound_declarings, binding_path) { + match v2.std.collection.map_get_checked(index.bound_declarings, binding_path) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: declarings } => declarings Absent => Empty diff --git a/src/v2/std/type_binder.dag b/src/v2/std/type_binder.dag index f1e43bc482e..05a00d5cd97 100644 --- a/src/v2/std/type_binder.dag +++ b/src/v2/std/type_binder.dag @@ -31,7 +31,7 @@ import v2.std.node { TypeNode, match_arm_pattern_edge, arrow_signature_order_label, - arrow_named_edge_is_contract, + arrow_named_edge_is_metadata, node_subtree_nodes, node_synthetic, type_binder_conj_conforms, @@ -234,7 +234,7 @@ fn declaration_binder_edge(parent: Node, edge: Edge) -> Bool { // (infer_facts_key_conflict). v2.compiler.infer's gather skips the same cells by the same accessor. fn node_inferred_subtree_nodes(root: Node) -> List { fold(root.children, init: [root], f: fn(acc, e) { - if (e.label == arrow_signature_order_label) || arrow_named_edge_is_contract(e: e) || declaration_binder_edge(parent: root, edge: e) || match_arm_pattern_edge(parent: root, edge: e) || (resolved_reference_body_kind(target: root) != NotMarkedReference) { + if (e.label == arrow_signature_order_label) || arrow_named_edge_is_metadata(e: e) || declaration_binder_edge(parent: root, edge: e) || match_arm_pattern_edge(parent: root, edge: e) || (resolved_reference_body_kind(target: root) != NotMarkedReference) { acc } else { concat(acc, node_inferred_subtree_nodes(root: e.target)) diff --git a/src/v2/test/claim/advisories/fatal_first_readers_test.dag b/src/v2/test/claim/advisories/fatal_first_readers_test.dag index 976ee41e813..76a74bac49d 100644 --- a/src/v2/test/claim/advisories/fatal_first_readers_test.dag +++ b/src/v2/test/claim/advisories/fatal_first_readers_test.dag @@ -66,9 +66,11 @@ test fn grammar_coverage_stage_rejected_reports_each_chains_fatal_holds() -> Boo } // THE DOOR LEDGER'S HISTOGRAM IS OF FATAL CAUSES. The three refusal chains the long claim -// v2.test.long.compile_door_ledger's real subjects produce (determinism, error_primitives, bit) are -// SUPPLIED here, each headed by one shared advisory: the histogram must count their fatals, and every -// fatal must be owned. A head-grain reader counts the advisory three times and nothing else. +// v2.test.long.compile_door_ledger's real subjects historically produced (determinism, +// error_primitives, bit) are SUPPLIED here, each headed by one shared advisory: the histogram +// must count their fatals, and every fatal must be owned. A head-grain reader counts the +// advisory three times and nothing else. Do not substitute unrelated causes for +// member_not_a_binder on error_primitives/bit. test fn door_ledger_histogram_counts_each_refusals_fatal_holds() -> Bool { let rows = [ door_refused_row(path: "dag/std/determinism.dag", d: residue_headed_chain(fatal: ^resolve_reason_unbound_symbol)), diff --git a/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag b/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag index d16fc6f172b..55a8e49ba39 100644 --- a/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag +++ b/src/v2/test/claim/arrow_order_edge/admission_wall_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.arrow_order_edge.admission_wall +import v2.std.member_declaration_kind { no_member_declaration_kinds } import std.occurrence_identity { OccurrenceSynthetic } import std.algebra { Cons, Empty } import v2.compiler.infer { infer } @@ -81,7 +82,7 @@ test fn aoe_well_formed_refuses_a_named_domain_without_order() -> Bool { // PATH: infer, entered directly with a ResolvedTree. fn aoe_infer_reason(arrow: Node) -> Symbol { - match infer(tree: ResolvedTree { root: aoe_module_root(arrow: arrow), symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] }) { + match infer(tree: ResolvedTree { root: aoe_module_root(arrow: arrow), symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [], member_kinds: no_member_declaration_kinds() }) { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) Accepted { value: _, diagnostics: _ } => ^aoe_accepted } diff --git a/src/v2/test/claim/binder_admission/arm_scope_exit_marker_test.dag b/src/v2/test/claim/binder_admission/arm_scope_exit_marker_test.dag index f35847e9848..b19e7f614d7 100644 --- a/src/v2/test/claim/binder_admission/arm_scope_exit_marker_test.dag +++ b/src/v2/test/claim/binder_admission/arm_scope_exit_marker_test.dag @@ -52,7 +52,7 @@ fn asem_scope_binding_y() -> Scope { fb: frame_binders_add( fb: frame_binders_empty(), name: ^y, - binding: LexicalBinding { binder_site: OccurrenceMinted { id: OccurrenceId { value: 1 } }, declared: optional_absent() } + binding: LexicalBinding { binder_site: OccurrenceMinted { id: OccurrenceId { value: 1 } }, declared: optional_absent(), value: optional_absent() } ), outer: ScopeRoot { module: empty_namespace() } ) diff --git a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag index c2f8441146b..3f0b34a5550 100644 --- a/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag +++ b/src/v2/test/claim/binder_admission/callable_binder_slice_test.dag @@ -26,7 +26,8 @@ import v2.compiler.resolve { } import v2.std.resolution_policy { default_name_resolution_policy } import v2.compiler.fold_lowering { fold_carrier_binder_atom } -import v2.std.collection { Map, empty_map, map_insert, map_lookup } +import v2.std.collection { Map, empty_map, list_at_optional, map_insert, map_lookup } +import v2.std.node_query { node_positional_child_targets } import std.algebra { Cons, Empty } import v2.std.language_model { void_language_model } import v2.std.text { String } @@ -56,10 +57,10 @@ import std.occurrence_identity { OccurrenceId, OccurrenceMinted, OccurrenceSynth // witness rule) -- and it is exactly the reach whose cost refused this lane's other claim sets at the // enrolment margin. // -// WHAT THE SLICE ESTABLISHES. `fold(xs, init: false, f: fn(found, e) { found || ... })` refuses at -// resolve with resolve_unbound_name_is_declared_in_several_modules on `found`. The rows below locate -// that in the lowering rather than in resolve's binder model: the Arrow path admits its binders, and -// the fold seam the step is lowered to carries none. +// WHAT THE SLICE ESTABLISHES. A fn-literal-shaped Arrow admits every Named domain binder and no +// undeclared name. The fold encoding's Loop positional is that same Arrow (v2.compiler.fold_lowering +// fold_recurrence_encoding), so harvesting the step admits the handler's binders; harvesting the Loop +// itself does not, because a Loop is not an Arrow. // THE LOOP THESE ROWS SUPPLY MIRRORS THE FOLD ENCODING'S, and it is built here rather than by calling the // producer because gunbc#12550 retired the old single-purpose seam builder: the encoding is now a Bind @@ -130,22 +131,21 @@ test fn cbs_the_arrow_path_admits_no_undeclared_name_holds() -> Bool { !cbs_admits(locals: cbs_arrow_locals(), name: ^unrelated) } -// (3) THE FOLD SEAM ADMITS NEITHER BINDER -- THE DISCRIMINATING RED FOR THIS SLICE. The step literal -// is destructured into an iteration body and ONE carrier symbol (v2.compiler.fold_lowering -// fold_call_seam_from_step), and the seam it builds is a Loop, not an Arrow. arrow_domain_frame_binders -// answers the empty map for it because it is not an Arrow at all, and resolve harvests binders in -// exactly two places -- an Arrow's domain and a Bind's atom -- with NO loop-carrier harvester. So the -// body's `found` reaches the bare-name census and refuses as declared in several modules. -// -// THIS ROW FLIPS WHEN THE CONSOLIDATION LANDS: once the step lowers through the one callable Arrow -// constructor and loop carriers go through the one binder-admission operation, the seam admits its -// binders and this row must be rewritten to assert that, not deleted (DESIGN section 4b(4)). -test fn cbs_the_fold_seam_admits_no_binder_today_holds() -> Bool { - let seam_locals = arrow_domain_frame_binders( - n: cbs_carrier_loop(body: cbs_atom(id: ^step_body), carrier: ^found) - ).locals - !cbs_admits(locals: seam_locals, name: ^found) - && !cbs_admits(locals: seam_locals, name: ^e) +// (3) THE FOLD ENCODING'S STEP IS AN ARROW, SO ITS HANDLER BINDERS HARVEST. The Loop around it still +// harvests none: arrow_domain_frame_binders is an Arrow reader. Resolve walks the positional step as +// that Arrow (resolve_arrow_node), which is why a handler binder binds on the native ingest route +// (v2.test.claim.resolve.fold_handler_binder_resolve). +test fn cbs_the_fold_encoding_step_admits_handler_binders_holds() -> Bool { + let loop_node = cbs_carrier_loop(body: cbs_fn_literal_shaped_arrow(), carrier: ^found) + let loop_locals = arrow_domain_frame_binders(n: loop_node).locals + match list_at_optional(xs: node_positional_child_targets(node: loop_node), index: 0) { + Absent => false + Present { value: step } => + let step_locals = arrow_domain_frame_binders(n: step).locals + !cbs_admits(locals: loop_locals, name: ^found) + && cbs_admits(locals: step_locals, name: ^found) + && cbs_admits(locals: step_locals, name: ^e) + } } // THE ADMISSION'S THREE RULES, each at its own interface with a supplied Scope. The scopes are @@ -177,7 +177,7 @@ fn cbs_frame_binding(name: Symbol) -> Scope { fb: frame_binders_add( fb: frame_binders_empty(), name: name, - binding: LexicalBinding { binder_site: OccurrenceMinted { id: OccurrenceId { value: 1 } }, declared: optional_absent() } + binding: LexicalBinding { binder_site: OccurrenceMinted { id: OccurrenceId { value: 1 } }, declared: optional_absent(), value: optional_absent() } ), outer: cbs_root_scope() ) diff --git a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag index 7f9afbe01e7..5b587fd9d1e 100644 --- a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag +++ b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag @@ -481,6 +481,10 @@ test fn arrow_body_form_reference_shape_without_marker_refused_at_resolve_holds( } } -test fn arrow_body_form_declaration_reference_not_an_eval_entry_holds() -> Bool { - !arrow_body_admits_eval_entry(form: DeclarationReferenceBody) +// FLIPPED (v2.std.node arrow_body_admits_eval_entry): a declaration-reference body enters eval, and the +// reference refuses or evaluates at its USE -- a `data` constant evaluates to its value +// (v2.test.claim.compiler.data_reference_value drv_a_data_reference_is_typed_at_its_declared_type_and_evaluates_to_its_value), any other +// declaration reference refuses with the binding-miss reason. The body's FORM is no longer the refusal. +test fn arrow_body_form_declaration_reference_is_an_eval_entry_holds() -> Bool { + arrow_body_admits_eval_entry(form: DeclarationReferenceBody) } diff --git a/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag index 7f852d7e06b..7f5ce058cdc 100644 --- a/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag +++ b/src/v2/test/claim/body_lowering/arrow_lambda_block_body_test.dag @@ -227,7 +227,7 @@ test fn no_statement_begins_with_a_comma_or_a_close_paren_holds() -> Bool { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => false - PreparedModeled { modeled: modeled, materialization: _ } => + PreparedModeled { modeled: modeled } => let first = production_first_row_lookup(rows: modeled.analysis.first_rows, name: ^dag_production_stmt) !alb_first_has(keys: first, token_class: ^dag_token_comma) && !alb_first_has(keys: first, token_class: ^dag_token_rparen) diff --git a/src/v2/test/claim/body_lowering/peeled_shell_fold_head_test.dag b/src/v2/test/claim/body_lowering/peeled_shell_fold_head_test.dag index 0d1e70bfc07..2528e155003 100644 --- a/src/v2/test/claim/body_lowering/peeled_shell_fold_head_test.dag +++ b/src/v2/test/claim/body_lowering/peeled_shell_fold_head_test.dag @@ -3,9 +3,9 @@ module v2.test.claim.body_lowering.peeled_shell_fold_head import v2.compiler.body_lowering_fold { body_lower_is_pass_through_emitted } import v2.compiler.fold_lowering { fold_call_head_symbol, fold_family_realization } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node } @@ -27,18 +27,17 @@ data peeled_shell_fold_head_source: String = "module w.p\n\ndata d: I = fold(x, fn peeled_shell_fold_head_parsed() -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize( + o: tokenize_prepared( text: peeled_shell_fold_head_source, file: ^peeled_shell_fold_head_file, - rules: dag_lex() + prepared: dag_prepared_lex() ), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/body_lowering/single_arm_match_test.dag b/src/v2/test/claim/body_lowering/single_arm_match_test.dag index 7045d270d20..2596aec0dbf 100644 --- a/src/v2/test/claim/body_lowering/single_arm_match_test.dag +++ b/src/v2/test/claim/body_lowering/single_arm_match_test.dag @@ -9,6 +9,7 @@ import v2.compiler.reference_conservation_admission { } import v2.compiler.reference_conservation { ReferenceConservationSubject } import std.algebra { Cons, Empty, FreeMonoid } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.test.claim.body_lowering.normalize_outcome_helpers { body_lowering_normalize_outcome } diff --git a/src/v2/test/claim/body_lowering_block_body_call_test.dag b/src/v2/test/claim/body_lowering_block_body_call_test.dag index e9288951b37..4d6b87e1fe8 100644 --- a/src/v2/test/claim/body_lowering_block_body_call_test.dag +++ b/src/v2/test/claim/body_lowering_block_body_call_test.dag @@ -3,9 +3,8 @@ module v2.test.claim.body_lowering_block_body_call import v2.compiler.reference_conservation_admission { conservation_subject_of_text, conserved_normalize, no_explained_drops } import v2.compiler.reference_conservation { ReferenceConservationSubject } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.compilers.body_lowering { Application, @@ -26,18 +25,17 @@ data body_lowering_block_call_source: String = "module wall.same\n\nfn leaf() -> fn body_lowering_block_call_parsed() -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize( + o: tokenize_prepared( text: body_lowering_block_call_source, file: ^body_lowering_block_call_file, - rules: dag_lex() + prepared: dag_prepared_lex() ), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/body_lowering_data_literal_door_test.dag b/src/v2/test/claim/body_lowering_data_literal_door_test.dag new file mode 100644 index 00000000000..569f5a85604 --- /dev/null +++ b/src/v2/test/claim/body_lowering_data_literal_door_test.dag @@ -0,0 +1,43 @@ +module v2.test.claim.body_lowering_data_literal_door + +import v2.compiler.normalize { normalize } +import v2.test.parse.expression_bodied_fn_decl_parse { g_tokenize_parse, g_tree_has_arrow_body } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Node } +import v2.std.text { String } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE NATIVE DOOR'S ONE-LINE CONTROL, ON THE INTERPRETED v2 ROUTE (MQ-5, #12942). The door's accepting +// control (fixture.native_cli_door.door_probe) is a `data` declaration whose initializer is one Int +// literal; the emitted compiler refused it with body_lowering_reason_unrecognised_lowered_shape at a +// synthetic node once the cut made every reader classify its input through body_term_of. Nothing on the +// required floor normalized that shape, so the refusal surfaced only on the native emit-build lane. This +// claim normalizes the same source through the same v2 pipeline, interpreted, and asserts it is admitted +// with its arrow body present -- the data declaration lowered to its nullary member. +// +// The parse and normalize are one nullary producer, filled in preparation under the single-claim fill +// debt of v2.workflow.floor_pure_producer_share floor_single_claim_fill_debt: +// a whole-grammar parse plus normalize is past one claim's budget, and the claim reads the result. + +data door_data_literal_source: String = "module fixture.native_cli_door.door_probe\n\ndata native_cli_door_probe_value: Int = 606060\n" + +fn door_data_literal_normalized() -> Outcome { + match g_tokenize_parse(text: door_data_literal_source, file: ^probe_door_data_literal) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: tree, diagnostics: _ } => + match normalize(parse_tree: tree) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: normalized, diagnostics: d } => Accepted { value: normalized.root, diagnostics: d } + } + } +} + +test fn a_data_declaration_with_an_int_literal_normalizes_holds() -> Bool { + match door_data_literal_normalized() { + Rejected { diagnostics: _ } => false + Accepted { value: root, diagnostics: _ } => g_tree_has_arrow_body(root: root) + } +} diff --git a/src/v2/test/claim/body_lowering_qualified_path_test.dag b/src/v2/test/claim/body_lowering_qualified_path_test.dag index 3cebefcc46d..ba28f78d11f 100644 --- a/src/v2/test/claim/body_lowering_qualified_path_test.dag +++ b/src/v2/test/claim/body_lowering_qualified_path_test.dag @@ -21,9 +21,8 @@ import v2.std.compilers.sugar { sugar_sequence_pair_optional } import v2.compiler.reference_conservation_admission { conserved_normalize_of_text, no_explained_drops } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import std.algebra { Cons, Empty } import v2.std.algebra { length, list_snoc_item } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } @@ -527,14 +526,13 @@ data body_lowering_qualified_call_source: String = "module wall.peer\n\nfn uses_ fn body_lowering_source_parsed(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/body_lowering_unrecognised_lowered_shape_test.dag b/src/v2/test/claim/body_lowering_unrecognised_lowered_shape_test.dag index a0c343e3cd6..cd4e8bbac97 100644 --- a/src/v2/test/claim/body_lowering_unrecognised_lowered_shape_test.dag +++ b/src/v2/test/claim/body_lowering_unrecognised_lowered_shape_test.dag @@ -1,21 +1,35 @@ module v2.test.claim.body_lowering_unrecognised_lowered_shape +import gunbc.compile_diagnostic_census { + CensusNotRunnable, + CensusObserved, + census_rows_of_class, + census_total_count +} import v2.compiler.body_lowering_fold { OperandRefAbsent, OperandRefLowered, - OperandRefRead, OperandRefRefused, - body_lower_find_core_substrate_passes_unrecognised, body_lower_fold, - body_lower_operand_ref_read + body_lower_operand_ref_read, + body_term_of +} +import v2.test.parse.expression_bodied_fn_decl_parse { g_tokenize_parse } +import v2.extdeps.languages.dag { + dag_int_literal_node_from_lexeme, + parse_production_emitted_identity_optional } +import v2.std.compilers.body_lowering { BodyTerm, Lowered, RawLeaf, RawParse } import v2.std.diagnostic { Accepted, Outcome, Rejected, node_locus } +import std.types { Bool } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } import v2.std.node { Atom, + Authored, ComputationNode, Conj, Edge, - Authored, Node, Positional, Symbol, @@ -23,25 +37,28 @@ import v2.std.node { TypeNode, node_synthetic } -import v2.std.node_query { field_projection_node } -import std.optional { Absent, Present } +import std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.text { String } +import std.types { Int } +import std.occurrence_identity { OccurrenceSynthetic } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -// The subject is the refusal body lowering gives a lowered shape no reader recognises -// (v2.compiler.body_lowering_fold body_lower_is_unrecognised_lowered_shape), so the inputs are -// SUPPLIED at that boundary (DESIGN section 3): a producer-shaped node no producer emits today, beside -// the shapes that must still pass. The route pairing -- that real producers' outputs still lower whole -// on the real stages -- is held by the enrolled claims over real text: -// v2.test.claim.namespace_xl0.value_position_whole_read -// a_field_projection_on_a_call_result_in_an_operand_lowers_whole and -// v2.test.claim.namespace_xl0.reference_conservation_admission -// a_field_projection_on_a_call_result_in_an_operand_is_admitted_holds. +// THE SUBJECT IS THE TERM BODY LOWERING READS OFF ITS TREE (v2.compiler.body_lowering_fold +// body_term_of) AND THE WALLS AROUND IT (MQ-5). The first group supplies inputs at that boundary +// (DESIGN section 3): a node that is neither wrapped by a producer nor stamped by the parse refuses +// wherever it is read, and the raw forms the parse does write are read positively. The second group +// runs the real route: source text through tokenize, parse and the bottom-up fold, so the claim is +// about what a producer actually emits. The third hands the compiler fixture sources that try to +// forge the Lowered label, on the seed acceptance path. The route pairing for real programs is held +// by v2.test.claim.namespace_xl0.value_position_whole_read and +// v2.test.claim.namespace_xl0.reference_conservation_admission. fn ulsh_leaf() -> Node { node_synthetic(kind: TypeNode { connective: Atom { identity: ^ulsh_leaf } }, children: []) } -// A Conj under a label no grammar production and no lowering emits: lowered-shaped (no grammar -// projection edge) and outside the allowlist. +// A Conj under a label no grammar production and no lowering emits, with no wrapper over it. fn ulsh_unknown_shape() -> Node { node_synthetic( kind: TypeNode { connective: Conj }, @@ -49,8 +66,6 @@ fn ulsh_unknown_shape() -> Node { ) } -// The same unknown shape wrapping a recognised node: the first-match descendant search would answer -// the Transform inside it, a part for the whole. fn ulsh_unknown_over_transform() -> Node { node_synthetic( kind: TypeNode { connective: Conj }, @@ -66,6 +81,14 @@ fn ulsh_unknown_over_transform() -> Node { ) } +// A producer's output with its seal missing: the allowlist used to take this whole. +fn ulsh_unsealed_transform() -> Node { + node_synthetic( + kind: ComputationNode { behavior: Transform }, + children: [Edge { label: Positional, target: ulsh_leaf() }] + ) +} + fn ulsh_raw_sequence() -> Node { node_synthetic( kind: TypeNode { connective: Conj }, @@ -76,6 +99,13 @@ fn ulsh_raw_sequence() -> Node { ) } +fn ulsh_int_literal_token() -> Optional { + match dag_int_literal_node_from_lexeme(lexeme: "7", occurrence_id: OccurrenceSynthetic) { + Accepted { value: lit, diagnostics: _ } => optional_present(value: lit) + Rejected { diagnostics: _ } => optional_absent() + } +} + fn ulsh_refused_unrecognised(o: Outcome) -> Bool { match o { Rejected { diagnostics: d } => d.head.reason == ^body_lowering_reason_unrecognised_lowered_shape @@ -83,7 +113,14 @@ fn ulsh_refused_unrecognised(o: Outcome) -> Bool { } } -// RED if the fold re-reads the shape (the re-lower fallback restored) or refuses it for another reason. +fn ulsh_term_refused(n: Node) -> Bool { + match body_term_of(node: n) { + Rejected { diagnostics: d } => d.head.reason == ^body_lowering_reason_unrecognised_lowered_shape + Accepted { value: _, diagnostics: _ } => false + } +} + +// RED if the fold re-reads the shape as raw parse or refuses it for another reason. test fn an_unrecognised_lowered_shape_refuses_at_the_fold() -> Bool { ulsh_refused_unrecognised(o: body_lower_fold(node: ulsh_unknown_shape())) } @@ -105,22 +142,123 @@ test fn an_unrecognised_lowered_shape_refuses_at_the_operand_reader() -> Bool { } } -test fn the_descendant_search_does_not_answer_a_part_of_an_unrecognised_shape() -> Bool { - match body_lower_find_core_substrate_passes_unrecognised(root: ulsh_unknown_over_transform()) { - Present { value: passed } => passed == ulsh_unknown_over_transform() +// THE RE-READ IS UNWRITABLE, NOT RECOGNISED BY SHAPE. A Transform no producer sealed was taken whole +// by the deleted allowlist; it is now neither a term nor raw parse, and refuses. +test fn an_unsealed_producer_output_refuses_at_the_fold() -> Bool { + ulsh_refused_unrecognised(o: body_lower_fold(node: ulsh_unsealed_transform())) +} + +// POSITIVE CONTROLS: what the parse writes is read as the parse wrote it, by a positive test. +test fn a_raw_parse_sequence_is_raw_parse() -> Bool { + match body_term_of(node: ulsh_raw_sequence()) { + Accepted { value: RawParse { node: _ }, diagnostics: _ } => true + _ => false + } +} + +test fn a_childless_atom_is_a_raw_leaf() -> Bool { + match body_term_of(node: ulsh_leaf()) { + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => true + _ => false + } +} + +test fn a_parse_minted_literal_token_is_a_raw_leaf() -> Bool { + match ulsh_int_literal_token() { + Absent => false + Present { value: lit } => + match body_term_of(node: lit) { + Accepted { value: RawLeaf { node: _ }, diagnostics: _ } => true + _ => false + } + } +} + +// The literal-token arm admits the token's own payload edge and nothing beside it. +test fn a_literal_token_carrying_a_foreign_edge_refuses() -> Bool { + match ulsh_int_literal_token() { Absent => false + Present { value: lit } => + ulsh_term_refused(n: node_synthetic(kind: lit.kind, children: [Edge { label: Authored { name: ^ulsh_probe_edge }, target: ulsh_leaf() }])) } } -// POSITIVE CONTROLS: a known producer shape passes whole, and raw parse is never mistaken for output. -test fn a_field_projection_still_lowers_whole() -> Bool { - let projection = field_projection_node(base: ulsh_leaf(), field: ulsh_leaf(), source: ulsh_leaf()) - match body_lower_fold(node: projection) { - Accepted { value: v, diagnostics: _ } => v == projection - Rejected { diagnostics: _ } => false +// THE ROUTE: a value position the fold lowers to a bare identifier. The postfix producer passes the +// atom through unchanged, and the atom must still leave the fold sealed as that producer's output. +// RED if the postfix arm of body_lower_reduce_emitted stops sealing: the atom then reads as a raw leaf. +data ulsh_route_source: String = "module ulsh_route\n\ndata ulsh_value: Int = ulsh_name\n" + +// THE PARSE IS ONE NULLARY PRODUCER, filled in preparation under the single-claim fill debt of +// v2.workflow.floor_pure_producer_share floor_single_claim_fill_debt, as the door claim's is +// (v2.test.claim.body_lowering_data_literal_door door_data_literal_normalized). The +// route claim reads the postfix node it yields; building the language model, tokenizing and parsing +// inside the claim cost 399ms against a 302ms enrolment margin on the merge-group floor. It parses +// through the shared prepared grammar (g_tokenize_parse), the same route the door claim reads. +fn ulsh_route_postfix() -> Optional { + match g_tokenize_parse(text: ulsh_route_source, file: ^ulsh_route_file) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: tree, diagnostics: _ } => + ulsh_first_emitted(root: tree, production: ^dag_surface_postfix_expr) + } +} + +fn ulsh_first_emitted(root: Node, production: Symbol) -> Optional { + let own = match parse_production_emitted_identity_optional(node: root) { + Present { value: e } => e == production + Absent => false } + if own { + optional_present(value: root) + } else { + fold(root.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => ulsh_first_emitted(root: e.target, production: production) + } + }) + } +} + +test fn a_passed_through_atom_leaves_the_fold_sealed() -> Bool { + match ulsh_route_postfix() { + Absent => false + Present { value: postfix } => + match body_lower_fold(node: postfix) { + Rejected { diagnostics: _ } => false + Accepted { value: folded, diagnostics: _ } => + match body_term_of(node: folded) { + Accepted { value: Lowered { node: atom, shape: _ }, diagnostics: _ } => + match node_atom_identity_optional(node: atom) { + Present { value: id } => id == ^ulsh_name + Absent => false + } + _ => false + } + } + } +} + +// THE FORGERY WALL, ON THE SEED ACCEPTANCE PATH. The Lowered label carries a sole_constructor seal +// (v2.std.node BodyTermLoweredSeal) minted only for the body-lowering mint. A fixture that calls the +// seal's mint from outside its admitted caller refuses, and so does one that builds the seal record +// directly; the positive control is that the corpus compiles the real mint (this module resolves). +// Each fixture is well typed apart from the forgery (the seal carries a LoweredShape arm), so the +// forgery is the only thing that can refuse. +fn ulsh_refusals(source: String, wanted: String) -> Int { + match compile_dag_diagnostic_census(source) { + CensusObserved { rows: rows } => census_total_count(rows: census_rows_of_class(rows: rows, wanted: wanted)) + CensusNotRunnable { cause: _ } => 0 - 1 + } +} + +data ulsh_forged_mint_call_source: String = "module ulsh_forge_call\nimport v2.std.compilers.lowered_shape { LoweredOperand }\nimport v2.std.node { BodyTermLoweredSeal, body_term_lowered_seal }\nfn ulsh_forge() -> BodyTermLoweredSeal = body_term_lowered_seal(shape: LoweredOperand)\n" + +data ulsh_forged_record_source: String = "module ulsh_forge_record\nimport v2.std.compilers.lowered_shape { LoweredOperand }\nimport v2.std.node { BodyTermLoweredSeal }\nfn ulsh_forge() -> BodyTermLoweredSeal = BodyTermLoweredSeal { shape: LoweredOperand }\n" + +test fn a_forged_seal_mint_call_refuses() -> Bool { + ulsh_refusals(source: ulsh_forged_mint_call_source, wanted: "ConstructorCallAdmissionRefused") >= 1 } -test fn a_raw_parse_sequence_is_not_an_unrecognised_lowered_shape() -> Bool { - !ulsh_refused_unrecognised(o: body_lower_fold(node: ulsh_raw_sequence())) +test fn a_forged_seal_record_refuses() -> Bool { + ulsh_refusals(source: ulsh_forged_record_source, wanted: "SoleConstructorViolation") >= 1 } diff --git a/src/v2/test/claim/caller_admission_native_wall_test.dag b/src/v2/test/claim/caller_admission_native_wall_test.dag new file mode 100644 index 00000000000..026237ed24b --- /dev/null +++ b/src/v2/test/claim/caller_admission_native_wall_test.dag @@ -0,0 +1,390 @@ +// caller_admission_native_wall_test -- HERMETIC WALL WITNESSES for the former drop +// gunbc.rung_drop.admit_callers_discarded_on_the_native_route. +// +// The wall's contract is judged over VALUES, so these claims build the roster records, the marker +// atoms, and the caller coordinates by hand (the parameter_reference_test idiom: minted occurrence +// identities, Node literals) and call the wall's pure entry points directly. No claim here runs the +// ingest pipeline; the ONE real-path inhabitance claim -- the native route refusing an outside +// cross-module call end to end -- lives in the long home +// (src/v2/test/claim/long/caller_admission_native_wall_red_test.dag): it runs the real +// parse -> assemble -> lower -> infer pipeline over a two-module source, so it is withheld from +// the required floor's charged clock by its typed v2.workflow.floor_cost_debt row and +// verdict-proven in the changed-witness lane; deleting the carrier or the consumer turns it red. +// +// THE RECORD SHAPE these claims build is the image the lowering's carrier hangs: a left-named +// roster spine (each node a conj with one @left record edge and one @right rest edge, the empty +// conj root ending it), each record carrying the authored operator edge (name ^decl_ref, target the +// ^decl_ref atom) and the two string lexemes as authored edges (module_path, decl_name). An entry +// that reads but is not such a call lowers to a MARKER record whose single edge names the entry +// refusal (^infer_reason_admit_callers_entry_not_decl_ref); a clause whose spine does not read +// lowers to the clause-fallback marker (^infer_reason_admit_callers_entries_unreadable). The claims +// pin both failure arms and the positive direction, so: +// - deleting the judge or its refusal arm turns (1) red, and (2) red if it refuses everything; +// - dropping an uninterpretable entry turns (3) red; +// - accepting a non-conj roster (silently narrowing the roster) turns (4) red; +// - breaking the record reader's coordinates turns (5) red. +// +// THE SEAM CLAIMS close the fail-open review found (side chat review 5439746794): a callee marked +// as a declaration reference whose declaration is unavailable refuses (typed, located) rather than +// falling out of the lookup as "absent"; an unmarked callee is not judged at all. The marker +// closed-list control -- the codec decoding AdmitCallersEdge -- lives in the codec's own witness +// (dag/test/claim/scm/scm_object_table_json_witness_test.dag), where the decode idiom already +// stands; hand-building the codec's sealed records here is not possible outside their defining +// module. + +module v2.test.claim.caller_admission_native_wall + +import std.algebra { Cons, Empty } +import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId, OccurrenceMinted, OccurrenceProjected, OccurrencePending, OccurrenceSynthetic } +import v2.compiler.infer { InferAdmissionCallerKey, infer_admission_judge_entries, infer_admit_clause_entries, infer_admit_record_check, infer_caller_admission_application_diags, infer_caller_admission_judged_diags } +import v2.std.node_query { declaration_reference_path_optional } +import v2.std.symbol_index { empty_symbol_index, symbol_index_insert } +import std.optional { optional_absent, optional_present } +import v2.std.diagnostic { None, Some, Suggested, Unavailable, diagnostics_has_reason } +import v2.std.logic { Bool } +import v2.std.node { Edge, Node, Symbol, symbol_eq, Atom, Conj, TypeNode, Authored, StructuralLabel, CoreMarker, DeclarationReferenceEdge, Positional } +import std.optional { Absent, Present } + +// --------------------------------------------------------------------------- +// Hand-built values: the lowering's roster record image, plus the two markers. +// --------------------------------------------------------------------------- + +// The application locus: the judge reports the refused call at its call site. Any node carries a +// locus; the atom below is the wall's witness spelling for one. +data wall_call_site_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 910 } } +data wall_record_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 911 } } +data wall_marker_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 912 } } +data wall_roster_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 913 } } +data wall_clause_fallback_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 914 } } + +data wall_application: Node = Node { + kind: TypeNode { connective: Atom { identity: ^wall_call_site } }, + children: [], + occurrence_id: wall_call_site_locus +} + +// ONE RECORD: the image of `decl_ref(module_path: "", decl_name: "")` -- the authored +// operator edge naming the ^decl_ref atom, the lexemes as authored edges. +fn wall_record(mp: Symbol, dn: Symbol) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^decl_ref }, + target: Node { kind: TypeNode { connective: Atom { identity: ^decl_ref } }, children: [], occurrence_id: OccurrenceSynthetic } + }, + Edge { + label: Authored { name: ^module_path }, + target: Node { kind: TypeNode { connective: Atom { identity: mp } }, children: [], occurrence_id: OccurrenceSynthetic } + }, + Edge { + label: Authored { name: ^decl_name }, + target: Node { kind: TypeNode { connective: Atom { identity: dn } }, children: [], occurrence_id: OccurrenceSynthetic } + } + ], + occurrence_id: wall_record_locus + } +} + +// THE UNINTERPRETABLE-ENTRY MARKER: what the carrier hangs for an entry that reads but is not a +// decl_ref(module_path, decl_name) call -- a record conj whose single edge names the refusal. +data wall_marker_entry: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^infer_reason_admit_callers_entry_not_decl_ref }, + target: Node { kind: TypeNode { connective: Atom { identity: ^infer_reason_admit_callers_entry_not_decl_ref } }, children: [], occurrence_id: OccurrenceSynthetic } + } + ], + occurrence_id: wall_marker_locus +} + +// THE CLAUSE-FALLBACK MARKER: what the carrier hangs under AdmitCallersEdge when the authored +// spine does not read. +data wall_clause_fallback: Node = Node { + kind: TypeNode { connective: Atom { identity: ^infer_reason_admit_callers_entries_unreadable } }, + children: [], + occurrence_id: wall_clause_fallback_locus +} + +// A ROSTER SPINE with one well-formed record: the record rides the grammar's +// sequence-left projection edge, the empty conj root rides the sequence-right one — the declared +// sequence-pair shape (sugar_sequence_pair_optional reads exactly these symbols). +data wall_roster_single: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^grammar_sequence_left_node_projection }, + target: wall_record(mp: ^bcb, dn: ^listed_caller) + }, + Edge { + label: Authored { name: ^grammar_sequence_right_node_projection }, + target: Node { kind: TypeNode { connective: Conj }, children: [], occurrence_id: OccurrenceSynthetic } + } + ], + occurrence_id: wall_roster_locus +} + +// --------------------------------------------------------------------------- +// (1) THE RED: a call from a declaration outside the list refuses. +// --------------------------------------------------------------------------- + +test fn the_wall_refuses_a_call_from_a_declaration_outside_the_list_holds() -> Bool { + match infer_admission_judge_entries( + application: wall_application, + entries: [wall_record(mp: ^bcb, dn: ^listed_caller)], + caller: InferAdmissionCallerKey { decl_name: ^outside_caller }, + module_symbol: ^bcb + ) { + None => false + Some { diagnostics: d } => + diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^infer_reason_caller_admission_refused) + } +} + +// --------------------------------------------------------------------------- +// (2) THE CONTROL: the listed declaration is admitted -- the judge's positive direction. +// --------------------------------------------------------------------------- +test fn the_wall_admits_a_call_from_the_listed_declaration_holds() -> Bool { + match infer_admission_judge_entries( + application: wall_application, + entries: [wall_record(mp: ^bcb, dn: ^listed_caller)], + caller: InferAdmissionCallerKey { decl_name: ^listed_caller }, + module_symbol: ^bcb + ) { + None => true + Some { diagnostics: _ } => false + } +} + +// --------------------------------------------------------------------------- +// (3) THE ENTRY-SHAPE RED: an entry that is not a decl_ref call refuses at the entry and is +// reported beside the refusal -- never dropped from the roster. +// --------------------------------------------------------------------------- + +test fn an_entry_that_is_not_a_decl_ref_call_refuses_at_the_entry_holds() -> Bool { + match infer_admission_judge_entries( + application: wall_application, + entries: [wall_marker_entry], + caller: InferAdmissionCallerKey { decl_name: ^outside_caller }, + module_symbol: ^bcb + ) { + None => false + Some { diagnostics: d } => + diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^infer_reason_admit_callers_entry_not_decl_ref) + } +} + +// --------------------------------------------------------------------------- +// (4) THE CLAUSE-FALLBACK RED: a clause the carrier could not read refuses at the clause as +// entries_unreadable -- a malformed clause is never a shorter roster. +// --------------------------------------------------------------------------- + +test fn a_clause_the_carrier_cannot_read_refuses_as_entries_unreadable_holds() -> Bool { + match infer_admit_clause_entries(roster: wall_clause_fallback) { + Rejected { diagnostics: d } => + diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^infer_reason_admit_callers_entries_unreadable) + Accepted { value: _, diagnostics: _ } => false + } +} + +// --------------------------------------------------------------------------- +// (5) THE READER'S POSITIVE CONTROL: a well-formed roster reads as exactly the authored +// coordinates -- one record, spelling bcb.listed_caller. +// --------------------------------------------------------------------------- + +test fn a_well_formed_roster_reads_as_the_authored_coordinates_holds() -> Bool { + match infer_admit_clause_entries(roster: wall_roster_single) { + Rejected { diagnostics: _ } => false + Accepted { value: records, diagnostics: _ } => + match records { + Empty => false + Cons { head: record, tail: tail } => + match tail { + Empty => + match infer_admit_record_check(record: record) { + Absent => false + Present { value: coords } => + if symbol_eq(a: coords.module_path, b: ^bcb) { + symbol_eq(a: coords.decl_name, b: ^listed_caller) + } else { + false + } + } + Cons { head: _, tail: _ } => false + } + } + } +} + +// --------------------------------------------------------------------------- +// (6) FAIL CLOSED: a DECLARED callee whose arrow cannot be read refuses with a typed, +// located cause carrying the looked-up key -- never an admit. The declaration is present in +// resolved_declarations (keyed by the reference's own qualified path) but is not an Arrow, so +// the arrow read fails: undetermined check, refusal. The boundary: a child not marked as a +// declaration reference is no judgment (the ordinary grounding machinery owns it); a child +// MARKED as a declaration reference with no index entry is unresolved and refuses (the seam +// claims below pin that boundary). +// --------------------------------------------------------------------------- + +data wall_decl_ref_locus: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 915 } } + +fn wall_decl_ref_target() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: StructuralLabel { label: CoreMarker { marker: DeclarationReferenceEdge } }, + target: Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^fold_list_node_head }, + target: Node { + kind: TypeNode { connective: Atom { identity: ^wall_callee } }, + children: [], + occurrence_id: wall_call_site_locus + } + }, + Edge { + label: Authored { name: ^fold_list_node_tail }, + target: Node { + kind: TypeNode { connective: Conj }, + children: [], + occurrence_id: wall_call_site_locus + } + } + ], + occurrence_id: wall_call_site_locus + } + } + ], + occurrence_id: wall_decl_ref_locus + } +} + +fn wall_unreadable_callee_application() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [Edge { label: Positional, target: wall_decl_ref_target() }], + occurrence_id: wall_call_site_locus + } +} + +// THE INDEX FOR THE FAIL-CLOSED WITNESS: the declaration IS present — keyed by the very qualified +// path the callee reference yields — but the declared node is not an Arrow, so the arrow read +// cannot succeed. This pins the refusal's true scope: a DECLARED callee whose arrow is unreadable +// refuses (fail closed); a callee that names no declaration at all is not judged (the ordinary +// grounding machinery owns it). +fn wall_index_with_unreadable_arrow() -> SymbolIndex { + match declaration_reference_path_optional(node: wall_decl_ref_target()) { + Absent => empty_symbol_index() + Present { value: qn } => + symbol_index_insert( + index: empty_symbol_index(), + qualified_path: qn, + resolved: Node { + kind: TypeNode { connective: Conj }, + children: [], + occurrence_id: OccurrenceSynthetic + } + ) + } +} + +test fn a_declared_callee_whose_arrow_is_unreadable_refuses_holds() -> Bool { + match infer_caller_admission_application_diags( + application: wall_unreadable_callee_application(), + caller: optional_present(value: InferAdmissionCallerKey { decl_name: ^outside_caller }), + module_symbol: optional_present(value: ^bcb), + entries: [], + declarations: wall_index_with_unreadable_arrow() + ) { + None => false + Some { diagnostics: d } => + diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^infer_reason_caller_admission_callee_unreadable) + && match d.head.correction { + Suggested { node: key } => + match key.occurrence_id { + OccurrenceMinted { id: kid } => kid.value == 915 + OccurrenceProjected { id: kid, caused_by: _ } => kid.value == 915 + OccurrencePending { caused_by: _ } => false + OccurrenceSynthetic => false + } + Unavailable { reason: _ } => false + } + } +} + +// THE SEAM CONTROLS (side chat review 5439746794): a callee marked as a declaration reference +// whose declaration is unavailable is NOT "not declared" -- it is a grounding failure, and the +// call refuses with a typed, located cause. A callee that is not marked as a declaration +// reference at all stays outside the judgment: no marker, no admit list to consult. + +test fn a_marked_callee_whose_declaration_is_unavailable_refuses_holds() -> Bool { + match infer_caller_admission_application_diags( + application: wall_unreadable_callee_application(), + caller: optional_present(value: InferAdmissionCallerKey { decl_name: ^outside_caller }), + module_symbol: optional_present(value: ^bcb), + entries: [], + declarations: empty_symbol_index() + ) { + None => false + Some { diagnostics: d } => diagnostics_has_reason(d: Some { diagnostics: d }, reason: ^infer_reason_caller_admission_callee_unresolved) + } +} + +fn wall_unmarked_application() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [Edge { label: Positional, target: Node { + kind: TypeNode { connective: Atom { identity: ^wall_not_a_declaration } }, + children: [], + occurrence_id: OccurrenceSynthetic + } }], + occurrence_id: wall_call_site_locus + } +} + +test fn an_unmarked_callee_is_not_judged_holds() -> Bool { + match infer_caller_admission_application_diags( + application: wall_unmarked_application(), + caller: optional_present(value: InferAdmissionCallerKey { decl_name: ^outside_caller }), + module_symbol: optional_present(value: ^bcb), + entries: [], + declarations: empty_symbol_index() + ) { + None => true + Some { diagnostics: _ } => false + } +} + +// --------------------------------------------------------------------------- +// (8) THE FACTS-PRESENT DISCRIMINATOR (side chat review 5442100127): the wrapper's first branch +// (facts Absent) refuses on its own; the JUDGED path must refuse too when the facts DO supply a +// callee Arrow -- one that carries no carrier edge -- and the resolved-declarations index lacks +// the marked callee. The shared judged helper is called in exactly that state: roster lookup +// Absent (no edge on the arrow), child0 marked, declaration missing -> RosterLookupUnresolved -> +// the typed refusal. Collapsing RosterLookupUnresolved back into RosterLookupAbsent (the +// fail-open seam) turns exactly this claim red. +// --------------------------------------------------------------------------- +fn wall_arrow_without_the_carrier_edge() -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [], + occurrence_id: OccurrenceSynthetic + } +} + +test fn a_facts_arrow_without_the_carrier_edge_and_an_unresolved_declaration_refuses_holds() -> Bool { + diagnostics_has_reason( + d: infer_caller_admission_judged_diags( + application: wall_unreadable_callee_application(), + caller: optional_present(value: InferAdmissionCallerKey { decl_name: ^outside_caller }), + module_symbol: optional_present(value: ^bcb), + arrow: wall_arrow_without_the_carrier_edge(), + declarations: empty_symbol_index() + ), + reason: ^infer_reason_caller_admission_callee_unresolved + ) +} diff --git a/src/v2/test/claim/cli/v2_native_cli_test.dag b/src/v2/test/claim/cli/v2_native_cli_test.dag index d0818b9f6a7..5bae435b17b 100644 --- a/src/v2/test/claim/cli/v2_native_cli_test.dag +++ b/src/v2/test/claim/cli/v2_native_cli_test.dag @@ -416,12 +416,12 @@ test fn an_emit_refusal_exits_general_error_holds() -> Bool { // THE REFUSAL NAMES THE REFUSING LINK, NOT THE FIRST ADVISORY. This is the run-level claim the emit // arm's frontier above does not block: a refusal at ASSEMBLE happens before the single-module // emit frontier is reached, so a supplied ingest holding one file with tokens left after its module -// body refuses through the real front end (tokenize, parse, normalize over dag_language_model), -// with the grammar's overlap-residue advisory riding in front. The outcome's `reason` must be the +// body refuses through the real front end (tokenize, parse, normalize over dag_language_model). +// The outcome's `reason` must be the // FATAL -- the last link, parse_g0_tokens_remain -- and the detail must locate it: the compilation // unit the front end was handed (v2.compiler.program_assembly names the read's compilation_unit as // the diagnostic file) and the byte extent of the first remaining token. Before this change the `reason` field carried the head, -// which on every such refusal is the same grammar-global advisory, so three different broken +// which on every such refusal was then the same grammar-global advisory (a residue since retired), so three different broken // entries refused with one indistinguishable reason (gunbc#11507 measured it on the built binary). data cli_probe_entry_source: String = "module v2.cli_probe.entry\n\nimport std.types { Bool }\n\nfn cli_probe_entry_value(p: Bool) -> Bool { p }\n" diff --git a/src/v2/test/claim/coercion/identity_cast_emission_route_test.dag b/src/v2/test/claim/coercion/identity_cast_emission_route_test.dag index 004f4d2237a..3306e95285b 100644 --- a/src/v2/test/claim/coercion/identity_cast_emission_route_test.dag +++ b/src/v2/test/claim/coercion/identity_cast_emission_route_test.dag @@ -6,6 +6,7 @@ import std.optional { Absent } import v2.compiler.self_host.closure_emission { ClosureEmissionLocated, ClosureMemberEmitted, ClosureMemberRefused, emit_closure_from_ingest_located, } +import v2.compiler.target_carriers { emit_module_decl_separator } import v2.compiler.source_authority { DagSourceReadWitness } import v2.extdeps.languages.dag { dag_type_atom_node } import v2.extdeps.languages.rust { rust_target_model } @@ -28,12 +29,14 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // section 3). Two inhabitance rows still drive the door on this module, on the required gate. // gunbc.rung_drop identity_cast_route_new_witness_eval_step_cost retired when those inhabitance // identities billed inside the new-witness eval-step budget on gunbc#13489 required floor run -// 37522243686 (artifact required-floor-claim-cost). The native partner builds and runs the emitted -// text; it is not the last execution of the door. +// 37522243686 (artifact required-floor-claim-cost). Native builds the same member text this +// inhabitance asserts by exact equality. data identity_cast_source: String = "module v2.test.identity_cast\n\nfn f(x: Int) -> Int { x as Int }\n" data refused_cast_source: String = "module v2.test.identity_cast\n\nfn f(x: Int) -> Bool { x as Bool }\n" +data identity_cast_supplied_emitted_text: String = concat("fn f(x: i32) -> i32 { x }", emit_module_decl_separator) + type IdentityCastRouteVerdict = IdentityCastEntryEmitted { text: String, members: Int } | IdentityCastEntryRefused { reason: Symbol, members: Int } @@ -84,7 +87,7 @@ fn refused_cast_route_verdict() -> IdentityCastRouteVerdict { fn identity_cast_route_admitted_holds(v: IdentityCastRouteVerdict) -> Bool { match v { IdentityCastEntryEmitted { text: t, members: n } => - n == 1 && string_contains(s: t, pattern: "fn f(x: i32) -> i32") + n == 1 && t == identity_cast_supplied_emitted_text IdentityCastEntryRefused { reason: _, members: _ } => false IdentityCastClosureRefused { reason: _ } => false } diff --git a/src/v2/test/claim/compiler/collection_count_realization_test.dag b/src/v2/test/claim/compiler/collection_count_realization_test.dag new file mode 100644 index 00000000000..cbc088e0028 --- /dev/null +++ b/src/v2/test/claim/compiler/collection_count_realization_test.dag @@ -0,0 +1,123 @@ +module v2.test.claim.compiler.collection_count_realization + +import v2.compiler.fold_lowering { collection_roster_primitive_call_row_optional, collection_roster_signature_arrow } +import v2.compiler.infer { InferFormalMatched, InferFormalShapeMismatch, infer_formals_from_domain, infer_match_generic_formal } +import v2.test.claim.compiler.infer_fold_member_instance { fmi_assemble, fmi_infer_tree, fmi_reason } +import std.types { Bool, List } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_to_dotted_string } +import std.optional { Absent, Optional, Present } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { Atom, Instantiation, Node, TypeNode, node_subtree_nodes, node_synthetic } +import v2.std.node_query { node_positional_child_targets } +import v2.std.collection { list_at_optional } +import v2.std.symbol_index { empty_symbol_index } +import v2.extdeps.languages.dag { dag_binding_denotation } +import std.algebra { Cons, Empty } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: a bare `count` whose name no declaration claims binds to the std.algebra collection roster's +// primitive-call row (collection_count_shape), and v2 infer types the call by that row: the free-monoid carrier in, +// a STATED Int out -- the declared departure whose authority is gunbc.rung_drop collection_count_result_stated_int_until_declared_results_are_judged. +// +// SHAPE, PER DESIGN SECTION 3's WITNESS RULE (sharp-raven-357): exactly ONE claim runs the real route (assemble plus +// infer, ccc_route_runs_on_the_real_path); the others supply the row's signature at its interface, through the +// production projection, reader and matcher (v2.compiler.infer infer_match_generic_formal, gunbc#13187). DELETING THE +// ROUTE REDS THE REAL-PATH CLAIM: remove the count row from collection_roster_rows and `count(` is refused +// resolve_reason_unbound_symbol (mutation run cited in the PR). + +data ccc_route_source: String = "module p\n\nimport v2.std.collection { List }\n\ntype Pair {\n target: Int\n}\n\nfn f(xs: List) -> Bool {\n count(xs) == 0\n}\n\nfn g(xs: List) -> Int {\n count(xs)\n}\n" + +type CccRouteReading { + row_references: Int + infer_accepted: Bool +} + +fn ccc_route_reading() -> CccRouteReading { + match fmi_assemble(src: ccc_route_source) { + Rejected { diagnostics: _ } => CccRouteReading { row_references: 0, infer_accepted: false } + Accepted { value: tree, diagnostics: _ } => + CccRouteReading { + row_references: fold(node_subtree_nodes(root: tree.root), init: 0, f: fn(acc, n) { + match declaration_reference_path_optional(node: n) { + Present { value: p } => if qualified_name_to_dotted_string(qn: p) == "std.algebra.collection_count_shape" { acc + 1 } else { acc } + Absent => acc + } + }), + infer_accepted: fmi_reason(o: fmi_infer_tree(tree: tree)) == ^accepted + } + } +} + +// THE REAL PATH: both heads bind the row; a size compared with a literal, and a size returned at a declared Int, +// are accepted. +test fn ccc_route_runs_on_the_real_path() -> Bool { + let r = ccc_route_reading() + r.row_references == 2 && r.infer_accepted +} + +fn ccc_arrow() -> Optional { + match collection_roster_primitive_call_row_optional(head: ^count) { + Absent => Absent + Present { value: row } => collection_roster_signature_arrow(row: row) + } +} + +fn ccc_formals() -> List { + match ccc_arrow() { + Absent => Empty + Present { value: arrow } => + match list_at_optional(xs: node_positional_child_targets(node: arrow), index: 0) { + Absent => Empty + Present { value: domain } => + match infer_formals_from_domain(domain: domain) { + Absent => Empty + Present { value: formals } => fold(formals, init: Empty, f: fn(acc, formal) { Cons { head: formal.declared, tail: acc } }) + } + } + } +} + +fn ccc_int_type() -> Node { + match dag_binding_denotation(sym: ^dag_binding_type_int) { + Present { value: t } => t + Absent => node_synthetic(kind: TypeNode { connective: Atom { identity: ^dag_binding_type_int } }, children: []) + } +} + +fn ccc_is_instantiation(t: Node) -> Bool { + match t.kind { + TypeNode { connective: Instantiation } => true + TypeNode { connective: _ } => false + _ => false + } +} + +// SUPPLIED: one formal, the free monoid. +test fn ccc_signature_is_one_free_monoid_formal() -> Bool { + match ccc_formals() { + Cons { head: f, tail: Empty } => ccc_is_instantiation(t: f) + _ => false + } +} + +// SUPPLIED, RED: an Int at the row's formal is a shape mismatch to the production matcher (the size of a non-collection). +test fn ccc_size_of_a_non_collection_is_a_shape_mismatch() -> Bool { + match list_at_optional(xs: ccc_formals(), index: 0) { + Absent => false + Present { value: formal } => + match infer_match_generic_formal(formal: formal, produced: ccc_int_type(), type_params: [^collection_roster_element], instances: Empty, declarations: empty_symbol_index()) { + InferFormalShapeMismatch => true + InferFormalMatched { instances: _ } => false + } + } +} + +// SUPPLIED: the row's result is the stated kernel Int (the declared departure), read off the signature's codomain. +test fn ccc_size_result_is_the_stated_int() -> Bool { + match ccc_arrow() { + Absent => false + Present { value: arrow } => list_at_optional(xs: node_positional_child_targets(node: arrow), index: 1) == Present { value: ccc_int_type() } + } +} diff --git a/src/v2/test/claim/compiler/data_reference_value_test.dag b/src/v2/test/claim/compiler/data_reference_value_test.dag new file mode 100644 index 00000000000..77ddda3e1d2 --- /dev/null +++ b/src/v2/test/claim/compiler/data_reference_value_test.dag @@ -0,0 +1,182 @@ +module v2.test.claim.compiler.data_reference_value + +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { infer, infer_reference_value_type } +import v2.compiler.inferred_tree { InferredTree } +import v2.compiler.eval { eval, inputs_root_only } +import v2.extdeps.runtimes.v2_evaluator { v2_evaluator_interpretation } +import v2.test.claim.callexec.declaration_reference_eval { CrefReading, cref_assemble, cref_call_carries_declaration, cref_executes_to, cref_reading_of_outcome, cref_the_call_to } +import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.node { Arrow, Atom, Symbol, Authored, ConstantMemberEdge, Conj, Edge, Node, Positional, TypeNode, core_edge, edge_is_core, node_subtree_nodes } +import v2.std.node_query { arrow_constant_member_codomain } +import std.occurrence_identity { OccurrenceSynthetic } +import std.optional { Absent, Present } +import std.algebra { list_append } +import std.types { List } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: a reference to a module-level `data` row USES ITS VALUE. Body lowering realizes `data x: T = e` +// as the nullary Arrow a `fn x() -> T` lowers to, and marks it (v2.std.node ConstantMemberEdge, written by +// v2.compiler.body_lowering_fold body_lower_constant_member_arrow); v2.compiler.infer types a reference to a +// marked Arrow at its declared codomain (infer_reference_value_type) and v2.compiler.eval applies it +// (eval_constant_member_body), both through the one reader v2.std.node_query arrow_constant_member_codomain. +// BOUNDARY: source text in, through the production assembly route, then infer (and eval for the +// execution row) -- except the marker-reader rows, whose input is supplied (see their header). +// +// RED ON THE BASE: before the marker, a bare `k` over `data k: Int` was typed as the function () -> Int, so +// `data j: Int = k` refused arrow_body_does_not_inhabit_declared_return, `fn consumer() -> Int { k }` refused +// the same way, and eval refused the reference with eval_rejected_runtime_binding_lookup_miss. + +// THE REAL PATH, ONCE: one source carrying every accepted case, assembled once and read whole by the rows +// below, so the floor fills it once (v2.workflow.floor_pure_producer_share). The smallest source that holds +// the subject: one constant, a fn whose body is a reference to it, and a call to that fn. +data drv_source: String = "module p\n\ndata k: Int = 7\n\nfn callee() -> Int {\n k\n}\n\nfn consumer() -> Int {\n callee()\n}\n" + +// ONE ASSEMBLY, read whole by every real-path row below (the infer rows, the marker row and the eval row), +// so the floor fills it once. The call-execution route's assembly is used because the eval row needs +// exactly the tree that route evaluates, and this source needs no peer modules. +fn drv_assembled() -> Outcome { + cref_assemble(src: drv_source) +} + +// THE ONE INFERENCE of that assembly, read by row (1) only: an inferred tree is not a portable served value, +// so a second reader would pay it again rather than share it. +fn drv_inferred() -> Outcome { + match drv_assembled() { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: tree, diagnostics: _ } => infer(tree: tree) + } +} + +fn drv_reason(o: Outcome) -> Symbol { + match o { + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) + Accepted { value: _, diagnostics: _ } => ^accepted + } +} + +// The call to `callee`, evaluated over the shared inference: the call-execution route's own locator and +// reader (cref_the_call_to, cref_reading_of_outcome), so this row reads the same vocabulary those rows do. +fn drv_eval_reading(o: Outcome) -> CrefReading { + let unfound = CrefReading { call_found: false, declaration_carried: false, executed: false, int_result: Absent, result_bytes: Absent } + match o { + Rejected { diagnostics: _ } => unfound + Accepted { value: inferred, diagnostics: _ } => + match cref_the_call_to(root: inferred.root, name: ^callee) { + Absent => unfound + Present { value: call } => + cref_reading_of_outcome( + declaration_carried: cref_call_carries_declaration(inferred: inferred, call: call), + outcome: eval(tree: inferred, interpretation: v2_evaluator_interpretation(), inputs: inputs_root_only(root: call)) + ) + } + } +} + +// (1) THE ONE REAL-PATH ROW: infer accepts a data reference at its declared position (a constant as a fn +// body, at the fn's declared return), AND eval agrees -- the call to `callee` evaluates its body `k` to the +// constant's VALUE 7, not a closure and not the binding-miss refusal it took before. ONE ROW BECAUSE BOTH +// ASSERTIONS READ ONE INFERENCE and an inferred tree is not a portable served value +// (ServeCacheValueNotPortable), so two rows would pay the inference twice for no coverage the one row lacks. +test fn drv_a_data_reference_is_typed_at_its_declared_type_and_evaluates_to_its_value() -> Bool { + let o = drv_inferred() + drv_reason(o: o) == ^accepted && cref_executes_to(r: drv_eval_reading(o: o), n: 7) +} + +// (2) THE MARKER IS PRODUCED, NOT ONLY SUPPLIED: lowering the real source yields exactly the data Arrow +// marked (k) and leaves the nullary fn Arrows (callee, consumer) unmarked -- so a nullary fn is not +// mistaken for a constant, which is the meaning fork a marker-free "type every nullary Arrow by its +// codomain" rule would have been. +fn drv_marked_arrow_count(o: Outcome) -> Int { + match o { + Rejected { diagnostics: _ } => 0 - 1 + Accepted { value: tree, diagnostics: _ } => + fold(node_subtree_nodes(root: tree.root), init: 0, f: fn(acc, n) { + match arrow_constant_member_codomain(arrow: n) { + Present { value: _ } => acc + 1 + Absent => acc + } + }) + } +} + +fn drv_unmarked_arrow_count(o: Outcome) -> Int { + match o { + Rejected { diagnostics: _ } => 0 - 1 + Accepted { value: tree, diagnostics: _ } => + fold(node_subtree_nodes(root: tree.root), init: 0, f: fn(acc, n) { + match n.kind { + TypeNode { connective: Arrow } => + match arrow_constant_member_codomain(arrow: n) { + Present { value: _ } => acc + Absent => acc + 1 + } + _ => acc + } + }) + } +} + +test fn drv_lowering_marks_the_data_arrow_and_not_the_two_fn_arrows() -> Bool { + let o = drv_assembled() + drv_marked_arrow_count(o: o) == 1 && drv_unmarked_arrow_count(o: o) == 2 +} + +// (4) TYPING THE REFERENCE AT T IS NOT ADMITTING IT ANYWHERE: a reference to a marked Arrow is typed at +// EXACTLY its declared codomain (v2.compiler.infer infer_reference_value_type), so an Int constant presents +// Int to the judge and an Int-at-String position refuses there by the judge's own existing rule. The input +// is supplied (DESIGN section 3: one interface); the real path that types references through it is row (1). +test fn drv_a_marked_reference_is_typed_at_exactly_its_codomain() -> Bool { + infer_reference_value_type(callable: drv_nullary_arrow(extra: [drv_marker()])) == drv_int() +} + +test fn drv_an_unmarked_callable_is_typed_as_itself() -> Bool { + let f = drv_nullary_arrow(extra: []) + infer_reference_value_type(callable: f) == f +} + +// THE MARKER READER, SUPPLIED (DESIGN section 3: a witness discriminates at one interface). Its subject is +// v2.std.node_query arrow_constant_member_codomain alone: a marked nullary Arrow answers its codomain, the +// SAME Arrow unmarked answers Absent, and a non-Arrow carrying the marker answers Absent. The real path +// that produces the marker is row (2). +fn drv_int() -> Node { Node { kind: TypeNode { connective: Atom { identity: ^Int } }, children: [], occurrence_id: OccurrenceSynthetic } } +fn drv_empty() -> Node { Node { kind: TypeNode { connective: Conj }, children: [], occurrence_id: OccurrenceSynthetic } } +fn drv_marker() -> Edge { core_edge(marker: ConstantMemberEdge, target: drv_empty()) } + +fn drv_nullary_arrow(extra: List) -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: list_append( + left: [ + Edge { label: Positional, target: drv_empty() }, + Edge { label: Positional, target: drv_int() }, + Edge { label: Positional, target: drv_int() } + ], + right: extra + ), + occurrence_id: OccurrenceSynthetic + } +} + +test fn drv_a_marked_nullary_arrow_answers_its_codomain() -> Bool { + match arrow_constant_member_codomain(arrow: drv_nullary_arrow(extra: [drv_marker()])) { + Present { value: t } => t == drv_int() + Absent => false + } +} + +test fn drv_an_unmarked_nullary_arrow_denotes_its_function() -> Bool { + match arrow_constant_member_codomain(arrow: drv_nullary_arrow(extra: [])) { + Present { value: _ } => false + Absent => true + } +} + +test fn drv_a_marked_non_arrow_is_not_a_constant() -> Bool { + match arrow_constant_member_codomain(arrow: Node { kind: TypeNode { connective: Conj }, children: [drv_marker()], occurrence_id: OccurrenceSynthetic }) { + Present { value: _ } => false + Absent => true + } +} diff --git a/src/v2/test/claim/compiler/declared_return_result_test.dag b/src/v2/test/claim/compiler/declared_return_result_test.dag new file mode 100644 index 00000000000..39a723c99eb --- /dev/null +++ b/src/v2/test/claim/compiler/declared_return_result_test.dag @@ -0,0 +1,195 @@ +module v2.test.claim.compiler.declared_return_result + +import v2.test.claim.compiler.infer_fold_member_instance { fmi_assemble, fmi_infer_tree, fmi_reason } +import v2.compiler.infer { + infer_application_result_type, + infer_arrow_declared_return_type, + infer_projected_field_type, + infer_projection_receiver_of_type, + infer_receiver_arity_mismatch_diagnostic, + infer_type_equal_ignoring_provenance, + ProjectionReceiver, + ReceiverArityMismatch, + ReceiverDeclarationUnavailable, + ReceiverPayload, + ReceiverTypeNotADeclaration, + ReceiverTypeUnderived +} +import v2.std.qualified_name { declaration_reference_node } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.inhabitance { TypeVariableInstance } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { Arrow, Atom, Authored, Conj, Edge, Instantiation, Node, OccurrenceSynthetic, Positional, Symbol, TypeNode, node_synthetic } +import std.optional { Absent, Optional, Present } +import v2.std.algebra { length, list_map } +import std.algebra { Cons, list_snoc_item } +import std.types { List, String } +import v2.std.symbol_index { SymbolIndex, empty_symbol_index, symbol_index_insert, symbol_index_mark_declared_type_params } +import v2.std.type_binder { type_binder_set_children, type_params_edge } +import std.types { Bool } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: a call's result type when its callee declares a non-kernel return +// (v2.compiler.infer infer_application_result_type), and a field read off an instantiated generic +// record (infer_projected_field_type). +// +// WITNESS SHAPE (DESIGN section 3, a witness discriminates at one interface): five claims SUPPLY the +// callee's arrow and the call's instances at the result-type interface, built here, so each judges +// only its own case and pays no assembly. Each asserts where the replaced reader disagrees: +// infer_arrow_declared_return_type, the only reader before this change, answers Absent for a +// declared return. ONE claim runs the real path, source through the production assembly route into +// infer, and asserts the ROUTE: a generic declared return, instantiated at the call, projected, and +// judged at a declared parameter, refuses for a reason only that whole route produces. + +fn drr_atom(sym: Symbol) -> Node { + node_synthetic(kind: TypeNode { connective: Atom { identity: sym } }, children: []) +} + +fn drr_positional(target: Node) -> Edge { + Edge { label: Positional, target: target } +} + +// fn () -> ret +fn drr_arrow(params: List, ret: Node) -> Node { + let domain = node_synthetic(kind: TypeNode { connective: Conj }, children: []) + let positional = [drr_positional(target: domain), drr_positional(target: ret)] + if length(xs: params) == 0 { + node_synthetic(kind: TypeNode { connective: Arrow }, children: positional) + } else { + let binders = drr_binder_set(params: params) + node_synthetic(kind: TypeNode { connective: Arrow }, children: list_snoc_item(xs: positional, item: type_params_edge(conj: binders))) + } +} + +// A binder set built through the one mint (v2.std.type_binder type_binder_set_children), so it carries +// its declared-order edge and conforms, as a lowered signature's does. +fn drr_binder_set(params: List) -> Node { + let edges = list_map(xs: params, f: fn(p) { Edge { label: Authored { name: p }, target: drr_atom(sym: p) } }) + let source = drr_atom(sym: ^drr_binder_source) + node_synthetic(kind: TypeNode { connective: Conj }, children: type_binder_set_children(binders: edges, source: source)) +} + +fn drr_box_of(arg: Node) -> Node { + node_synthetic(kind: TypeNode { connective: Instantiation }, children: [drr_positional(target: drr_atom(sym: ^drr_box)), drr_positional(target: arg)]) +} + +fn drr_present_equals(o: Optional, expected: Node) -> Bool { + match o { + Absent => false + Present { value: v } => infer_type_equal_ignoring_provenance(a: v, b: expected) + } +} + +fn drr_is_absent(o: Optional) -> Bool { + match o { + Absent => true + Present { value: _ } => false + } +} + +// DECLARED RETURN: `fn mk() -> Tally` types its call as Tally. The replaced reader answered Absent. +test fn drr_a_declared_return_is_the_call_result() -> Bool { + let arrow = drr_arrow(params: [], ret: drr_atom(sym: ^drr_tally)) + drr_is_absent(o: infer_arrow_declared_return_type(arrow: arrow)) + && drr_present_equals(o: infer_application_result_type(arrow: arrow, instances: [], declarations: empty_symbol_index()), expected: drr_atom(sym: ^drr_tally)) +} + +// KERNEL FIRST: a kernel return answers exactly what the replaced reader answers. +test fn drr_a_kernel_return_is_unchanged() -> Bool { + let arrow = drr_arrow(params: [], ret: drr_atom(sym: ^dag_binding_type_int)) + match infer_arrow_declared_return_type(arrow: arrow) { + Absent => false + Present { value: kernel } => + drr_present_equals(o: infer_application_result_type(arrow: arrow, instances: [], declarations: empty_symbol_index()), expected: kernel) + } +} + +// INSTANTIATED: `fn boxed(..) -> Box` called with T bound to Tally types as Box. +test fn drr_a_generic_return_is_instantiated_at_the_call() -> Bool { + let arrow = drr_arrow(params: [^drr_t], ret: drr_box_of(arg: drr_atom(sym: ^drr_t))) + let instances = [TypeVariableInstance { binder: ^drr_t, instance: drr_atom(sym: ^drr_tally) }] + drr_is_absent(o: infer_arrow_declared_return_type(arrow: arrow)) + && drr_present_equals(o: infer_application_result_type(arrow: arrow, instances: instances, declarations: empty_symbol_index()), expected: drr_box_of(arg: drr_atom(sym: ^drr_tally))) +} + +// FRONTIER: the same callee with T bound by no argument stays NOT derived -- never typed as Box. +test fn drr_a_return_mentioning_an_unbound_type_parameter_is_not_derived() -> Bool { + let arrow = drr_arrow(params: [^drr_t], ret: drr_box_of(arg: drr_atom(sym: ^drr_t))) + drr_is_absent(o: infer_application_result_type(arrow: arrow, instances: [], declarations: empty_symbol_index())) +} + +// PROJECTED FIELD: `item: T` read off a Box receiver is Tally (the replaced reader kept T as +// written); a receiver that binds nothing keeps the field's declared type. +test fn drr_a_generic_record_field_projects_at_its_instantiation() -> Bool { + let bound = [TypeVariableInstance { binder: ^drr_t, instance: drr_atom(sym: ^drr_tally) }] + infer_type_equal_ignoring_provenance(a: infer_projected_field_type(field_type: drr_atom(sym: ^drr_t), instances: bound), b: drr_atom(sym: ^drr_tally)) + && infer_type_equal_ignoring_provenance(a: infer_projected_field_type(field_type: drr_atom(sym: ^drr_t), instances: []), b: drr_atom(sym: ^drr_t)) +} + +// WRONG TYPE-ARGUMENT COUNT: Box declares one type parameter, so a receiver typed Box +// has no instantiation to read a field at and refuses as an arity mismatch; Box reads its payload. +fn drr_box_path() -> List { + [^drr_p, ^drr_box] +} + +fn drr_box_index() -> SymbolIndex { + let payload = node_synthetic(kind: TypeNode { connective: Conj }, children: [Edge { label: Authored { name: ^item }, target: drr_atom(sym: ^drr_t) }]) + symbol_index_mark_declared_type_params( + index: symbol_index_insert(index: empty_symbol_index(), qualified_path: drr_box_path(), resolved: payload), + qualified_path: drr_box_path(), + params: drr_binder_set(params: [^drr_t]) + ) +} + +fn drr_box_applied(args: List) -> Node { + let head = declaration_reference_node(qn: drr_box_path(), occurrence_id: OccurrenceSynthetic) + node_synthetic(kind: TypeNode { connective: Instantiation }, children: list_map(xs: Cons { head: head, tail: args }, f: fn(n) { drr_positional(target: n) })) +} + +fn drr_is_arity_mismatch(r: ProjectionReceiver) -> Bool { + match r { + ReceiverArityMismatch => true + ReceiverPayload { payload: _, instances: _ } => false + ReceiverTypeNotADeclaration => false + ReceiverDeclarationUnavailable { path: _ } => false + ReceiverTypeUnderived => false + } +} + +fn drr_is_payload(r: ProjectionReceiver) -> Bool { + match r { + ReceiverPayload { payload: _, instances: _ } => true + ReceiverArityMismatch => false + ReceiverTypeNotADeclaration => false + ReceiverDeclarationUnavailable { path: _ } => false + ReceiverTypeUnderived => false + } +} + +test fn drr_a_wrong_type_argument_count_at_a_generic_record_projection_refuses() -> Bool { + let tally = drr_atom(sym: ^drr_tally) + drr_is_arity_mismatch(r: infer_projection_receiver_of_type(receiver_type: drr_box_applied(args: [tally, tally]), declarations: drr_box_index())) + && drr_is_payload(r: infer_projection_receiver_of_type(receiver_type: drr_box_applied(args: [tally]), declarations: drr_box_index())) + && infer_receiver_arity_mismatch_diagnostic(node: tally).reason == ^infer_reason_projection_receiver_type_arity_mismatch +} + +// THE ONE REAL-PATH CLAIM, ASSERTING THE ROUTE. `boxed(1).item` at a Bool parameter refuses as an +// argument mismatch only if the call's generic declared return is derived (Box), the field is +// projected at that instantiation (Int), and the projection is judged at the declared Bool. Before +// this change the call stayed underived and the program was ACCEPTED. +data drr_route_source: String = "module p\n\ntype Box {\n item: T\n}\n\nfn boxed(x: T) -> Box {\n Box { item: x }\n}\n\nfn wants_bool(b: Bool) -> Int {\n 0\n}\n\nfn k() -> Int {\n wants_bool(boxed(1).item)\n}\n" + +// The route's verdict is one nullary producer, assembled AND inferred once, so the floor fills the +// whole real path as this claim's shared fill (its single-claim fill debt) and the claim itself only +// compares the reason. +fn drr_route_reason() -> Symbol { + match fmi_assemble(src: drr_route_source) { + Rejected { diagnostics: _ } => ^drr_assembly_refused + Accepted { value: tree, diagnostics: _ } => fmi_reason(o: fmi_infer_tree(tree: tree)) + } +} + +test fn drr_the_real_route_judges_a_projected_generic_call_result() -> Bool { + drr_route_reason() == ^application_argument_does_not_inhabit +} diff --git a/src/v2/test/claim/compiler/generic_closure_outcome_construct_probe_test.dag b/src/v2/test/claim/compiler/generic_closure_outcome_construct_probe_test.dag new file mode 100644 index 00000000000..a87b764c309 --- /dev/null +++ b/src/v2/test/claim/compiler/generic_closure_outcome_construct_probe_test.dag @@ -0,0 +1,128 @@ +module v2.test.claim.compiler.generic_closure_outcome_construct_probe + +import v2.compiler.infer { + infer, + infer_kernel_value_type_optional, + infer_type_equal_ignoring_provenance +} +import v2.compiler.name_resolve { + Admission, + ResolutionSubject, + resolution_context, + resolve_in_context +} +import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.program_assembly { module_roots_from_source_root_ingest } +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.source_authority { SourceRootIngest } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.cross_tree.resolution { source_root_index_empty } +import v2.std.diagnostic { Accepted, None, Outcome, Rejected, Some, diagnostics_fatal_reason } +import v2.std.integer { integer_int_type_node } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.std.node { Atom, Node, Symbol, TypeNode, node_synthetic } +import std.optional { Absent, Present } +import v2.std.qualified_name { declaration_reference_node, qualified_name_from_dotted_string } +import v2.std.resolution_policy { default_name_resolution_policy } +import v2.std.text { String } +import std.algebra { Empty, FreeMonoid } +import v2.test.claim.compiler.infer_record_construct_field_inhabitance_witness_test { + rcf_member_accepted_and_decided, + rcf_read +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: infer_declared_type_obligation's kernel-value join (infer_kernel_value_type_optional, applied at +// every step of infer_type_alias_normalized). A position declared with the kernel Int (dag_binding_type_int) +// and one declared with the corpus v2.std.integer.Int (type Int = GroupCompletion) are one value type. The +// join keys the kernel declaration roster (full path) and dag_binding_denotation, never a last-segment Int. +// +// SUPPLIED claims judge the join at its own interface. ONE claim runs the real ingest -> resolve -> infer route +// (the alias claim). The negative side of the join is +// supplied (gbo_a_bool_name_is_not_the_kernel_int_holds), not re-run end to end. +// +// DECLARED FRONTIER, NOT CLAIMED HERE: generic-variant constructs undecided. A direct generic-variant construct +// (Acc { value: 1, n: 0 } at Out) is not accepted-and-decided on this base, with or without the join (measured +// at 41afbbbb, BuildBuddy 36e48268). Owner: gunbc#13558 (payload binders over Outcome via #13210's formal-payload +// obligation), which carries the claim that turns it green. + +data gbo_integer_stub_source: String = "module v2.std.integer\n\ntype Nat = Zero | One\n\ntype Int = Nat\n" + +data gbo_alias_source: String = "module v2.test.gbo_alias\n\nimport v2.std.integer { Int }\n\ntype Alias = v2.std.integer.Int\n\nfn one() -> Alias {\n 1\n}\n" + +data gbo_ingest: SourceRootIngest = [ + rcf_read(source: gbo_integer_stub_source, id: ^gbo_integer_stub_read, unit: ^gbo_integer_stub_cu, path: "src/v2/test/fixture/gbo/integer.dag"), + rcf_read(source: gbo_alias_source, id: ^gbo_alias_read, unit: ^gbo_alias_cu, path: "src/v2/test/fixture/gbo/alias.dag") +] + +fn gbo_fixture_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: gbo_ingest, lm: dag_language_model()) +} + +fn gbo_resolve_subject(subject: String) -> Outcome { + match gbo_fixture_normalized() { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: roots, diagnostics: _ } => + resolve_in_context( + context: resolution_context(lm: dag_language_model(), roots: roots, index: source_root_index_empty(), active_roots: Empty, policy: default_name_resolution_policy()), + admission: Admission { + subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: subject) }, + imports: Empty + } + ).resolved + } +} + +fn gbo_resolved_alias() -> Outcome { gbo_resolve_subject(subject: "v2.test.gbo_alias") } + +fn gbo_atom(id: Symbol) -> Node { + node_synthetic(kind: TypeNode { connective: Atom { identity: id } }, children: []) +} + +fn gbo_is_kernel_int(t: Node) -> Bool { + match infer_kernel_value_type_optional(t: t) { + Absent => false + Present { value: k } => infer_type_equal_ignoring_provenance(a: k, b: integer_int_type_node()) + } +} + +test fn gbo_an_int_spelling_atom_is_not_the_kernel_int_holds() -> Bool { + !gbo_is_kernel_int(t: gbo_atom(id: ^Int)) +} + +test fn gbo_a_module_int_declaration_is_not_the_kernel_int_holds() -> Bool { + !gbo_is_kernel_int( + t: declaration_reference_node( + qn: qualified_name_from_dotted_string(dotted: "v2.test.gbo_shadow.Int"), + occurrence_id: OccurrenceSynthetic + ) + ) +} + +test fn gbo_the_integer_int_declaration_is_the_kernel_int_holds() -> Bool { + gbo_is_kernel_int( + t: declaration_reference_node( + qn: qualified_name_from_dotted_string(dotted: "v2.std.integer.Int"), + occurrence_id: OccurrenceSynthetic + ) + ) +} + +test fn gbo_a_kernel_int_atom_is_the_kernel_int_holds() -> Bool { + gbo_is_kernel_int(t: gbo_atom(id: ^dag_binding_type_int)) +} + +test fn gbo_a_bool_name_is_not_the_kernel_int_holds() -> Bool { + !gbo_is_kernel_int(t: gbo_atom(id: ^Bool)) +} + +// THE ROUTE: a kernel Int literal returned at a declared `Alias` of the corpus Int (`fn one() -> Alias { 1 }`), +// on the real ingest -> resolve -> infer path, ACCEPTED AND DECIDED. The literal is the kernel atom; the declared +// side reaches the kernel atom only through the join, and only if recognition re-enters after the alias step. So it +// goes red both when the join is deleted (Alias unfolds to its right-hand side) and when recognition happens only at +// the input -- the integration and the regression in one claim (BuildBuddy 50c58a76 and 2002ea86). +test fn gbo_a_kernel_literal_inhabits_an_alias_of_the_corpus_int_holds() -> Bool { + rcf_member_accepted_and_decided(resolved: gbo_resolved_alias(), member: ^one) +} diff --git a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag index a123a758a94..288fae16854 100644 --- a/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag +++ b/src/v2/test/claim/compiler/infer_fold_member_instance_test.dag @@ -216,7 +216,8 @@ fn fmi_infer_with_member_formal_authored(tree: ResolvedTree) -> Outcome { symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, - unavailable_providers: tree.unavailable_providers + unavailable_providers: tree.unavailable_providers, + member_kinds: tree.member_kinds }) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: _, diagnostics: d } => Accepted { value: true, diagnostics: d } diff --git a/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag b/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag index d9c43a793a3..18332fd701d 100644 --- a/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag +++ b/src/v2/test/claim/compiler/infer_record_construct_field_inhabitance_witness_test.dag @@ -53,11 +53,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // can be satisfied by a check that refuses every construct, and no acceptance by one that judges // nothing: the accepted module is also asserted DECIDED (no counted obligation at its constructs). // -// WHAT IS NOT CLAIMED. A record declared in ANOTHER module has its field SET checked and its field -// VALUES counted, not judged: v2.compiler.resolve ResolvedTree resolved_declarations holds the -// subject module's declarations with resolved bodies, and no other module's. rcf_far_value pins that state as it is -- accepted, with -// a counted UndecidableFormalUnresolved at the value -- so it flips when infer can read a resolved -// declaration across modules (the row's trigger), and is then rewritten as a refusal. +// CROSS-MODULE FIELD VALUES ARE JUDGED TOO. A record declared in another module has its field SET +// checked (missing still refuses) and its field VALUES judged: `n: true` against `RcfFar.n: Int` +// refuses as `record_field_value_does_not_inhabit`, the same reason as a same-module Int field. +// The former pin (`UndecidableFormalUnresolved` on an Accepted construct) went red when that +// trigger landed. data rcf_provider_source: String = "module v2.test.rcfp\n\ntype RcfFar {\n n: Int\n m: Int\n}\n" data rcf_bad_source: String = "module v2.test.rcf_bad\n\ntype ArB {\n n: Int\n m: Int\n}\n\ndata ar_bad: ArB = ArB { n: true, m: 3 }\n" @@ -299,7 +299,7 @@ fn rcf_member_accepted_and_decided(resolved: Outcome, member: Symb match rcf_member_arrow(root: tree.root, name: member) { Absent => false Present { value: arrow } => - match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers }) { + match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers, member_kinds: tree.member_kinds }) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: ds } => !diagnostics_has_reason(d: ds, reason: ^inhabitance_undecidable_formal_unresolved) @@ -389,15 +389,10 @@ test fn rcf_a_cross_module_records_missing_field_refuses_holds() -> Bool { rcf_refuses_with(resolved: rcf_resolved_far_missing(), reason: ^record_construct_field_missing) } -// Its field VALUES are counted, not judged: the specimen's mismatch against a record declared in -// another module is accepted with a counted UndecidableFormalUnresolved. This claim goes red when -// infer reads a resolved declaration across modules, and is then rewritten to assert the refusal. -test fn rcf_a_cross_module_records_field_value_is_counted_not_judged_holds() -> Bool { - match rcf_verdict(resolved: rcf_resolved_far_value()) { - RcfInferAccepted { diagnostics: ds } => diagnostics_has_reason(d: ds, reason: ^inhabitance_undecidable_formal_unresolved) - RcfInferRefused { diagnostics: _ } => false - RcfResolveRefused => false - } +// Its field VALUES are judged: `n: true` against a record declared in another module refuses at +// the value, same class as the same-module Int specimen. +test fn rcf_a_cross_module_records_field_value_of_the_wrong_type_refuses_holds() -> Bool { + rcf_refuses_with(resolved: rcf_resolved_far_value(), reason: ^record_field_value_does_not_inhabit) } // A PATTERN IS NOT A CONSTRUCTION. `ArB { n: x }` in a match arm names one of ArB's two fields, and diff --git a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag index 8df7137c1ea..161eb4b8994 100644 --- a/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag +++ b/src/v2/test/claim/compiler/kernel_value_type_roster_witness_test.dag @@ -200,7 +200,7 @@ test fn kvr_a_named_calls_bool_formal_holding_an_int_refuses_holds() -> Bool { match rcf_member_arrow(root: tree.root, name: ^kvr_g) { Absent => false Present { value: arrow } => - match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers }) { + match infer(tree: ResolvedTree { root: arrow, symbol_index: tree.symbol_index, resolved_declarations: tree.resolved_declarations, lexical_bindings: tree.lexical_bindings, unavailable_providers: tree.unavailable_providers, member_kinds: tree.member_kinds }) { Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^application_argument_does_not_inhabit Accepted { value: _, diagnostics: _ } => false } diff --git a/src/v2/test/claim/compiler/literal_elaboration_test.dag b/src/v2/test/claim/compiler/literal_elaboration_test.dag new file mode 100644 index 00000000000..627ec0340c3 --- /dev/null +++ b/src/v2/test/claim/compiler/literal_elaboration_test.dag @@ -0,0 +1,231 @@ +module v2.test.claim.compiler.literal_elaboration + +import std.literal_elaboration { KernelGroundingAbsent, KernelGroundingAmbiguous, KernelGroundingFound, KernelIntLiteral, kernel_grounding_admits, kernel_grounding_for } +import gunbc.structural_realization_bindings { kernel_grounding_rows } +import std.decl_ref { decl_ref } +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.infer { InferLiteralElaborationRefused, InferLiteralElaboratesTo, InferLiteralNotElaborated, infer, infer_application_callee_use, infer_literal_elaborates_at } +import v2.compiler.inferred_tree { InferredTree } +import v2.compiler.eval { eval, inputs_root_only } +import v2.extdeps.runtimes.v2_evaluator { v2_eval_bool_runtime_value, v2_evaluator_interpretation } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_binding_denotation, dag_int_literal_node_from_lexeme, dag_language_model, dag_string_literal_node } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty, list_append } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.node { Atom, Edge, Node, NodeFold, Positional, Symbol, TypeNode, fold_node, node_synthetic } +import v2.std.compilers.body_lowering { lower_unary_prefix } +import v2.std.symbol_index { empty_symbol_index } +import v2.std.qualified_name { declaration_reference_node, qualified_name_from_dotted_string } +import std.occurrence_identity { OccurrenceSynthetic } +import std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.runtime { RuntimeValue } +import v2.std.qualified_name { declaration_reference_path_optional } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import std.types { Bool, List } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// SUBJECT: v2 infer elaborates an integer literal at an expected type through the declared tables +// (v2.compiler.infer infer_literal_elaborates_at) at a declared position. std.nat Nat is KERNEL-GROUNDED +// (gunbc.structural_realization_bindings kernel_grounding_rows, declared range min 0): a literal at Nat is its own +// value when std.literal_elaboration kernel_grounding_admits says so, and `-1` refuses from that range +// (calm-boar-904, ruling A on review 77219). Nat has no literal homomorphism row. +// +// SHAPE, PER DESIGN SECTION 3's WITNESS RULE (sharp-raven-357): exactly ONE claim runs the real route +// (lel_route_runs_on_the_real_path: one module, assembled with a std.nat peer, inferred, and two calls evaluated on +// the v2 eval route). Every other claim supplies a literal node and an expected type to infer_literal_elaborates_at, +// the one interface under test. DELETING THE ROUTE REDS THE REAL-PATH CLAIM: remove std.nat Nat's row from +// kernel_grounding_rows and `g(n: 0)` at Nat is refused. + +data lel_artifact: Artifact = Artifact { kind: SourceFile, id: ^literal_elaboration_p_artifact, file_path: "src/v2/pilot/literal_elaboration_p.dag" } +data lel_nat_artifact: Artifact = Artifact { kind: SourceFile, id: ^literal_elaboration_nat_artifact, file_path: "src/v2/pilot/literal_elaboration_nat.dag" } + +data lel_nat_peer: String = "module std.nat\n\ntype Nat\n = Zero\n | Succ { prev: Nat }\n" + +fn lel_source(src: String, artifact: Artifact, cu: Symbol) -> DagSourceReadWitness { + DagSourceReadWitness { source: Medium { carried: src, fidelity: Lossless }, artifact: artifact, compilation_unit: cu, source_root: V2Tree } +} + +fn lel_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: lel_source(src: src, artifact: lel_artifact, cu: ^literal_elaboration_p_cu), + tail: Cons { head: lel_source(src: lel_nat_peer, artifact: lel_nat_artifact, cu: ^literal_elaboration_nat_cu), tail: Empty } + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +fn lel_inferred(src: String) -> Outcome { + match lel_assemble(src: src) { + Rejected { diagnostics: d } => Rejected { diagnostics: d } + Accepted { value: resolved, diagnostics: _ } => infer(tree: resolved) + } +} + +fn lel_reason(src: String) -> Symbol { + match lel_inferred(src: src) { + Accepted { value: _, diagnostics: _ } => ^accepted + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) + } +} + +// THE ONE REAL-PATH FIXTURE: literals at a declared Nat (0, 3), a literal at Int, an Int expression, and two calls +// whose comparison the v2 eval route is asked to run. +data lel_route_source: String = "module p\n\nimport std.nat { Nat, Zero, Succ }\n\nfn g(n: Nat) -> Bool {\n true\n}\n\nfn gi(n: Int) -> Bool {\n true\n}\n\nfn uses() -> Bool {\n g(n: 0) && g(n: 3) && gi(n: 3)\n}\n\nfn sum() -> Int {\n 0 + 1\n}\n\nfn zz(n: Nat) -> Bool {\n n == 0\n}\n\nfn zo(n: Nat) -> Bool {\n n == 0\n}\n\nfn k0() -> Bool {\n zz(n: Zero)\n}\n\nfn k1() -> Bool {\n zo(n: Succ { prev: Zero })\n}\n" + +fn lel_callee_path_is(callee: Node, name: Symbol) -> Bool { + match declaration_reference_path_optional(node: callee) { + Absent => false + Present { value: path } => path == Cons { head: ^p, tail: Cons { head: name, tail: Empty } } + } +} + +fn lel_calls_to(root: Node, name: Symbol) -> List { + fold_node( + n: root, + algebra: NodeFold { + init: fn(n0) { + match infer_application_callee_use(node: n0) { + Absent => [] + Present { value: callee } => if lel_callee_path_is(callee: callee, name: name) { [n0] } else { [] } + } + }, + step: fn(acc, _e, child) { list_append(left: acc, right: child) } + } + ) +} + +fn lel_eval_call_bool(inferred: InferredTree, name: Symbol) -> Optional { + match lel_calls_to(root: inferred.root, name: name) { + Cons { head: call, tail: Empty } => + match eval(tree: inferred, interpretation: v2_evaluator_interpretation(), inputs: inputs_root_only(root: call)) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: v, diagnostics: _ } => + if v == v2_eval_bool_runtime_value(is_true: true) { + optional_present(value: true) + } else if v == v2_eval_bool_runtime_value(is_true: false) { + optional_present(value: false) + } else { + optional_absent() + } + } + Cons { head: _, tail: _ } => optional_absent() + Empty => optional_absent() + } +} + +type LelRouteReading { + infer_accepted: Bool + zero_answer: Optional + one_answer: Optional +} + +fn lel_route_reading() -> LelRouteReading { + match lel_inferred(src: lel_route_source) { + Rejected { diagnostics: _ } => LelRouteReading { infer_accepted: false, zero_answer: optional_absent(), one_answer: optional_absent() } + Accepted { value: inferred, diagnostics: _ } => + LelRouteReading { infer_accepted: true, zero_answer: lel_eval_call_bool(inferred: inferred, name: ^zz), one_answer: lel_eval_call_bool(inferred: inferred, name: ^zo) } + } +} + +// THE REAL PATH: the module is accepted (0 and 3 at a declared Nat, 3 at Int, 0 + 1 an Int), and on the v2 eval +// route a Nat compared with 0 NEVER ANSWERS WRONGLY. Today eval refuses both calls -- `==` is not judged in v2 infer +// (gunbc.recurring_failure_mode comparison_operands_never_judged_in_v2_infer), so the comparison is underived, and a +// Nat constructor argument is refused too; refusing is not answering wrongly. When comparisons are judged, Zero must +// answer true and Succ { prev: Zero } false; a one-sided answer or a wrong Bool -- the constructor-versus-literal +// representation mismatch -- turns this red. +test fn lel_route_runs_on_the_real_path() -> Bool { + let r = lel_route_reading() + r.infer_accepted + && ((r.zero_answer == optional_absent() && r.one_answer == optional_absent()) + || (r.zero_answer == optional_present(value: true) && r.one_answer == optional_present(value: false))) +} + +// SUPPLIED INPUTS: a literal node and an expected type, handed to infer_literal_elaborates_at. +fn lel_literal(lexeme: String) -> Node { + match dag_int_literal_node_from_lexeme(lexeme: lexeme, occurrence_id: OccurrenceSynthetic) { + Accepted { value: n, diagnostics: _ } => n + Rejected { diagnostics: _ } => node_synthetic(kind: TypeNode { connective: Atom { identity: ^lel_literal_unbuilt } }, children: []) + } +} + +fn lel_nat_type() -> Node { declaration_reference_node(qn: qualified_name_from_dotted_string(dotted: "std.nat.Nat"), occurrence_id: OccurrenceSynthetic) } +fn lel_pair_type() -> Node { declaration_reference_node(qn: qualified_name_from_dotted_string(dotted: "p.Pair"), occurrence_id: OccurrenceSynthetic) } +fn lel_int_type() -> Node { + match dag_binding_denotation(sym: ^dag_binding_type_int) { + Present { value: t } => t + Absent => node_synthetic(kind: TypeNode { connective: Atom { identity: ^dag_binding_type_int } }, children: []) + } +} + +fn lel_elaborates(literal: Node, expected: Node) -> Bool { + match infer_literal_elaborates_at(literal_node: literal, expected: expected, declarations: empty_symbol_index()) { + InferLiteralElaboratesTo { destination_type: _ } => true + InferLiteralNotElaborated => false + InferLiteralElaborationRefused { cause: _ } => false + } +} + +fn lel_not_elaborated(literal: Node, expected: Node) -> Bool { + match infer_literal_elaborates_at(literal_node: literal, expected: expected, declarations: empty_symbol_index()) { + InferLiteralElaboratesTo { destination_type: _ } => false + InferLiteralNotElaborated => true + InferLiteralElaborationRefused { cause: _ } => false + } +} + +// SUPPLIED: 0 and 3 elaborate at std.nat Nat through its kernel grounding (inside its declared range). +test fn lel_literals_elaborate_at_nat() -> Bool { + lel_elaborates(literal: lel_literal(lexeme: "0"), expected: lel_nat_type()) && lel_elaborates(literal: lel_literal(lexeme: "3"), expected: lel_nat_type()) +} + +// SUPPLIED: a literal at the kernel Int is not elaborated (it is already an Int). +test fn lel_literal_at_int_is_not_elaborated() -> Bool { + lel_not_elaborated(literal: lel_literal(lexeme: "3"), expected: lel_int_type()) +} + +// SUPPLIED, RED: a literal at a type with no row is not elaborated, so the judge refuses it with its own cause +// (application_argument_does_not_inhabit; v2.test.claim.compiler.generic_formal_instantiation is that judge's subject). +test fn lel_literal_at_a_type_without_a_row_is_not_elaborated() -> Bool { + lel_not_elaborated(literal: lel_literal(lexeme: "0"), expected: lel_pair_type()) +} + +// SUPPLIED, RED: `-1` -- a unary Transform over the literal 1, not a literal node -- refuses at Nat, typed, pinned to the +// RANGE's reason so a later parse change (a signed literal token) cannot make it silent and so the refusal is the +// grounding row's, not a check of infer's own. Before #13320 it was admitted at Nat. +test fn lel_negative_literal_at_nat_refuses() -> Bool { + match infer_literal_elaborates_at(literal_node: lower_unary_prefix(operator: node_synthetic(kind: TypeNode { connective: Atom { identity: ^dag_token_minus } }, children: []), operand: lel_literal(lexeme: "1")), expected: lel_nat_type(), declarations: empty_symbol_index()) { + InferLiteralElaborationRefused { cause: c } => c == ^infer_reason_literal_outside_kernel_grounding_range + InferLiteralElaboratesTo { destination_type: _ } => false + InferLiteralNotElaborated => false + } +} + +// SUPPLIED, RED: a string literal has no row into Nat and is not elaborated (never a row picked for another kind). +test fn lel_literal_of_another_kind_is_not_elaborated_at_nat() -> Bool { + lel_not_elaborated(literal: dag_string_literal_node(value: "a", occurrence_id: OccurrenceSynthetic), expected: lel_nat_type()) +} + +// THE SEED ROUTE EXECUTES THE SAME COMPARISON (the claim body runs on the seed interpreter). +test fn lel_count_compared_with_a_literal_executes_on_the_seed() -> Bool { + count([]) == 0 && !(count([1]) == 0) +} + +// SUPPLIED: THE RANGE IS THE ROW'S. Nat's grounding row, read from the production table, admits 0 and 3 and not -1 -- +// so the RED above is derived from the declared range (std.literal_elaboration kernel_grounding_admits over the +// std.types range bounds the row carries), and changing the row's min is what would move it. +test fn lel_nat_grounding_row_declares_the_range() -> Bool { + match kernel_grounding_for(rows: kernel_grounding_rows, source_kind: KernelIntLiteral, carrier: decl_ref(module_path: "std.nat", decl_name: "Nat")) { + KernelGroundingFound { row: row } => + kernel_grounding_admits(row: row, value: 0) && kernel_grounding_admits(row: row, value: 3) && !kernel_grounding_admits(row: row, value: 0 - 1) + KernelGroundingAbsent => false + KernelGroundingAmbiguous { row_count: _ } => false + } +} diff --git a/src/v2/test/claim/compiler_materialization_witness_test.dag b/src/v2/test/claim/compiler_materialization_witness_test.dag deleted file mode 100644 index 247f4fbd67d..00000000000 --- a/src/v2/test/claim/compiler_materialization_witness_test.dag +++ /dev/null @@ -1,225 +0,0 @@ -module v2.test.claim.compiler_materialization_witness - -import v2.std.live_tree { LiveTreeDisposition } -import std.realization { LocalInProcess, Memoize, Recompute } -import v2.std.diagnostic { Accepted, Rejected } -import v2.compiler.materialization_carriers { - cached_stage_realization, - compile_stage_memo_provider_id, - compile_stage_memo_retention_preserved, - compile_stage_memo_verdict_is_single_recompute, - compile_stage_realized_step, - discharged_scope_admits_store, - materialization_admission_admits_store, - parse_memo_no_recurrence, - ParseMemoRecurrence, - parse_table_carrier_grounded_on_catalog, - parse_table_memo_materialization_for, - parse_table_memo_plural_holds_without_provider, - parse_table_memo_plural_holds_with_provider, - parse_table_memo_plural_refused_without_provider_count, - parse_table_memo_plural_scope_too_narrow_count, - parse_table_memo_provider_id, - parse_table_memo_retention_preserved, - parse_table_memo_verdict_is_discharged_for, - parse_table_memo_verdict_is_single_recompute_for, - parse_table_realized_step, - refused_scope_is_not_admitted, - refused_scope_prescription, - unruled_identity_is_not_admitted, - v2_compiler_materialization_ladder_holds -} -import v2.compiler.parse { prepare_grammar, prepare_grammar_with_admission } -import extdeps.realization.compile_stage_memo { compile_stage_memo_id } -import extdeps.realization.parse_table_memo { parse_table_memo_id } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -// Green-by-execution receipt that v2 ParseTable and cached_stage memoization flows through the -// governed carrier door, and that the materialization each door consumes is the one the ladder -// ADMITTED for that demand's identity rather than a literal standing beside it, and that an -// admission the ladder did not green is REFUSED rather than re-encoded as an operational -// materialization. RED: plural RefusedScopeTooNarrow / RefusedNoProvider; governed-store refusal -// when Recompute; bypass insert on non-Memoize carrier; an identity with no verdict being -// admitted; a refused admission letting the parse realization be built anyway. -// reconcile-as-lookup-path is the next increment. - -test fn compiler_materialization_ladder_holds() -> Bool { - v2_compiler_materialization_ladder_holds() -} - -test fn parse_table_memo_provider_preserves_catalog_retention() -> Bool { - parse_table_memo_retention_preserved() - && (parse_table_memo_provider_id() == parse_table_memo_id) -} - -test fn compile_stage_memo_provider_preserves_catalog_retention() -> Bool { - compile_stage_memo_retention_preserved() - && (compile_stage_memo_provider_id() == compile_stage_memo_id) -} - -// The parse demand's site is derived from the grammar's recurrence census, so the carrier is -// asked for a census rather than for one grammar-less answer. The real producer's route -- a real -// grammar through compute_grammar_first_analysis into the table -- is the inhabitance claim in -// v2.test.provenance.parse_memo_frame_replay. -fn recurrent_census() -> ParseMemoRecurrence { - ParseMemoRecurrence { discarding_backtrack_sites: 1, nullable_nonterminals: 0 } -} - -fn nullable_only_census() -> ParseMemoRecurrence { - ParseMemoRecurrence { discarding_backtrack_sites: 0, nullable_nonterminals: 1 } -} - -test fn parse_table_carrier_inhabits_realization() -> Bool { - match parse_table_realized_step(r: recurrent_census()) { - Rejected { diagnostics: _ } => false - Accepted { value: step, diagnostics: _ } => - (step.materialization == Memoize) - && (step.placement == LocalInProcess) - && materialization_admission_admits_store( - a: parse_table_memo_materialization_for(r: recurrent_census()) - ) - && !materialization_admission_admits_store( - a: parse_table_memo_materialization_for(r: parse_memo_no_recurrence) - ) - } -} - -// Recompute, not Memoize, and the change is a correction rather than a regression: the -// compile-stage lookup is resolve_cache_miss, which returns Miss unconditionally, so the -// governed door has been taking the recompute path on every compile regardless of what the -// literal said. The literal was the only thing claiming otherwise. -test fn compile_stage_carrier_inhabits_realization() -> Bool { - match compile_stage_realized_step() { - Rejected { diagnostics: _ } => false - Accepted { value: step, diagnostics: _ } => - match cached_stage_realization() { - Rejected { diagnostics: _ } => false - Accepted { value: realization, diagnostics: _ } => - (step.materialization == Recompute) - && (step.placement == LocalInProcess) - && (realization.materialization == Recompute) - && (realization.provider_id == compile_stage_memo_id) - } - } -} - -test fn parse_table_empty_uses_catalog_identity() -> Bool { - parse_table_carrier_grounded_on_catalog() -} - -// Both arms read off the VERDICT: a refusal also prescribes Memoize, so the prescription alone -// could not tell a discharge from an unmet obligation. The nullable-only census discriminates the -// second recurrence source -- dropping it from parse_memo_recurrence_is_possible reds here. -test fn parse_table_demand_is_discharged_by_its_catalog_provider() -> Bool { - parse_table_memo_verdict_is_discharged_for(r: recurrent_census()) - && parse_table_memo_verdict_is_discharged_for(r: nullable_only_census()) - && !parse_table_memo_verdict_is_discharged_for(r: parse_memo_no_recurrence) -} - -test fn parse_table_demand_without_recurrence_is_accepted_single_recompute() -> Bool { - parse_table_memo_verdict_is_single_recompute_for(r: parse_memo_no_recurrence) - && !parse_table_memo_verdict_is_single_recompute_for(r: recurrent_census()) -} - -test fn compile_stage_demand_is_accepted_single_recompute() -> Bool { - compile_stage_memo_verdict_is_single_recompute() -} - -// The fail-closed arm, by execution: an identity carrying no verdict yields no admitted -// decision, and the door REFUSES rather than handing back an operational materialization. -test fn an_unruled_identity_is_not_admitted() -> Bool { - unruled_identity_is_not_admitted() -} - -// Review 69770's finding, as a discriminating pair. A REFUSED identity must not permit the -// store: ladder_verdict_prescribes answers Memoize for five of the six Refused arms, so a door -// that read the bare prescription would take an unmet obligation as permission. The negative -// is RefusedScopeTooNarrow against the very provider that discharges the single-site demand, -// and the positive is that same provider still admitting that same demand -- so the negative -// cannot pass by the admission having been broken for everything. -// STRENGTHENED past "refuses the store": the previous form asked only whether the store was -// permitted, which a refusal and an admitted Recompute both answer false, so it could not tell -// a preserved refusal from one re-encoded as recomputation -- and that was the defect. -test fn a_refused_identity_is_not_admitted() -> Bool { - refused_scope_is_not_admitted() - && discharged_scope_admits_store() -} - -test fn parse_table_memo_plural_reds_without_provider() -> Bool { - !parse_table_memo_plural_holds_with_provider() - && (parse_table_memo_plural_scope_too_narrow_count() == 1) - && !parse_table_memo_plural_holds_without_provider() - && (parse_table_memo_plural_refused_without_provider_count() == 1) -} - -// THE CONSUMER-BOUNDARY CONTROL the delta review asked for, and the one the module-level pair -// above cannot stand in for. Those show the DOOR refuses; this shows the thing that consumes the -// door refuses WITH it -- that a parse-table realization is not constructed, and the prepare that -// would have built it hands back the located diagnostic instead. The negative drives -// prepare_grammar_with_admission, which IS the body prepare_grammar runs, and the refusing -// admission it supplies is produced by the real door fed the plural verdict list, not by a -// literal: re-encoding the refusal as Recompute inside admitted_prescription_in makes this parse -// succeed and the control dies with it. -fn materialization_control_grammar() -> ParseGrammar { - ModeledGrammar { - root: GrammarRoot { - start: ^mat_control_start, - productions: Cons { - head: GrammarProduction { - name: ^mat_control_start, - expression: v2.std.grammar.Terminal { token_class: ^mat_control_tok, stamp: StampClass }, - emitted: grammar_atom(identity: ^mat_control_start) - }, - tail: std.algebra.Empty - }, - sync_tokens: grammar_empty_sync_tokens() - } - } -} - -fn materialization_control_prepare_is_refused() -> Bool { - match prepare_grammar_with_admission( - admit: fn(_) { refused_scope_prescription() }, - grammar: materialization_control_grammar() - ) { - Rejected { diagnostics: _ } => true - Accepted { value: _, diagnostics: _ } => false - } -} - -// The positive control on the same grammar through the REAL door: without it the negative would -// also pass if prepare_grammar_with_admission simply refused this grammar for any reason. -// -// IT ASSERTS THE DERIVED VALUE, NOT MERELY THAT SOMETHING WAS ADMITTED, and the value is -// Recompute rather than Memoize BECAUSE OF THE DERIVATION: this grammar is one Terminal -// production, so its recurrence census is zero on both counts, no (nonterminal, position) is -// asked twice, and the ladder answers AcceptedSingleRecompute. That is the admitted -// direct-computation arm reaching the real prepare -- a grammar that needs no memo does not get -// one -- and pinning Memoize here would have pinned a memo the grammar does not demand. -// -// WHERE THE OTHER ADMITTED ARM IS EXERCISED, cited because this control no longer carries it. -// THIS pair drives prepare_grammar to a Recompute admission; the MEMOIZE admission through the -// same door is driven by v2.test.provenance.parse_memo_frame_replay -// parse_memo_demand_derived_discharged_when_the_grammar_abandons_a_demand, on a grammar whose -// census counts a discarding backtrack site. Those two claims are the only place the real -// prepare produces a store-permitting answer, and their stated subject is REPLAY, so retargeting -// them for replay reasons would delete this module's Memoize counterpart while every control -// here stayed green. That is why the citation runs both ways: the replay claims carry a matching -// note naming this control. -fn materialization_control_prepare_is_admitted() -> Bool { - match prepare_grammar(grammar: materialization_control_grammar()) { - Rejected { diagnostics: _ } => false - Accepted { value: prepared, diagnostics: _ } => - match prepared { - PreparedVoidGrammar => false - PreparedModeled { modeled: _, materialization: m } => m == Recompute - } - } -} - -test fn a_refused_admission_refuses_the_parse_realization() -> Bool { - materialization_control_prepare_is_refused() - && materialization_control_prepare_is_admitted() -} diff --git a/src/v2/test/claim/demand_engine/demand_engine_test.dag b/src/v2/test/claim/demand_engine/demand_engine_test.dag index 01a03c3ab9c..dca6fedd4ae 100644 --- a/src/v2/test/claim/demand_engine/demand_engine_test.dag +++ b/src/v2/test/claim/demand_engine/demand_engine_test.dag @@ -8,6 +8,7 @@ import v2.std.demand_engine { BlockedByCycle, BlockedByMissingEdge, BlockedByRefusedPrerequisite, + BlockedKeyConflict, BlockedNoDemand, BlockedWaitingForCapacity, DemandAdmitted, @@ -40,7 +41,7 @@ import std.optional { Absent, Present } import v2.std.text { String } import std.cache_identity { parse_source_artifact_kind } import std.content_hash { Fnv1a64Structural } -import std.materialization_ladder { FreshEffect, PureComputation } +import std.materialization_ladder { DemandNature, FreshEffect, IdempotentEffect, PureComputation, WorldRead, nature_eq } import std.judgment_contract { DemandIdentity } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -111,6 +112,11 @@ fn g_cause_eq(a: DemandBlockedCause, b: DemandBlockedCause) -> Bool { BlockedWaitingForCapacity => match b { BlockedWaitingForCapacity => true _ => false } BlockedNoDemand => match b { BlockedNoDemand => true _ => false } + BlockedKeyConflict { stored: sa, requested: ra } => + match b { + BlockedKeyConflict { stored: bs, requested: br } => nature_eq(a: sa, b: bs) && nature_eq(a: ra, b: br) + _ => false + } _ => false } } @@ -726,3 +732,169 @@ test fn a_duplicated_dependency_does_not_duplicate_a_ready_identity_holds() -> B ) ) == 1 } + +// ── one key, two natures ─────────────────────────────────────────────────────────────────────── +// +// An existing entry answers a request only when the request declares the SAME complete nature +// (std.materialization_ladder nature_eq). Each pair below is a request the engine used to attach or +// re-produce under the stored nature alone; each must now stand as a typed key conflict naming both. + +fn g_conflict(stored: DemandNature, requested: DemandNature) -> DemandState { + DemandBlocked { cause: BlockedKeyConflict { stored: stored, requested: requested } } +} + +fn g_requested_twice(first: DemandNature, second: DemandNature) -> DemandEngine { + demand_engine_request( + e: demand_engine_request(e: demand_engine_empty(), id: g_a(), nature: first), + id: g_a(), + nature: second + ) +} + +// Neither request is served from the other: no attachment and no extra production is recorded. +fn g_conflict_recorded(first: DemandNature, second: DemandNature) -> Bool { + let e = g_requested_twice(first: first, second: second) + g_state_is(e: e, id: g_a(), want: g_conflict(stored: first, requested: second)) + && match demand_engine_entry(e: e, id: g_a()) { + Absent => false + Present { value: entry } => entry.attachments == 0 && entry.productions == 0 + } +} + +test fn a_fresh_request_does_not_attach_to_a_pure_entry_holds() -> Bool { + g_conflict_recorded(first: PureComputation, second: FreshEffect) +} + +test fn a_pure_request_does_not_attach_to_a_fresh_entry_holds() -> Bool { + g_conflict_recorded(first: FreshEffect, second: PureComputation) +} + +// Both share, and they are still different contracts. +test fn pure_and_idempotent_are_different_demands_holds() -> Bool { + g_conflict_recorded(first: PureComputation, second: IdempotentEffect) + && g_conflict_recorded(first: IdempotentEffect, second: PureComputation) +} + +test fn a_changed_world_read_envelope_is_a_different_demand_holds() -> Bool { + g_conflict_recorded( + first: WorldRead { staleness_envelope_declared: true }, + second: WorldRead { staleness_envelope_declared: false } + ) + && g_conflict_recorded( + first: WorldRead { staleness_envelope_declared: false }, + second: WorldRead { staleness_envelope_declared: true } + ) +} + +// THE POSITIVE CONTROL. Equal natures still share, for every share-admitted nature, so the conflict +// above is decided by the nature and not by the second request alone. +fn g_equal_nature_attaches(n: DemandNature) -> Bool { + let e = g_requested_twice(first: n, second: n) + match demand_engine_entry(e: e, id: g_a()) { + Absent => false + Present { value: entry } => entry.attachments == 1 && entry.productions == 0 + } +} + +test fn equal_share_admitted_natures_still_attach_holds() -> Bool { + g_equal_nature_attaches(n: PureComputation) + && g_equal_nature_attaches(n: IdempotentEffect) + && g_equal_nature_attaches(n: WorldRead { staleness_envelope_declared: true }) +} + +// AND THE SHARED DEMAND RUNS ONCE. Two equal requests, one admission, one settlement: the second +// request is served by that one execution and nothing is left ready to run again. +test fn equal_natures_share_one_execution_holds() -> Bool { + let sealed = demand_engine_seal(e: g_requested_twice(first: PureComputation, second: PureComputation)) + let admitted = demand_engine_next(e: sealed, offer: DemandLeaseOffer { seats: 1 }) + let after = demand_engine_settle(e: sealed, id: g_a(), state: DemandAvailable { value: 7 }) + match admitted { + DemandAdmitted { identity: id } => demand_identity_key(id: id) == demand_identity_key(id: g_a()) + DemandNotAdmitted { cause: _ } => false + } + && g_state_is(e: after, id: g_a(), want: DemandAvailable { value: 7 }) + && length(xs: demand_engine_ready_queue(e: after)) == 0 + && match demand_engine_entry(e: after, id: g_a()) { + Absent => false + Present { value: entry } => entry.attachments == 1 && entry.productions == 1 + } +} + +// A -> B, both pure, sealed. Used to show the conflict reaches a dependent and outlives the lifecycle. +fn g_chain() -> DemandEngine { + demand_engine_seal( + e: demand_engine_relate( + e: demand_engine_request( + e: demand_engine_request(e: demand_engine_empty(), id: g_a(), nature: PureComputation), + id: g_b(), + nature: PureComputation + ), + relation: g_edge(prerequisite: g_a(), dependent: g_b()) + ) + ) +} + +// AFTER THE RESULT IS AVAILABLE. A was settled with a value and B became ready on it; a fresh request +// for A's key must not be served that value, and B -- which named A's key -- leaves the ready order +// with a refused prerequisite rather than running on an answer to a different demand. +test fn a_conflict_after_an_available_result_is_not_served_holds() -> Bool { + let available = demand_engine_settle(e: g_chain(), id: g_a(), state: DemandAvailable { value: 1 }) + let conflicted = demand_engine_request(e: available, id: g_a(), nature: FreshEffect) + g_state_is(e: available, id: g_b(), want: DemandReady) + && g_state_is(e: conflicted, id: g_a(), want: g_conflict(stored: PureComputation, requested: FreshEffect)) + && g_state_is(e: conflicted, id: g_b(), want: DemandBlocked { cause: BlockedByRefusedPrerequisite { prerequisite: g_a() } }) + && length(xs: demand_engine_ready_queue(e: conflicted)) == 0 +} + +// THE CONFLICT IS DURABLE. Readiness used to preserve only settled and cycle-blocked states, so a +// bare blocked state would have been re-derived to ready by seal. Here it survives seal, a later +// settlement of the in-flight production, and a further request under either nature -- and is +// never admitted. +test fn a_key_conflict_survives_seal_settle_and_rerequest_holds() -> Bool { + let want = g_conflict(stored: PureComputation, requested: FreshEffect) + let conflicted = g_requested_twice(first: PureComputation, second: FreshEffect) + let sealed = demand_engine_seal(e: conflicted) + let settled = demand_engine_settle(e: sealed, id: g_a(), state: DemandAvailable { value: 1 }) + let again_pure = demand_engine_request(e: settled, id: g_a(), nature: PureComputation) + let again_fresh = demand_engine_request(e: settled, id: g_a(), nature: FreshEffect) + g_state_is(e: sealed, id: g_a(), want: want) + && g_state_is(e: settled, id: g_a(), want: want) + && g_state_is(e: again_pure, id: g_a(), want: want) + && g_state_is(e: again_fresh, id: g_a(), want: want) + && match demand_engine_next(e: sealed, offer: DemandLeaseOffer { seats: 1 }) { + DemandAdmitted { identity: _ } => false + DemandNotAdmitted { cause: _ } => true + } +} + +// THE CONFLICT DOES NOT END THE WORK UNDER IT. A is running when its key is found conflicted: the +// seat stays held until A's production reports, and the report releases it without replacing the +// conflict. Releasing at the conflict would admit past the seat count while A still runs. +test fn a_conflict_on_a_running_demand_keeps_its_seat_until_it_reports_holds() -> Bool { + let running = demand_engine_settle(e: g_chain(), id: g_a(), state: DemandRunning) + let conflicted = demand_engine_request(e: running, id: g_a(), nature: FreshEffect) + let reported = demand_engine_settle(e: conflicted, id: g_a(), state: DemandAvailable { value: 1 }) + demand_engine_in_flight(e: running) == 1 + && demand_engine_in_flight(e: conflicted) == 1 + && demand_engine_in_flight(e: reported) == 0 + && g_state_is(e: reported, id: g_a(), want: g_conflict(stored: PureComputation, requested: FreshEffect)) + && g_state_is(e: reported, id: g_b(), want: DemandBlocked { cause: BlockedByRefusedPrerequisite { prerequisite: g_a() } }) +} + +// AND A RUNNING DEPENDENT KEEPS BOTH ITS SEAT AND ITS REFUSAL. B is running on A's value when A's key +// is found conflicted: B is refused at once and leaves no seat behind it, and when B's production +// later reports a value, the seat is released and the refusal still stands -- B read an answer to a +// different demand, so its value answers nothing. +test fn a_running_dependent_keeps_its_seat_and_refusal_when_a_prerequisite_conflicts_holds() -> Bool { + let want_b = DemandBlocked { cause: BlockedByRefusedPrerequisite { prerequisite: g_a() } } + let available = demand_engine_settle(e: g_chain(), id: g_a(), state: DemandAvailable { value: 1 }) + let b_running = demand_engine_settle(e: available, id: g_b(), state: DemandRunning) + let conflicted = demand_engine_request(e: b_running, id: g_a(), nature: FreshEffect) + let reported = demand_engine_settle(e: conflicted, id: g_b(), state: DemandAvailable { value: 2 }) + demand_engine_in_flight(e: b_running) == 1 + && demand_engine_in_flight(e: conflicted) == 1 + && g_state_is(e: conflicted, id: g_b(), want: want_b) + && demand_engine_in_flight(e: reported) == 0 + && g_state_is(e: reported, id: g_b(), want: want_b) + && length(xs: demand_engine_ready_queue(e: reported)) == 0 +} diff --git a/src/v2/test/claim/diagnostic_sequence_outcome_witness_test.dag b/src/v2/test/claim/diagnostic_sequence_outcome_witness_test.dag index b4635134ba2..29b5ceb8bba 100644 --- a/src/v2/test/claim/diagnostic_sequence_outcome_witness_test.dag +++ b/src/v2/test/claim/diagnostic_sequence_outcome_witness_test.dag @@ -54,7 +54,13 @@ test fn witness_sequence_outcome_collects_values() -> Bool { policy: short_circuit_policy ) { Accepted { value: vs, diagnostics: None } => - (vs.length() == 2) && (vs.first() == 1) && (vs.skip(n: 1).first() == 2) + (vs.length() == 2) && ((match vs.first() { + Present { value: v } => v == 1 + Absent => false + })) && ((match vs.skip(n: 1).first() { + Present { value: v } => v == 2 + Absent => false + })) Accepted { value: _, diagnostics: Some { diagnostics: _ } } => false Rejected { diagnostics: _ } => false } @@ -80,7 +86,13 @@ test fn witness_sequence_outcome_accumulate_collects_both_values() -> Bool { policy: accumulate_policy ) { Accepted { value: vs, diagnostics: None } => - (vs.length() == 2) && (vs.first() == 1) && (vs.skip(n: 1).first() == 2) + (vs.length() == 2) && ((match vs.first() { + Present { value: v } => v == 1 + Absent => false + })) && ((match vs.skip(n: 1).first() { + Present { value: v } => v == 2 + Absent => false + })) Accepted { value: _, diagnostics: Some { diagnostics: _ } } => false Rejected { diagnostics: _ } => false } diff --git a/src/v2/test/claim/emit/dag_bind_let_block_scoped_test.dag b/src/v2/test/claim/emit/dag_bind_let_block_scoped_test.dag index 3cabd8d55a5..6ea3abfeb2c 100644 --- a/src/v2/test/claim/emit/dag_bind_let_block_scoped_test.dag +++ b/src/v2/test/claim/emit/dag_bind_let_block_scoped_test.dag @@ -58,13 +58,12 @@ fn dbl_parse(concrete: List) -> Optional { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => Absent - PreparedModeled { modeled: modeled, materialization: m } => + PreparedModeled { modeled: modeled } => match parse_production_prepared( tokens: dbl_lexed(concrete: concrete), validated: modeled.validated, analysis: modeled.analysis, - production_name: ^dag_production_expr, - materialization: m + production_name: ^dag_production_expr ) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => dbl_tree(node: artifact.tree) diff --git a/src/v2/test/claim/emit/dag_target_text_round_trip_test.dag b/src/v2/test/claim/emit/dag_target_text_round_trip_test.dag index ff8e7d1994d..2583ed9ce85 100644 --- a/src/v2/test/claim/emit/dag_target_text_round_trip_test.dag +++ b/src/v2/test/claim/emit/dag_target_text_round_trip_test.dag @@ -1,9 +1,9 @@ module v2.test.emit.dag_target_text_round_trip import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } import v2.compiler.target_serialize { serialize_concrete_syntax_tokens_to_source_string } -import v2.compiler.tokenize { tokenize } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_lex, dag_target_model, @@ -92,7 +92,7 @@ fn trt_text(tokens: List, transforms: Map List { - match tokenize(text: text, file: ^trt_probe, rules: dag_lex()) { + match tokenize_prepared(text: text, file: ^trt_probe, prepared: dag_prepared_lex()) { Accepted { value: stream, diagnostics: _ } => stream.all Rejected { diagnostics: _ } => [] } @@ -122,13 +122,12 @@ fn trt_parse(tokens: List, production: Symbol) -> Optional { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => Absent - PreparedModeled { modeled: modeled, materialization: m } => + PreparedModeled { modeled: modeled } => match parse_production_prepared( tokens: tokens, validated: modeled.validated, analysis: modeled.analysis, - production_name: production, - materialization: m + production_name: production ) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => trt_present(node: artifact.tree) diff --git a/src/v2/test/claim/emit/produced_item_parameter_order_test.dag b/src/v2/test/claim/emit/produced_item_parameter_order_test.dag index ac2a0986eb2..65d0d3e2194 100644 --- a/src/v2/test/claim/emit/produced_item_parameter_order_test.dag +++ b/src/v2/test/claim/emit/produced_item_parameter_order_test.dag @@ -6,7 +6,7 @@ import v2.std.arrow_signature { ArrowParameterOrderMalformed, arrow_declared_par import v2.std.compilers.target_model { ProducedDeclSignatureSubject, arrow_domain_binder_names, produced_decl_subject_from_name_and_arrow } import v2.std.diagnostic { Accepted, Rejected, diagnostics_fatal_reason } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import v2.std.node_query { binder_edge } +import v2.std.node_query { binder_edge, declared_field_from_edge } import v2.std.node { Arrow, Atom, @@ -97,20 +97,22 @@ fn ipo_without_order(arrow: Node) -> Node { } } -// Each parameter as its label then its type atom, so the claim reads order AND the binding of each -// label to its own type. +// Each parameter as its label then its declared type atom, so the claim reads order AND the binding +// of each label to its own type. The renderer’s param edges are binders (v2.std.node_query +// binder_edge): the type lives on the binder, not as the edge target’s connective. Reading the +// target as an Atom is the pre-XL-2 shape and would accept a renderer that dropped the type into +// the edge target while refusing the production encoding. fn ipo_param_symbols(params: List) -> List { fold(params, init: [], f: fn(acc, e) { - let name = match e.label { - StructuralLabel { label: _ } => ^ipo_structural - Authored { name: n } => n - Positional => ^ipo_positional - } - let ty = match e.target.kind { - TypeNode { connective: Atom { identity: t } } => t - _ => ^ipo_not_an_atom + match declared_field_from_edge(e: e) { + Present { value: field } => + let ty = match field.type_node.kind { + TypeNode { connective: Atom { identity: t } } => t + _ => ^ipo_not_an_atom + } + concat(acc, [field.name, ty]) + Absent => concat(acc, [^ipo_not_a_binder, ^ipo_not_an_atom]) } - concat(acc, [name, ty]) }) } diff --git a/src/v2/test/claim/emit/rust_logic_complement_emit_test.dag b/src/v2/test/claim/emit/rust_logic_complement_emit_test.dag index 55c8ac43e3a..d173f91f784 100644 --- a/src/v2/test/claim/emit/rust_logic_complement_emit_test.dag +++ b/src/v2/test/claim/emit/rust_logic_complement_emit_test.dag @@ -28,6 +28,7 @@ import v2.std.diagnostic { None, Rejected, Unavailable, + diagnostics_fatal_reason, node_locus } import v2.std.node { Node } @@ -58,7 +59,8 @@ test fn rust_logic_complement_emit_accepts() -> Bool { ) { Accepted { value: source, diagnostics: d } => (source.carried == rust_logic_complement_source_text) && (d == None) - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^rust_logic_complement_emit_accepts } } @@ -71,7 +73,8 @@ test fn rust_logic_complement_emit_swapped_discriminates() -> Bool { (source.carried == rust_logic_complement_swapped_arms_source_text) && (source.carried != rust_logic_complement_source_text) && (d == None) - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^rust_logic_complement_emit_swapped_discriminates } } diff --git a/src/v2/test/claim/emit/rust_logic_meet_join_emit_test.dag b/src/v2/test/claim/emit/rust_logic_meet_join_emit_test.dag index a52ccdfb0e4..0c886313d3c 100644 --- a/src/v2/test/claim/emit/rust_logic_meet_join_emit_test.dag +++ b/src/v2/test/claim/emit/rust_logic_meet_join_emit_test.dag @@ -34,6 +34,7 @@ import v2.std.diagnostic { None, Rejected, Unavailable, + diagnostics_fatal_reason, node_locus } import v2.std.node { Node } @@ -57,7 +58,8 @@ fn meet_join_emit_source_equals(root: Node, target: TargetModel, expected: Strin match emit(tree: meet_join_emit_tree_for(root: root), target: target) { Accepted { value: source, diagnostics: d } => (source.carried == expected) && (d == None) - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^rust_logic_meet_emit_holds } } @@ -70,7 +72,8 @@ fn meet_join_emit_discriminates( match emit(tree: meet_join_emit_tree_for(root: root), target: target) { Accepted { value: source, diagnostics: d } => (source.carried == expected) && (source.carried != wrong) && (d == None) - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^rust_logic_meet_emit_holds } } diff --git a/src/v2/test/claim/emitted_closure_crate_partition_test.dag b/src/v2/test/claim/emitted_closure_crate_partition_test.dag index 4f5868da1cb..393b8004086 100644 --- a/src/v2/test/claim/emitted_closure_crate_partition_test.dag +++ b/src/v2/test/claim/emitted_closure_crate_partition_test.dag @@ -2,7 +2,7 @@ module v2.test.claim.emitted_closure_crate_partition import std.types { Bool, Int, List, String } import v2.lens.module_graph { dependency_resolution_facts_live, module_declaration_facts_live } -import v2.std.collection { map_get } +import v2.std.collection { map_get_checked } import v2.std.diagnostic { Accepted, Rejected } import std.optional { Absent, Present } import v2.std.compilers.compilation_unit { @@ -59,7 +59,7 @@ fn fixture_partition_at(root: String, entry: String) -> EmittedClosurePartitionO } fn owner_of(assignment: Map, module_id: String) -> String { - match map_get(assignment, module_id as ModuleId) { + match map_get_checked(assignment, module_id as ModuleId) { Accepted { value: Present { value: unit }, diagnostics: _ } => unit as String Accepted { value: Absent, diagnostics: _ } => "unowned" Rejected { diagnostics: _ } => "unowned" diff --git a/src/v2/test/claim/execution/dag_grammar_backward_round_trip_test.dag b/src/v2/test/claim/execution/dag_grammar_backward_round_trip_test.dag index 99d31721b47..ac9147312dd 100644 --- a/src/v2/test/claim/execution/dag_grammar_backward_round_trip_test.dag +++ b/src/v2/test/claim/execution/dag_grammar_backward_round_trip_test.dag @@ -31,7 +31,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn rt_parse(text: String) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => dag_round_trip_parse(prepared: prepared, residue: residue, text: text) + Accepted { value: prepared, diagnostics: _ } => dag_round_trip_parse(prepared: prepared, text: text) } } diff --git a/src/v2/test/claim/execution/emit_host_complement_equals_eval_test.dag b/src/v2/test/claim/execution/emit_host_complement_equals_eval_test.dag index e2e450e1e22..4428a4072dc 100644 --- a/src/v2/test/claim/execution/emit_host_complement_equals_eval_test.dag +++ b/src/v2/test/claim/execution/emit_host_complement_equals_eval_test.dag @@ -141,9 +141,9 @@ fn emit_complement_eval_bool_false(value: RuntimeValue) -> Bool { fn emit_complement_eval_allocate_literal(node: Node, _env: EvaluationEnvironment) -> Outcome { match node.kind { TypeNode { connective: Atom { identity: id } } => - if id == ^dag_complement_lit_true { + if id == ^dag_token_kw_true { outcome_accepted(value: emit_complement_eval_octet_value(n: 1)) - } else if id == ^dag_complement_lit_false { + } else if id == ^dag_token_kw_false { outcome_accepted(value: emit_complement_eval_octet_value(n: 0)) } else { outcome_rejected( diff --git a/src/v2/test/claim/execution/emit_host_identity_cast_native_test.dag b/src/v2/test/claim/execution/emit_host_identity_cast_native_test.dag index 634d8e9d899..4079912436d 100644 --- a/src/v2/test/claim/execution/emit_host_identity_cast_native_test.dag +++ b/src/v2/test/claim/execution/emit_host_identity_cast_native_test.dag @@ -14,16 +14,15 @@ import v2.std.text { String } import v2.std.witness { Violates } import v2.test.claim.coercion.identity_cast_emission_route { IdentityCastClosureRefused, IdentityCastEntryEmitted, IdentityCastEntryRefused, - identity_cast_route_admitted_holds, identity_cast_route_verdict, + identity_cast_route_verdict, } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// THE ADMITTED IDENTITY CAST, BUILT AND RUN. identity_cast_route_verdict is the real door; this -// module reads the same verdict the required-gate inhabitance already asserted, then builds and -// runs it. The last merge-time execution of the door is -// v2.test.claim.coercion.identity_cast_emission_route.an_admitted_identity_cast_emits_through_the_closure_route_holds, -// not this module. +// THE ADMITTED IDENTITY CAST, BUILT AND RUN. One native control reads the REAL member text from +// identity_cast_route_verdict and every runtime case builds and runs that artifact (DESIGN section 3 +// pairing). The last merge-time execution of the door on the required gate is still +// v2.test.claim.coercion.identity_cast_emission_route.an_admitted_identity_cast_emits_through_the_closure_route_holds. // // STANDING, STATED SO IT IS NOT READ AS COVERAGE: this module is outside the required gate // (v2.workflow.required_floor required_gate_authored_modules) by operator decision, because every @@ -46,19 +45,18 @@ fn identity_cast_native_inputs() -> Inputs { inputs_root_only(root: node_synthetic(kind: v2.std.node.TypeNode { connective: v2.std.node.Atom { identity: ^identity_cast_native_probe } }, children: [])) } -// The entry member's emitted text, read from the route module's shared verdict rather than emitted a -// second time: the text built here is the text the admission row asserted on. Absent when the route -// did not emit exactly the entry. +// The entry member's emitted text from the closure door. Absent when the route did not emit exactly +// one admitted entry. Runtime cases share this control; they do not substitute a supplied spelling. fn identity_cast_native_emitted() -> Optional { - let v = identity_cast_route_verdict() - if identity_cast_route_admitted_holds(v: v) { - match v { - IdentityCastEntryEmitted { text: t, members: _ } => optional_present(value: t) - IdentityCastEntryRefused { reason: _, members: _ } => optional_absent() - IdentityCastClosureRefused { reason: _ } => optional_absent() - } - } else { - optional_absent() + match identity_cast_route_verdict() { + IdentityCastEntryEmitted { text: t, members: n } => + if n == 1 { + optional_present(value: t) + } else { + optional_absent() + } + IdentityCastEntryRefused { reason: _, members: _ } => optional_absent() + IdentityCastClosureRefused { reason: _ } => optional_absent() } } diff --git a/src/v2/test/claim/execution/emit_host_meet_join_equals_eval_test.dag b/src/v2/test/claim/execution/emit_host_meet_join_equals_eval_test.dag index ce7e332aeaf..3918c781669 100644 --- a/src/v2/test/claim/execution/emit_host_meet_join_equals_eval_test.dag +++ b/src/v2/test/claim/execution/emit_host_meet_join_equals_eval_test.dag @@ -148,9 +148,9 @@ fn meet_join_eval_bool_false(value: RuntimeValue) -> Bool { fn meet_join_eval_allocate_literal(node: Node, _env: EvaluationEnvironment) -> Outcome { match node.kind { TypeNode { connective: Atom { identity: id } } => - if id == ^dag_complement_lit_true { + if id == ^dag_token_kw_true { outcome_accepted(value: meet_join_eval_octet_value(n: 1)) - } else if id == ^dag_complement_lit_false { + } else if id == ^dag_token_kw_false { outcome_accepted(value: meet_join_eval_octet_value(n: 0)) } else { outcome_rejected( diff --git a/src/v2/test/claim/execution/emit_host_variant_construct_equals_eval_test.dag b/src/v2/test/claim/execution/emit_host_variant_construct_equals_eval_test.dag index 55e2a0f2e4b..f571a23991a 100644 --- a/src/v2/test/claim/execution/emit_host_variant_construct_equals_eval_test.dag +++ b/src/v2/test/claim/execution/emit_host_variant_construct_equals_eval_test.dag @@ -49,6 +49,7 @@ import v2.std.diagnostic { import v2.std.node { Atom, Conj, + ConstructTagEdge, Edge, Node, Symbol, @@ -56,7 +57,6 @@ import v2.std.node { MatchArmBodyEdge, } import v2.std.node_query { - find_named_child, find_core_child, } import v2.std.runtime { @@ -141,7 +141,7 @@ fn emit_variant_construct_eval_variant_tag_id(node: Node) -> Outcome { match node.kind { TypeNode { connective: Atom { identity: id } } => outcome_accepted(value: id) TypeNode { connective: Conj } => - match find_named_child(root: node, name: ^dag_binding_type_variant_holds) { + match find_core_child(root: node, marker: ConstructTagEdge) { Accepted { value: tag_node, diagnostics: _ } => match tag_node.kind { TypeNode { connective: Atom { identity: id } } => outcome_accepted(value: id) diff --git a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag index 976bf823584..f63f6434c8f 100644 --- a/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag +++ b/src/v2/test/claim/execution/long/emit_host_classical_not_ingested_equals_eval_test.dag @@ -1,5 +1,6 @@ module v2.test.long.emit_host_classical_not_ingested_equals_eval +import v2.std.member_declaration_kind { no_member_declaration_kinds } import std.types { List } import v2.compiler.refinement_discharge { infer_and_discharge } import std.occurrence_identity { OccurrenceSynthetic } @@ -148,7 +149,7 @@ fn ingested_arrow_from_source(source: String) -> Outcome { f: fn(resolved) { match ingested_find_arrow_in_module(root: resolved.root) { Present { value: arrow } => - outcome_accepted(value: ResolvedTree { root: arrow, symbol_index: empty_symbol_index(), resolved_declarations: resolved.resolved_declarations, lexical_bindings: resolved.lexical_bindings, unavailable_providers: resolved.unavailable_providers }) + outcome_accepted(value: ResolvedTree { root: arrow, symbol_index: empty_symbol_index(), resolved_declarations: resolved.resolved_declarations, lexical_bindings: resolved.lexical_bindings, unavailable_providers: resolved.unavailable_providers, member_kinds: no_member_declaration_kinds() }) Absent => outcome_rejected( d: ingested_classical_not_diagnostic(reason: ^ingested_classical_not_arrow_miss, node: resolved.root) diff --git a/src/v2/test/claim/execution/self_host_candidate_generation_test.dag b/src/v2/test/claim/execution/self_host_candidate_generation_test.dag index e914a9d3d8c..bcfc589788e 100644 --- a/src/v2/test/claim/execution/self_host_candidate_generation_test.dag +++ b/src/v2/test/claim/execution/self_host_candidate_generation_test.dag @@ -37,7 +37,7 @@ import v2.test.manual.sg2_type_expression_projection { } import v2.std.algebra { bag_eq, length } import std.algebra { list_snoc_item } -import v2.std.diagnostic { Accepted, None, Rejected } +import v2.std.diagnostic { Accepted, None, Rejected, diagnostics_fatal_reason } import v2.std.node { Symbol } import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } @@ -154,7 +154,8 @@ fn translate_error_profile_sg2_projected_arrow_serialize_known() -> Bool { emitted: sg2_project_arrow_emitted() ) { Accepted { value: source, diagnostics: _ } => source.carried == sg2_expected_arrow_text - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^translate_error_profile_sg2_projected_arrow_serialize_accepts } } @@ -164,7 +165,8 @@ fn translate_error_profile_sg2_projected_conj_serialize_known() -> Bool { emitted: sg2_project_conj_emitted() ) { Accepted { value: source, diagnostics: _ } => source.carried == sg2_expected_conj_text - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^translate_error_profile_sg2_projected_conj_serialize_accepts } } @@ -174,7 +176,8 @@ fn translate_error_profile_sg2_projected_instantiation_serialize_known() -> Bool emitted: sg2_project_instantiation_emitted() ) { Accepted { value: source, diagnostics: _ } => source.carried == sg2_expected_type_text - Rejected { diagnostics: _ } => false + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^translate_error_profile_sg2_projected_instantiation_serialize_accepts } } diff --git a/src/v2/test/claim/fold_lowering_test.dag b/src/v2/test/claim/fold_lowering_test.dag index 414fa990b89..b65c01fa67d 100644 --- a/src/v2/test/claim/fold_lowering_test.dag +++ b/src/v2/test/claim/fold_lowering_test.dag @@ -82,7 +82,7 @@ data paramless_step_fold_snippet: String = "module paramless_step_probe\n\nfn f( // refusal it produced was correct. fn ingest(source_text: String, file: Symbol) -> Optional { let lm = dag_language_model() - match tokenize(text: source_text, file: file, rules: lm.lex) { + match tokenize_prepared(text: source_text, file: file, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => Absent Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { diff --git a/src/v2/test/claim/generated/language_behavior_equivalence_test.dag b/src/v2/test/claim/generated/language_behavior_equivalence_test.dag index 0287688d57c..acdb1e5a669 100644 --- a/src/v2/test/claim/generated/language_behavior_equivalence_test.dag +++ b/src/v2/test/claim/generated/language_behavior_equivalence_test.dag @@ -9,7 +9,6 @@ import v2.compiler.eval { eval_diagnostic, run_test_claim, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.std.collection { Absent } @@ -238,15 +237,6 @@ fn lbe_emit_rejection_sentinel_claim( } } -fn lbe_rejected_emit_cache_receipt( - label: String, - mock: LanguageBehaviorIoMock, - expected: NonEmptyDiagnostics -) -> TestClaimCacheReceipt { - let claim = lbe_emit_rejection_sentinel_claim(label: label, mock: mock, expected: expected) - test_claim_cache_receipt(subject: lbe_subject_from_claim(claim: claim)) -} - fn lbe_run_test_claim_assert_from_emit( anchor: ManualAnchorKey, label: String, @@ -270,7 +260,7 @@ fn lbe_test_claim_run_from_emit( lbe_run_via_test_claim(claim: claim) Rejected { diagnostics: d } => TestClaimRun { - cache: lbe_rejected_emit_cache_receipt(label: label, mock: mock, expected: d), + claim: lbe_emit_rejection_sentinel_claim(label: label, mock: mock, expected: d), verdict: SemanticMismatch { actual: Rejected { diagnostics: d }, falsification: Absent } } } diff --git a/src/v2/test/claim/integer/interval_derivation_test.dag b/src/v2/test/claim/integer/interval_derivation_test.dag index e20f959b651..bab9b6fee5f 100644 --- a/src/v2/test/claim/integer/interval_derivation_test.dag +++ b/src/v2/test/claim/integer/interval_derivation_test.dag @@ -4,28 +4,8 @@ import std.types { List } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { Equal, Ordering, fold_list, length } import v2.std.diagnostic { Accepted, Outcome, Rejected } -import v2.std.integer { - D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, - DecimalNonZeroMagnitude, - IntegerBoundNegative, IntegerBoundNonNegative, - IntegerIntervalBound, IntegerIntervalSpec, - NonZeroD1, NonZeroD2, NonZeroD3, NonZeroD4, NonZeroD6, NonZeroD9, - Signed, Unsigned, Signedness, - StandardIntegerType, - Int8, Int16, Int32, Int64, Int128, - UInt8, UInt16, UInt32, UInt64, UInt128, - integer_concrete_bound, integer_concrete_zero, integer_interval, - integer_interval_bound_compare, integer_interval_bound_less_or_equal, - integer_interval_spec_node, - integer_machine_interval_spec, - integer_pow2_magnitude, - integer_standard_integer_type_from_node, - integer_standard_integer_type_interval_spec, - integer_standard_integer_type_roster, - integer_standard_integer_type_signedness, - integer_standard_integer_type_width_bits, - integer_nonzero_decimal_digit_widen -} +import v2.std.integer { DecimalNonZeroMagnitude, IntegerBoundNegative, IntegerBoundNonNegative, IntegerIntervalBound, IntegerIntervalSpec, NonZeroD1, NonZeroD2, NonZeroD3, NonZeroD4, NonZeroD6, NonZeroD9, Signed, Unsigned, Signedness, StandardIntegerType, Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128, integer_concrete_bound, integer_concrete_zero, integer_interval, integer_interval_bound_compare, integer_interval_bound_less_or_equal, integer_interval_spec_node, integer_machine_interval_spec, integer_pow2_magnitude, integer_standard_integer_type_from_node, integer_standard_integer_type_interval_spec, integer_standard_integer_type_roster, integer_standard_integer_type_signedness, integer_standard_integer_type_width_bits, integer_nonzero_decimal_digit_widen } +import std.nat { D0, D1, D2, D3, D4, D5, D6, D7, D8, D9 } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/src/v2/test/claim/long/caller_admission_native_wall_red_test.dag b/src/v2/test/claim/long/caller_admission_native_wall_red_test.dag new file mode 100644 index 00000000000..5443f8220b2 --- /dev/null +++ b/src/v2/test/claim/long/caller_admission_native_wall_red_test.dag @@ -0,0 +1,116 @@ +// caller_admission_native_wall_red_test -- THE ONE REAL-PATH INHABITANCE CLAIM for the +// former drop gunbc.rung_drop.admit_callers_discarded_on_the_native_route, in the LONG HOME +// (module v2.test.claim.long.* is structurally too expensive for the armed line: the interpreter's +// parse of even a trivial source dominates -- 496,179 eval steps / 1421ms cpu for a two-module +// fixture against the 72,300 new-witness budget, CI floor run at 8528a59b45 -- so the whole-pipeline +// cost is admitted here and nowhere else: withheld from the required floor's charged clock by its +// typed v2.workflow.floor_cost_debt row, verdict-proven in the changed-witness lane where +// required_floor long_home_prefixes admit it as ChangedCostDebtVerdictOnly, and proven for its +// verdict without the eval budget: the claim PASSED when run for its verdict, receipts in the row +// and in the PR). +// +// WHAT IT INHABITS: the native route end to end -- the peer module's body lowering carries the +// admit clause onto the sealed fn's Arrow as a structured roster, the subject module imports the +// sealed fn, infer gathers the imported callee's clause through the call's facts entry, and the +// wall refuses a call from a declaration the roster does not name. That is the trigger the drop +// was declared against, and the claim goes red if any stage of it is removed: no carrier edge, no +// facts read across the import, no judge, and the wall does not fire. +// +// The judge's positive direction and both failure arms are pinned hermetically in +// src/v2/test/claim/caller_admission_native_wall_test.dag over hand-built values, so this file +// carries exactly one pipeline claim (DESIGN section 3's witness rule: one real-path inhabitance +// claim, admitted where over-budget claims are admitted). + +module v2.test.claim.long.caller_admission_native_wall_red + +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty } +import v2.compiler.infer { infer } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.resolve { ResolvedTree } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.algebra { fold_list } +import v2.std.diagnostic { Accepted, Diagnostic, Outcome, Rejected, diagnostics_fatal_reason } +import v2.std.integer { Int } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } + +// THE CROSS-MODULE PEER: the sealed fn in module aca, naming its one permitted caller by the +// caller's OWN module path and declaration name. +fn wall_red_peer_text() -> String { + "module aca\n\nfn sealed_mint(n: Int) -> Int\n admit_callers: [decl_ref(module_path: \"bcb\", decl_name: \"listed_caller\")]\n = n + 1\n" +} + +// THE CROSS-MODULE RED: the subject module imports the sealed fn and calls it from a declaration +// the peer's list does not name. The refusal must reach across the import: the judge reads the +// callee's declared Arrow through the call's facts entry, so the edge the peer's body_lowering +// carried survives into the subject module's judgment. +fn wall_red_subject_text() -> String { + "module bcb\n\nimport aca { sealed_mint }\n\nfn listed_caller(n: Int) -> Int = sealed_mint(n: n)\n\nfn unlisted_caller(n: Int) -> Int = sealed_mint(n: n)\n\nfn second_unlisted_caller(n: Int) -> Int = sealed_mint(n: n)\n" +} + +data wall_red_subject_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^wall_red_subject_artifact, + file_path: "src/v2/pilot/caller_admission_wall_red_subject.dag" +} + +data wall_red_peer_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^wall_red_peer_artifact, + file_path: "src/v2/pilot/caller_admission_wall_red_peer.dag" +} + +fn wall_red_source(src: String, artifact: Artifact, cu: Symbol) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: artifact, + compilation_unit: cu, + source_root: V2Tree + } +} + +fn wall_red_assembled(subject_src: String, peer_src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: wall_red_source(src: subject_src, artifact: wall_red_subject_artifact, cu: ^wall_red_subject_cu), + tail: [ + wall_red_source(src: peer_src, artifact: wall_red_peer_artifact, cu: ^wall_red_peer_cu) + ] + }, + admission: Admission { + subject: ResolutionSubject { name: Cons { head: ^bcb, tail: Empty } }, + imports: Empty + }, + lm: dag_language_model() + ) +} + +fn wall_red_refusal_count(head: Diagnostic, tail: List) -> Int { + fold_list( + xs: tail, + empty: if head.reason == ^infer_reason_caller_admission_refused { 1 } else { 0 }, + cons: fn(n, d) { if d.reason == ^infer_reason_caller_admission_refused { n + 1 } else { n } } + ) +} + +// THE INHABITANCE CLAIM (the retargeted evidence of the withdrawn construction_confinement +// stage, DESIGN section 4b(4)): assemble peer + subject through the real ingest, infer, and the +// wall refuses EXACTLY the two outside cross-module calls with caller_admission_refused while the +// listed caller beside them is admitted -- one extra or one missing refusal moves the count, and +// with the wall removed the program is accepted, so the claim is red. +test fn the_native_route_refuses_exactly_the_outside_cross_module_calls_holds() -> Bool { + match wall_red_assembled(subject_src: wall_red_subject_text(), peer_src: wall_red_peer_text()) { + Rejected { diagnostics: d } => wall_red_refusal_count(head: d.head, tail: d.tail) == 2 + Accepted { value: rt, diagnostics: _ } => + match infer(tree: rt) { + Rejected { diagnostics: d } => wall_red_refusal_count(head: d.head, tail: d.tail) == 2 + Accepted { value: _, diagnostics: _ } => false + } + } +} diff --git a/src/v2/test/claim/long/cwc_resolution_probe_test.dag b/src/v2/test/claim/long/cwc_resolution_probe_test.dag index 5755b5432a2..b932aa9bc21 100644 --- a/src/v2/test/claim/long/cwc_resolution_probe_test.dag +++ b/src/v2/test/claim/long/cwc_resolution_probe_test.dag @@ -1,144 +1,282 @@ module v2.test.long.cwc_resolution_probe -import v2.std.live_tree { LiveTreeDisposition } -import v2.compiler.normalized_tree { NormalizedTree } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.compiler.body_lowering_fold { DeclaredRecord } +import v2.compiler.normalize { normalize } +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree, no_import_bindings, no_type_declaration_modifiers } +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_string_literal_node, namespace_graft_module_body_marker_node } +import v2.std.arrow_signature { declared_signature } +import v2.std.declaration_marker { test_marker_channel_empty } +import v2.std.diagnostic { diagnostics_fatal_reason } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { Connective, content_hash, core_edge_label } +import v2.std.node_query { binder_edge, construct_node, construct_node_elided } +import v2.std.qualified_name { qualified_name_spine_of_atoms } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -data cwc_src_type_two: String = "module probe\ntype T = A | B\n" -data cwc_src_type_three: String = "module probe\ntype T = A | B | C\n" -data cwc_src_type_review: String = "module probe\ntype ReviewEvent = Approve | RequestChanges | Comment | Pending\n" +// EACH CLAIM DISCRIMINATES AT ONE INTERFACE (DESIGN section 3). A claim that tokenizes, parses and +// normalizes its own source costs about twice the required floor's new-witness budget before it +// asserts anything (v2.workflow.required_floor claim_eval_step_budget_for_identity), so the claims +// below take their input at the boundary under test as a SUPPLIED value: +// - the normalize claim takes a parse tree, built with the four shapes the parser emits +// (a production, a sequence, a token, an empty node); +// - the resolve claims take normalized roots, built with the constructors normalize itself uses +// (binder_edge, declared_signature, construct_node, dag_string_literal_node). +// One claim runs the real path, cwc_cwc_module_resolve_accepts, and it pairs both kinds of fixture +// with their producers: the real parser's tree for the malformed source has the supplied parse +// tree's content hash, and real normalize of the cwc module has the supplied cwc root's content +// hash. If a fixture drifts from what its producer emits, that claim turns red. It is admitted as a +// cost row in v2.workflow.floor_cost_debt. -data cwc_src_type_scalar: String = "module probe\ntype T = Int\n" - -data cwc_src_type_malformed_coproduct: String = "module probe\ntype T = { x: Int } | B | C\n" +data cwc_src_type_malformed_coproduct: String = "module probe\ntype T = { x: Int } | B\n" data cwc_src_cwc_module: String = "module probe\ntype VariantNaming = AsAuthored | ScreamingSnakeCase\ntype VariantEncoding = StringVariant { naming: VariantNaming }\ntype CoproductWireContract { coproduct: String, encoding: VariantEncoding }\ntype ReviewEvent = Approve | RequestChanges | Comment | Pending\ndata review_event_wire_contract: CoproductWireContract = { coproduct: \"ReviewEvent\", encoding: StringVariant { naming: ScreamingSnakeCase } }\n" -fn cwc_norm_of(src: String) -> Outcome { - let lm = dag_language_model() - bind_outcome( - o: tokenize(text: src, file: ^cwc_probe_file, rules: lm.lex), - f: fn(toks) { - bind_outcome(o: parse(tokens: toks, grammar: lm.grammar), f: fn(ptree) { - normalize(parse_tree: ptree) - }) - } - ) +// ---- the parse-tree fixture ------------------------------------------------------------------- + +fn cwc_pt_empty() -> Node { + Node { kind: TypeNode { connective: Conj }, children: [], occurrence_id: OccurrenceSynthetic } } -fn cwc_norm_accepts(src: String) -> Bool { - match cwc_norm_of(src: src) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false +fn cwc_pt_token(sym: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: sym } }, children: [], occurrence_id: OccurrenceSynthetic } +} + +fn cwc_pt_production(identity: Symbol, captured: Node) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^grammar_production_identity_node_projection }, target: cwc_pt_token(sym: identity) }, + Edge { label: Authored { name: ^grammar_production_captured_node_projection }, target: captured } + ], + occurrence_id: OccurrenceSynthetic } } -test fn cwc_norm_two_variant_accepts() -> Bool { - cwc_norm_accepts(src: cwc_src_type_two) +fn cwc_pt_seq(left: Node, right: Node) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^grammar_sequence_left_node_projection }, target: left }, + Edge { label: Authored { name: ^grammar_sequence_right_node_projection }, target: right } + ], + occurrence_id: OccurrenceSynthetic + } } -test fn cwc_norm_three_variant_accepts() -> Bool { - cwc_norm_accepts(src: cwc_src_type_three) +// `module probe` / `type T = { x: Int } | B`, as the parser emits it. +fn cwc_pt_malformed() -> Node { + cwc_pt_production(identity: ^dag_surface_module, captured: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_module_header, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_kw_module), right: cwc_pt_production(identity: ^dag_surface_qualified_name, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^probe), right: cwc_pt_empty())))), right: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_top_level_item, captured: cwc_pt_production(identity: ^dag_surface_type_decl, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_kw_type), right: cwc_pt_seq(left: cwc_pt_token(sym: ^T), right: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_empty()), right: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_eq), right: cwc_pt_production(identity: ^dag_surface_type_alias_rhs_lead, captured: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_production(identity: ^dag_surface_type_alias_rhs, captured: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_type_variant, captured: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_type_expr, captured: cwc_pt_production(identity: ^dag_surface_field_decl_block, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_lbrace), right: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_token(sym: ^x), right: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_colon), right: cwc_pt_production(identity: ^dag_surface_type_expr, captured: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_qualified_name, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^Int), right: cwc_pt_empty())), right: cwc_pt_empty()), right: cwc_pt_empty())))), right: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_empty())), right: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_empty())), right: cwc_pt_token(sym: ^dag_token_rbrace))))), right: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_empty()))), right: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_token(sym: ^dag_token_pipe), right: cwc_pt_production(identity: ^dag_surface_type_variant, captured: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_type_expr, captured: cwc_pt_seq(left: cwc_pt_seq(left: cwc_pt_production(identity: ^dag_surface_qualified_name, captured: cwc_pt_seq(left: cwc_pt_token(sym: ^B), right: cwc_pt_empty())), right: cwc_pt_empty()), right: cwc_pt_empty())), right: cwc_pt_seq(left: cwc_pt_empty(), right: cwc_pt_empty())))), right: cwc_pt_empty())))))))))))), right: cwc_pt_empty()))) } -fn cwc_tree_has_disj_algebra() -> NodeFold { - NodeFold { - init: fn(node) { - match node.kind { - TypeNode { connective: Disj } => true - _ => false - } - }, - step: fn(acc, _edge, child_acc) { - if acc { - true - } else { - child_acc - } - } - } +// ---- the normalized-root fixtures ------------------------------------------------------------- + +fn cwc_atom(identity: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: identity } }, children: [], occurrence_id: OccurrenceSynthetic } } -fn cwc_tree_has_disj(n: Node) -> Bool { - fold_node(n: n, algebra: cwc_tree_has_disj_algebra()) +fn cwc_node(connective: Connective, children: List) -> Node { + Node { kind: TypeNode { connective: connective }, children: children, occurrence_id: OccurrenceSynthetic } } -fn cwc_disj_arm_count_algebra() -> NodeFold { - NodeFold { - init: fn(node) { - match node.kind { - TypeNode { connective: Disj } => count(node.children) - _ => 0 - } - }, - step: fn(acc, _edge, child_acc) { - if acc > 0 { - acc - } else { - child_acc - } - } - } +fn cwc_member(name: Symbol, target: Node) -> Edge { + Edge { label: Authored { name: name }, target: target } } -fn cwc_disj_arm_count(n: Node) -> Int { - fold_node(n: n, algebra: cwc_disj_arm_count_algebra()) +// `type Name = V1 | V2 ...`, each variant with its own fields (none for a bare variant). +fn cwc_coproduct(name: Symbol, variants: List) -> Edge { + cwc_member(name: name, target: cwc_node(connective: Disj, children: variants)) } -fn cwc_norm_rejects(src: String) -> Bool { - match cwc_norm_of(src: src) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true - } +fn cwc_variant(name: Symbol, fields: List) -> Edge { + cwc_member(name: name, target: cwc_node(connective: Conj, children: fields)) } -test fn cwc_non_coproduct_rhs_passes_through() -> Bool { - match cwc_norm_of(src: cwc_src_type_scalar) { - Accepted { value: root, diagnostics: _ } => !cwc_tree_has_disj(n: root.root) - Rejected { diagnostics: _ } => false - } +fn cwc_record(name: Symbol, fields: List) -> Edge { + cwc_member(name: name, target: cwc_node(connective: Conj, children: fields)) } -test fn cwc_malformed_nary_coproduct_normalize_rejects() -> Bool { +fn cwc_field(name: Symbol, type_name: Symbol) -> Edge { + binder_edge(name: name, type_node: cwc_atom(identity: type_name)) +} - cwc_norm_rejects(src: cwc_src_type_malformed_coproduct) +// `data name: type_name = body`. +fn cwc_data(name: Symbol, type_name: Symbol, body: Node) -> Edge { + let signature = declared_signature(params: [], source: body) + cwc_member(name: name, target: cwc_node(connective: Arrow, children: [ + signature.domain, + Edge { label: Positional, target: cwc_atom(identity: type_name) }, + Edge { label: Positional, target: cwc_atom(identity: type_name) }, + signature.order, + Edge { label: core_edge_label(marker: ArrowBodyEdge), target: body } + ])) } -test fn cwc_malformed_nary_coproduct_fail_open_control() -> Bool { - !cwc_norm_rejects(src: cwc_src_type_malformed_coproduct) +fn cwc_headless(fields: List) -> Node { + construct_node_elided(field_edges: fields, source: cwc_pt_empty()) } -test fn cwc_norm_review_event_accepts() -> Bool { - cwc_norm_accepts(src: cwc_src_type_review) +fn cwc_headed(tag: Symbol, fields: List) -> Node { + construct_node(reference: qualified_name_spine_of_atoms(atoms: [cwc_atom(identity: tag)], occurrence_id: OccurrenceSynthetic), field_edges: fields, source: cwc_pt_empty()) } -fn cwc_resolve_accepts(src: String) -> Bool { - let lm = dag_language_model() - match cwc_norm_of(src: src) { +// `module probe` with `members`, as normalize grafts a one-segment module. +fn cwc_module_root(members: List) -> Node { + cwc_node(connective: Conj, children: [ + Edge { label: Authored { name: ^grammar_production_identity_node_projection }, target: cwc_atom(identity: ^dag_surface_module) }, + Edge { + label: Authored { name: ^grammar_production_captured_node_projection }, + target: cwc_node(connective: Conj, children: [ + Edge { + label: Authored { name: ^probe }, + target: cwc_node(connective: Conj, children: concat( + [Edge { label: Authored { name: ^grammar_production_identity_node_projection }, target: namespace_graft_module_body_marker_node() }], + members + )) + } + ]) + } + ]) +} + +fn cwc_variant_naming() -> Edge { + cwc_coproduct(name: ^VariantNaming, variants: [cwc_variant(name: ^AsAuthored, fields: []), cwc_variant(name: ^ScreamingSnakeCase, fields: [])]) +} + +fn cwc_coproduct_wire_contract() -> Edge { + cwc_record(name: ^CoproductWireContract, fields: [cwc_field(name: ^coproduct, type_name: ^String), cwc_field(name: ^encoding, type_name: ^VariantEncoding)]) +} + +// The cwc module, member for member. The ReviewEvent arms are in the order normalize emits them. +fn cwc_root_cwc_module() -> Node { + cwc_module_root(members: [ + cwc_variant_naming(), + cwc_coproduct(name: ^VariantEncoding, variants: [cwc_variant(name: ^StringVariant, fields: [cwc_field(name: ^naming, type_name: ^VariantNaming)])]), + cwc_coproduct_wire_contract(), + cwc_coproduct(name: ^ReviewEvent, variants: [ + cwc_variant(name: ^Approve, fields: []), cwc_variant(name: ^Pending, fields: []), + cwc_variant(name: ^Comment, fields: []), cwc_variant(name: ^RequestChanges, fields: []) + ]), + cwc_data(name: ^review_event_wire_contract, type_name: ^CoproductWireContract, body: cwc_headless(fields: [ + cwc_member(name: ^coproduct, target: dag_string_literal_node(value: "ReviewEvent", occurrence_id: OccurrenceSynthetic)), + cwc_member(name: ^encoding, target: cwc_headed(tag: ^StringVariant, fields: [cwc_member(name: ^naming, target: cwc_atom(identity: ^ScreamingSnakeCase))])) + ])) + ]) +} + +fn cwc_tree(root: Node, records: List) -> Outcome { + admit_normalized_tree( + import_bindings: no_import_bindings(), + type_declaration_kinds: fold(records, init: [], f: fn(acc, name) { list_snoc_item(xs: acc, item: DeclaredRecord { name: name }) }), + type_declaration_modifiers: no_type_declaration_modifiers(), + root: root, + test_markers: test_marker_channel_empty(), + diagnostics: None + ) +} + +// The resolve verdict over a supplied root: ACCEPTED, or the fatal refusal reason. +fn cwc_resolve_verdict(root: Node, records: List) -> Symbol { + match cwc_tree(root: root, records: records) { + Rejected { diagnostics: _ } => ^cwc_fixture_not_admitted Accepted { value: tree, diagnostics: _ } => - match v2.compiler.resolve.resolve(tree: tree, lm: lm) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false + match v2.compiler.resolve.resolve(tree: tree, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => ^ACCEPTED + Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) } - Rejected { diagnostics: _ } => false } } -test fn cwc_cwc_module_resolve_accepts() -> Bool { - cwc_resolve_accepts(src: cwc_src_cwc_module) +// ---- the real path ---------------------------------------------------------------------------- + +fn cwc_parse_of(src: String) -> Outcome { + let lm = dag_language_model() + bind_outcome(o: tokenize(text: src, file: ^cwc_probe_file, rules: lm.lex), f: fn(toks) { + parse(tokens: toks, grammar: lm.grammar) + }) } -test fn cwc_review_normalized_has_disj() -> Bool { - match cwc_norm_of(src: cwc_src_type_review) { - Accepted { value: root, diagnostics: _ } => cwc_tree_has_disj(n: root.root) - Rejected { diagnostics: _ } => false +fn cwc_norm_of_module() -> Outcome { + match cwc_parse_of(src: cwc_src_cwc_module) { + Accepted { value: p, diagnostics: _ } => normalize(parse_tree: p) + Rejected { diagnostics: d } => Rejected { diagnostics: d } } } -test fn cwc_review_disj_has_four_arms() -> Bool { - match cwc_norm_of(src: cwc_src_type_review) { - Accepted { value: root, diagnostics: _ } => cwc_disj_arm_count(n: root.root) == 4 +// THE ONE REAL-PATH CLAIM, and the pairing for every fixture above. It asserts the route, not only +// the answer: the real parser emits the supplied malformed-coproduct parse tree; real normalize of the +// cwc module emits the supplied cwc root, so its ReviewEvent Disj and the shapes the resolve fixtures +// are built from are what the producer emits; and resolve accepts that tree. Before gunbc#13333 the +// last conjunct was red: the single-tree door looked a variant value up from the corpus root and +// marked no declaration as a record. +test fn cwc_cwc_module_resolve_accepts() -> Bool { + let parse_pairs = match cwc_parse_of(src: cwc_src_type_malformed_coproduct) { + Accepted { value: p, diagnostics: _ } => content_hash(n: p) == content_hash(n: cwc_pt_malformed()) Rejected { diagnostics: _ } => false } + parse_pairs && match cwc_norm_of_module() { + Accepted { value: tree, diagnostics: _ } => + (content_hash(n: tree.root) == content_hash(n: cwc_root_cwc_module())) + && match v2.compiler.resolve.resolve(tree: tree, lm: dag_language_model()) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } + Rejected { diagnostics: _ } => false + } +} + +// ---- normalize, over a supplied parse tree ---------------------------------------------------- + +// A coproduct arm with no constructor name cannot be built or matched, so normalize owes a refusal. +// RED today: normalize accepts it (gunbc.recurring_failure_mode +// a_coproduct_arm_without_a_constructor_name_is_accepted; enrolled in v2.workflow.floor_expected_red). +test fn cwc_malformed_nary_coproduct_normalize_rejects() -> Bool { + match normalize(parse_tree: cwc_pt_malformed()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } +} + +// ---- resolve on the single-tree door, over supplied roots ------------------------------------- + +// A variant in value position binds to the module's own declaration. Red before gunbc#13333, where +// the door's lookup started at the corpus root. +test fn cwc_single_tree_variant_value_resolves() -> Bool { + cwc_resolve_verdict( + root: cwc_module_root(members: [cwc_variant_naming(), cwc_data(name: ^naming, type_name: ^VariantNaming, body: cwc_atom(identity: ^ScreamingSnakeCase))]), + records: [] + ) == ^ACCEPTED +} + +// A headless record literal elaborates against a record the module declares. Red before gunbc#13333, +// where the door marked no declaration as a record. +test fn cwc_single_tree_headless_record_resolves() -> Bool { + cwc_resolve_verdict( + root: cwc_module_root(members: [ + cwc_variant_naming(), + cwc_record(name: ^NamingHolder, fields: [cwc_field(name: ^naming, type_name: ^VariantNaming)]), + cwc_data(name: ^holder, type_name: ^NamingHolder, body: cwc_headless(fields: [cwc_member(name: ^naming, target: cwc_atom(identity: ^AsAuthored))])) + ]), + records: [^NamingHolder] + ) == ^ACCEPTED +} + +// Controls: the door still refuses what it should, for the reason it should. +test fn cwc_single_tree_undeclared_variant_refuses() -> Bool { + cwc_resolve_verdict( + root: cwc_module_root(members: [cwc_variant_naming(), cwc_data(name: ^naming, type_name: ^VariantNaming, body: cwc_atom(identity: ^Undeclared))]), + records: [] + ) == ^resolve_reason_unbound_symbol +} + +test fn cwc_single_tree_headless_at_coproduct_refuses() -> Bool { + cwc_resolve_verdict( + root: cwc_module_root(members: [ + cwc_variant_naming(), + cwc_data(name: ^naming, type_name: ^VariantNaming, body: cwc_headless(fields: [cwc_member(name: ^a, target: cwc_atom(identity: ^AsAuthored))])) + ]), + records: [] + ) == ^resolve_anonymous_record_expected_type_not_record } diff --git a/src/v2/test/claim/long/door_real_module_probe_test.dag b/src/v2/test/claim/long/door_real_module_probe_test.dag index 9a705e3c7aa..816c5418b32 100644 --- a/src/v2/test/claim/long/door_real_module_probe_test.dag +++ b/src/v2/test/claim/long/door_real_module_probe_test.dag @@ -19,8 +19,6 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // Cause ownership (sharp-bee-290 + nimble-boar-198 reconciliation 2026-07-13; durable table in // v2.workflow.compile_door_ledger known_frontier_causes): resolve_ambiguous_export LANDED fix in // phase-2 flip (this file's ImportScoped RED / NamespaceOnlyY GREEN pair). -// parse_grammar_choice_overlap_residue on import syntax remains migration-owned until phase-3 -// grammar deletion. fn quad_qn_node(seg: Symbol) -> Node { fold_list_node( diff --git a/src/v2/test/claim/long/frontier_probe_closure_matrix_integration_test.dag b/src/v2/test/claim/long/frontier_probe_closure_matrix_integration_test.dag index 333f7ffad6f..966f76a317f 100644 --- a/src/v2/test/claim/long/frontier_probe_closure_matrix_integration_test.dag +++ b/src/v2/test/claim/long/frontier_probe_closure_matrix_integration_test.dag @@ -17,6 +17,7 @@ import v2.compiler.source_authority { SourceRootCoverageComplete, frontier_probe_discovered_closure_seal } +import v2.std.cross_tree.import_model { DagTree, V2Tree } import v2.compiler.source_authority_read { source_ref_for_observed_storage_path } @@ -34,11 +35,11 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // exact-head eval (784772ms / 742109ms). fn fp_matrix_ref_a() -> SourceRef { - source_ref_for_observed_storage_path(path: "dag/std/algebra.dag") + source_ref_for_observed_storage_path(path: "dag/std/algebra.dag", source_root: DagTree) } fn fp_matrix_ref_v2() -> SourceRef { - source_ref_for_observed_storage_path(path: "src/v2/std/text.dag") + source_ref_for_observed_storage_path(path: "src/v2/std/text.dag", source_root: V2Tree) } fn fp_matrix_closure_subject_matching_digest( diff --git a/src/v2/test/claim/long/mandatory_tag_gate_witness_test.dag b/src/v2/test/claim/long/mandatory_tag_gate_witness_test.dag index 46339204fa7..cf9e6043703 100644 --- a/src/v2/test/claim/long/mandatory_tag_gate_witness_test.dag +++ b/src/v2/test/claim/long/mandatory_tag_gate_witness_test.dag @@ -7,7 +7,10 @@ import v2.compiler.normalize { normalize } import v2.compiler.compile { CompileLens, CompileLensRoot, required_lenses_at_grain } import v2.compiler.infer { InferredTree } import v2.extdeps.languages.dag { dag_language_model } -import v2.lens.mandatory_tag { mandatory_tag_compile_gate } +import v2.lens.mandatory_tag { mandatory_tag_compile_gate_with_provenance } +import v2.compiler.resolve { member_kind_channel_of_tree } +import v2.std.member_declaration_kind { MemberKindChannel } +import v2.compiler.namespace_graft { namespace_graft_build_body_conj, namespace_graft_fold_spine, namespace_graft_wrap_module_shell } import std.algebra { Cons, Empty } import v2.std.collection { empty_map} import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, Outcome, Rejected } @@ -27,12 +30,16 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // fast-lane eval budget - discovery excludes test/claim/long/ at dir grain and the run-29892063728 // receipt shows all 12 witnesses EvalBudgetExceeded in place. Recipe: claim_batch --source-root // src/v2 --source-root dag --entry src/v2/test/claim/long/mandatory_tag_gate_witness_test.dag -// --functions --claim-run --wet (~2min; run receipted green 2026-07-22, +// --functions --claim-run --wet (~2min; run receipted green 2026-07-22, // 12/12 PASS). Parse grain carries the full declaration (name lexeme, type annotation, value // vocabulary), so every arm is exercised there, including the misnamed-anchor control - the exact -// #6991 regression class this wall makes unwritable. Normalize grain currently collapses data-decl -// value internals (general-body-producer frontier), so its controls cover name/type/missing only; -// scheme enforcement at corpus scale rides the parse-grain corpus witness. The roster control +// #6991 regression class this wall makes unwritable. Normalize grain lowers a data declaration to a named +// nullary Arrow member (body_lower_data_decl_to_member, #12197, 2026-09-24), which the gate reads +// for name, type and value; the normalized controls cover clean/missing/misnamed (the wrong-type, +// file-scheme and bogus-scheme reds at this grain were measured and each exceeds the new-witness +// eval-step budget, so they are not enrolled; parse grain carries those arms). Regression receipt: the 2026-07-22 12/12 run predates that +// lowering, after which the gate read no declaration at this grain and reported every tagged module +// missing (eea7e8ea86); scheme enforcement at corpus scale rides the parse-grain corpus witness. The roster control // proves enrollment by execution: a violating module tree pushed through every enrolled root lens // yields a rejection whose head reason is mandatory_tag_missing_required_decl - a reason only this // lens emits. @@ -55,7 +62,7 @@ data src_exempt_fixture: String = "module extdeps.fixture.gate_probe_planted\n\n type GrainProbe = GrainStageRejected - | GrainTree { root: Node } + | GrainTree { root: Node, channel: MemberKindChannel } fn parse_grain_tree(source: String) -> GrainProbe { let lm = dag_language_model() @@ -64,7 +71,7 @@ fn parse_grain_tree(source: String) -> GrainProbe { Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { Rejected { diagnostics: _ } => GrainStageRejected - Accepted { value: artifact, diagnostics: _ } => GrainTree { root: artifact.tree } + Accepted { value: artifact, diagnostics: _ } => GrainTree { root: artifact.tree, channel: no_member_declaration_kinds() } } } } @@ -75,7 +82,11 @@ fn normalized_grain_tree(source: String) -> GrainProbe { GrainTree { root: parse_root } => match normalize(parse_tree: parse_root) { Rejected { diagnostics: _ } => GrainStageRejected - Accepted { value: normalized, diagnostics: _ } => GrainTree { root: normalized.root } + Accepted { value: normalized, diagnostics: _ } => + match member_kind_channel_of_tree(tree: normalized) { + Rejected { diagnostics: _ } => GrainStageRejected + Accepted { value: channel, diagnostics: _ } => GrainTree { root: normalized.root, channel: channel } + } } } } @@ -83,8 +94,8 @@ fn normalized_grain_tree(source: String) -> GrainProbe { fn gate_accepts_tree(probe: GrainProbe) -> Bool { match probe { GrainStageRejected => false - GrainTree { root: root } => - match mandatory_tag_compile_gate(node: root) { + GrainTree { root: root, channel: channel } => + match mandatory_tag_compile_gate_with_provenance(node: root, channel: channel) { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } @@ -94,8 +105,8 @@ fn gate_accepts_tree(probe: GrainProbe) -> Bool { fn gate_rejects_tree_with(probe: GrainProbe, reason: Symbol) -> Bool { match probe { GrainStageRejected => false - GrainTree { root: root } => - match mandatory_tag_compile_gate(node: root) { + GrainTree { root: root, channel: channel } => + match mandatory_tag_compile_gate_with_provenance(node: root, channel: channel) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => if d.head.reason == reason { @@ -175,7 +186,7 @@ test fn mandatory_tag_normalized_grain_misnamed_rejects() -> Bool { fn roster_rejects_with(probe: GrainProbe, reason: Symbol) -> Bool { match probe { GrainStageRejected => false - GrainTree { root: root } => + GrainTree { root: root, channel: _ } => let inferred = InferredTree { root: root, facts: empty_map() } fold(required_lenses_at_grain(grain: CompileLensRoot), init: false, f: fn(acc, lens) { acc diff --git a/src/v2/test/claim/long/parse_table_memo_amortization_test.dag b/src/v2/test/claim/long/parse_table_memo_amortization_test.dag deleted file mode 100644 index 5a0c77b92ad..00000000000 --- a/src/v2/test/claim/long/parse_table_memo_amortization_test.dag +++ /dev/null @@ -1,171 +0,0 @@ -module v2.test.long.parse_table_memo_amortization - -import v2.compiler.program_assembly { module_roots_from_source_root_ingest } -import extdeps.communication.medium { Lossless, Medium } -import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } -import v2.extdeps.languages.dag { dag_language_model } -import std.algebra { Cons, Empty } -import v2.std.algebra { fold_list, length } -import v2.std.artifact { Artifact, SourceFile } -import v2.std.diagnostic { - Accepted, - CorrectionNotModeled, - Diagnostic, - Outcome, - Rejected, - Unavailable, - diagnostics_singleton, - outcome_accepted, - invariant_port_locus -} -import v2.std.cross_tree.import_model { V2Tree } -import v2.std.node { Hash, Node, content_hash } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -data memo_amort_empty_a_source: Medium = Medium { carried: "module memo_amort_a\n", fidelity: Lossless } -data memo_amort_empty_b_source: Medium = Medium { carried: "module memo_amort_b\n", fidelity: Lossless } -data memo_amort_bisect_add_source: Medium = Medium { carried: "module bisect.add\n\nfn add(x:Int, y:Int) -> Int { x + y }", fidelity: Lossless } -data memo_amort_file_a_read: DagSourceReadWitness = DagSourceReadWitness { - source: memo_amort_empty_a_source, - artifact: Artifact { - kind: SourceFile, - id: ^memo_amort_file_a_artifact, - file_path: "src/v2/test/fixture/memo_amort_file_a.dag" - }, - compilation_unit: ^memo_amort_cu_a, - source_root: V2Tree -} - -data memo_amort_file_b_read: DagSourceReadWitness = DagSourceReadWitness { - source: memo_amort_empty_b_source, - artifact: Artifact { - kind: SourceFile, - id: ^memo_amort_file_b_artifact, - file_path: "src/v2/test/fixture/memo_amort_file_b.dag" - }, - compilation_unit: ^memo_amort_cu_b, - source_root: V2Tree -} - -data memo_amort_bisect_add_read: DagSourceReadWitness = DagSourceReadWitness { - source: memo_amort_bisect_add_source, - artifact: Artifact { - kind: SourceFile, - id: ^memo_amort_bisect_add_artifact, - file_path: "src/v2/test/fixture/memo_amort_bisect_add.dag" - }, - compilation_unit: ^memo_amort_cu_bisect_add, - source_root: V2Tree -} - -data memo_amort_ingest_one_file: SourceRootIngest = Cons { - head: memo_amort_file_a_read, - tail: Empty -} - -data memo_amort_ingest_two_files: SourceRootIngest = Cons { - head: memo_amort_file_a_read, - tail: Cons { head: memo_amort_file_b_read, tail: Empty } -} - -data memo_amort_ingest_bisect_add: SourceRootIngest = Cons { - head: memo_amort_bisect_add_read, - tail: Empty -} - -fn memo_amort_not_single_root_rejection() -> Outcome { - Rejected { - diagnostics: diagnostics_singleton( - d: Diagnostic { - reason: ^memo_amort_expected_exactly_one_module_root, - at: invariant_port_locus(port: ^memo_amort_single_root_locus_port, invariant: ^memo_amort_expected_exactly_one_module_root), - correction: Unavailable { reason: CorrectionNotModeled } - } - ) - } -} - -fn memo_amort_single_root_content_hash(ingest: SourceRootIngest) -> Outcome { - match module_roots_from_source_root_ingest( - ingest: ingest, - lm: dag_language_model() - ) { - Accepted { value: roots, diagnostics: _ } => - match length(xs: roots) == 1 { - true => - fold_list( - xs: roots, - empty: memo_amort_not_single_root_rejection(), - cons: fn(_acc, root) { - outcome_accepted(value: content_hash(n: root.root)) - } - ) - false => - memo_amort_not_single_root_rejection() - } - Rejected { diagnostics: d } => - Rejected { diagnostics: d } - } -} - -test fn memo_amort_parse_one_file_holds() -> Bool { - match module_roots_from_source_root_ingest( - ingest: memo_amort_ingest_one_file, - lm: dag_language_model() - ) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn memo_amort_parse_two_files_holds() -> Bool { - match module_roots_from_source_root_ingest( - ingest: memo_amort_ingest_two_files, - lm: dag_language_model() - ) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn memo_amort_parse_two_files_twice_holds() -> Bool { - memo_amort_parse_two_files_holds() && memo_amort_parse_two_files_holds() -} - -fn witness_parse_table_memo_hit_path_content_hash_stable() -> Bool { - match memo_amort_single_root_content_hash(ingest: memo_amort_ingest_bisect_add) { - Accepted { value: hash_miss, diagnostics: _ } => - match memo_amort_single_root_content_hash(ingest: memo_amort_ingest_bisect_add) { - Accepted { value: hash_hit, diagnostics: _ } => - match memo_amort_single_root_content_hash(ingest: memo_amort_ingest_one_file) { - Accepted { value: hash_empty, diagnostics: _ } => - hash_miss == hash_hit && hash_miss != hash_empty - Rejected { diagnostics: _ } => false - } - Rejected { diagnostics: _ } => false - } - Rejected { diagnostics: _ } => false - } -} - -test fn parse_table_grammar_memo_multi_file_ingest_parses() -> Bool { - memo_amort_parse_two_files_holds() -} - -test fn parse_table_memo_hit_path_content_hash_stable_holds() -> Bool { - witness_parse_table_memo_hit_path_content_hash_stable() -} - -data empty_ingest_guard_note: String = "Covers the empty-ingest branch of program_assembly_fold_ingest, added with the prepare-once hoist: an empty ingest must assemble to empty roots WITHOUT paying the one-time grammar preparation (well-formed + 5-check validation + FIRST analysis) for zero modules. Discriminating — RED if the guard rejected, fabricated a root, or (regression) fell through to prepare_grammar for no modules. Fast by construction (no prepare, no parse), unlike the multi-file amortization witnesses above which each pay exactly ONE preparation for the whole fold." - -test fn empty_ingest_assembles_to_empty_roots_without_prepare() -> Bool { - match module_roots_from_source_root_ingest( - ingest: Empty, - lm: dag_language_model() - ) { - Accepted { value: roots, diagnostics: _ } => length(xs: roots) == 0 - Rejected { diagnostics: _ } => false - } -} diff --git a/src/v2/test/claim/long/parse_table_memo_counter_witness_test.dag b/src/v2/test/claim/long/parse_table_memo_counter_witness_test.dag deleted file mode 100644 index 04561a3c324..00000000000 --- a/src/v2/test/claim/long/parse_table_memo_counter_witness_test.dag +++ /dev/null @@ -1,58 +0,0 @@ -module v2.test.long.parse_table_memo_counter_witness - - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -data parse_memo_counter_fixture_source: String = "module bisect.add\n\nfn add(x:Int, y:Int) -> Int { x + y }" - -fn parse_memo_counter_stats_for_grammar( - source_text: String, - grammar: ParseGrammar -) -> ParseMemoStats { - let lm = dag_language_model() - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_table_memo_counter_witness_test_grammar_contract) - match tokenize(text: source_text, file: ^probe, rules: lm.lex) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: stream, diagnostics: _ } => - match grammar { - ModeledGrammar { root: root } => - match grammar_validate_for_parse(root: root, at: at) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: validated, diagnostics: _ } => - parse_production_memo_stats( - tokens: token_stream_remaining(stream: stream), - validated: validated, - production_name: validated.root.start, - materialization: Memoize - ) - } - VoidGrammar => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - } - } -} - -fn parse_memo_counter_fixture_stats() -> ParseMemoStats { - parse_memo_counter_stats_for_grammar( - source_text: parse_memo_counter_fixture_source, - grammar: dag_language_model().grammar - ) -} - -fn parse_memo_counter_counters_consistent(stats: ParseMemoStats) -> Bool { - stats.lookup_calls == (stats.hits + stats.misses) -} - -test fn parse_memo_counter_lookup_calls_recorded_holds() -> Bool { - parse_memo_counter_fixture_stats().lookup_calls > 0 -} - -test fn parse_memo_counter_counters_consistent_holds() -> Bool { - parse_memo_counter_counters_consistent(stats: parse_memo_counter_fixture_stats()) -} - -test fn parse_memo_counter_misses_recorded_holds() -> Bool { - parse_memo_counter_fixture_stats().misses > 0 -} diff --git a/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag b/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag deleted file mode 100644 index 49a31f42ed7..00000000000 --- a/src/v2/test/claim/long/parse_table_memo_governed_witness_test.dag +++ /dev/null @@ -1,150 +0,0 @@ -module v2.test.long.parse_table_memo_governed_witness - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -// Governed memo door receipts: Memoize verdict consumed at parse_table_lookup/insert (non-Memoize refuses store); warm==cold content-hash purity on the bisect fixture; RED on bypass insert (Recompute carrier with hand-populated entries). Door fns parse_table_memo_lookup_refuses_store / parse_table_memo_insert_refused are the direct oracle — witnesses call them, not map shape alone. Both doors refuse independently on the Recompute carrier (Case A: seed ParseTableMemo must not serve/populate on non-Memoize — v1_interpreter.rs gated). reconcile-as-lookup-path is the next increment. -fn governed_witness_probe_key() -> ParsePositionKey { - ParsePositionKey { position: 0, production: ^governed_witness_probe_prod } -} - -fn governed_witness_dummy_entry() -> ParseMemoEntry { - ParseMemoRejected { diagnostics: [], furthest: FurthestNone, refusal: optional_absent() } -} - -fn parse_table_recompute_fixture() -> ParseTable { - let base = parse_table_empty(materialization: Memoize) - ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: Recompute, - grammar_digest: base.grammar_digest, - token_stream_digest: base.token_stream_digest, - entries: base.entries, - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } -} - -fn parse_table_memoize_fixture() -> ParseTable { - let base = parse_table_empty(materialization: Memoize) - ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: Memoize, - grammar_digest: base.grammar_digest, - token_stream_digest: base.token_stream_digest, - entries: base.entries, - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } -} - -test fn witness_materialization_gate_recompute_denies_store() -> Bool { - !materialization_allows_memo_store(m: Recompute) -} - -test fn witness_recompute_fixture_carries_recompute_verdict() -> Bool { - !materialization_allows_memo_store(m: parse_table_recompute_fixture().materialization) -} - -test fn witness_door_insert_refused_for_recompute_table() -> Bool { - parse_table_memo_insert_refused( - table: parse_table_recompute_fixture(), - key: governed_witness_probe_key(), - value: governed_witness_dummy_entry() - ) -} - -test fn witness_door_lookup_refused_for_recompute_table() -> Bool { - parse_table_memo_lookup_refuses_store( - table: parse_table_recompute_fixture(), - key: governed_witness_probe_key() - ) -} - -fn parse_table_entries_contain_key(table: ParseTable, key: ParsePositionKey) -> Bool { - match v2.std.collection.map_get(m: table.entries, key: key) { - Accepted { value: Present { value: _ } } => true - Accepted { value: Absent } => false - Rejected { diagnostics: _ } => false - } -} - -fn parse_table_memo_store_well_formed_at(table: ParseTable, key: ParsePositionKey) -> Bool { - match materialization_allows_memo_store(m: table.materialization) { - true => true - false => !parse_table_entries_contain_key(table: table, key: key) - } -} - -fn witness_bypass_store_reds_non_memoize_carrier() -> Bool { - let key = governed_witness_probe_key() - let base = parse_table_recompute_fixture() - let bypass = ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: Recompute, - grammar_digest: base.grammar_digest, - token_stream_digest: base.token_stream_digest, - entries: map_insert( - m: empty_map(), - key: key, - value: governed_witness_dummy_entry() - ), - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } - !parse_table_memo_store_well_formed_at(table: bypass, key: key) -} - -test fn witness_memoize_carrier_allows_store() -> Bool { - let table = parse_table_memoize_fixture() - let key = governed_witness_probe_key() - let after = parse_table_insert( - table: table, - key: key, - value: governed_witness_dummy_entry() - ) - parse_table_entries_contain_key(table: after, key: key) -} - -test fn witness_content_key_subject_stable() -> Bool { - let table = parse_table_memoize_fixture() - let key = governed_witness_probe_key() - let a = parse_table_subject(table: table, key: key) - let b = parse_table_subject(table: table, key: key) - a == b -} - -test fn parse_table_memo_bypass_store_reds() -> Bool { - witness_bypass_store_reds_non_memoize_carrier() -} - -test fn parse_table_memo_warm_equals_cold_content_hash() -> Bool { - witness_parse_table_memo_hit_path_content_hash_stable() -} - diff --git a/src/v2/test/claim/long/program_assembly_advisory_route_test.dag b/src/v2/test/claim/long/program_assembly_advisory_route_test.dag index c6aeaccb315..b25768d9aa3 100644 --- a/src/v2/test/claim/long/program_assembly_advisory_route_test.dag +++ b/src/v2/test/claim/long/program_assembly_advisory_route_test.dag @@ -4,15 +4,14 @@ import v2.compiler.program_assembly { ProgramAssemblyLocatedFoldOk, ProgramAssemblyLocatedFoldFailed, dag_prepared_grammar, + dag_prepared_lex, program_assembly_fold_step, program_assembly_ingest_scope_outcome } import v2.compiler.source_authority { DagSourceReadWitness } -import v2.compiler.tokenize { prepare_lex_rules } import v2.std.artifact { Artifact, SourceFile } import v2.std.cross_tree.import_model { DagTree } import extdeps.communication.medium { Lossless, Medium } -import v2.extdeps.languages.dag { dag_language_model } import v2.std.cross_tree.resolution { source_root_index_empty } import v2.std.provenance { span_index_empty } import std.occurrence_identity { occurrence_id_allocator_initial } @@ -35,66 +34,6 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// THE REAL ASSEMBLY FOLD CARRIES EACH ACCEPTED FILE'S ADVISORIES (advisories step 1b, the -// program_assembly half). No advisory reaches this route from real source today -- the dag grammar's -// overlap residue is empty (gunbc#13056) and body lowering's postfix_suffix_carried advisory has no -// known reaching source -- so the one advisory input is SUPPLIED at the boundary the fold already -// takes it through: program_assembly_fold_step's `residue`, which the real parse merges into every -// file's accepted outcome. One real read goes through the real fold step; the supplied advisory must -// reach the fold's chain, once. Before the fold carried advisories it matched them -// away, so this reds there. It lives in the long home because a real parse exceeds the new-witness -// CPU margin; the extend's own discrimination is the cheap supplied claim -// v2.test.provenance.occurrence_file_attribution -// the_assembly_advisory_extend_keeps_every_files_advisories_in_order_holds. -// ONE minimal accepted read: the claim's subject is the fold's carry, not parsing, so the smallest -// module the front end accepts is the honest input (two reads measured 280014 eval steps on CI, -// past the new-witness budget). -fn advisory_route_read() -> DagSourceReadWitness { - DagSourceReadWitness { - source: Medium { carried: "module v2.test.fixture.advisory_route\n", fidelity: Lossless }, - artifact: Artifact { kind: SourceFile, id: ^advisory_route_read, file_path: "src/v2/test/fixture/advisory_route.dag" }, - compilation_unit: ^advisory_route_file, - source_root: DagTree - } -} - -test fn a_supplied_residue_rides_each_accepted_file_through_the_real_assembly_fold_holds() -> Bool { - let advisory = Some { - diagnostics: diagnostics_singleton(d: Diagnostic { - reason: ^program_assembly_supplied_advisory, - at: invariant_port_locus(port: ^program_assembly_advisory_route, invariant: ^program_assembly_supplied_advisory), - correction: Unavailable { reason: ExternalContractUnknown } - }) - } - let reads = [advisory_route_read()] - match dag_prepared_grammar() { - Rejected { diagnostics: _ } => false - Accepted { value: prepared, diagnostics: _ } => - match program_assembly_ingest_scope_outcome(ingest: reads) { - Rejected { diagnostics: _ } => false - Accepted { value: scope, diagnostics: _ } => - let start = ProgramAssemblyLocatedFoldOk { - located: Empty, - index: source_root_index_empty(), - alloc: occurrence_id_allocator_initial(), - spans: span_index_empty(), - advisories: None - } - match program_assembly_fold_step(state: start, read: advisory_route_read(), lex: prepare_lex_rules(rules: dag_language_model().lex), prepared: prepared, residue: advisory, scope: scope) { - ProgramAssemblyLocatedFoldFailed { diagnostics: _, spans: _ } => false - ProgramAssemblyLocatedFoldOk { located: located, index: _, alloc: _, spans: _, advisories: carried } => - length(xs: located) == 1 - && (match carried { - None => false - Some { diagnostics: d } => - d.head.reason == ^program_assembly_supplied_advisory - && length(xs: d.tail) == 0 - }) - } - } - } -} - // A REFUSING FILE KEEPS THE ADVISORIES THE INGEST ACCUMULATED BEFORE IT. The fold state is supplied // already holding one accepted file's advisory (adv_a); the next real read refuses at parse. The // failed state must carry adv_a ahead of the refusal, exactly as rejected_with_pending orders a @@ -131,7 +70,7 @@ test fn a_later_refusal_keeps_the_accumulated_advisories_ahead_of_its_fatal_hold }) } } - match program_assembly_fold_step(state: accumulated, read: advisory_route_refusing_read(), lex: prepare_lex_rules(rules: dag_language_model().lex), prepared: prepared, residue: None, scope: scope) { + match program_assembly_fold_step(state: accumulated, read: advisory_route_refusing_read(), lex: prepare_lex_rules(rules: dag_language_model().lex), prepared: prepared, scope: scope) { ProgramAssemblyLocatedFoldOk { located: _, index: _, alloc: _, spans: _, advisories: _ } => false ProgramAssemblyLocatedFoldFailed { diagnostics: d, spans: _ } => d.head.reason == ^adv_a diff --git a/src/v2/test/claim/long/reference_conservation_advisory_route_test.dag b/src/v2/test/claim/long/reference_conservation_advisory_route_test.dag deleted file mode 100644 index e8349a7e5c4..00000000000 --- a/src/v2/test/claim/long/reference_conservation_advisory_route_test.dag +++ /dev/null @@ -1,49 +0,0 @@ -module v2.test.claim.long.reference_conservation_advisory_route - -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.reference_conservation { reference_conservation_report_lines, reference_conservation_subject_of_read } -import v2.test.claim.namespace_xl0.reference_conservation { conservation_fixture_read, conservation_report_of } -import v2.compiler.tokenize { prepare_lex_rules } -import v2.extdeps.languages.dag { dag_language_model } -import v2.std.algebra { any } -import v2.std.diagnostic { Accepted, Diagnostic, Rejected, Some, Unavailable, UserInputBoundary, diagnostics_has_reason, diagnostics_singleton, invariant_port_locus } -import v2.std.logic { Bool } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -// AN ACCEPTED SUBJECT'S ADVISORIES REACH THE REPORT AND ITS CENSUS LINES BY IDENTITY, THROUGH THE -// REAL ROUTE. No advisory reaches the front end from real source today: the dag grammar's overlap -// residue is empty (gunbc#13056). So the one advisory input is SUPPLIED at the boundary the route -// already takes it through, reference_conservation_subject_of_read's `residue`, which the real -// parse merges into the accepted outcome. A one-line module then goes through the real subject and -// report path, and the report and its census lines must name the supplied reason. Dropping the -// advisories at reference_conservation_of_subject_admitted (the old `diagnostics: _`) reds both -// conjuncts. It lives in the long home because one real parse exceeds the new-witness CPU margin -// (450ms against 302ms on CI); the renderer's supplied claim stays in -// v2.test.claim.namespace_xl0.reference_conservation. -data supplied_route_source: String = "module v2.test.reference_conservation_advisory_route\n" - -test fn an_accepted_subjects_advisories_reach_the_report_by_identity_holds() -> Bool { - match dag_prepared_grammar() { - Rejected { diagnostics: _ } => false - Accepted { value: prepared, diagnostics: _ } => - let report = conservation_report_of(subject: reference_conservation_subject_of_read( - read: conservation_fixture_read(id_tail: "advisory_route", source: supplied_route_source), - lex: prepare_lex_rules(rules: dag_language_model().lex), - prepared: prepared, - residue: Some { - diagnostics: diagnostics_singleton(d: Diagnostic { - reason: ^reference_conservation_supplied_advisory, - at: invariant_port_locus(port: ^body_lowering_locus_port, invariant: ^reference_conservation_supplied_advisory), - correction: Unavailable { reason: UserInputBoundary } - }) - } - )) - diagnostics_has_reason(d: report.advisories, reason: ^reference_conservation_supplied_advisory) - && any( - xs: reference_conservation_report_lines(report: report), - predicate: fn(line) { line == "advisory " + report.path + " reference_conservation_supplied_advisory" } - ) - } -} diff --git a/src/v2/test/claim/mandatory_tag_supplied_carrier_witness_test.dag b/src/v2/test/claim/mandatory_tag_supplied_carrier_witness_test.dag new file mode 100644 index 00000000000..4b0b12c0965 --- /dev/null +++ b/src/v2/test/claim/mandatory_tag_supplied_carrier_witness_test.dag @@ -0,0 +1,285 @@ +module v2.test.claim.mandatory_tag_supplied_carrier_witness + +import std.types { List } +import v2.lens.mandatory_tag { mandatory_tag_compile_gate_with_provenance, mandatory_tag_lowered_candidate_findings, extdeps_external_authority_region } +import v2.compiler.namespace_graft { namespace_graft_build_body_conj, namespace_graft_fold_spine, namespace_graft_wrap_module_shell } +import v2.std.member_declaration_kind { + DeclaredDataMember, + DeclaredFnMember, + DeclaredMember, + MemberKindChannel, + MemberKindsCaptured, + QualifiedDeclaredMember, + declared_members_have_duplicate, + no_member_declaration_kinds +} +import v2.std.qualified_name { QualifiedName } +import std.occurrence_identity { OccurrenceSynthetic } +import v2.std.node { Arrow, Authored, ArrowBodyEdge, Atom, Edge, Node, Positional, TypeNode, Conj, core_edge_label } +import std.algebra { Cons, Empty } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.integer { Int } +import v2.std.node { Symbol } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +// FAST-LANE supplied-carrier controls for the mandatory-tag gate's lowered-member path (DESIGN section 3: a +// witness discriminates at ONE interface, its inputs supplied rather than derived). No tokenize, parse or +// normalize runs: the candidate Arrow, the grafted module and the member-kind channel are built values, so each +// control costs a few hundred to ~2k eval steps and is selected by the required floor. The real route +// (normalize -> captured member channel -> member_kind_channel_of_tree -> mandatory_tag_compile_gate_with_provenance) +// is the pairing inhabitance claim in v2.test.long.mandatory_tag_gate_witness +// (mandatory_tag_normalized_grain_clean_accepts); NOT executed by any claim: resolve_in_context -> +// ResolvedTree.member_kinds -> validate_then_compile rebinding (gunbc.guarantee_stall +// member_kind_door_binding_executed_by_no_claim_stall). + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +type GateProbe { root: Node channel: MemberKindChannel } + +fn gate_accepts_tree(probe: GateProbe) -> Bool { + match mandatory_tag_compile_gate_with_provenance(node: probe.root, channel: probe.channel) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn gate_rejects_tree_with(probe: GateProbe, reason: Symbol) -> Bool { + match mandatory_tag_compile_gate_with_provenance(node: probe.root, channel: probe.channel) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => + if d.head.reason == reason { + true + } else { + fold(d.tail, init: false, f: fn(acc, diag) { acc || diag.reason == reason }) + } + } +} + +// SUPPLIED-CARRIER CONTROLS at the lens's own interface (DESIGN section 3, a witness discriminates at one +// interface): the candidate Arrow and the member-kind channel are supplied values, so no frontend runs. The +// real route is executed only as far as: normalize -> captured member channel -> member_kind_channel_of_tree -> +// mandatory_tag_compile_gate_with_provenance (mandatory_tag_normalized_grain_clean_accepts above; deleting that +// integration makes it fail). NOT executed by any claim: resolve_in_context -> ResolvedTree.member_kinds -> +// validate_then_compile rebinding; that link is the standing GuaranteeStall +// gunbc.guarantee_stall member_kind_door_binding_executed_by_no_claim_stall. +fn mt_atom(s: Symbol) -> Node { + Node { kind: TypeNode { connective: Atom { identity: s } }, children: [], occurrence_id: OccurrenceSynthetic } +} + +fn mt_domain() -> Node { + Node { kind: TypeNode { connective: Conj }, children: [], occurrence_id: OccurrenceSynthetic } +} + +fn mt_candidate(codomain: Symbol, body: List) -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [ + Edge { label: Positional, target: mt_domain() }, + Edge { label: Positional, target: mt_atom(s: codomain) }, + Edge { label: core_edge_label(marker: ArrowBodyEdge), target: mt_body(atoms: body) } + ], + occurrence_id: OccurrenceSynthetic + } +} + +fn mt_body(atoms: List) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: fold(atoms, init: [], f: fn(acc, a) { concat(acc, [Edge { label: Positional, target: mt_atom(s: a) }]) }), + occurrence_id: OccurrenceSynthetic + } +} + +fn mt_no_codomain() -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [Edge { label: Positional, target: mt_domain() }, Edge { label: core_edge_label(marker: ArrowBodyEdge), target: mt_body(atoms: [^Https]) }], + occurrence_id: OccurrenceSynthetic + } +} + +fn mt_no_body() -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [Edge { label: Positional, target: mt_domain() }, Edge { label: Positional, target: mt_atom(s: ^ExternalAuthority) }], + occurrence_id: OccurrenceSynthetic + } +} + +fn mt_channel(kind: MemberKindChannel) -> MemberKindChannel { + kind +} + +fn mt_captured(kind: MemberKindDeclaredFor) -> MemberKindChannel { + MemberKindsCaptured { + covered: [mt_module()], + members: mt_members(kind: kind) + } +} + +type MemberKindDeclaredFor + = ForData + | ForFn + | ForNothing + +fn mt_module() -> QualifiedName { + [^extdeps, ^gate_probe_supplied] +} + +fn mt_members(kind: MemberKindDeclaredFor) -> List { + match kind { + ForData => [QualifiedDeclaredMember { module: mt_module(), name: ^extdeps_external_authority_anchor, kind: DeclaredDataMember }] + ForFn => [QualifiedDeclaredMember { module: mt_module(), name: ^extdeps_external_authority_anchor, kind: DeclaredFnMember }] + ForNothing => [] + } +} + +fn mt_findings(candidate: Node, channel: MemberKindChannel) -> List { + fold( + mandatory_tag_lowered_candidate_findings( + region: extdeps_external_authority_region, + module_node: candidate, + dotted: "extdeps.gate_probe_supplied", + candidate: candidate, + channel: channel + ), + init: [], + f: fn(acc, d) { concat(acc, [d.reason]) } + ) +} + +fn mt_only(candidate: Node, channel: MemberKindChannel, reason: Symbol) -> Bool { + match mt_findings(candidate: candidate, channel: channel) { + Cons { head: h, tail: Empty } => h == reason + Cons { head: _, tail: Cons { head: _, tail: _ } } => false + Empty => false + } +} + +// TOP-LEVEL SELECTOR, SUPPLIED-CARRIER GRAIN: the grafted module is built directly (no parse, no normalize) by the +// graft's own constructors. Its one DIRECT member is unrelated; the required name appears only NESTED inside that +// member's Arrow as a valid Arrow, and the channel would admit that name as a data member. Must refuse as missing. +fn mt_unrelated_member_nesting_anchor() -> Node { + Node { + kind: TypeNode { connective: Arrow }, + children: [ + Edge { label: Positional, target: mt_domain() }, + Edge { label: Positional, target: mt_atom(s: ^Int) }, + Edge { label: Authored { name: ^extdeps_external_authority_anchor }, target: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]) } + ], + occurrence_id: OccurrenceSynthetic + } +} + +fn mt_nested_anchor_module() -> Node { + namespace_graft_wrap_module_shell(captured: mt_nested_anchor_spine(), source: mt_domain()) +} + +fn mt_nested_anchor_spine() -> Node { + mt_spine_with(edges: [Edge { label: Authored { name: ^gate_probe_unrelated_row }, target: mt_unrelated_member_nesting_anchor() }]) +} + +fn mt_direct_anchor_module() -> Node { + namespace_graft_wrap_module_shell( + captured: mt_spine_with(edges: [Edge { label: Authored { name: ^extdeps_external_authority_anchor }, target: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]) }]), + source: mt_domain() + ) +} + +fn mt_spine_with(edges: List) -> Node { + namespace_graft_fold_spine( + qn: mt_module(), + body: namespace_graft_build_body_conj( + edges: edges, + source: mt_domain() + ), + source: mt_domain() + ) +} + +test fn mandatory_tag_supplied_nested_anchor_under_unrelated_member_refuses_as_missing() -> Bool { + gate_rejects_tree_with( + probe: GateProbe { root: mt_nested_anchor_module(), channel: mt_captured(kind: ForData) }, + reason: ^mandatory_tag_missing_required_decl + ) +} + +test fn mandatory_tag_supplied_direct_anchor_member_in_grafted_module_accepts() -> Bool { + gate_accepts_tree(probe: GateProbe { root: mt_direct_anchor_module(), channel: mt_captured(kind: ForData) }) +} + +test fn mandatory_tag_supplied_data_member_with_good_carrier_accepts() -> Bool { + match mt_findings(candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]), channel: mt_captured(kind: ForData)) { + Empty => true + Cons { head: _, tail: _ } => false + } +} + +test fn mandatory_tag_supplied_zero_arg_fn_same_shape_refuses_as_missing() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]), + channel: mt_captured(kind: ForFn), + reason: ^mandatory_tag_missing_required_decl + ) +} + +test fn mandatory_tag_supplied_member_not_declared_refuses_as_missing() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]), + channel: mt_captured(kind: ForNothing), + reason: ^mandatory_tag_missing_required_decl + ) +} + +test fn mandatory_tag_supplied_unread_provenance_refuses_as_unavailable() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]), + channel: no_member_declaration_kinds(), + reason: ^mandatory_tag_member_provenance_unavailable + ) +} + +test fn mandatory_tag_supplied_uncovered_module_refuses_as_unavailable() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^Https]), + channel: MemberKindsCaptured { covered: [[^extdeps, ^some_other_module]], members: [] }, + reason: ^mandatory_tag_member_provenance_unavailable + ) +} + +test fn mandatory_tag_supplied_wrong_codomain_refuses_as_type_mismatch() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^SomethingElse, body: [^Https]), + channel: mt_captured(kind: ForData), + reason: ^mandatory_tag_required_decl_type_mismatch + ) +} + +test fn mandatory_tag_supplied_missing_codomain_or_body_refuses() -> Bool { + mt_only(candidate: mt_no_codomain(), channel: mt_captured(kind: ForData), reason: ^mandatory_tag_required_decl_type_mismatch) + && mt_only(candidate: mt_no_body(), channel: mt_captured(kind: ForData), reason: ^mandatory_tag_value_vocabulary_unreadable) +} + +test fn mandatory_tag_supplied_wrong_or_missing_vocabulary_atom_refuses() -> Bool { + mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: [^File]), + channel: mt_captured(kind: ForData), + reason: ^mandatory_tag_value_not_accepted + ) + && mt_only( + candidate: mt_candidate(codomain: ^ExternalAuthority, body: []), + channel: mt_captured(kind: ForData), + reason: ^mandatory_tag_value_vocabulary_unreadable + ) +} + +test fn member_kind_channel_duplicate_declaration_is_detected() -> Bool { + declared_members_have_duplicate(members: [ + DeclaredMember { name: ^extdeps_external_authority_anchor, kind: DeclaredDataMember }, + DeclaredMember { name: ^extdeps_external_authority_anchor, kind: DeclaredFnMember } + ]) + && !declared_members_have_duplicate(members: [ + DeclaredMember { name: ^extdeps_external_authority_anchor, kind: DeclaredDataMember }, + DeclaredMember { name: ^other_member, kind: DeclaredFnMember } + ]) +} diff --git a/src/v2/test/claim/manual/bind_demand_driven_eval_test.dag b/src/v2/test/claim/manual/bind_demand_driven_eval_test.dag index 64b708f8015..7fced9579db 100644 --- a/src/v2/test/claim/manual/bind_demand_driven_eval_test.dag +++ b/src/v2/test/claim/manual/bind_demand_driven_eval_test.dag @@ -28,7 +28,7 @@ import v2.extdeps.runtimes.v2_evaluator { import v2.std.cardinality { termination_proof_witness_for_node } import v2.std.collection { Map, - map_get, + map_get_checked, map_insert } import std.optional { @@ -171,7 +171,7 @@ fn bind_eval_int_from_runtime(value: RuntimeValue) -> Int { } fn bind_eval_touch_from_env(env: EvaluationEnvironment, key: EnvironmentBindingKey) -> Int { - match v2.std.collection.map_get(m: env.bindings, key: key) { + match v2.std.collection.map_get_checked(m: env.bindings, key: key) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: v } => bind_eval_int_from_runtime(value: v) diff --git a/src/v2/test/claim/manual/branch_lazy_arm_eval_acceptance_test.dag b/src/v2/test/claim/manual/branch_lazy_arm_eval_acceptance_test.dag index 54f891e64a4..b95d3437bbd 100644 --- a/src/v2/test/claim/manual/branch_lazy_arm_eval_acceptance_test.dag +++ b/src/v2/test/claim/manual/branch_lazy_arm_eval_acceptance_test.dag @@ -25,7 +25,7 @@ import v2.std.cardinality { termination_proof_witness_for_node } import v2.std.collection { Map, list_at_optional, - map_get, + map_get_checked, map_insert } import std.optional { @@ -228,7 +228,7 @@ fn lazy_arm_int_from_runtime(value: RuntimeValue) -> Int { } fn lazy_arm_touch_from_env(env: EvaluationEnvironment) -> Int { - match v2.std.collection.map_get(m: env.bindings, key: lazy_arm_touch_key()) { + match v2.std.collection.map_get_checked(m: env.bindings, key: lazy_arm_touch_key()) { Accepted { value: opt, diagnostics: _ } => match opt { Present { value: v } => lazy_arm_int_from_runtime(value: v) diff --git a/src/v2/test/claim/manual/cpp_add_emit_translate_test.dag b/src/v2/test/claim/manual/cpp_add_emit_translate_test.dag index 37e4dd2dbad..4c9878b3cf3 100644 --- a/src/v2/test/claim/manual/cpp_add_emit_translate_test.dag +++ b/src/v2/test/claim/manual/cpp_add_emit_translate_test.dag @@ -13,7 +13,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.translate { translate } diff --git a/src/v2/test/claim/manual/go_add_emit_translate_test.dag b/src/v2/test/claim/manual/go_add_emit_translate_test.dag index 35d03876949..9964e5d93d9 100644 --- a/src/v2/test/claim/manual/go_add_emit_translate_test.dag +++ b/src/v2/test/claim/manual/go_add_emit_translate_test.dag @@ -13,7 +13,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.translate { translate } diff --git a/src/v2/test/claim/manual/grammar_choice_ambiguity_audit.dag b/src/v2/test/claim/manual/grammar_choice_ambiguity_audit.dag index a1916fd25a4..0e69b642523 100644 --- a/src/v2/test/claim/manual/grammar_choice_ambiguity_audit.dag +++ b/src/v2/test/claim/manual/grammar_choice_ambiguity_audit.dag @@ -5,7 +5,7 @@ fn witness_python_choice_ambiguity_count() -> Int { grammar_choice_ambiguity_count(root: python_grammar_root()) } -fn witness_python_grammar_validate_accepts_with_overlap_residue() -> Bool { +fn witness_python_grammar_validate_accepts() -> Bool { let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_choice_ambiguity_audit_grammar_contract) match grammar_validate_for_parse(root: python_grammar_root(), at: at) { Accepted { value: _, diagnostics: _ } => true @@ -17,16 +17,6 @@ fn witness_dag_choice_ambiguity_count() -> Int { grammar_choice_ambiguity_count(root: dag_grammar_root()) } -fn witness_dag_zero_nullable_choice_ambiguity() -> Bool { - is_empty(xs: grammar_nullable_ambiguity_rows( - roster: grammar_choice_ambiguity_roster(root: dag_grammar_root()) - )) -} - -fn witness_dag_overlap_residue_row_count() -> Int { - length(xs: grammar_choice_ambiguity_roster(root: dag_grammar_root())) -} - fn witness_dag_grammar_validate_accepts() -> Bool { let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_choice_ambiguity_audit_grammar_contract) match grammar_validate_for_parse(root: dag_grammar_root(), at: at) { @@ -35,7 +25,7 @@ fn witness_dag_grammar_validate_accepts() -> Bool { } } -// THE RESIDUE ROSTER BY IDENTITY, not a count plus its first row. Two rosters of equal length can +// THE OVERLAP ROSTER BY IDENTITY, not a count plus its first row. Two rosters of equal length can // differ in every row, so a count cannot show a row was removed and another added. One line per // row: `production#k: overlap=[..] left=[..] right=[..]`, in roster order. // @@ -56,7 +46,7 @@ fn grammar_choice_terminals_text(xs: FreeMonoid) -> String { } fn grammar_choice_ambiguity_row_identity(row: GrammarChoiceAmbiguityRow, ordinal: Int) -> String { - symbol_lexeme(sym: row.production) + "#" + integer_int_to_decimal_string(value: ordinal) + symbol_lexeme(sym: row.production) + "#" + int_to_decimal_string(value: ordinal) + ": overlap=[" + grammar_choice_terminals_text(xs: row.overlap_terminals) + "] left=[" + grammar_choice_terminals_text(xs: row.left_first_terminals) + "] right=[" + grammar_choice_terminals_text(xs: row.right_first_terminals) + "]" @@ -69,7 +59,7 @@ type AuditRosterFold { lines: FreeMonoid } -fn witness_dag_overlap_residue_row_identities() -> String { +fn witness_dag_overlap_row_identities() -> String { let folded = fold_list( xs: grammar_choice_ambiguity_roster(root: dag_grammar_root()), empty: AuditRosterFold { seen: empty_map(), lines: [] }, diff --git a/src/v2/test/claim/manual/infer_ground_add.dag b/src/v2/test/claim/manual/infer_ground_add.dag index d4644b997bc..a38d7e0a6da 100644 --- a/src/v2/test/claim/manual/infer_ground_add.dag +++ b/src/v2/test/claim/manual/infer_ground_add.dag @@ -10,7 +10,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, eval_context_with_semantics, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.std.verdict { Verdict, Pass, verdict_fail } @@ -281,7 +280,7 @@ fn infer_add_receipt_run( ) -> TestClaimRun { let actual = anchor_infer_add() TestClaimRun { - cache: test_claim_cache_receipt(subject: subject), + claim: subject.claim, verdict: match test_claim_expected_outcome(c: subject.claim) { Accepted { value: expected, diagnostics: _ } => match actual { @@ -314,7 +313,7 @@ fn infer_descent_witness_receipt_run( ) -> TestClaimRun { let actual = infer_descent_witness_contract_node(n: subject.input.evaluator_input) TestClaimRun { - cache: test_claim_cache_receipt(subject: subject), + claim: subject.claim, verdict: match test_claim_expected_outcome(c: subject.claim) { Accepted { value: expected, diagnostics: _ } => if outcome_node_eq(a: actual, b: expected) { diff --git a/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag b/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag index 1ec0df9042b..d9d0f7030cc 100644 --- a/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag +++ b/src/v2/test/claim/manual/match_infer_fail_open_audit_test.dag @@ -19,7 +19,8 @@ import v2.std.diagnostic { NonEmptyDiagnostics, Outcome, Rejected, - Some + Some, + diagnostics_fatal_reason } import v2.std.node { Atom, @@ -185,10 +186,16 @@ fn classical_not_int_match_arrow_fixture() -> Node { ) } +// Arms use dag_token_kw_true/false (bool-literal authority). infer still Rejects: +// claim_resolved_tree_without_declarations never types the BindingRef scrutinee `a` +// (GroundingNotDerived, not infer_match_pattern_not_bool_literal). Frozen +// grandfathered identities stay the refuse names. + test fn complement_body_real_infer_refuses_unsupported_pattern_holds() -> Bool { match infer(tree: claim_resolved_tree_without_declarations(root: dag_complement_body_fixture)) { Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^infer_grounding_not_derived } } @@ -237,15 +244,10 @@ test fn classical_not_int_match_non_octet_arm_rejects_holds() -> Bool { } } -// complement infer refuses because match patterns use dag_complement_lit_* (unsupported by -// infer_bool_literal_pattern_classify — only dag_token_kw_true/false). Refuse is -// fail-closed-correct for that unsupported shape. Counted deficit: ground complement_lit on the -// bool-literal authority (post-exit breadth); target witness is then -// complement_arrow_real_infer_holds. - test fn complement_arrow_real_infer_refuses_unsupported_pattern_holds() -> Bool { match infer(tree: claim_resolved_tree_without_declarations(root: dag_complement_arrow_with_body_fixture)) { Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true + Rejected { diagnostics: ds } => + diagnostics_fatal_reason(d: ds) == ^infer_grounding_not_derived } } diff --git a/src/v2/test/claim/manual/parse_mechanism_probe.dag b/src/v2/test/claim/manual/parse_mechanism_probe.dag index 146eb5df6a8..d9da6d52055 100644 --- a/src/v2/test/claim/manual/parse_mechanism_probe.dag +++ b/src/v2/test/claim/manual/parse_mechanism_probe.dag @@ -20,54 +20,8 @@ fn pmp_tokenize_parse_path(path: String) -> Bool { pmp_tokenize_parse_text(source_text: source) } -fn pmp_memo_stats_text(source_text: String) -> ParseMemoStats { - let lm = dag_language_model() - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_mechanism_probe_grammar_contract) - match tokenize(text: source_text, file: pmp_anchor, rules: lm.lex) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: stream, diagnostics: _ } => - match lm.grammar { - VoidGrammar => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - ModeledGrammar { root: root } => - match grammar_validate_for_parse(root: root, at: at) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: validated, diagnostics: _ } => - parse_production_memo_stats( - tokens: token_stream_remaining(stream: stream), - validated: validated, - production_name: validated.root.start, - materialization: Memoize - ) - } - } - } -} - -fn pmp_memo_stats_path(path: String) -> ParseMemoStats { - let source = filesystem_read(path: path).content - pmp_memo_stats_text(source_text: source) -} - -fn pmp_memo_stats_report_path(path: String) -> List { - let stats = pmp_memo_stats_path(path: path) - Cons { - head: stats.hits, - tail: Cons { - head: stats.misses, - tail: Cons { head: stats.lookup_calls, tail: std.algebra.Empty } - } - } -} - data pmp_through_decimal_path: String = "src/v2/test/fixture/integer_census_trunc/through_decimal_compare.txt" -fn pmp_through_decimal_memo_report() -> List { - pmp_memo_stats_report_path(path: pmp_through_decimal_path) -} - fn exp_nest_d1_parse_holds() -> Bool { pmp_tokenize_parse_path(path: "src/v2/test/fixture/parse_mechanism/nest_d1.dag") } @@ -104,10 +58,6 @@ fn exp_through_compose_parse_holds() -> Bool { pmp_tokenize_parse_path(path: "src/v2/test/fixture/integer_census_trunc/through_compose_types.txt") } -fn pmp_through_compose_memo_report() -> List { - pmp_memo_stats_report_path(path: "src/v2/test/fixture/integer_census_trunc/through_compose_types.txt") -} - fn exp_through_decimal_parse_holds() -> Bool { pmp_tokenize_parse_path(path: pmp_through_decimal_path) } diff --git a/src/v2/test/claim/manual/python_add_emit_translate_test.dag b/src/v2/test/claim/manual/python_add_emit_translate_test.dag index c61d3617e0c..d43a3142295 100644 --- a/src/v2/test/claim/manual/python_add_emit_translate_test.dag +++ b/src/v2/test/claim/manual/python_add_emit_translate_test.dag @@ -14,7 +14,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.translate { translate } diff --git a/src/v2/test/claim/manual/python_grammar_claim_test.dag b/src/v2/test/claim/manual/python_grammar_claim_test.dag index bc25edc43fb..21675624869 100644 --- a/src/v2/test/claim/manual/python_grammar_claim_test.dag +++ b/src/v2/test/claim/manual/python_grammar_claim_test.dag @@ -1,11 +1,8 @@ module v2.test.manual.python_grammar_claim import v2.compiler.parse { - grammar_choice_ambiguity_count, grammar_validate_for_parse, - parse_module_prepared, parse_production_tree, - prepare_grammar, } import v2.compiler.tokenize { tokenize } import v2.extdeps.languages.python { @@ -15,7 +12,7 @@ import v2.extdeps.languages.python { } import v2.std.compilers.lexing { token_stream_remaining } import v2.std.diagnostic { Accepted, Outcome, Rejected, invariant_port_locus } -import v2.std.grammar { ModeledGrammar, ParseTree } +import v2.std.grammar { ParseTree } test fn python_grammar_validates_fixture() -> Bool { let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^python_grammar_claim_test_grammar_contract) @@ -52,52 +49,3 @@ fn python_grammar_parse_fixture() -> Outcome { } } } - -// ROUTE CONTROL for the suite production on real python source: a two-statement body -// (assignment, return) lexes with python_lex and parses through python_production_fn_def, so the -// suite's repetition is executed past its first statement. Two statements is the least input -// that reaches the repetition; a longer body adds parse cost without adding a route. -data python_multi_statement_suite_text: String = "def f(x):\n y = x\n return y" - -// The discriminating red: a suite is one or more statements, so a def whose body is empty refuses. -data python_empty_suite_text: String = "def f(x):\n " - -// Through the prepared door: prepare_grammar validates AND analyzes the grammar once, and the -// module parse starts at the grammar's start production, python_production_fn_def. (Validating -// with grammar_validate_for_parse and then calling parse_production_tree computes the FIRST -// analysis twice, which is what put this control over its floor budget.) -fn python_fn_def_parse(text: String) -> Outcome { - match prepare_grammar(grammar: ModeledGrammar { root: python_grammar_root() }) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: prepared, diagnostics: residue } => - match tokenize(text: text, file: ^python_parse_fixture_file, rules: python_lex()) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: stream, diagnostics: _ } => - match parse_module_prepared(tokens: stream, prepared: prepared, validation_residue: residue) { - Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } - } - } - } -} - -test fn python_multi_statement_suite_parses() -> Bool { - match python_fn_def_parse(text: python_multi_statement_suite_text) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn python_empty_suite_refuses() -> Bool { - match python_fn_def_parse(text: python_empty_suite_text) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true - } -} - -// The family's discriminating control: the suite was python's one choice-overlap row -// (count 1 as `stmt suite | stmt`); restructured, the roster is empty. The parse controls above -// hold on both shapes because the language did not change -- this claim is what goes red. -test fn python_grammar_has_no_choice_overlap() -> Bool { - grammar_choice_ambiguity_count(root: python_grammar_root()) == 0 -} diff --git a/src/v2/test/claim/manual/rust_add_emit_translate_test.dag b/src/v2/test/claim/manual/rust_add_emit_translate_test.dag index 1abd47de3b1..ee864317c2d 100644 --- a/src/v2/test/claim/manual/rust_add_emit_translate_test.dag +++ b/src/v2/test/claim/manual/rust_add_emit_translate_test.dag @@ -13,7 +13,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.translate { coerce_grounded_node, translate } diff --git a/src/v2/test/claim/manual/typescript_add_emit_translate_test.dag b/src/v2/test/claim/manual/typescript_add_emit_translate_test.dag index 604d7809670..7793128bb79 100644 --- a/src/v2/test/claim/manual/typescript_add_emit_translate_test.dag +++ b/src/v2/test/claim/manual/typescript_add_emit_translate_test.dag @@ -14,7 +14,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.translate { translate } diff --git a/src/v2/test/claim/manual/typescript_pr3_typed_fn_translate_test.dag b/src/v2/test/claim/manual/typescript_pr3_typed_fn_translate_test.dag index 4117f17bf30..1fb617564c9 100644 --- a/src/v2/test/claim/manual/typescript_pr3_typed_fn_translate_test.dag +++ b/src/v2/test/claim/manual/typescript_pr3_typed_fn_translate_test.dag @@ -11,7 +11,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.target_serialize { target_serialize_source_from_model } diff --git a/src/v2/test/claim/manual/typescript_record_task_translate_test.dag b/src/v2/test/claim/manual/typescript_record_task_translate_test.dag index eb86a1c3614..eb8b453e5a4 100644 --- a/src/v2/test/claim/manual/typescript_record_task_translate_test.dag +++ b/src/v2/test/claim/manual/typescript_record_task_translate_test.dag @@ -11,7 +11,6 @@ import v2.compiler.eval { TestClaimEvalSubject, TestClaimRun, run_test_claim_assert, - test_claim_cache_receipt, test_claim_typed_input_node } import v2.compiler.target_serialize { target_serialize_source_from_model } diff --git a/src/v2/test/claim/manual/value_null_split_witness_test.dag b/src/v2/test/claim/manual/value_null_split_witness_test.dag index 2f7f12de851..a0afcf0f256 100644 --- a/src/v2/test/claim/manual/value_null_split_witness_test.dag +++ b/src/v2/test/claim/manual/value_null_split_witness_test.dag @@ -40,7 +40,7 @@ test fn optional_present_matches_present_pattern() -> Bool { } test fn map_get_miss_is_absent_not_present() -> Bool { - match v2.std.collection.map_get(empty_map(), "missing") { + match v2.std.collection.map_get_checked(empty_map(), "missing") { Accepted { value: Absent, diagnostics: _ } => true Accepted { value: Present { value: _ }, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -49,7 +49,7 @@ test fn map_get_miss_is_absent_not_present() -> Bool { test fn map_get_hit_is_present() -> Bool { let m = empty_map() |> map_insert("k", 7) - match v2.std.collection.map_get(m, "k") { + match v2.std.collection.map_get_checked(m, "k") { Accepted { value: Present { value: v }, diagnostics: _ } => v == 7 Accepted { value: Absent, diagnostics: _ } => false Rejected { diagnostics: _ } => false diff --git a/src/v2/test/claim/match_arm_binder_frame_test.dag b/src/v2/test/claim/match_arm_binder_frame_test.dag index 583429fa7bc..a2b631e456a 100644 --- a/src/v2/test/claim/match_arm_binder_frame_test.dag +++ b/src/v2/test/claim/match_arm_binder_frame_test.dag @@ -9,13 +9,13 @@ import v2.compiler.name_resolve { } import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { assemble_program_from_ingest, dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { assemble_program_from_ingest, dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } -import v2.extdeps.languages.dag { dag_language_model, dag_lex } +import v2.extdeps.languages.dag { dag_language_model } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.cross_tree.import_model { V2Tree } import v2.std.artifact { Artifact, SourceFile } @@ -331,13 +331,13 @@ test fn mab_binder_spelling_a_variant_declared_in_two_modules_refuses() -> Bool // capture itself is asserted, over all four spellings: a record, a nullary record, a where-alias, // and a coproduct with a fielded variant -- exactly the two records, in declaration order. fn mab_record_declarations(src: String) -> Outcome> { - match tokenize(text: src, file: ^match_arm_binder_record_probe, rules: dag_lex()) { + match tokenize_prepared(text: src, file: ^match_arm_binder_record_probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: tokens, diagnostics: _ } => match dag_prepared_grammar() { Rejected { diagnostics: r } => Rejected { diagnostics: r } - Accepted { value: prepared, diagnostics: residue } => - match parse_module_prepared(tokens: tokens, prepared: prepared, validation_residue: residue) { + Accepted { value: prepared, diagnostics: _ } => + match parse_module_prepared(tokens: tokens, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: _ } => match normalize(parse_tree: artifact.tree) { diff --git a/src/v2/test/claim/match_binder/match_binder_typing_test.dag b/src/v2/test/claim/match_binder/match_binder_typing_test.dag index bdece9299d3..a1a69ae4b01 100644 --- a/src/v2/test/claim/match_binder/match_binder_typing_test.dag +++ b/src/v2/test/claim/match_binder/match_binder_typing_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.match_binder.match_binder_typing +import v2.std.member_declaration_kind { no_member_declaration_kinds } import v2.compiler.resolve { ResolvedTree } import v2.compiler.infer { infer, inferred_facts_resolved_type } import v2.std.qualified_name { declaration_reference_path_optional, lexical_reference_label_optional, qualified_name_last_segment } @@ -731,7 +732,8 @@ fn mbt_resolved() -> ResolvedTree { symbol_index: mbt_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), - unavailable_providers: [] + unavailable_providers: [], + member_kinds: no_member_declaration_kinds() } } diff --git a/src/v2/test/claim/name_resolve/arrow_domain_is_first_positional_test.dag b/src/v2/test/claim/name_resolve/arrow_domain_is_first_positional_test.dag index 84355b6d9ff..56f056137b2 100644 --- a/src/v2/test/claim/name_resolve/arrow_domain_is_first_positional_test.dag +++ b/src/v2/test/claim/name_resolve/arrow_domain_is_first_positional_test.dag @@ -2,7 +2,7 @@ module v2.test.name_resolve.arrow_domain_is_first_positional import v2.compiler.resolve { arrow_domain_frame_binders, arrow_domain_has_named_bindings } import v2.std.arrow_signature { declared_signature } -import v2.compiler.symbol_index_fill { symbol_index_fill_containment_node } +import v2.compiler.symbol_index_fill { disj_variant_counts_in_module, symbol_index_fill_containment_node } import v2.std.symbol_index { empty_symbol_index, symbol_index_lookup } import v2.std.qualified_name { qualified_name_from_dotted_string } import v2.std.collection { map_lookup } @@ -58,7 +58,9 @@ test fn arrow_domain_is_first_positional_in_symbol_index_fill_holds() -> Bool { index: symbol_index_fill_containment_node( index: empty_symbol_index(), path: qualified_name_from_dotted_string(dotted: "m.f"), - node: adf_named_first_arrow() + node: adf_named_first_arrow(), + counts: disj_variant_counts_in_module(root: adf_named_first_arrow()), + module_qn: qualified_name_from_dotted_string(dotted: "m.f") ), qualified_path: qualified_name_from_dotted_string(dotted: "m.f.x") ) { diff --git a/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag b/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag index 04523fe0e4c..b9a577d0982 100644 --- a/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag +++ b/src/v2/test/claim/name_resolve/one_member_cost_probe_test.dag @@ -10,14 +10,14 @@ import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree, no_i import v2.std.declaration_marker { test_marker_channel_empty } import v2.compiler.parse { parse_module } import v2.compiler.resolve { ResolvedTree } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_canonical_symbols, dag_grammar, dag_grammar_atom, dag_language_model, dag_language_model_surface_empty_prelude, - dag_lex, dag_named_edge, dag_surface_edge, } @@ -177,10 +177,10 @@ test fn language_model_carries_its_canonical_symbols() -> Bool { data probe_front_end_source: String = "module probe.one_member\n" test fn tokenize_parse_one_member_does_not_run_resolve() -> Bool { - match tokenize( + match tokenize_prepared( text: probe_front_end_source, file: ^probe_front_end_file, - rules: dag_lex() + prepared: dag_prepared_lex() ) { Rejected { diagnostics: _ } => false Accepted { value: tokens, diagnostics: _ } => diff --git a/src/v2/test/claim/namespace_graft/marker_strip_test.dag b/src/v2/test/claim/namespace_graft/marker_strip_test.dag index f80a39f8ce8..706436ac6e8 100644 --- a/src/v2/test/claim/namespace_graft/marker_strip_test.dag +++ b/src/v2/test/claim/namespace_graft/marker_strip_test.dag @@ -11,12 +11,8 @@ import v2.compiler.namespace_graft { namespace_graft_strip_provenance_marker } import v2.std.node { Atom, Edge, Authored, Node, Symbol, TypeNode } -import v2.std.symbol_index { - empty_symbol_index, - symbol_index_lexical_lookup, - symbol_index_lookup -} -import v2.compiler.symbol_index_fill { symbol_index_fill_containment_node } +import v2.std.symbol_index { empty_symbol_index, symbol_index_lexical_lookup, symbol_index_lookup } +import v2.compiler.symbol_index_fill { disj_variant_counts_in_module, symbol_index_fill_containment_node } import v2.std.qualified_name { QualifiedName, qualified_name_from_dotted_string, @@ -111,7 +107,9 @@ fn ms_fill_index(root: Node) -> SymbolIndex { symbol_index_fill_containment_node( index: empty_symbol_index(), path: Empty, - node: root + node: root, + counts: disj_variant_counts_in_module(root: root), + module_qn: Empty ) } diff --git a/src/v2/test/claim/namespace_xl0/body_shape_contract_test.dag b/src/v2/test/claim/namespace_xl0/body_shape_contract_test.dag index 840188d3dfb..6a86f6c885d 100644 --- a/src/v2/test/claim/namespace_xl0/body_shape_contract_test.dag +++ b/src/v2/test/claim/namespace_xl0/body_shape_contract_test.dag @@ -4,9 +4,8 @@ import std.types { List } import v2.compiler.body_lowering_fold { body_lower_binary_operators, body_lower_prefix_operators } import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import std.algebra { list_append } import v2.std.collection { list_at_optional } import v2.std.compilers.body_lowering { @@ -88,11 +87,11 @@ data body_shape_source: String = "module mq.shape\n\nimport std.types { Bool }\n fn body_shape_normalized_root() -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: body_shape_source, file: ^body_shape_contract_file, rules: dag_lex()), + o: tokenize_prepared(text: body_shape_source, file: ^body_shape_contract_file, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: _ } => match normalize(parse_tree: artifact.tree) { diff --git a/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag b/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag index a190cdecbd1..a30bb40e627 100644 --- a/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag +++ b/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag @@ -1,7 +1,6 @@ module v2.test.claim.namespace_xl0.reference_conservation -import v2.compiler.tokenize { prepare_lex_rules } -import v2.compiler.program_assembly { dag_prepared_grammar } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } import v2.compiler.reference_conservation { ReferenceConservationReport, ReferenceConservationSubject, @@ -20,7 +19,7 @@ import std.algebra { Cons, Empty } import std.optional { Absent, Present } import v2.compiler.source_authority { DagSourceReadWitness } import v2.compiler.occurrence_role { NameProductionTableAdmission, NameProductionTableRefused, NameRoleNotYetRead, dag_name_production_rows, name_production_table_admission, name_row } -import v2.extdeps.languages.dag { dag_grammar_root, dag_language_model } +import v2.extdeps.languages.dag { dag_grammar_root } import v2.std.algebra { any, fold_list, length, list_map } import v2.std.artifact { Artifact, SourceFile } import v2.std.cross_tree.import_model { DagTree } @@ -28,7 +27,8 @@ import v2.std.diagnostic { Accepted, ByteRange, Diagnostic, Rejected, Some, Text import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Symbol, symbol_intern_lexeme } import v2.std.text { String } -import v2.std.integer { Int, integer_int_to_decimal_string } +import v2.std.integer { Int } +import std.integer { int_to_decimal_string } import extdeps.communication.medium { Lossless, Medium } import std.process { ProcessExit, exit_failure } @@ -67,12 +67,11 @@ fn conservation_fixture_read(id_tail: String, source: String) -> DagSourceReadWi // once; every claim still executes reference_conservation_of_subject, which is what it names. fn conservation_subject(id_tail: String, source: String) -> ReferenceConservationSubject { match dag_prepared_grammar() { - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => reference_conservation_subject_of_read( read: conservation_fixture_read(id_tail: id_tail, source: source), lex: prepare_lex_rules(rules: dag_language_model().lex), - prepared: prepared, - residue: residue + prepared: prepared ) Rejected { diagnostics: d } => ReferenceConservationSubject { path: "grammar-unprepared", parsed: Rejected { diagnostics: d }, normalized: Rejected { diagnostics: d } } @@ -175,7 +174,7 @@ test fn a_block_with_a_second_statement_is_refused_not_accepted_holds() -> Bool // how the report counts a refused body. fn block_second_statement_numbers() -> ProcessExit { let r = block_second_statement_report() - exit_failure(reason: "authored=" + integer_int_to_decimal_string(value: r.authored) + " conserved=" + integer_int_to_decimal_string(value: r.conserved) + " locus_erased=" + integer_int_to_decimal_string(value: r.locus_erased) + " refused=" + integer_int_to_decimal_string(value: r.refused) + " dropped=" + integer_int_to_decimal_string(value: length(xs: r.dropped))) + exit_failure(reason: "authored=" + int_to_decimal_string(value: r.authored) + " conserved=" + int_to_decimal_string(value: r.conserved) + " locus_erased=" + int_to_decimal_string(value: r.locus_erased) + " refused=" + int_to_decimal_string(value: r.refused) + " dropped=" + int_to_decimal_string(value: length(xs: r.dropped))) } // ---- former drop 2: a call argument inside an if-arm (repaired by #12221) --------------------- diff --git a/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag b/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag index 45cbf69e8c6..2e05f68ca63 100644 --- a/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag +++ b/src/v2/test/claim/namespace_xl0/value_position_whole_read_test.dag @@ -117,8 +117,8 @@ data vpw_two_lambdas_source: String = "module v2.test.vpw_two_lambdas\n\nimport // once lowered to the literal's first token, dropping the literal and its `-> Bool`. data vpw_let_fn_literal_source: String = "module v2.test.vpw_let_fn_literal\n\nimport std.types { Bool }\n\nfn vpw_probe(x: Bool) -> Bool {\n let g = fn(y) -> Bool { y }\n x\n}\n" -// A FIELD PROJECTION ON A CALL RESULT (warm-bear-132's 02_parse c02590, parse_memo_recurrence_census: -// `acc + expr_parse_demand_fold(expr: p.expression).discarding_sites`), once as an operator operand +// A FIELD PROJECTION ON A CALL RESULT (warm-bear-132's 02_parse c02590, in a parse census since +// deleted with the parse memo: `acc + expr_parse_demand_fold(expr: p.expression).discarding_sites`), once as an operator operand // inside a fn literal -- the specimen -- and once as a two-step projection in fn-body value position. // Before the chain door lowered a call head, the first refused its module operator_operand_unread. data vpw_call_projection_source: String = "module v2.test.vpw_call_projection\n\nimport std.types { Bool }\n\nfn vpw_probe(p: Bool) -> Bool { vpw_fold(f: fn(acc, q) { acc + vpw_demand(e: q.expression).sites }) }\n\nfn vpw_probe_chain(p: Bool) -> Bool { vpw_demand_two(e: p).outer.inner }\n" diff --git a/src/v2/test/claim/namespace_xl0/value_read_refusal_test.dag b/src/v2/test/claim/namespace_xl0/value_read_refusal_test.dag index b7178591e51..02ffbdd0b18 100644 --- a/src/v2/test/claim/namespace_xl0/value_read_refusal_test.dag +++ b/src/v2/test/claim/namespace_xl0/value_read_refusal_test.dag @@ -2,9 +2,9 @@ module v2.test.claim.namespace_xl0.value_read_refusal import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.algebra { fold_list } import v2.std.diagnostic { Accepted, Diagnostic, NodeLocus, Rejected, diagnostics_fatal } import v2.std.grammar { node_atom_identity_optional } @@ -46,7 +46,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // // WHAT IS NOT CLAIMED HERE, AND WHY. The brief asked for a control that an arm missing `=>` still // refuses as match_arm_navigation_refused. That source does not reach lowering: the parser refuses -// it (parse_grammar_choice_overlap_residue), so no source text can drive the navigation arm from +// it, so no source text can drive the navigation arm from // this route, and a control that cannot go red on this route would be a decoration (DESIGN section // 4b). The navigation refusal survives only for a parse tree whose arm has no pattern or no `=>`. data vrr_header: String = "module mq.vrr\n\nimport std.types { Bool }\n\ntype VrrRecord { f: Bool }\n\n" @@ -142,11 +142,11 @@ fn vrr_observe_refusal(d: Diagnostic) -> VrrObserved { fn vrr_observe(source: String) -> VrrObserved { match dag_prepared_grammar() { Rejected { diagnostics: d } => vrr_observe_refusal(d: diagnostics_fatal(d: d)) - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => match tokenize(text: source, file: ^value_read_refusal_file, rules: dag_lex()) { Rejected { diagnostics: d } => vrr_observe_refusal(d: diagnostics_fatal(d: d)) Accepted { value: token_stream, diagnostics: _ } => - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: d } => vrr_observe_refusal(d: diagnostics_fatal(d: d)) Accepted { value: artifact, diagnostics: _ } => match normalize(parse_tree: artifact.tree) { diff --git a/src/v2/test/claim/native_cache_fusion_test.dag b/src/v2/test/claim/native_cache_fusion_test.dag deleted file mode 100644 index 0afc1a09d93..00000000000 --- a/src/v2/test/claim/native_cache_fusion_test.dag +++ /dev/null @@ -1,30 +0,0 @@ -module v2.test.claim.native_cache_fusion_test - -import v2.workflow.native_cache_fusion { - HostWarmObservation, - host_compile_skipped, - fusion_agreement_holds_for -} - -test fn witness_warm_marker_agrees_share() -> Bool { - fusion_agreement_holds_for( - o: HostWarmObservation { marker_exists: true, cold_control: false } - ) -} - -test fn witness_cold_marker_absent_agrees_recompute() -> Bool { - fusion_agreement_holds_for( - o: HostWarmObservation { marker_exists: false, cold_control: false } - ) -} - -test fn witness_cold_control_forces_recompute_agreement() -> Bool { - let skipped = host_compile_skipped( - o: HostWarmObservation { marker_exists: true, cold_control: true } - ) - skipped == false - && fusion_agreement_holds_for( - o: HostWarmObservation { marker_exists: true, cold_control: true } - ) -} - diff --git a/src/v2/test/claim/native_census/cascade_root_fan_out_test.dag b/src/v2/test/claim/native_census/cascade_root_fan_out_test.dag index f774715c27f..45ac2a4efd2 100644 --- a/src/v2/test/claim/native_census/cascade_root_fan_out_test.dag +++ b/src/v2/test/claim/native_census/cascade_root_fan_out_test.dag @@ -117,6 +117,7 @@ fn cascade_census(ingest: SourceRootIngest) -> CascadeCensus { cons: fn(acc, entry) { native_census_cause_groups_add_module( groups: acc, + closure: closure, entry: entry, step: native_census_module_resolution(context: context, refusal_index: refusal_index, entry: entry) ) diff --git a/src/v2/test/claim/native_census/infer_cascade_test.dag b/src/v2/test/claim/native_census/infer_cascade_test.dag new file mode 100644 index 00000000000..2877be8adb7 --- /dev/null +++ b/src/v2/test/claim/native_census/infer_cascade_test.dag @@ -0,0 +1,332 @@ +module v2.test.claim.native_census.infer_cascade + +import v2.compiler.compile { + NativeCensusCascadeMemberOf, + NativeCensusCascadeNone, + NativeCensusCascadeUnattributed, + NativeCensusCascadeRoot, + NativeCensusCauseFrontEnd, + NativeCensusCauseInfer, + NativeCensusCauseMember, + NativeCensusCauseResolve, + NativeCensusCauseStage, + NativeCensusClosureFanOutAtLeast, + NativeCensusClosureFanOutCounted, + NativeCensusClosureFanOutUnavailable, + NativeCensusClosureIndexRead, + NativeCensusInSubjectClosure, + NativeCensusModule, + NativeCensusOutsideSubjectClosure, + NativeCensusSubjectMembership, + NativeCensusSubjectMembershipUnavailable, + NativeImportTargetAttribution, + NativeImportTargetsAdmitted, + native_census_infer_cascade_of, + native_census_cause_group_rows, + native_census_cause_groups_add_infer_module, + native_census_cause_groups_from_file_refusals, + native_census_closure_index, + native_census_module_inference, + native_census_module_paths, + native_census_modules, + native_census_root_subject_membership, + native_census_roots_ranked, + native_census_subject_closure, + native_lane_file_refusal_index, + native_test_context_from_ingest +} +import v2.compiler.resolve { ClosureProviderRefusal } +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.std.algebra { fold_list, length, list_map } +import v2.std.integer { Int } +import std.algebra { Cons, Empty } +import v2.std.artifact { Artifact, SourceFile } +import extdeps.communication.medium { Lossless, Medium } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, CorrectionNotModeled, DeclarationLocus, Diagnostic, NonEmptyDiagnostics, Rejected, Unavailable } +import v2.std.collection { List, Map, empty_map, map_insert } +import std.optional { optional_present } +import v2.std.qualified_name { QualifiedName, qualified_name_from_dotted_string } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// CENSUS PART 2'S CASCADE CONTROLS, STRUCTURED BY THE DESIGN SECTION 3 WITNESS RULE. ONE inhabitance +// claim runs the real census-infer route over supplied source bytes: native_test_context_from_ingest, the +// census roster, the closure index, native_census_module_inference per module (resolve, then the real +// infer_and_discharge) and native_census_cause_groups_add_infer_module. Removing the infer cascade from +// that route reds it. Every other control supplies values to the decision it judges +// (native_census_infer_cascade_of, native_census_subject_closure, native_census_root_subject_membership) +// and runs no front end. +// +// THE EDGE THESE CONTROLS EXERCISE IS THE ONE v2 INFER HAS (v2.compiler.compile +// native_census_infer_cascade, superseding docs/plans/census-part-2-inference-design.md section 4 on +// this point). Provider b fails RESOLVE (an unbound name in a body beside the record). Importer a +// resolves, because b's declarations are indexed, and then fails INFER projecting that record, because +// a provider that never resolved contributes nothing to the closure index. a's chain carries b's +// resolve chain immediately ahead of its closure-miss fatal. + +data infer_b_unresolvable_source: String = "module v2.census_infer_b\n\ntype InferLeg {\n target: Int\n}\n\nfn census_infer_b_broken() -> Int {\n no_such_name_anywhere\n}\n" + +data infer_a_source: String = "module v2.census_infer_a\n\nimport v2.census_infer_b { InferLeg }\n\nfn census_infer_a_read(l: InferLeg) -> Int {\n l.target\n}\n" + +fn infer_read(source: String, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: unit, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +fn infer_ingest(b_source: String) -> SourceRootIngest { + [ + infer_read(source: infer_a_source, unit: ^census_infer_a_cu, path: "dag/census_infer_a.dag"), + infer_read(source: b_source, unit: ^census_infer_b_cu, path: "dag/census_infer_b.dag") + ] +} + +// WHAT THE CENSUS REPORTED, reduced to what the claims read. `walls` counts every member that is NOT a +// cascade member, so a census that filed a's infer refusal as a root of its own is counted there. +type InferCensus + = InferCensusContextRefused + | InferCensusReported { + walls: Int + roots: List + a_members: List + } + +fn infer_census(ingest: SourceRootIngest) -> InferCensus { + match native_test_context_from_ingest(ingest: ingest) { + Rejected { diagnostics: _ } => InferCensusContextRefused + Accepted { value: context, diagnostics: _ } => + { + let refusal_index = native_lane_file_refusal_index(refusals: context.file_refusals) + let modules = native_census_modules(ingest: ingest) + let closure = native_census_closure_index(context: context, refusal_index: refusal_index, modules: modules) + let module_paths = native_census_module_paths(modules: modules) + let groups = fold_list( + xs: modules, + empty: native_census_cause_groups_from_file_refusals(refusals: context.file_refusals, closure: closure, resolves: true), + cons: fn(acc, entry) { + native_census_cause_groups_add_infer_module( + groups: acc, + closure: closure, + module_paths: module_paths, + entry: entry, + step: native_census_module_inference(context: context, refusal_index: refusal_index, entry: entry) + ) + } + ) + let rows = native_census_cause_group_rows(groups: groups) + let roots = native_census_roots_ranked(rows: rows) + InferCensusReported { + walls: fold_list(xs: rows, empty: 0, cons: fn(n, g) { + fold_list(xs: g.members, empty: n, cons: fn(k, m) { + match m.cascade { + NativeCensusCascadeMemberOf { root: _, root_reason: _ } => k + NativeCensusCascadeNone => k + 1 + NativeCensusCascadeUnattributed { reason: _ } => k + 1 + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => k + 1 + } + }) + }), + roots: roots, + a_members: fold_list(xs: rows, empty: Empty, cons: fn(acc, g) { + fold_list(xs: g.members, empty: acc, cons: fn(ms, m) { + if m.path == ^census_infer_a_cu { Cons { head: m, tail: ms } } else { ms } + }) + }) + } + } + } +} + +// THE ONE REAL-ROUTE PRODUCER, read by the one inhabitance claim below. It is a single-claim fill-debt +// member of v2.workflow.floor_pure_producer_share floor_single_claim_fill_debt: assemble, resolve and +// infer of a two-module fixture is more than one claim's budget. +fn infer_cascade_census() -> InferCensus { infer_census(ingest: infer_ingest(b_source: infer_b_unresolvable_source)) } + +fn infer_stage_is(stage: NativeCensusCauseStage, infer: Bool) -> Bool { + match stage { + NativeCensusCauseInfer => infer + NativeCensusCauseResolve => !infer + NativeCensusCauseFrontEnd => false + } +} + +// THE ONE ROOT IS b, AT THE GIVEN STAGE, WITH AN EXACT CLOSURE FAN-OUT. +fn infer_single_root_is_b(roots: List, infer: Bool, fan_out: Int) -> Bool { + match roots { + Empty => false + Cons { head: m, tail: rest } => + length(xs: rest) == 0 && m.path == ^census_infer_b_cu && infer_stage_is(stage: m.stage, infer: infer) && match m.cascade { + NativeCensusCascadeRoot { closure_fan_out: f, resolve_refused_importers: _ } => + match f { + NativeCensusClosureFanOutCounted { modules: n } => n == fan_out + NativeCensusClosureFanOutAtLeast { modules: _, unread_refused_files: _ } => false + NativeCensusClosureFanOutUnavailable { reason: _ } => false + } + NativeCensusCascadeNone => false + NativeCensusCascadeMemberOf { root: _, root_reason: _ } => false + NativeCensusCascadeUnattributed { reason: _ } => false + } + } +} + +// THE INHABITANCE CLAIM (DESIGN section 3 pairing): THE CASCADE OVER THE REAL ROUTE. b refuses resolve, a refuses infer only because b's record never entered the +// closure index. Expected: ONE wall, b, a Resolve-stage root with closure fan-out 1 ({a}). a has exactly +// one member, at the Infer stage, a cascade member of b. RED if a's infer refusal is filed as its own root +// (walls 2), if a's row is stamped Resolve (the stage conflation design section 3 forbids), if b carries no +// fan-out, or if the infer call is gone (a has no member at all). +test fn census_infer_provider_cascade_is_one_root_with_fan_out_one_holds() -> Bool { + match infer_cascade_census() { + InferCensusContextRefused => false + InferCensusReported { walls: w, roots: roots, a_members: a } => + w == 1 + && infer_single_root_is_b(roots: roots, infer: false, fan_out: 1) + && match a { + Empty => false + Cons { head: m, tail: rest } => + length(xs: rest) == 0 && infer_stage_is(stage: m.stage, infer: true) && match m.cascade { + NativeCensusCascadeMemberOf { root: r, root_reason: _ } => r == ^census_infer_b_cu + NativeCensusCascadeNone => false + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => false + NativeCensusCascadeUnattributed { reason: _ } => false + } + } + } +} + + +// ── supplied-value claims: native_census_infer_cascade_of and the subject closure, at their interface ── +// +// The real route above establishes that infer emits the chain shape these claims supply: a provider's +// resolve chain carried immediately before the closure-miss fatal, with the module's own advisories +// ahead of it. Each claim below hands the decision that shape directly, so none re-runs a front end. + +fn sv_diagnostic(reason: Symbol) -> Diagnostic { + Diagnostic { reason: reason, at: DeclarationLocus { declaration: [] }, correction: Unavailable { reason: CorrectionNotModeled } } +} + +fn sv_chain(reasons: List) -> NonEmptyDiagnostics { + match reasons { + Empty => NonEmptyDiagnostics { head: sv_diagnostic(reason: ^sv_empty), tail: [] } + Cons { head: h, tail: t } => NonEmptyDiagnostics { head: sv_diagnostic(reason: h), tail: list_map(xs: t, f: fn(r) { sv_diagnostic(reason: r) }) } + } +} + +fn sv_provider(module: String, reasons: List) -> ClosureProviderRefusal { + ClosureProviderRefusal { module: qualified_name_from_dotted_string(dotted: module), refusal: sv_chain(reasons: reasons) } +} + +data sv_paths: Map = map_insert(map_insert(empty_map(), "v2.sv_b", ^sv_b_cu), "v2.sv_c", ^sv_c_cu) + +fn sv_admitted(provider: QualifiedName) -> NativeImportTargetAttribution { NativeImportTargetsAdmitted } + +// b's resolve chain [own advisory, unbound], carried before the miss, behind a's own advisory. +data sv_cascade_chain: List = [^sv_a_advisory, ^sv_b_advisory, ^resolve_reason_unbound_symbol, ^infer_reason_projection_receiver_declaration_unavailable] + +fn sv_cascade_is_member_of(cascade: NativeCensusCascade, root: Symbol) -> Bool { + match cascade { + NativeCensusCascadeMemberOf { root: r, root_reason: _ } => r == root + NativeCensusCascadeNone => false + NativeCensusCascadeUnattributed { reason: _ } => false + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => false + } +} + +fn sv_cascade_is_own_root(cascade: NativeCensusCascade) -> Bool { + match cascade { + NativeCensusCascadeNone => true + NativeCensusCascadeMemberOf { root: _, root_reason: _ } => false + NativeCensusCascadeUnattributed { reason: _ } => false + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => false + } +} + +fn sv_cascade_is_unattributed(cascade: NativeCensusCascade) -> Bool { + match cascade { + NativeCensusCascadeUnattributed { reason: _ } => true + NativeCensusCascadeNone => false + NativeCensusCascadeMemberOf { root: _, root_reason: _ } => false + NativeCensusCascadeRoot { closure_fan_out: _, resolve_refused_importers: _ } => false + } +} + +// THE PROVIDER CHAIN AT THE END, NOT THE HEAD. RED under a prefix match (a's own advisory leads), and RED +// if the module's own advisories were required to be absent. +test fn census_infer_cascade_matches_the_provider_chain_ahead_of_the_miss_holds() -> Bool { + sv_cascade_is_member_of( + cascade: native_census_infer_cascade_of(module_paths: sv_paths, providers: [sv_provider(module: "v2.sv_b", reasons: [^sv_b_advisory, ^resolve_reason_unbound_symbol])], chain: sv_chain(reasons: sv_cascade_chain), attribution: sv_admitted), + root: ^sv_b_cu + ) +} + +// THE NEGATIVE: an infer refusal whose fatal is not the closure miss is its own root even with a refused +// provider in the tree, and so is a miss no refused provider leads. RED if any refused provider in the +// tree were blamed (the infer->infer edge v2 infer does not have). +test fn census_infer_refusal_not_led_by_a_provider_is_its_own_root_holds() -> Bool { + let providers = [sv_provider(module: "v2.sv_b", reasons: [^resolve_reason_unbound_symbol])] + sv_cascade_is_own_root(cascade: native_census_infer_cascade_of(module_paths: sv_paths, providers: providers, chain: sv_chain(reasons: [^sv_a_advisory, ^infer_reason_projection_receiver_declares_no_fields]), attribution: sv_admitted)) + && sv_cascade_is_own_root(cascade: native_census_infer_cascade_of(module_paths: sv_paths, providers: [], chain: sv_chain(reasons: sv_cascade_chain), attribution: sv_admitted)) +} + +// TWO PROVIDERS WHOSE CHAINS BOTH MATCH: Unattributed, never the first. RED if the fold picked one. +test fn census_infer_cascade_two_matching_providers_is_unattributed_holds() -> Bool { + let providers = [ + sv_provider(module: "v2.sv_b", reasons: [^resolve_reason_unbound_symbol]), + sv_provider(module: "v2.sv_c", reasons: [^resolve_reason_unbound_symbol]) + ] + sv_cascade_is_unattributed(cascade: native_census_infer_cascade_of(module_paths: sv_paths, providers: providers, chain: sv_chain(reasons: [^resolve_reason_unbound_symbol, ^infer_reason_projection_receiver_declaration_unavailable]), attribution: sv_admitted)) +} + +// THE SUBJECT CLOSURE OVER A SUPPLIED IMPORT GRAPH: subject s imports a, a imports b; u imports nothing +// and nothing imports u. b is inside, u outside. RED if every root were marked inside, or if the walk +// followed importers instead of imports (b would be outside). +fn sv_member(module: String) -> NativeCensusCauseMember { + NativeCensusCauseMember { + stage: NativeCensusCauseInfer, + path: ^sv_root, + module: optional_present(value: qualified_name_from_dotted_string(dotted: module)), + head_reason: ^sv_head, + cascade: NativeCensusCascadeNone + } +} + +fn sv_membership_is_inside(m: NativeCensusSubjectMembership) -> Bool { + match m { + NativeCensusInSubjectClosure => true + NativeCensusOutsideSubjectClosure => false + NativeCensusSubjectMembershipUnavailable { reason: _ } => false + } +} + +fn sv_membership_is_outside(m: NativeCensusSubjectMembership) -> Bool { + match m { + NativeCensusOutsideSubjectClosure => true + NativeCensusInSubjectClosure => false + NativeCensusSubjectMembershipUnavailable { reason: _ } => false + } +} + +test fn census_infer_roots_are_marked_against_the_subject_closure_holds() -> Bool { + let index = NativeCensusClosureIndexRead { + importers_of: map_insert(map_insert(empty_map(), "v2.sv_a", ["v2.sv_s"]), "v2.sv_b", ["v2.sv_a"]), + imports_of: map_insert(map_insert(empty_map(), "v2.sv_s", ["v2.sv_a"]), "v2.sv_a", ["v2.sv_b"]), + modules: 4, + refused_modules: 0 + } + let modules = [ + NativeCensusModule { module: "v2.sv_s", path: ^sv_s_cu }, + NativeCensusModule { module: "v2.sv_a", path: ^sv_a_cu }, + NativeCensusModule { module: "v2.sv_b", path: ^sv_b_cu }, + NativeCensusModule { module: "v2.sv_u", path: ^sv_u_cu } + ] + let subject = native_census_subject_closure(index: index, modules: modules, label: "//v2/sv_s:all") + sv_membership_is_inside(m: native_census_root_subject_membership(subject: subject, root: sv_member(module: "v2.sv_b"))) + && sv_membership_is_outside(m: native_census_root_subject_membership(subject: subject, root: sv_member(module: "v2.sv_u"))) +} diff --git a/src/v2/test/claim/native_census/whole_tree_census_resolve_test.dag b/src/v2/test/claim/native_census/whole_tree_census_resolve_test.dag index f24f3580f31..0e1baa26fdb 100644 --- a/src/v2/test/claim/native_census/whole_tree_census_resolve_test.dag +++ b/src/v2/test/claim/native_census/whole_tree_census_resolve_test.dag @@ -520,7 +520,7 @@ fn census_supplied_file_refusal(path: Symbol, fatal: Symbol) -> NativeTestFileRe path: path, module: optional_absent(), chain: FreeSemigroup { - head: FileRefusalLink { reason: ^parse_grammar_choice_overlap_residue, at: FileRefusalAtWholeFile }, + head: FileRefusalLink { reason: ^tokenize_lex_e1_unrecognized_char, at: FileRefusalAtWholeFile }, tail: [FileRefusalLink { reason: fatal, at: FileRefusalAtWholeFile }] } } @@ -541,6 +541,6 @@ test fn census_file_refusals_group_under_fatal_not_shared_head_holds() -> Bool { length(xs: listed) == 2 && census_cause_group_of(groups: listed, reason: ^tokenize_lex_e0_source_not_exhausted) == 2 && census_cause_group_of(groups: listed, reason: ^parse_unexpected_token) == 1 - && census_cause_group_of(groups: listed, reason: ^parse_grammar_choice_overlap_residue) == 0 + && census_cause_group_of(groups: listed, reason: ^tokenize_lex_e1_unrecognized_char) == 0 && census_first_group_reason(groups: listed) == ^tokenize_lex_e0_source_not_exhausted } diff --git a/src/v2/test/claim/normalize/authored_marker_spelling_test.dag b/src/v2/test/claim/normalize/authored_marker_spelling_test.dag index 6995b238c35..ae1dbbbbbef 100644 --- a/src/v2/test/claim/normalize/authored_marker_spelling_test.dag +++ b/src/v2/test/claim/normalize/authored_marker_spelling_test.dag @@ -3,7 +3,8 @@ module v2.test.claim.normalize.authored_marker_spelling import std.types { List } import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import std.algebra { Cons, Empty } import v2.extdeps.languages.dag { dag_canonical_symbols, dag_language_model } import v2.std.diagnostic { Accepted, Rejected, diagnostics_fatal_reason } @@ -88,7 +89,7 @@ fn ams_names_member(nodes: List, name: Symbol) -> Bool { fn ams_verdict(src: String, authored: Symbol) -> MarkerSpellingVerdict { let lm = dag_language_model() - match tokenize(text: src, file: ^authored_marker_spelling_file, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^authored_marker_spelling_file, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => MarkerSpellingVerdict { outcome: ^ams_tokenize_refused, carries_authored_atom: false } Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { diff --git a/src/v2/test/claim/normalize/census_mode_test.dag b/src/v2/test/claim/normalize/census_mode_test.dag index fc5f61b9982..5558036b2d6 100644 --- a/src/v2/test/claim/normalize/census_mode_test.dag +++ b/src/v2/test/claim/normalize/census_mode_test.dag @@ -21,8 +21,8 @@ import v2.compiler.resolution_provenance { import v2.compiler.symbol_index_fill { symbol_index_fill_module_roots } import v2.test.parse.supplied_token_stream_support { supplied_stream_matches_tokenize, supplied_stream_parse } import v2.std.compilers.lexing { Token, TokenStream, token_stream_new } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.namespace_alias { ModuleBindingSource } import v2.std.symbol_index { SymbolIndex, empty_symbol_index, symbol_index_lookup } import v2.std.node_query { find_arrow_body_child } @@ -75,7 +75,7 @@ data census_member_qn: QualifiedName = qualified_name_from_dotted_string(dotted: fn census_parse(source: String) -> Outcome { bind_outcome( - o: tokenize(text: source, file: ^census_mode_probe, rules: dag_lex()), + o: tokenize_prepared(text: source, file: ^census_mode_probe, prepared: dag_prepared_lex()), f: fn(tokens) { supplied_stream_parse(tokens: tokens) } ) } diff --git a/src/v2/test/claim/parameter_reference_test.dag b/src/v2/test/claim/parameter_reference_test.dag index d0c101c78fd..64f21171d53 100644 --- a/src/v2/test/claim/parameter_reference_test.dag +++ b/src/v2/test/claim/parameter_reference_test.dag @@ -292,7 +292,8 @@ test fn pr_lambda_reference_grounds_through_recorded_binding_holds() -> Bool { // Lexical twin over the same name accepts, so the red is the kind, not the name. data pr_binder_site: NodeOccurrenceIdentity = OccurrenceMinted { id: OccurrenceId { value: 41 } } -data pr_lexical_binders: Map = map_insert(empty_map(), ^pr_x, LexicalBinding { binder_site: pr_binder_site, declared: Absent }) +data pr_lexical_binders: Map = map_insert(empty_map(), ^pr_x, LexicalBinding { binder_site: pr_binder_site, declared: Absent, + value: optional_absent() }) fn pr_ctx(kind: FrameKind) -> ResolveContext { let namespace = Namespace { diff --git a/src/v2/test/claim/parse/admit_callers_trailing_comma_test.dag b/src/v2/test/claim/parse/admit_callers_trailing_comma_test.dag index cc2b365fa9f..fd3688e48ce 100644 --- a/src/v2/test/claim/parse/admit_callers_trailing_comma_test.dag +++ b/src/v2/test/claim/parse/admit_callers_trailing_comma_test.dag @@ -1,9 +1,8 @@ module v2.test.claim.parse.admit_callers_trailing_comma import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node } @@ -24,11 +23,11 @@ fn admit_source(list: String) -> String { fn admit_parse(text: String) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: ^admit_trailing_comma_probe, rules: dag_lex()), + o: tokenize_prepared(text: text, file: ^admit_trailing_comma_probe, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } diff --git a/src/v2/test/claim/parse/alias_decl_parse.dag b/src/v2/test/claim/parse/alias_decl_parse.dag index 1d22e950785..3f93ec15a60 100644 --- a/src/v2/test/claim/parse/alias_decl_parse.dag +++ b/src/v2/test/claim/parse/alias_decl_parse.dag @@ -9,6 +9,7 @@ import std.optional { } import v2.std.diagnostic { Accepted, + Diagnostic, DeclarationLocus, InvariantPortLocus, NodeLocus, @@ -18,12 +19,26 @@ import v2.std.diagnostic { Textual, bind_outcome } -import v2.std.node { Node } -import v2.compiler.normalized_tree { NormalizedTree } +import std.algebra { Empty, FreeMonoid } +import v2.std.node { Node, symbol_lexeme } +import v2.std.grammar { node_atom_identity_optional } +import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_binding_sources } +import v2.compiler.name_resolve { Admission, ResolutionSubject, resolve_with_admission } +import v2.compiler.symbol_index_fill { symbol_index_fill_module_roots } +import v2.std.symbol_index { + LexicalAmbiguous, + LexicalHit, + LexicalUnbound, + SymbolIndex, + empty_symbol_index, + symbol_index_lexical_lookup, + symbol_index_lookup +} import v2.compiler.normalize { normalize } import v2.std.qualified_name { qualified_name_from_dotted_string } import v2.std.namespace_alias { AliasBindingRow } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.compiler.parse { parse, parse_module } import v2.extdeps.languages.dag { alias_binding_row_from_decl_node, @@ -36,10 +51,10 @@ data alias_decl_parse_source: String = "module v2.test.parse.alias_decl_probe\n\ fn alias_decl_parse_tree() -> Outcome { let lm = dag_language_model() bind_outcome( - o: tokenize( + o: tokenize_prepared( text: alias_decl_parse_source, file: ^alias_decl_parse_probe, - rules: lm.lex + prepared: dag_prepared_lex() ), f: fn(token_stream) { parse(tokens: token_stream, grammar: lm.grammar) @@ -95,10 +110,10 @@ fn alias_decl_parse_and_binding_row_holds() -> Bool { fn alias_decl_keyword_reserved_in_binding_position_red_control_holds() -> Bool { let lm = dag_language_model() match bind_outcome( - o: tokenize( + o: tokenize_prepared( text: "module probe\nalias alias = v2.std.node.Node\n", file: ^alias_decl_reserved_keyword_probe, - rules: lm.lex + prepared: dag_prepared_lex() ), f: fn(token_stream) { parse(tokens: token_stream, grammar: lm.grammar) @@ -112,12 +127,12 @@ fn alias_decl_keyword_reserved_in_binding_position_red_control_holds() -> Bool { data alias_decl_fill_parse_source: String = "module v2.test.parse.alias_decl_fill_probe\n\ntype AliasTargetType {}\n\nalias AliasBinding = v2.test.parse.alias_decl_fill_probe.AliasTargetType\n" // THE NORMALIZED ROUTE IS THE ONE EVERY PRODUCTION CALLER USES (v2.compiler.normalized_tree -// normalized_tree_roots_to_binding_sources into symbol_index_fill_module_roots), so the claim -// normalizes the real source rather than handing the fill a raw parse tree (on which nothing was -// indexed at all). -fn alias_decl_fill_normalized() -> Outcome { +// normalized_tree_roots_to_binding_sources into symbol_index_fill_module_roots), so every claim below +// normalizes real source and fills the index through that route rather than handing the fill a raw +// parse tree. +fn alias_decl_normalized(text: String, file: Symbol) -> Outcome { let lm = dag_language_model() - match tokenize(text: alias_decl_fill_parse_source, file: ^alias_decl_fill_probe, rules: lm.lex) { + match tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { @@ -127,30 +142,84 @@ fn alias_decl_fill_normalized() -> Outcome { } } -// THE DISCRIMINATING CONTROL FOR THE INTERIM REFUSAL (operator ruling via stern-bear-500, 2026-10-05). -// Measured at main 6296275 before the refusal: normalize ACCEPTED this source and the alias name -// resolved to its own grafted node, not AliasTargetType (lexical HIT, hit != target) -- a silent -// wrong answer. Normalize now refuses the declaration with -// body_lowering_reason_alias_binding_row_not_carried, located AT the alias declaration, so this claim -// was red on main and is green with the refusal. When alias rows are carried -// (gunbc.recurring_failure_mode alias_declaration_binding_row_not_carried), this claim returns to -// asserting that the lexical hit IS the target. -fn alias_decl_fill_lexical_lookup_holds() -> Bool { - match alias_decl_fill_normalized() { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: r } => - (r.head.reason == ^body_lowering_reason_alias_binding_row_not_carried) - && (match r.head.at { - NodeLocus { anchor: a } => - match parse_production_emitted_identity_optional(node: a.at) { - Present { value: id } => id == ^dag_surface_alias_decl - Absent => false - } - Textual { file: _, extent: _ } => false - SourcePortLocus { anchor: _, file: _, extent: _ } => false - InvariantPortLocus { anchor: _, invariant: _ } => false - DeclarationLocus { declaration: _ } => false - }) +fn alias_decl_index_of(trees: FreeMonoid) -> SymbolIndex { + symbol_index_fill_module_roots( + index: empty_symbol_index(), + roots: normalized_tree_roots_to_binding_sources(roots: trees) + ) +} + +// The lexical lookup of `name` at `position`, compared with the node the index holds at `target`. +fn alias_decl_lookup_hits_target(idx: SymbolIndex, position: String, name: Symbol, target: String) -> Bool { + match symbol_index_lexical_lookup(index: idx, position: qualified_name_from_dotted_string(dotted: position), name: name) { + LexicalHit { node: hit, path: _ } => + match symbol_index_lookup(index: idx, qualified_path: qualified_name_from_dotted_string(dotted: target)) { + Present { value: t } => hit == t + Absent => false + } + LexicalAmbiguous { candidates: _ } => false + LexicalUnbound => false } } +// AN ALIAS TO A TARGET NOTHING DECLARES BINDS NOTHING, AND THE REFERENCE THROUGH IT REFUSES, LOCATED. +// The row stays deferred at the fill's fixed point (v2.compiler.symbol_index_fill +// symbol_index_fill_bindings_to_fixed_point), so the alias name is UNBOUND -- never a hit on the +// alias's own node -- and resolution refuses the reference that wanted it, at the reference site +// (docs/plans/namespace-resolution-design.md section 13), exactly as it refuses an import of a +// name its module does not declare. +data alias_decl_unresolved_source: String = "module v2.test.parse.alias_decl_unresolved_probe\n\nalias Missing = v2.test.parse.alias_decl_unresolved_probe.Nowhere\n\nfn uses_missing(x: Missing) -> Missing = x\n" + +fn alias_decl_resolve_verdict(t: NormalizedTree, module: String) -> String { + match resolve_with_admission( + lm: dag_language_model(), + roots: [t], + admission: Admission { + subject: ResolutionSubject { name: qualified_name_from_dotted_string(dotted: module) }, + imports: Empty + } + ) { + Accepted { value: _, diagnostics: _ } => "ACCEPTED" + Rejected { diagnostics: d } => symbol_lexeme(sym: d.head.reason) + "@" + alias_decl_locus_lexeme(d: d.head) + } +} + +fn alias_decl_locus_lexeme(d: Diagnostic) -> String { + match d.at { + NodeLocus { anchor: a } => + match node_atom_identity_optional(node: a.at) { + Present { value: id } => symbol_lexeme(sym: id) + Absent => "node" + } + Textual { file: _, extent: _ } => "textual" + SourcePortLocus { anchor: _, file: _, extent: _ } => "source_port" + InvariantPortLocus { anchor: _, invariant: _ } => "invariant_port" + DeclarationLocus { declaration: _ } => "declaration" + } +} + +// AN ALIAS DISAMBIGUATES WHAT UNIQUE-ON-CHAIN REFUSES. Two imports bind X at the subject from two +// providers, so X is Ambiguous there (two declarations, one name -- refused, never first-wins). The +// alias names ONE of them under a name only it binds, so AX resolves to alias_amb_a.X: the fix menu of +// docs/plans/namespace-resolution-design.md section 13 ("qualify by containment path / introduce an +// alias / rename"), executed. The ambiguity on X itself is not hidden by the alias: X stays refused. +data alias_amb_a_source: String = "module v2.test.parse.alias_amb_a\n\ntype X {}\n" + +data alias_amb_b_source: String = "module v2.test.parse.alias_amb_b\n\ntype X {}\n" + +data alias_amb_subject_source: String = "module v2.test.parse.alias_amb_s\n\nimport v2.test.parse.alias_amb_a { X }\nimport v2.test.parse.alias_amb_b { X }\n\nalias AX = v2.test.parse.alias_amb_a.X\n" + +// THE LEGACY LONG-LANE ENTRY (gunbc.witness_deferral_freeze names its wrapper), kept as the +// conjunction of the per-PR route claims in test.claim.parse_test_alias_decl_carry, which are what execute. +fn alias_decl_fill_lexical_lookup_holds() -> Bool { + match alias_decl_normalized(text: alias_decl_fill_parse_source, file: ^alias_decl_fill_probe) { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + alias_decl_lookup_hits_target( + idx: alias_decl_index_of(trees: [t]), + position: "v2.test.parse.alias_decl_fill_probe", + name: ^AliasBinding, + target: "v2.test.parse.alias_decl_fill_probe.AliasTargetType" + ) + } +} diff --git a/src/v2/test/claim/parse/block_expr_as_binary_operand_parse_test.dag b/src/v2/test/claim/parse/block_expr_as_binary_operand_parse_test.dag index c9885ae2fbd..e0d7eab80e5 100644 --- a/src/v2/test/claim/parse/block_expr_as_binary_operand_parse_test.dag +++ b/src/v2/test/claim/parse/block_expr_as_binary_operand_parse_test.dag @@ -2,9 +2,8 @@ module v2.test.parse.block_expr_as_binary_operand_parse import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import std.optional { Absent, Optional, Present } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -44,14 +43,13 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn g_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/parse/choice_plan_test.dag b/src/v2/test/claim/parse/choice_plan_test.dag index 3010c248df2..d2c74fd0101 100644 --- a/src/v2/test/claim/parse/choice_plan_test.dag +++ b/src/v2/test/claim/parse/choice_plan_test.dag @@ -45,9 +45,8 @@ import v2.compiler.parse { import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.optional { Absent, Present } import v2.std.algebra { fold_list, length } -import v2.std.diagnostic { Accepted, Diagnostic, NonEmptyDiagnostics, None, Rejected, Some, invariant_port_locus } +import v2.std.diagnostic { Accepted, Rejected, declaration_locus, invariant_port_locus, locus_eq } import v2.std.node { Hash, Symbol, content_hash } -import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -78,13 +77,12 @@ fn pcp_choice_root(expr: GrammarExpr) -> GrammarRoot { } } -fn pcp_validate_accepts_without_overlap_residue(expr: GrammarExpr) -> Bool { +fn pcp_validate_accepts(expr: GrammarExpr) -> Bool { match grammar_validate_for_parse( root: pcp_choice_root(expr: expr), at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^pcp_grammar_contract) ) { - Accepted { value: _, diagnostics: None } => true - Accepted { value: _, diagnostics: Some { diagnostics: _ } } => false + Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } } @@ -134,7 +132,7 @@ fn pcp_observe(expr: GrammarExpr, tokens: List) -> ChoiceParseObservation ) { Rejected { diagnostics: _ } => ObservedRejected Accepted { value: validated, diagnostics: _ } => - let table = parse_table_for_production(validated: validated, all_tokens: tokens, materialization: Memoize) + let table = parse_table_for_production(validated: validated, all_tokens: tokens) match map_lookup(m: table.prepared_exprs, key: root.start) { Absent => ObservedRejected Present { value: prepared } => @@ -159,7 +157,7 @@ fn pcp_observe_root(root: GrammarRoot, expr: GrammarExpr, tokens: List) - tokens: tokens, all_tokens: tokens, root: root, - table: parse_table_empty(materialization: Memoize), + table: parse_table_empty(), prov: parse_prov_initial() ) ) @@ -196,7 +194,7 @@ fn oracle_parse_choice(left: GrammarExpr, right: GrammarExpr, tokens: List @@ -207,7 +205,7 @@ fn oracle_parse_choice(left: GrammarExpr, right: GrammarExpr, tokens: List @@ -287,7 +285,7 @@ test fn prepared_choice_nonterminal_alts_empty_table_agrees_with_ordered_oracle( tokens: tokens_b, all_tokens: tokens_b, root: root, - table: parse_table_empty(materialization: Memoize), + table: parse_table_empty(), prov: parse_prov_initial() ) { ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: _, prov: _, table: _ } => @@ -298,7 +296,7 @@ test fn prepared_choice_nonterminal_alts_empty_table_agrees_with_ordered_oracle( tokens: tokens_b, all_tokens: tokens_b, root: root, - table: parse_table_empty(materialization: Memoize), + table: parse_table_empty(), prov: parse_prov_initial() ) { ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: _, prov: _, table: _ } => @@ -316,7 +314,7 @@ test fn prepared_choice_one_nullable_alt_agrees_after_aa_rejects() -> Bool { let aa = seq(left: term(token_class: ^pcp_a), right: term(token_class: ^pcp_a)) let expr = Choice { left: aa, right: opt(element: term(token_class: ^pcp_b)) } let tokens_a = [pcp_token(token_class: ^pcp_a)] - pcp_validate_accepts_without_overlap_residue(expr: expr) + pcp_validate_accepts(expr: expr) && match pcp_choice_plan(expr: expr) { ChoiceByToken { on_token: _, on_other: _, on_end: _ } => pcp_observe(expr: expr, tokens: tokens_a) == oracle_parse_choice(left: aa, right: opt(element: term(token_class: ^pcp_b)), tokens: tokens_a) @@ -324,7 +322,7 @@ test fn prepared_choice_one_nullable_alt_agrees_after_aa_rejects() -> Bool { } } -// Two nullable siblings never reach the prepared runtime: validate refuses both_nullable residue. +// Two nullable siblings never reach the prepared runtime: validate refuses every overlap row, nullable or not. test fn prepared_choice_two_nullable_alts_validate_refuses() -> Bool { let aa = seq(left: term(token_class: ^pcp_a), right: term(token_class: ^pcp_a)) let both_nullable = Choice { @@ -389,14 +387,14 @@ test fn prepared_choice_stamp_split_agrees_with_ordered_oracle() -> Bool { } } -// LITERAL TERMINALS: DIFFERENT EXACT WORDS ELIMINATE A FIRST-OVERLAP RESIDUE THAT CLASS KEYS KEEP. +// LITERAL TERMINALS: DIFFERENT EXACT WORDS SEPARATE A PAIR THAT CLASS KEYS CANNOT. // // The comparison pair below is IDENTICAL once its leading word is erased: both alternatives are -// word . pcp_a. Keyed on (class, word) the two are FIRST-disjoint and validation reports no overlap -// residue; the control is the same pair with the words erased to the bare class, which validation -// must ACCEPT WITH the specific `^parse_grammar_choice_overlap_residue` diagnostic -- named, not -// inferred from the absence of a clean acceptance, so an unrelated rejection fails the control. -// What this establishes is the FIRST-overlap residue, which is a different fact from whether a +// word . pcp_a. Keyed on (class, word) the two are FIRST-disjoint and validation accepts; the +// control is the same pair with the words erased to the bare class, which validation must REFUSE +// with the specific `^parse_grammar_choice_ambiguity` diagnostic located at the owning production +// -- named, not inferred from the absence of an acceptance, so an unrelated rejection fails the +// control. What this establishes is the FIRST overlap, which is a different fact from whether a // deeper class lookahead could have separated some other pair. The unequal-suffix pair further // down is kept for a separate purpose: executing a parse through the second word's alternative. @@ -422,28 +420,15 @@ fn pcp_same_suffix_class_pair() -> GrammarExpr { } } -fn pcp_diagnostic_is_overlap_residue(d: Diagnostic) -> Bool { - d.reason == ^parse_grammar_choice_overlap_residue -} - -fn pcp_any_overlap_residue(ne: NonEmptyDiagnostics) -> Bool { - fold_list( - xs: ne.tail, - empty: pcp_diagnostic_is_overlap_residue(d: ne.head), - cons: fn(found, d) { found || pcp_diagnostic_is_overlap_residue(d: d) } - ) -} - -// Positive: the ACCEPTED arm carrying the overlap-residue diagnostic. Rejection, or acceptance with -// only unrelated diagnostics, or clean acceptance, all answer false. -fn pcp_validate_accepts_with_overlap_residue(expr: GrammarExpr) -> Bool { +fn pcp_validate_refuses_as_overlap_at_start(expr: GrammarExpr) -> Bool { match grammar_validate_for_parse( root: pcp_choice_root(expr: expr), at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^pcp_grammar_contract) ) { - Accepted { value: _, diagnostics: Some { diagnostics: ne } } => pcp_any_overlap_residue(ne: ne) - Accepted { value: _, diagnostics: None } => false - Rejected { diagnostics: _ } => false + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => + (d.head.reason == ^parse_grammar_choice_ambiguity) + && locus_eq(a: d.head.at, b: declaration_locus(declaration: Cons { head: ^pcp_start, tail: std.algebra.Empty })) } } @@ -454,12 +439,12 @@ fn pcp_word_pair() -> GrammarExpr { } } -test fn literal_words_sharing_a_class_prefix_validate_without_overlap_residue() -> Bool { - pcp_validate_accepts_without_overlap_residue(expr: pcp_same_suffix_word_pair()) +test fn literal_words_sharing_a_class_prefix_validate() -> Bool { + pcp_validate_accepts(expr: pcp_same_suffix_word_pair()) } -test fn the_same_pair_keyed_by_class_alone_leaves_overlap_residue() -> Bool { - pcp_validate_accepts_with_overlap_residue(expr: pcp_same_suffix_class_pair()) +test fn the_same_pair_keyed_by_class_alone_refuses_at_its_production() -> Bool { + pcp_validate_refuses_as_overlap_at_start(expr: pcp_same_suffix_class_pair()) } test fn the_class_bucket_of_a_word_pair_dispatches_on_the_word() -> Bool { diff --git a/src/v2/test/claim/parse/d1_declaration_grammar_parse_test.dag b/src/v2/test/claim/parse/d1_declaration_grammar_parse_test.dag index 93c8f4a4a10..ac5a80795d6 100644 --- a/src/v2/test/claim/parse/d1_declaration_grammar_parse_test.dag +++ b/src/v2/test/claim/parse/d1_declaration_grammar_parse_test.dag @@ -2,10 +2,9 @@ module v2.test.parse.d1_declaration_grammar_parse import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { - dag_lex, parse_subtree_find_production_captured, ParseSubtreeAbsent, ParseSubtreeFound, @@ -22,14 +21,13 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn d1_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/parse/d5_expression_grammar_parse_test.dag b/src/v2/test/claim/parse/d5_expression_grammar_parse_test.dag index 12dea97c443..708c2f9e8c7 100644 --- a/src/v2/test/claim/parse/d5_expression_grammar_parse_test.dag +++ b/src/v2/test/claim/parse/d5_expression_grammar_parse_test.dag @@ -1,8 +1,7 @@ module v2.test.parse.d5_expression_grammar_parse import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.grammar { node_atom_identity_optional } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -14,14 +13,13 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn d5_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/parse/data_keyword_as_value_test.dag b/src/v2/test/claim/parse/data_keyword_as_value_test.dag index fe8d6707aa1..3a4620c1377 100644 --- a/src/v2/test/claim/parse/data_keyword_as_value_test.dag +++ b/src/v2/test/claim/parse/data_keyword_as_value_test.dag @@ -2,9 +2,8 @@ module v2.test.claim.parse.data_keyword_as_value import v2.compiler.parse { parse_module_prepared } import v2.compiler.reference_conservation_admission { conservation_subject_of_text } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node } @@ -22,11 +21,11 @@ fn value_source(name: String) -> String { fn value_parse(text: String) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: ^data_keyword_value_probe, rules: dag_lex()), + o: tokenize_prepared(text: text, file: ^data_keyword_value_probe, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } diff --git a/src/v2/test/claim/parse/expression_bodied_continuation_test.dag b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag index ff90b1858fc..0f06e1a9d2b 100644 --- a/src/v2/test/claim/parse/expression_bodied_continuation_test.dag +++ b/src/v2/test/claim/parse/expression_bodied_continuation_test.dag @@ -3,18 +3,14 @@ module v2.test.claim.parse.expression_bodied_continuation import v2.compiler.normalize { normalize } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.parse { ParseArtifact, parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, - Diagnostics, - None, Outcome, Rejected, bind_outcome, - outcome_accepted, - outcome_diagnostics + outcome_accepted } import v2.std.node { Arrow, ArrowBodyEdge, Atom, Conj, Edge, Authored, Node, Positional, Symbol, TypeNode, core_edge_label } import v2.std.node_query { find_arrow_body_child } @@ -53,13 +49,12 @@ fn cont_parse(text: String, file: Symbol) -> Outcome { o: dag_prepared_grammar(), f: fn(prepared) { bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { bind_outcome( o: parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: outcome_diagnostics(o: dag_prepared_grammar()) + prepared: prepared ), f: fn(artifact) { cont_artifact_tree(artifact: artifact) } ) @@ -78,19 +73,6 @@ fn cont_artifact_tree(artifact: ParseArtifact) -> Outcome { outcome_accepted(value: artifact.tree) } -// THE RESIDUE THE SUBJECT THREADS EXPLICITLY IS THREADED HERE TOO, and that is not a formality: the -// prepared grammar DOES carry residue. An earlier version of this file passed None on the assumption -// that it did not, and the claim written to check that assumption went RED -- which is the only -// reason this control now forwards the real thing rather than parsing under a residue the seven -// never use. bind_outcome cannot supply it, because it merges diagnostics into the continuation -// instead of handing them back, so the residue is read with v2.std.diagnostic outcome_diagnostics. -test fn cont_the_prepared_grammar_carries_residue_holds() -> Bool { - match outcome_diagnostics(o: dag_prepared_grammar()) { - None => false - _ => true - } -} - fn cont_accepted(o: Outcome) -> Bool { match o { Accepted { value: _, diagnostics: _ } => true @@ -275,7 +257,7 @@ fn cont_root_itself_has_arrow_body(root: Node) -> Bool { // THE CONTROL PRODUCES WHAT THE SUBJECT PRODUCES, asserted rather than assumed. Everything above is // only evidence about the seven if this chain and theirs agree, and "I used the same functions" is -// not that evidence -- the residue defect this file already caught is exactly how they can diverge +// not that evidence -- a diagnostics-threading difference this file once caught is how they can diverge // while every row still looks green. So the two trees are compared directly, for each source, through // the subject's own entry point. // diff --git a/src/v2/test/claim/parse/expression_bodied_fn_decl_parse_test.dag b/src/v2/test/claim/parse/expression_bodied_fn_decl_parse_test.dag index 3107c142807..118b7de331e 100644 --- a/src/v2/test/claim/parse/expression_bodied_fn_decl_parse_test.dag +++ b/src/v2/test/claim/parse/expression_bodied_fn_decl_parse_test.dag @@ -2,9 +2,8 @@ module v2.test.parse.expression_bodied_fn_decl_parse import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, ByteRange, @@ -36,14 +35,13 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn g_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag b/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag index b0f69b8aee5..d492361cd8c 100644 --- a/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag +++ b/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag @@ -2,7 +2,8 @@ module v2.test.parse.g0_service_decl_parse_probe import v2.compiler.normalize { normalize } import v2.compiler.parse { ParseArtifact, parse_module } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_language_model } import v2.std.diagnostic { Accepted, Diagnostic, NonEmptyDiagnostics, Outcome, Rejected } import v2.std.language_model { LanguageModel } @@ -24,7 +25,7 @@ data probe_cached_lm: LanguageModel = dag_language_model() fn parse_src(src: String) -> Outcome { let lm = probe_cached_lm - match tokenize(text: src, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) { Accepted { value: ts, diagnostics: _ } => parse_module(tokens: ts, grammar: lm.grammar) Rejected { diagnostics: d } => Rejected { diagnostics: d } } @@ -50,7 +51,7 @@ fn smallest_service_parsed() -> Outcome { // stage is pinned and a fixture that dies in the lexer reds instead of flattering the claim. fn refuses_at_parse(src: String) -> Bool { let lm = probe_cached_lm - match tokenize(text: src, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) { Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { Accepted { value: _, diagnostics: _ } => false diff --git a/src/v2/test/claim/parse/go.dag b/src/v2/test/claim/parse/go.dag deleted file mode 100644 index 6a716923c2a..00000000000 --- a/src/v2/test/claim/parse/go.dag +++ /dev/null @@ -1,41 +0,0 @@ -module v2.test.parse.go - -fn go_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^go_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^go_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn go_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn go_tokenize_and_parse() -> Outcome { - match tokenize( - text: go_source_text, - file: ^go_parse_file, - rules: go_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: go_grammar()) - } -} - -data claim_go_add_function_parses: TestClaim = EqualsClaim { - label: "Go corpus symmetry accepts the current add-function source through the Go lex/grammar", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: go_parse_bool_atom( - b: go_parse_accepted(out: go_tokenize_and_parse()) - ), - rhs: go_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/grammar_validation_test.dag b/src/v2/test/claim/parse/grammar_validation_test.dag index 59d80749888..27a9408599a 100644 --- a/src/v2/test/claim/parse/grammar_validation_test.dag +++ b/src/v2/test/claim/parse/grammar_validation_test.dag @@ -28,7 +28,7 @@ import v2.compiler.parse { parse_skip_to_sync, } import v2.std.witness { Holds, Violates } -import v2.std.diagnostic { Accepted, Diagnostics, None, Outcome, Rejected, Some, invariant_port_locus } +import v2.std.diagnostic { Accepted, Outcome, Rejected, declaration_locus, invariant_port_locus, locus_eq } import std.algebra { Cons, Empty } import v2.std.node { Atom, Node, Symbol, TypeNode } import v2.std.verification { @@ -172,18 +172,20 @@ data validation_disjoint_choice_root: GrammarRoot = GrammarRoot { sync_tokens: grammar_empty_sync_tokens() } -fn validation_overlap_choice_validate_accepts_with_residue() -> Bool { +// An overlapping choice is REFUSED at validation, located at the production that owns it. The +// single-overlap control is enrolled on the merge path in test.claim.parse_test_grammar_choice_overlap; +// this one holds the same wall for an overlap in the TAIL of a three-way choice. +fn validation_refuses_overlap_at_production(root: GrammarRoot, production: Symbol) -> Bool { let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_validation_grammar_contract) - match grammar_validate_for_parse(root: validation_ambiguous_choice_root, at: at) { - Accepted { value: _, diagnostics: d } => - match d { - Some { diagnostics: residue } => residue.head.reason == ^parse_grammar_choice_overlap_residue - None => false - } - Rejected { diagnostics: _ } => false + match grammar_validate_for_parse(root: root, at: at) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => + (d.head.reason == ^parse_grammar_choice_ambiguity) + && locus_eq(a: d.head.at, b: declaration_locus(declaration: Cons { head: production, tail: Empty })) } } + data validation_nullable_ambiguous_choice_root: GrammarRoot = GrammarRoot { start: ^validation_start, productions: Cons { @@ -254,28 +256,8 @@ test fn validation_three_alt_inner_nullable_pair_rejects() -> Bool { } } -test fn validation_three_alt_tail_overlap_accepts_with_residue() -> Bool { - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_validation_grammar_contract) - match grammar_validate_for_parse(root: validation_three_alt_tail_overlap_root, at: at) { - Accepted { value: _, diagnostics: d } => - match d { - Some { diagnostics: residue } => residue.head.reason == ^parse_grammar_choice_overlap_residue - None => false - } - Rejected { diagnostics: _ } => false - } -} - -fn validation_disjoint_choice_residue_is_none() -> Bool { - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^grammar_validation_grammar_contract) - match grammar_validate_for_parse(root: validation_disjoint_choice_root, at: at) { - Accepted { value: _, diagnostics: d } => - match d { - None => true - Some { diagnostics: _ } => false - } - Rejected { diagnostics: _ } => false - } +test fn validation_three_alt_tail_overlap_refuses_at_production() -> Bool { + validation_refuses_overlap_at_production(root: validation_three_alt_tail_overlap_root, production: ^validation_start) } fn validation_disjoint_choice_validate_accepts() -> Bool { @@ -430,14 +412,6 @@ data claim_disjoint_choice_not_ambiguous: TestClaim = EqualsClaim { classification: TestClassification { tier: Tier1, layer: Unit } } -data claim_validate_accepts_overlap_with_counted_residue: TestClaim = EqualsClaim { - label: "grammar_validate_for_parse accepts non-nullable FIRST-overlap OrderedBacktrack as counted typed residue (reason parse_grammar_choice_overlap_residue); the roster is overlap/both-nullable rows, not every backtrack node", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: validation_bool_atom(b: validation_overlap_choice_validate_accepts_with_residue()), - rhs: validation_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Unit } -} - data claim_validate_rejects_nullable_choice_ambiguity: TestClaim = EqualsClaim { label: "grammar_validate_for_parse stays fail-closed on genuine ambiguity: both-nullable Choice arms sharing a FIRST terminal are Rejected", anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), @@ -446,14 +420,6 @@ data claim_validate_rejects_nullable_choice_ambiguity: TestClaim = EqualsClaim { classification: TestClassification { tier: Tier1, layer: Unit } } -data claim_disjoint_choice_validate_residue_none: TestClaim = EqualsClaim { - label: "disjoint-FIRST Choice validates with zero residue diagnostics (residue channel discriminates overlap from clean)", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: validation_bool_atom(b: validation_disjoint_choice_residue_is_none()), - rhs: validation_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Unit } -} - data claim_validate_accepts_disjoint_choice: TestClaim = EqualsClaim { label: "grammar_validate_for_parse accepts grammars whose Choice branches have disjoint FIRST sets", anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), diff --git a/src/v2/test/claim/parse/keyword_value_name_parse_test.dag b/src/v2/test/claim/parse/keyword_value_name_parse_test.dag index d74b20fd9ce..c45ab0f29d2 100644 --- a/src/v2/test/claim/parse/keyword_value_name_parse_test.dag +++ b/src/v2/test/claim/parse/keyword_value_name_parse_test.dag @@ -6,7 +6,8 @@ import v2.std.compilers.lexing { StreamLayout, Token, TokenStream, stream_layout import v2.test.parse.expression_bodied_fn_decl_parse { g_fatal_at, g_tokenize_parse } import v2.test.parse.supplied_token_stream_support { supplied_stream_parse } import v2.std.algebra { Cons, Empty } -import v2.std.collection { List } +import v2.std.collection { List, Map, map_lookup } +import v2.std.integer { Int } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.node { Symbol } import std.optional { Absent, Optional, Present } @@ -222,7 +223,7 @@ test fn kvn_contextual_pattern_binder_control_holds() -> Bool { // EACH SUPPLIED STREAM IS PROVEN TO BE THE TOKENIZER'S OUTPUT, POSITIONS AND LAYOUT INCLUDED. The located // claims assert byte ranges and the statement claims depend on line breaks, so the shared class-and-lexeme // projection is not enough here: kvn_stream_matches_tokenize compares every token's class, lexeme, start and -// end, and the recorded layout's digest, against the real tokenizer over the same source text. +// end, and the recorded layout's line breaks, against the real tokenizer over the same source text. fn kvn_tokens_agree(left: List, right: List) -> Bool { match left { Empty => @@ -246,11 +247,20 @@ fn kvn_layouts_agree(left: Optional, right: Optional Present { value: l } => match right { Absent => false - Present { value: r } => l.digest == r.digest + Present { value: r } => kvn_line_breaks_agree(left: l.line_break_before, right: r.line_break_before) } } } +// The same recorded gaps on both sides: equal entry counts, and every left entry found on the right +// with the same gap start. +fn kvn_line_breaks_agree(left: Map, right: Map) -> Bool { + ((map_keys(left) |> count) == (map_keys(right) |> count)) + && (map_keys(left) |> fold(init: true, f: (acc, k) => + acc && (map_lookup(m: right, key: k) == map_lookup(m: left, key: k)) + )) +} + fn kvn_stream_matches_tokenize(text: String, file: Symbol, supplied: TokenStream) -> Bool { match tokenize(text: text, file: file, rules: dag_lex()) { Rejected { diagnostics: _ } => false diff --git a/src/v2/test/claim/parse/kotlin.dag b/src/v2/test/claim/parse/kotlin.dag deleted file mode 100644 index 1438086e3a6..00000000000 --- a/src/v2/test/claim/parse/kotlin.dag +++ /dev/null @@ -1,43 +0,0 @@ -module v2.test.parse.kotlin - -data kotlin_extended_fn_source: String = "fun choose(x: Int): Int { val y: Int = x + 1; if (y > 0) { return y }; return 0 }" - -fn kotlin_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^kotlin_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^kotlin_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn kotlin_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn kotlin_tokenize_and_parse() -> Outcome { - match tokenize( - text: kotlin_extended_fn_source, - file: ^kotlin_parse_file, - rules: kotlin_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: kotlin_grammar()) - } -} - -data claim_kotlin_extended_function_parses: TestClaim = EqualsClaim { - label: "Kotlin lex/grammar accepts an extended function with val, if, return, and binary expressions", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: kotlin_parse_bool_atom( - b: kotlin_parse_accepted(out: kotlin_tokenize_and_parse()) - ), - rhs: kotlin_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/layout_line_break_test.dag b/src/v2/test/claim/parse/layout_line_break_test.dag index 1d6321d917a..3099ec141dc 100644 --- a/src/v2/test/claim/parse/layout_line_break_test.dag +++ b/src/v2/test/claim/parse/layout_line_break_test.dag @@ -1,6 +1,6 @@ module v2.test.parse.layout_line_break -import v2.compiler.parse { ParseArtifact, parse_module, parse_module_prepared, parse_token_stream_digest, parse_token_stream_layout_digest } +import v2.compiler.parse { ParseArtifact, parse_module, parse_module_prepared } import v2.compiler.program_assembly { dag_prepared_grammar } import v2.compiler.tokenize { tokenize } import v2.extdeps.languages.dag { dag_lex } @@ -72,7 +72,7 @@ data same_line_text: String = "x - 1" data next_line_text: String = "x\n- 1" fn lexed(text: String, file: Symbol) -> Outcome { - tokenize(text: text, file: file, rules: dag_lex()) + tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()) } // The gap before the second token (`-`): from the end of `x` to the start of `-`. @@ -156,8 +156,8 @@ data identity_module_text: String = "module probe.layout_identity\n\ndata d: Int fn identity_parse(stream: TokenStream) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => - parse_module_prepared(tokens: stream, prepared: prepared, validation_residue: residue) + Accepted { value: prepared, diagnostics: _ } => + parse_module_prepared(tokens: stream, prepared: prepared) } } @@ -272,25 +272,6 @@ test fn a_layout_question_without_source_refuses_holds() -> Bool { } } -// THE MEMO CANNOT SERVE ONE LAYOUT'S ANSWER TO THE OTHER. `x - 1` and `x` newline `- 1` lex to the -// same tokens at the same octets, so a stream digest over tokens alone would key them together and a -// warm memo entry could replay a same-line acceptance for a next-line refusal. The layout (its line-break-preceded starts) is part of -// the stream's digest (v2.compiler.parse parse_token_stream_layout_digest): same tokens, different -// digest; and the token-only digest, which is what a sourceless stream keys by, is unchanged. -test fn same_tokens_different_layout_have_different_stream_digests_holds() -> Bool { - match lexed(text: same_line_text, file: ^layout_digest) { - Rejected { diagnostics: _ } => false - Accepted { value: same, diagnostics: _ } => - match lexed(text: next_line_text, file: ^layout_digest) { - Rejected { diagnostics: _ } => false - Accepted { value: next, diagnostics: _ } => - (parse_token_stream_digest(all_tokens: token_stream_all(stream: same)) == parse_token_stream_digest(all_tokens: token_stream_all(stream: next))) - && (parse_token_stream_layout_digest(all_tokens: token_stream_all(stream: same), layout: token_stream_layout(stream: same)) - != parse_token_stream_layout_digest(all_tokens: token_stream_all(stream: next), layout: token_stream_layout(stream: next))) - } - } -} - // A LINE FEED INSIDE A TOKEN IS NOT LAYOUT. The layout walk counts a line feed only at or after the // previous token's end, so a multi-line string literal between two tokens on one line leaves the next // token's gap line-break-free; only the gap after the string is asked about. diff --git a/src/v2/test/claim/parse/lean.dag b/src/v2/test/claim/parse/lean.dag deleted file mode 100644 index bae662517bc..00000000000 --- a/src/v2/test/claim/parse/lean.dag +++ /dev/null @@ -1,41 +0,0 @@ -module v2.test.parse.lean - -fn lean_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^lean_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^lean_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn lean_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn lean_tokenize_and_parse() -> Outcome { - match tokenize( - text: lean_source_text, - file: ^lean_parse_fixture_file, - rules: lean_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: lean_grammar()) - } -} - -data claim_lean_source_parses: TestClaim = EqualsClaim { - label: "Lean lex/grammar accepts the PROOF-1 def+termination_by fixture via tokenize+parse", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: lean_parse_bool_atom( - b: lean_parse_accepted(out: lean_tokenize_and_parse()) - ), - rhs: lean_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/list_literal_parse_cost_test.dag b/src/v2/test/claim/parse/list_literal_parse_cost_test.dag index d68f1da467d..e6895e0e48d 100644 --- a/src/v2/test/claim/parse/list_literal_parse_cost_test.dag +++ b/src/v2/test/claim/parse/list_literal_parse_cost_test.dag @@ -100,8 +100,8 @@ fn lpc_text(n: Int) -> String { fn lpc_parses(n: Int) -> Bool { match dag_prepared_grammar() { Rejected { diagnostics: _ } => false - Accepted { value: prepared, diagnostics: residue } => - match parse_module_prepared(tokens: lpc_stream(n: n), prepared: prepared, validation_residue: residue) { + Accepted { value: prepared, diagnostics: _ } => + match parse_module_prepared(tokens: lpc_stream(n: n), prepared: prepared) { Rejected { diagnostics: _ } => false Accepted { value: _, diagnostics: _ } => true } diff --git a/src/v2/test/claim/parse/newline_dual_role_operator_parse_test.dag b/src/v2/test/claim/parse/newline_dual_role_operator_parse_test.dag index d950913498e..8168564ab27 100644 --- a/src/v2/test/claim/parse/newline_dual_role_operator_parse_test.dag +++ b/src/v2/test/claim/parse/newline_dual_role_operator_parse_test.dag @@ -1,9 +1,9 @@ module v2.test.parse.newline_dual_role_operator_parse import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, dag_lex_rules, dag_target_model, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { dag_lex_rules, dag_target_model, parse_production_emitted_identity_optional } import v2.std.algebra { fold_list } import v2.std.collection { map_lookup } import v2.std.compilers.lexing { lex_rule_set_matching_rules, lex_rule_token_class_optional } @@ -41,9 +41,10 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // (v1.compiler.parse is_ambiguous_prefix_infix_newline_boundary). `-` is infix and prefix, and the // dag grammar has no statement terminator, so `x` then a line `- 1` is either one subtraction or two // statements; v2 read it as one, silently, and since block forms became operands (#12817) a block -// statement followed by a `-` line merged the same way. The dag grammar's additive continuation now -// refuses a line-break-preceded `-` with NewlineBeforeDualRoleOperator at the `-` -// (v2.extdeps.languages.dag dag_grammar_additive_expr_helper). Each side of the rule, through the +// statement followed by a `-` line merged the same way. The dag grammar's binary operator table now +// refuses a line-break-preceded `-` with NewlineBeforeDualRoleOperator at the `-`: its continuation +// guard (v2.extdeps.languages.dag dag_grammar_additive_newline_minus_guard) runs at every +// continuation point of dag_grammar_binary_expr_expr. Each side of the rule, through the // live prepared grammar on a lexed stream (which carries its layout): the next-line forms refuse at // the `-`, for a plain operand and for a block, and the same-line form parses. The // mutation that reds the refusals is test.claim.parse_test_newline_dual_role_operator_mutation. @@ -51,11 +52,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn n_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } @@ -103,13 +104,13 @@ test fn a_newline_minus_after_a_block_refuses_holds() -> Bool { n_refuses_at_minus(text: newline_minus_after_block_source, file: ^probe_nl_block, start: 58, end: 59) } -// THE ROUTE, NOT ONLY THE VERDICT. The newline refusal is a not-predicate guard in sequence before -// the additive operator, not a choice alternative beside subtraction (the 2026-10-02 zero-ambiguity +// THE ROUTE, NOT ONLY THE VERDICT. The newline refusal is a not-predicate guard run before the +// operator table reads an operator, not a choice alternative beside subtraction (the 2026-10-02 zero-ambiguity // ruling: both arms began with `-`, so only order kept them apart). A same-line `-` must therefore // reach the subtraction route: its tree holds no NEGATION -- a unary_expr node with another // unary_expr beneath it (`-` then the operand's own unary layer), as opposed to the single unary layer // every operand passes through, which subtraction's two operands each hold side by side under the -// binary node. A leading `-` takes the negation route and holds exactly one, which is the control +// one application node. A leading `-` takes the negation route and holds exactly one, which is the control // that the count discriminates the two routes at all. Each claim pays one parse. data leading_minus_source: String = "module probe.nl_neg\n\ndata d: Int = -1\n" diff --git a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag index e8ccc247aa7..e7163f16aa9 100644 --- a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag +++ b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag @@ -16,7 +16,8 @@ import v2.compiler.ingested_fixture_arrows { import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module } import v2.compiler.resolve { ResolvedTree, resolve } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_grammar_atom, dag_language_model_surface_empty_prelude, @@ -62,8 +63,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // consumer). Fast-lane tests stay in parse_binding_fidelity_test.dag. fn pbf_tokenize() -> Outcome { - let lm = dag_language_model() - tokenize(text: ingested_add_module_source, file: ^pbf_source_file, rules: lm.lex) + tokenize_prepared(text: ingested_add_module_source, file: ^pbf_source_file, prepared: dag_prepared_lex()) } fn pbf_parse_tokens(tokens: TokenStream) -> Outcome { diff --git a/src/v2/test/claim/parse/parse_g0_tokens_remain_furthest_locus_test.dag b/src/v2/test/claim/parse/parse_g0_tokens_remain_furthest_locus_test.dag index 1f4ea877ec4..7174f70c3d4 100644 --- a/src/v2/test/claim/parse/parse_g0_tokens_remain_furthest_locus_test.dag +++ b/src/v2/test/claim/parse/parse_g0_tokens_remain_furthest_locus_test.dag @@ -2,8 +2,8 @@ module v2.test.claim.parse.parse_g0_tokens_remain_furthest_locus import v2.test.parse.supplied_token_stream_support { supplied_stream_matches_tokenize, supplied_stream_parse } import v2.std.compilers.lexing { Token, TokenStream, token_stream_new } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, ByteRange, @@ -111,14 +111,10 @@ test fn control_stream_is_what_tokenize_emits() -> Bool { supplied_stream_matches_tokenize(text: control_source, file: ^furthest_locus_probe, supplied: control_stream()) } -// THE TREATMENT, AND THE LOCUS DOES NOT DEPEND ON CACHE WARMTH. The seed serves parse memo entries -// across parses of one token stream (v1_interpreter parse_table_memo_scope_and_key), so the second -// parse meets entries the first stored; each entry carries its own subparse's furthest record, and -// the served parse must land on the same token. The cold assertion alone is implied by this claim. -test fn tokens_remain_mid_item_locus_holds_cold_and_served() -> Bool { - let cold = tokens_remain_start(parsed: supplied_stream_parse(tokens: treatment_stream())) - let warm = tokens_remain_start(parsed: supplied_stream_parse(tokens: treatment_stream())) - cold == 24 && warm == 24 +// THE TREATMENT: the refusal lands on the token where the furthest rejected alternative stopped, +// inside the item, not at the item's start. +test fn tokens_remain_mid_item_locus_holds() -> Bool { + tokens_remain_start(parsed: supplied_stream_parse(tokens: treatment_stream())) == 24 } test fn tokens_remain_item_level_failure_stays_at_the_item_start() -> Bool { @@ -126,7 +122,7 @@ test fn tokens_remain_item_level_failure_stays_at_the_item_start() -> Bool { } fn rbracket_start(src: String) -> Int { - match tokenize(text: src, file: ^furthest_locus_probe, rules: dag_lex()) { + match tokenize_prepared(text: src, file: ^furthest_locus_probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => -1 Accepted { value: ts, diagnostics: _ } => fold(ts.all, init: -1, f: fn(acc, t) { if t.lexeme == "]" { t.start } else { acc } }) diff --git a/src/v2/test/claim/parse/bare_node_statement_route_test.dag b/src/v2/test/claim/parse/parse_test_bare_node_statement_route_test.dag similarity index 90% rename from src/v2/test/claim/parse/bare_node_statement_route_test.dag rename to src/v2/test/claim/parse/parse_test_bare_node_statement_route_test.dag index 419fe139600..04191ca37ea 100644 --- a/src/v2/test/claim/parse/bare_node_statement_route_test.dag +++ b/src/v2/test/claim/parse/parse_test_bare_node_statement_route_test.dag @@ -1,9 +1,9 @@ -module v2.test.parse.bare_node_statement_route +module test.claim.parse_test_bare_node_statement_route import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.diagnostic { Accepted, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } @@ -33,7 +33,7 @@ fn bns_parse(text: String, file: Symbol, production: Symbol) -> Optional { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => Absent - PreparedModeled { modeled: modeled, materialization: m } => + PreparedModeled { modeled: modeled } => match tokenize(text: text, file: file, rules: dag_lex()) { Rejected { diagnostics: _ } => Absent Accepted { value: token_stream, diagnostics: _ } => @@ -41,8 +41,7 @@ fn bns_parse(text: String, file: Symbol, production: Symbol) -> Optional { tokens: token_stream.all, validated: modeled.validated, analysis: modeled.analysis, - production_name: production, - materialization: m + production_name: production ) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => Present { value: artifact.tree } diff --git a/src/v2/test/claim/parse/parse_test_block_headed_operand_class_test.dag b/src/v2/test/claim/parse/parse_test_block_headed_operand_class_test.dag index f65bb3e8c0c..0449a32bc86 100644 --- a/src/v2/test/claim/parse/parse_test_block_headed_operand_class_test.dag +++ b/src/v2/test/claim/parse/parse_test_block_headed_operand_class_test.dag @@ -1,6 +1,6 @@ module test.claim.parse_test_block_headed_operand_class -import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared_measured_seeded } +import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared_seeded } import v2.compiler.parse_acceptance_census { parse_acceptance_of_text } import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } import v2.compiler.tokenize { tokenize_prepared } @@ -41,16 +41,15 @@ fn bc_parse_at(text: String, production: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: PreparedVoidGrammar, diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: PreparedModeled { modeled: modeled, materialization: materialization }, diagnostics: _ } => - match parse_production_prepared_measured_seeded( + Accepted { value: PreparedModeled { modeled: modeled }, diagnostics: _ } => + match parse_production_prepared_seeded( tokens: token_stream_remaining(stream: ts), validated: modeled.validated, analysis: modeled.analysis, production_name: production, seed: occurrence_id_allocator_initial(), - layout: token_stream_layout(stream: ts), - materialization: materialization - ).outcome { + layout: token_stream_layout(stream: ts) + ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } diff --git a/src/v2/test/claim/parse/brace_and_lambda_head_route_test.dag b/src/v2/test/claim/parse/parse_test_brace_and_lambda_head_route_test.dag similarity index 90% rename from src/v2/test/claim/parse/brace_and_lambda_head_route_test.dag rename to src/v2/test/claim/parse/parse_test_brace_and_lambda_head_route_test.dag index b373a66c3b2..0de190175fc 100644 --- a/src/v2/test/claim/parse/brace_and_lambda_head_route_test.dag +++ b/src/v2/test/claim/parse/parse_test_brace_and_lambda_head_route_test.dag @@ -1,9 +1,9 @@ -module v2.test.parse.brace_and_lambda_head_route +module test.claim.parse_test_brace_and_lambda_head_route import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.diagnostic { Accepted, Rejected } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node, Symbol } @@ -30,7 +30,7 @@ fn bhr_parse(text: String, file: Symbol, production: Symbol) -> Optional { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => Absent - PreparedModeled { modeled: modeled, materialization: m } => + PreparedModeled { modeled: modeled } => match tokenize(text: text, file: file, rules: dag_lex()) { Rejected { diagnostics: _ } => Absent Accepted { value: token_stream, diagnostics: _ } => @@ -38,8 +38,7 @@ fn bhr_parse(text: String, file: Symbol, production: Symbol) -> Optional { tokens: token_stream.all, validated: modeled.validated, analysis: modeled.analysis, - production_name: production, - materialization: m + production_name: production ) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => Present { value: artifact.tree } diff --git a/src/v2/test/claim/parse/declaration_choice_route_test.dag b/src/v2/test/claim/parse/parse_test_declaration_choice_route_test.dag similarity index 96% rename from src/v2/test/claim/parse/declaration_choice_route_test.dag rename to src/v2/test/claim/parse/parse_test_declaration_choice_route_test.dag index 2639affc398..ccbf2024bde 100644 --- a/src/v2/test/claim/parse/declaration_choice_route_test.dag +++ b/src/v2/test/claim/parse/parse_test_declaration_choice_route_test.dag @@ -1,4 +1,4 @@ -module v2.test.claim.parse.declaration_choice_route +module test.claim.parse_test_declaration_choice_route import v2.compiler.body_lowering_fold { BodyLowerTypedParam, @@ -43,7 +43,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // header and items are not this claim's subject (DESIGN section 3, a witness discriminates at one // interface) -- and asserts the ROUTE each former alternative took, read by the real reader where one // exists. The real module path still parses every one of these forms in the corpus. The `test` -// marker's half of the family is held in v2.test.claim.parse.test_marker_channel. +// marker's half of the family is held in test.claim.parse_test_marker_channel. // One fragment parsed at one production of the real prepared grammar. fn route_parse_at(text: String, production: Symbol) -> Outcome { @@ -53,13 +53,12 @@ fn route_parse_at(text: String, production: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: PreparedVoidGrammar, diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: PreparedModeled { modeled: modeled, materialization: materialization }, diagnostics: _ } => + Accepted { value: PreparedModeled { modeled: modeled }, diagnostics: _ } => match parse_production_prepared( tokens: token_stream_remaining(stream: ts), validated: modeled.validated, analysis: modeled.analysis, - production_name: production, - materialization: materialization + production_name: production ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } diff --git a/src/v2/test/claim/parse/else_less_if_value_category_test.dag b/src/v2/test/claim/parse/parse_test_else_less_if_value_category_test.dag similarity index 94% rename from src/v2/test/claim/parse/else_less_if_value_category_test.dag rename to src/v2/test/claim/parse/parse_test_else_less_if_value_category_test.dag index 05192cbacb2..19b28916c8f 100644 --- a/src/v2/test/claim/parse/else_less_if_value_category_test.dag +++ b/src/v2/test/claim/parse/parse_test_else_less_if_value_category_test.dag @@ -1,4 +1,4 @@ -module v2.test.parse.else_less_if_value_category +module test.claim.parse_test_else_less_if_value_category import v2.compiler.body_lowering_fold { body_lower_admitted_guard_optional, @@ -6,9 +6,9 @@ import v2.compiler.body_lowering_fold { body_lower_if_expr } import v2.compiler.parse { PreparedModeled, PreparedVoidGrammar, parse_production_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex, parse_production_emitted_identity_optional } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } +import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } import v2.std.diagnostic { Accepted, DeclarationLocus, InvariantPortLocus, Locus, NodeLocus, Rejected, SourcePortLocus, Textual, diagnostics_fatal } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node, Symbol } @@ -38,7 +38,7 @@ fn elif_parse_as(text: String, production: Symbol) -> Optional { Accepted { value: prepared, diagnostics: _ } => match prepared { PreparedVoidGrammar => Absent - PreparedModeled { modeled: modeled, materialization: m } => + PreparedModeled { modeled: modeled } => match tokenize(text: text, file: ^elif_probe, rules: dag_lex()) { Rejected { diagnostics: _ } => Absent Accepted { value: token_stream, diagnostics: _ } => @@ -46,8 +46,7 @@ fn elif_parse_as(text: String, production: Symbol) -> Optional { tokens: token_stream.all, validated: modeled.validated, analysis: modeled.analysis, - production_name: production, - materialization: m + production_name: production ) { Rejected { diagnostics: _ } => Absent Accepted { value: artifact, diagnostics: _ } => elif_some(node: artifact.tree) diff --git a/src/v2/test/claim/parse/parse_test_go_grammar_test.dag b/src/v2/test/claim/parse/parse_test_go_grammar_test.dag new file mode 100644 index 00000000000..05b0d3f553f --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_go_grammar_test.dag @@ -0,0 +1,28 @@ +module test.claim.parse_test_go_grammar + +fn go_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn go_tokenize_and_parse() -> Outcome { + match tokenize( + text: go_source_text, + file: ^go_parse_file, + rules: go_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: go_grammar()) + } +} + +// Go corpus symmetry accepts the current add-function source through the Go lex/grammar. +// Formerly the data row `claim_go_add_function_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_go_add_function_parses_holds() -> Bool { + (go_parse_accepted(out: go_tokenize_and_parse())) == true +} diff --git a/src/v2/test/claim/parse/parse_test_grammar_expect_test.dag b/src/v2/test/claim/parse/parse_test_grammar_expect_test.dag index de2e7c5a4fd..203142af2f9 100644 --- a/src/v2/test/claim/parse/parse_test_grammar_expect_test.dag +++ b/src/v2/test/claim/parse/parse_test_grammar_expect_test.dag @@ -17,6 +17,7 @@ import v2.std.diagnostic { diagnostics_fatal } import v2.std.grammar { + InfixTable, AfterLineBreak, Choice, Expect, @@ -158,7 +159,8 @@ fn ge_wrap_all(expr: GrammarExpr) -> GrammarExpr { expect: fn(e, reason) { Expect { element: Expect { element: e, reason: reason }, reason: FnExpressionBodyMissingAfterEq } }, refuse_on_match: fn(e, reason) { Expect { element: RefuseOnMatch { element: e, reason: reason }, reason: FnExpressionBodyMissingAfterEq } }, after_line_break: fn(e) { Expect { element: AfterLineBreak { element: e }, reason: FnExpressionBodyMissingAfterEq } }, - not_predicate: fn(e) { Expect { element: NotPredicate { element: e }, reason: FnExpressionBodyMissingAfterEq } } + not_predicate: fn(e) { Expect { element: NotPredicate { element: e }, reason: FnExpressionBodyMissingAfterEq } }, + infix_table: fn(operand, guard, operators) { Expect { element: InfixTable { operand: operand, continuation_guard: guard, operators: operators }, reason: FnExpressionBodyMissingAfterEq } } } ) } @@ -173,23 +175,26 @@ fn ge_plain_item() -> GrammarExpr { } } -// `a b a c a`: exercises both alternatives, the optional present and absent, and the repeat. +// `a b d c d`: exercises both alternatives, the optional present and absent, and the repeat. The +// second alternative's head production `ge_a` is the token `d` here, so the two alternatives start +// differently: grammar validation refuses any choice whose alternatives overlap (gunbc#13126), and +// this claim's subject is Expect transparency, not choice resolution. data ge_accept_tokens: List = [ ge_token(class: ^ge_tok_a, start: 0), ge_token(class: ^ge_tok_b, start: 2), - ge_token(class: ^ge_tok_a, start: 4), + ge_token(class: ^ge_tok_d, start: 4), ge_token(class: ^ge_tok_c, start: 6), - ge_token(class: ^ge_tok_a, start: 8) + ge_token(class: ^ge_tok_d, start: 8) ] test fn expect_wrapping_every_row_changes_no_accepted_tree_holds() -> Bool { - let plain = ge_root(item: ge_plain_item(), a_expr: ge_terminal(class: ^ge_tok_a)) + let plain = ge_root(item: ge_plain_item(), a_expr: ge_terminal(class: ^ge_tok_d)) let wrapped = GrammarRoot { start: ^ge_start, productions: [ ge_production(name: ^ge_start, expression: ge_wrap_all(expr: Repeat { element: Nonterminal { production: ^ge_item } })), ge_production(name: ^ge_item, expression: ge_wrap_all(expr: ge_plain_item())), - ge_production(name: ^ge_a, expression: ge_wrap_all(expr: ge_terminal(class: ^ge_tok_a))) + ge_production(name: ^ge_a, expression: ge_wrap_all(expr: ge_terminal(class: ^ge_tok_d))) ], sync_tokens: grammar_empty_sync_tokens() } diff --git a/src/v2/test/claim/parse/parse_test_kotlin_grammar_test.dag b/src/v2/test/claim/parse/parse_test_kotlin_grammar_test.dag new file mode 100644 index 00000000000..0032c6ba95f --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_kotlin_grammar_test.dag @@ -0,0 +1,30 @@ +module test.claim.parse_test_kotlin_grammar + +data kotlin_extended_fn_source: String = "fun choose(x: Int): Int { val y: Int = x + 1; if (y > 0) { return y }; return 0 }" + +fn kotlin_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn kotlin_tokenize_and_parse() -> Outcome { + match tokenize( + text: kotlin_extended_fn_source, + file: ^kotlin_parse_file, + rules: kotlin_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: kotlin_grammar()) + } +} + +// Kotlin lex/grammar accepts an extended function with val, if, return, and binary expressions. +// Formerly the data row `claim_kotlin_extended_function_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_kotlin_extended_function_parses_holds() -> Bool { + (kotlin_parse_accepted(out: kotlin_tokenize_and_parse())) == true +} diff --git a/src/v2/test/claim/parse/parse_test_lean_grammar_test.dag b/src/v2/test/claim/parse/parse_test_lean_grammar_test.dag new file mode 100644 index 00000000000..71dff873243 --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_lean_grammar_test.dag @@ -0,0 +1,28 @@ +module test.claim.parse_test_lean_grammar + +fn lean_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn lean_tokenize_and_parse() -> Outcome { + match tokenize( + text: lean_source_text, + file: ^lean_parse_fixture_file, + rules: lean_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: lean_grammar()) + } +} + +// Lean lex/grammar accepts the PROOF-1 def+termination_by fixture via tokenize+parse. +// Formerly the data row `claim_lean_source_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_lean_source_parses_holds() -> Bool { + (lean_parse_accepted(out: lean_tokenize_and_parse())) == true +} diff --git a/src/v2/test/claim/parse/parse_test_marker_channel_test.dag b/src/v2/test/claim/parse/parse_test_marker_channel_test.dag new file mode 100644 index 00000000000..f82fba1e01a --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_marker_channel_test.dag @@ -0,0 +1,40 @@ +module test.claim.parse_test_marker_channel + +import test.claim.parse_test_declaration_choice_route { route_parse_at } +import v2.test.claim.parse.test_marker_channel { + test_marker_find_emitted, + test_marker_inner_declaration, + test_marker_lowered_pair_agrees +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE TEST MARKER'S GRAMMAR ROUTE CONTROL AT top_level_item (gunbc#13126). One marked declaration is +// parsed AT the item production, and the real readers of v2.test.claim.parse.test_marker_channel +// consume that parse: the marked shell lowers to the identical semantic projection as the declaration +// inside it (DESIGN 4c). This is the production-grain subject; the module-grain capture and projection +// claims stay in v2.test.claim.parse.test_marker_channel, unchanged and off the gate (their per-token +// parse and lowering cost exceeds the new-witness budget until 02_parse's slope work lands). +data marked_declaration_source: String = "test data w: Bool = true\n" + +fn marked_item_shell_agrees_with_its_declaration() -> Optional { + match route_parse_at(text: marked_declaration_source, production: ^dag_production_top_level_item) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: tree, diagnostics: _ } => + match test_marker_find_emitted(n: tree, emitted: ^dag_surface_test_fn_decl) { + Absent => optional_absent() + Present { value: shell } => + match test_marker_inner_declaration(shell: shell) { + Absent => optional_absent() + Present { value: inner } => test_marker_lowered_pair_agrees(left: shell, right: inner) + } + } + } +} + +test fn a_marked_item_shares_one_semantic_projection_with_its_declaration_holds() -> Bool { + match marked_item_shell_agrees_with_its_declaration() { + Present { value: agrees } => agrees + Absent => false + } +} diff --git a/src/v2/test/claim/parse/parse_test_parse_table_claims_test.dag b/src/v2/test/claim/parse/parse_test_parse_table_claims_test.dag index c3c98d841f9..1f7e201a2d7 100644 --- a/src/v2/test/claim/parse/parse_test_parse_table_claims_test.dag +++ b/src/v2/test/claim/parse/parse_test_parse_table_claims_test.dag @@ -209,7 +209,7 @@ test fn parse_table_undefined_nonterminal_morphism_holds() -> Bool { test fn parse_table_parse_expr_combinator_holds() -> Bool { let prov = parse_prov_initial() - let table = parse_table_empty(materialization: Memoize) + let table = parse_table_empty() let root = GrammarRoot { start: ^pt_parse_expr_start, productions: std.algebra.Empty, diff --git a/src/v2/test/claim/parse/parse_test_parse_table_content_key_test.dag b/src/v2/test/claim/parse/parse_test_parse_table_content_key_test.dag deleted file mode 100644 index c29a559c4cf..00000000000 --- a/src/v2/test/claim/parse/parse_test_parse_table_content_key_test.dag +++ /dev/null @@ -1,107 +0,0 @@ -module test.claim.parse_test_parse_table_content_key - -data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly - -fn mk_table(grammar: Hash, tokens: Hash) -> ParseTable { - let base = parse_table_empty(materialization: Memoize) - ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: base.materialization, - grammar_digest: grammar, - token_stream_digest: tokens, - entries: base.entries, - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } -} - -fn grammar_one() -> Hash { symbol_identity_digest(sym: ^ctk_grammar_one) } -fn grammar_two() -> Hash { symbol_identity_digest(sym: ^ctk_grammar_two) } -fn tokens_one() -> Hash { symbol_identity_digest(sym: ^ctk_tokens_one) } -fn tokens_two() -> Hash { symbol_identity_digest(sym: ^ctk_tokens_two) } - -fn table_g1_t1() -> ParseTable { mk_table(grammar: grammar_one(), tokens: tokens_one()) } -fn table_g2_t1() -> ParseTable { mk_table(grammar: grammar_two(), tokens: tokens_one()) } -fn table_g1_t2() -> ParseTable { mk_table(grammar: grammar_one(), tokens: tokens_two()) } - -fn key_p0_a() -> ParsePositionKey { ParsePositionKey { position: 0, production: ^ctk_prod_a } } -fn key_p1_a() -> ParsePositionKey { ParsePositionKey { position: 1, production: ^ctk_prod_a } } -fn key_p0_b() -> ParsePositionKey { ParsePositionKey { position: 0, production: ^ctk_prod_b } } - -test fn witness_subject_deterministic() -> Bool { - let a = parse_table_subject(table: table_g1_t1(), key: key_p0_a()) - let b = parse_table_subject(table: mk_table(grammar: grammar_one(), tokens: tokens_one()), key: key_p0_a()) - a == b -} - -test fn witness_subject_position_sensitive() -> Bool { - let a = parse_table_subject(table: table_g1_t1(), key: key_p0_a()) - let b = parse_table_subject(table: table_g1_t1(), key: key_p1_a()) - a != b -} - -test fn witness_subject_production_sensitive() -> Bool { - let a = parse_table_subject(table: table_g1_t1(), key: key_p0_a()) - let b = parse_table_subject(table: table_g1_t1(), key: key_p0_b()) - a != b -} - -test fn witness_subject_grammar_scope_sensitive() -> Bool { - let a = parse_table_subject(table: table_g1_t1(), key: key_p0_a()) - let b = parse_table_subject(table: table_g2_t1(), key: key_p0_a()) - a != b -} - -test fn witness_subject_token_stream_scope_sensitive() -> Bool { - let a = parse_table_subject(table: table_g1_t1(), key: key_p0_a()) - let b = parse_table_subject(table: table_g1_t2(), key: key_p0_a()) - a != b -} - -// DOMAINS PAST ONE BYTE LIMB. The subject and the stream digest used to send positions, offsets -// and lexeme codepoints through the single-byte-limb digest, which gives every value above 255 one -// tag: positions 256 and 257 hashed equal, as did two token streams differing only in an offset -// past byte 255 or only in a non-Latin-1 lexeme. Each witness below returned false under it. -fn key_at(position: Int) -> ParsePositionKey { ParsePositionKey { position: position, production: ^ctk_prod_a } } - -fn subject_at(position: Int) -> Hash { - parse_table_subject(table: table_g1_t1(), key: key_at(position: position)) -} - -test fn witness_subject_positions_255_256_257_pairwise_distinct() -> Bool { - (subject_at(position: 255) != subject_at(position: 256)) - && (subject_at(position: 256) != subject_at(position: 257)) - && (subject_at(position: 255) != subject_at(position: 257)) -} - -fn ctk_token(lexeme: Lexeme, start: Int, end: Int) -> Token { - Token { class: ^ctk_token_class, lexeme: lexeme, file: ^ctk_token_file, start: start, end: end } -} - -fn stream_digest_one(tok: Token) -> Hash { - parse_token_stream_digest(all_tokens: Cons { head: tok, tail: std.algebra.Empty }) -} - -test fn witness_stream_digest_offsets_past_one_limb_distinct() -> Bool { - stream_digest_one(tok: ctk_token(lexeme: "a", start: 300, end: 301)) - != stream_digest_one(tok: ctk_token(lexeme: "a", start: 301, end: 302)) -} - -test fn witness_stream_digest_lexemes_past_latin1_distinct() -> Bool { - stream_digest_one(tok: ctk_token(lexeme: "λ", start: 0, end: 2)) - != stream_digest_one(tok: ctk_token(lexeme: "μ", start: 0, end: 2)) -} - -test fn witness_stream_digest_deterministic() -> Bool { - stream_digest_one(tok: ctk_token(lexeme: "λ", start: 300, end: 302)) - == stream_digest_one(tok: ctk_token(lexeme: "λ", start: 300, end: 302)) -} diff --git a/src/v2/test/claim/parse/pattern_and_let_sugar_test.dag b/src/v2/test/claim/parse/parse_test_pattern_and_let_sugar_test.dag similarity index 99% rename from src/v2/test/claim/parse/pattern_and_let_sugar_test.dag rename to src/v2/test/claim/parse/parse_test_pattern_and_let_sugar_test.dag index 9befbcd1874..7f1e2c9e750 100644 --- a/src/v2/test/claim/parse/pattern_and_let_sugar_test.dag +++ b/src/v2/test/claim/parse/parse_test_pattern_and_let_sugar_test.dag @@ -1,4 +1,4 @@ -module v2.test.claim.parse.pattern_and_let_sugar +module test.claim.parse_test_pattern_and_let_sugar import v2.compiler.reference_conservation_admission { conservation_subject_of_text } import v2.extdeps.languages.dag { parse_production_emitted_identity_optional } diff --git a/src/v2/test/claim/parse/parse_test_python_grammar_test.dag b/src/v2/test/claim/parse/parse_test_python_grammar_test.dag new file mode 100644 index 00000000000..1802dcabb73 --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_python_grammar_test.dag @@ -0,0 +1,30 @@ +module test.claim.parse_test_python_grammar + +data python_extended_fn_source: String = "def choose(x):\n y = x + 1\n if y > 0:\n return y\n return 0" + +fn python_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn python_tokenize_and_parse_source(text: String) -> Outcome { + match tokenize( + text: text, + file: ^python_parse_file, + rules: python_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: stream, diagnostics: _ } => + parse(tokens: stream, grammar: python_grammar()) + } +} + +// Python lex/grammar accepts an extended function with assignment, if, and indented returns. +// Formerly the data row `claim_python_extended_function_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_python_extended_function_parses_holds() -> Bool { + (python_parse_accepted(out: python_tokenize_and_parse_source(text: python_extended_fn_source))) == true +} diff --git a/src/v2/test/claim/parse/parse_test_python_suite_route_test.dag b/src/v2/test/claim/parse/parse_test_python_suite_route_test.dag new file mode 100644 index 00000000000..ee57a708b10 --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_python_suite_route_test.dag @@ -0,0 +1,63 @@ +module test.claim.parse_test_python_suite_route + +import v2.compiler.parse { + grammar_choice_ambiguity_count, + parse_module_prepared, + prepare_grammar, +} +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.python { python_grammar_root, python_lex } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.grammar { ModeledGrammar, ParseTree } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// ROUTE CONTROL for the suite production on real python source: a two-statement body +// (assignment, return) lexes with python_lex and parses through python_production_fn_def, so the +// suite's repetition is executed past its first statement. Two statements is the least input +// that reaches the repetition; a longer body adds parse cost without adding a route. +data python_multi_statement_suite_text: String = "def f(x):\n y = x\n return y" + +// The discriminating red: a suite is one or more statements, so a def whose body is empty refuses. +data python_empty_suite_text: String = "def f(x):\n " + +// Through the prepared door: prepare_grammar validates AND analyzes the grammar once, and the +// module parse starts at the grammar's start production, python_production_fn_def. (Validating +// with grammar_validate_for_parse and then calling parse_production_tree computes the FIRST +// analysis twice, which is what put this control over its floor budget.) +fn python_fn_def_parse(text: String) -> Outcome { + match prepare_grammar(grammar: ModeledGrammar { root: python_grammar_root() }) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: prepared, diagnostics: _ } => + match tokenize(text: text, file: ^python_parse_fixture_file, rules: python_lex()) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: stream, diagnostics: _ } => + match parse_module_prepared(tokens: stream, prepared: prepared) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } + } + } + } +} + +test fn python_multi_statement_suite_parses() -> Bool { + match python_fn_def_parse(text: python_multi_statement_suite_text) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn python_empty_suite_refuses() -> Bool { + match python_fn_def_parse(text: python_empty_suite_text) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } +} + +// The family's discriminating control: the suite was python's one choice-overlap row +// (count 1 as `stmt suite | stmt`); restructured, the roster is empty. The parse controls above +// hold on both shapes because the language did not change -- this claim is what goes red. +test fn python_grammar_has_no_choice_overlap() -> Bool { + grammar_choice_ambiguity_count(root: python_grammar_root()) == 0 +} diff --git a/src/v2/test/claim/parse/parse_test_rust_grammar_test.dag b/src/v2/test/claim/parse/parse_test_rust_grammar_test.dag new file mode 100644 index 00000000000..118d7c6477e --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_rust_grammar_test.dag @@ -0,0 +1,58 @@ +module test.claim.parse_test_rust_grammar + +data rust_extended_fn_source: String = "fn choose(x: i32, y: i32) -> i32 { let z = x + 1;\nif z > 0 { return z }\nreturn 0 }" + +data rust_return_semi_source: String = "fn r() -> i32 { return; }" + +fn rust_parse_bool_atom(b: Bool) -> Node { + match b { + true => Node { kind: TypeNode { connective: Atom { identity: ^rust_parse_bool_true } }, children: [], + occurrence_id: OccurrenceSynthetic +} + false => Node { kind: TypeNode { connective: Atom { identity: ^rust_parse_bool_false } }, children: [], + occurrence_id: OccurrenceSynthetic +} + } +} + +fn rust_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn rust_tokenize_and_parse_source(text: String) -> Outcome { + match tokenize( + text: text, + file: ^rust_parse_file, + rules: rust_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: rust_grammar()) + } +} + +fn rust_tokenize_and_parse() -> Outcome { + rust_tokenize_and_parse_source(text: rust_extended_fn_source) +} + +// Rust lex/grammar accepts an extended function with let, if, return, and binary expressions. +// Formerly the data row `claim_rust_extended_function_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_rust_extended_function_parses_holds() -> Bool { + (rust_parse_accepted(out: rust_tokenize_and_parse())) == true +} + +// Rust grammar accepts return; with layout-free return lexeme and trivia between tokens. +// Formerly the data row `claim_rust_return_semi_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_rust_return_semi_parses_holds() -> Bool { + (rust_parse_accepted( + out: rust_tokenize_and_parse_source(text: rust_return_semi_source) + )) == true +} + diff --git a/src/v2/test/claim/parse/parse_test_swift_grammar_test.dag b/src/v2/test/claim/parse/parse_test_swift_grammar_test.dag new file mode 100644 index 00000000000..07b2a0f05ef --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_swift_grammar_test.dag @@ -0,0 +1,30 @@ +module test.claim.parse_test_swift_grammar + +data swift_extended_fn_source: String = "func choose(x: Int) -> Int { let y = x + 1\nif y > 0 { return y }\nreturn 0 }" + +fn swift_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn swift_tokenize_and_parse_source(text: String) -> Outcome { + match tokenize( + text: text, + file: ^swift_parse_file, + rules: swift_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: swift_grammar()) + } +} + +// Swift lex/grammar accepts an extended function with let, if, return, and binary expressions. +// Formerly the data row `claim_swift_extended_function_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_swift_extended_function_parses_holds() -> Bool { + (swift_parse_accepted(out: swift_tokenize_and_parse_source(text: swift_extended_fn_source))) == true +} diff --git a/src/v2/test/claim/parse/parse_test_type_decl_modifier_carrier_test.dag b/src/v2/test/claim/parse/parse_test_type_decl_modifier_carrier_test.dag index 38e74ea354f..f032bd3e10e 100644 --- a/src/v2/test/claim/parse/parse_test_type_decl_modifier_carrier_test.dag +++ b/src/v2/test/claim/parse/parse_test_type_decl_modifier_carrier_test.dag @@ -11,7 +11,8 @@ import v2.compiler.body_lowering_fold { } import v2.compiler.normalize { normalize } import v2.compiler.parse { parse } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_language_model, parse_production_emitted_identity_optional } import v2.std.algebra { Cons, Empty, FreeMonoid, fold_list } import v2.std.diagnostic { Accepted, Outcome, Rejected, bind_outcome } @@ -39,7 +40,7 @@ data tdm_lm: LanguageModel = dag_language_model() fn tdm_parse(src: String) -> Outcome { bind_outcome( - o: tokenize(text: src, file: ^type_decl_modifier_carrier_probe, rules: tdm_lm.lex), + o: tokenize_prepared(text: src, file: ^type_decl_modifier_carrier_probe, prepared: dag_prepared_lex()), f: fn(ts) { parse(tokens: ts, grammar: tdm_lm.grammar) } ) } diff --git a/src/v2/test/claim/parse/parse_test_type_decl_modifier_g0_parse_probe_test.dag b/src/v2/test/claim/parse/parse_test_type_decl_modifier_g0_parse_probe_test.dag index df9d2dff6ce..67f7cc5026d 100644 --- a/src/v2/test/claim/parse/parse_test_type_decl_modifier_g0_parse_probe_test.dag +++ b/src/v2/test/claim/parse/parse_test_type_decl_modifier_g0_parse_probe_test.dag @@ -2,7 +2,8 @@ module test.claim.parse_test_type_decl_modifier_g0_parse_probe import std.types { List } import v2.compiler.parse { parse_module } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_language_model } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.grammar { node_atom_identity_optional } @@ -26,7 +27,7 @@ data probe_cached_lm: LanguageModel = dag_language_model() fn parse_outcome(src: String) -> String { let lm = probe_cached_lm - match tokenize(text: src, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => "TOKENIZE_REFUSED" Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { @@ -85,7 +86,7 @@ test fn sole_constructor_as_a_caret_symbol_still_parses_holds() -> Bool { // v2.workflow.floor_pure_producer_share) and each claim reads the list rather than re-parsing. fn caret_tree_atom_identities() -> List { let lm = probe_cached_lm - match tokenize(text: modifier_spelling_as_caret_source, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: modifier_spelling_as_caret_source, file: ^probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => [^caret_probe_tokenize_refused] Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { @@ -111,7 +112,7 @@ fn caret_tree_carries(identity: Symbol) -> Bool { fn parsed_tree_carries_atom(src: String, identity: Symbol) -> Bool { let lm = probe_cached_lm - match tokenize(text: src, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => false Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { diff --git a/src/v2/test/claim/parse/parse_test_typescript_grammar_test.dag b/src/v2/test/claim/parse/parse_test_typescript_grammar_test.dag new file mode 100644 index 00000000000..31ae8e39dd0 --- /dev/null +++ b/src/v2/test/claim/parse/parse_test_typescript_grammar_test.dag @@ -0,0 +1,79 @@ +module test.claim.parse_test_typescript_grammar + +data ts_type_alias_task_source: String = "type Task = { id: TaskId, title: string, status: TaskStatus };" + +data ts_type_alias_minimal_source: String = "type Box = { value: number };" + +data ts_type_alias_missing_eq_source: String = "type Box { value: number };" + +fn ts_parse_bool_atom(b: Bool) -> Node { + match b { + true => Node { kind: TypeNode { connective: Atom { identity: ^ts_parse_bool_true } }, children: [], + occurrence_id: OccurrenceSynthetic +} + false => Node { kind: TypeNode { connective: Atom { identity: ^ts_parse_bool_false } }, children: [], + occurrence_id: OccurrenceSynthetic +} + } +} + +fn ts_parse_accepted(out: Outcome) -> Bool { + match out { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +fn ts_tokenize_and_parse_source(text: String) -> Outcome { + match tokenize( + text: text, + file: ^ts_parse_file, + rules: ts_lex() + ) { + Rejected { diagnostics: diags } => Rejected { diagnostics: diags } + Accepted { value: tokens, diagnostics: _ } => + parse(tokens: tokens, grammar: ts_grammar()) + } +} + +// TS grammar accepts the Task record-type alias: type Task = { id: TaskId, title: string, status: TaskStatus };. +// Formerly the data row `claim_ts_type_alias_task_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_ts_type_alias_task_parses_holds() -> Bool { + (ts_parse_accepted( + out: ts_tokenize_and_parse_source(text: ts_type_alias_task_source) + )) == true +} + +// TS grammar accepts a minimal single-field record-type alias with the spec-required `=` token. +// Formerly the data row `claim_ts_type_alias_minimal_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_ts_type_alias_minimal_parses_holds() -> Bool { + (ts_parse_accepted( + out: ts_tokenize_and_parse_source(text: ts_type_alias_minimal_source) + )) == true +} + +// TS grammar REJECTS the no-`=` form `type Box { value: number };` — parse rejects missing ts_token_eq after ident. +// Formerly the data row `claim_ts_type_alias_missing_eq_rejected: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_ts_type_alias_missing_eq_rejected_holds() -> Bool { + (ts_parse_accepted( + out: ts_tokenize_and_parse_source(text: ts_type_alias_missing_eq_source) + )) == false +} + +data ts_typed_fn_list_tasks_source: String = "function list_tasks(): Task[] { return x; }" + +// TS grammar accepts a typed-return fn with array-of-named-type: function list_tasks(): Task[] { return x; }. +// Formerly the data row `claim_ts_typed_fn_list_tasks_parses: TestClaim = EqualsClaim`, which no runner executes (the +// required floor runs `test fn` claims only): the same lhs == rhs predicate, made runnable by the +// v2.test.parse census (gunbc#13372). +test fn claim_ts_typed_fn_list_tasks_parses_holds() -> Bool { + (ts_parse_accepted( + out: ts_tokenize_and_parse_source(text: ts_typed_fn_list_tasks_source) + )) == true +} diff --git a/src/v2/test/claim/parse/parse_test_weather_g0_prefix_parse_probe_test.dag b/src/v2/test/claim/parse/parse_test_weather_g0_prefix_parse_probe_test.dag index 973d428cd4b..f01daa12327 100644 --- a/src/v2/test/claim/parse/parse_test_weather_g0_prefix_parse_probe_test.dag +++ b/src/v2/test/claim/parse/parse_test_weather_g0_prefix_parse_probe_test.dag @@ -12,7 +12,8 @@ import v2.compiler.resolve { resolve_node, scope_from_namespace } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_float_literal_node_from_lexeme, dag_language_model @@ -57,7 +58,7 @@ data probe_cached_lm: LanguageModel = dag_language_model() fn parses(src: String) -> Bool { let lm = probe_cached_lm - match tokenize(text: src, file: ^probe, rules: lm.lex) { + match tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) { Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { Accepted { value: _, diagnostics: _ } => true @@ -68,8 +69,7 @@ fn parses(src: String) -> Bool { } fn lexed_tokens(src: String) -> Outcome { - let lm = probe_cached_lm - tokenize(text: src, file: ^probe, rules: lm.lex) + tokenize_prepared(text: src, file: ^probe, prepared: dag_prepared_lex()) } fn three_classes(src: String, a: Symbol, b: Symbol, c: Symbol) -> Bool { diff --git a/src/v2/test/claim/parse/python.dag b/src/v2/test/claim/parse/python.dag deleted file mode 100644 index 1c44db2faf4..00000000000 --- a/src/v2/test/claim/parse/python.dag +++ /dev/null @@ -1,43 +0,0 @@ -module v2.test.parse.python - -data python_extended_fn_source: String = "def choose(x):\n y = x + 1\n if y > 0:\n return y\n return 0" - -fn python_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^python_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^python_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn python_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn python_tokenize_and_parse() -> Outcome { - match tokenize( - text: python_extended_fn_source, - file: ^python_parse_file, - rules: python_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: stream, diagnostics: _ } => - parse(tokens: stream, grammar: python_grammar()) - } -} - -data claim_python_extended_function_parses: TestClaim = EqualsClaim { - label: "Python lex/grammar accepts an extended function with assignment, if, and indented returns", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: python_parse_bool_atom( - b: python_parse_accepted(out: python_tokenize_and_parse()) - ), - rhs: python_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/repeat_progress_test.dag b/src/v2/test/claim/parse/repeat_progress_test.dag index eba19a4f880..a8faa30cc54 100644 --- a/src/v2/test/claim/parse/repeat_progress_test.dag +++ b/src/v2/test/claim/parse/repeat_progress_test.dag @@ -6,7 +6,6 @@ import v2.std.grammar { GrammarExpr, ModeledGrammar, GrammarProduction, GrammarR import v2.compiler.parse { ParseExprAccepted, ParseExprRejected, PreparedModeled, PreparedVoidGrammar, parse_expr, parse_production_prepared, parse_prov_initial, parse_refusal_reason_diagnostic, parse_table_empty, prepare_grammar } import v2.std.diagnostic { Accepted, Rejected } import v2.std.parse_refusal_reason { TokenStartsNotIncreasing } -import std.realization { Memoize } import std.algebra { Cons } import v2.std.algebra { length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -35,7 +34,7 @@ fn rp_root() -> GrammarRoot { } fn rp_remaining(expr: GrammarExpr, tokens: List) -> Int { - match parse_expr(expr: expr, tokens: tokens, all_tokens: tokens, root: rp_root(), table: parse_table_empty(materialization: Memoize), prov: parse_prov_initial()) { + match parse_expr(expr: expr, tokens: tokens, all_tokens: tokens, root: rp_root(), table: parse_table_empty(), prov: parse_prov_initial()) { ParseExprAccepted { remaining: remaining, captured: _, diagnostics: _, prov: _, table: _ } => length(xs: remaining) ParseExprRejected { diagnostics: _, table: _ } => -1 } @@ -65,13 +64,12 @@ test fn colliding_token_starts_refuse_at_the_first_offending_token_holds() -> Bo match prepare_grammar(grammar: ModeledGrammar { root: rp_root() }) { Rejected { diagnostics: _ } => false Accepted { value: PreparedVoidGrammar, diagnostics: _ } => false - Accepted { value: PreparedModeled { modeled: m, materialization: mat }, diagnostics: _ } => + Accepted { value: PreparedModeled { modeled: m }, diagnostics: _ } => match parse_production_prepared( tokens: [rp_token(token_class: ^rp_x, start: 0), Token { class: ^rp_x, lexeme: "", file: ^rp_file, start: 0, end: 2 }], validated: m.validated, analysis: m.analysis, - production_name: ^rp_start, - materialization: mat + production_name: ^rp_start ) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: ds } => diff --git a/src/v2/test/claim/parse/rust.dag b/src/v2/test/claim/parse/rust.dag index 2a8f85d2861..2d61373eb76 100644 --- a/src/v2/test/claim/parse/rust.dag +++ b/src/v2/test/claim/parse/rust.dag @@ -1,66 +1,13 @@ module v2.test.parse.rust -data rust_extended_fn_source: String = "fn choose(x: i32, y: i32) -> i32 { let z = x + 1;\nif z > 0 { return z }\nreturn 0 }" - -data rust_return_semi_source: String = "fn r() -> i32 { return; }" +import v2.std.verification { EqualsClaim, Integration, ManualAnchorAbsent, TestClaim, TestClassification, Tier1, manual_claim_anchor } +import test.claim.parse_test_rust_grammar { rust_parse_accepted, rust_parse_bool_atom, rust_tokenize_and_parse_source } data rust_layout_tight_source: String = "fn choose(x: i32, y: i32) -> i32 { let\nz=x+1;\nif z>0 { return z; }\nreturn 0; }" -fn rust_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^rust_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^rust_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn rust_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn rust_tokenize_and_parse_source(text: String) -> Outcome { - match tokenize( - text: text, - file: ^rust_parse_file, - rules: rust_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: rust_grammar()) - } -} - -fn rust_tokenize_and_parse() -> Outcome { - rust_tokenize_and_parse_source(text: rust_extended_fn_source) -} - -data claim_rust_extended_function_parses: TestClaim = EqualsClaim { - label: "Rust lex/grammar accepts an extended function with let, if, return, and binary expressions", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: rust_parse_bool_atom( - b: rust_parse_accepted(out: rust_tokenize_and_parse()) - ), - rhs: rust_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} - -data claim_rust_return_semi_parses: TestClaim = EqualsClaim { - label: "Rust grammar accepts return; with layout-free return lexeme and trivia between tokens", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: rust_parse_bool_atom( - b: rust_parse_accepted( - out: rust_tokenize_and_parse_source(text: rust_return_semi_source) - ) - ), - rhs: rust_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} +// THE RUST GRAMMAR CLAIMS THAT ARE RED ON MAIN, held here ungated while they are diagnosed +// (v2.test.parse census, gunbc#13372, phase 4 item D). Their green siblings are gated in test.claim.parse_test_rust_grammar. +// These rows are data TestClaims, which no runner executes; the diagnosis PR, gunbc#13426, converts or frontiers them. data claim_rust_layout_tight_parses: TestClaim = EqualsClaim { label: "Rust grammar accepts tight/newline-separated layout with trivia-separated tokens", diff --git a/src/v2/test/claim/parse/status_class_pattern_test.dag b/src/v2/test/claim/parse/status_class_pattern_test.dag index e2fecbf95e4..57a3f2b38e8 100644 --- a/src/v2/test/claim/parse/status_class_pattern_test.dag +++ b/src/v2/test/claim/parse/status_class_pattern_test.dag @@ -1,9 +1,8 @@ module v2.test.claim.parse.status_class_pattern import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, ByteRange, @@ -35,11 +34,11 @@ fn status_source(pattern: String) -> String { fn status_parse(text: String) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: ^status_class_probe, rules: dag_lex()), + o: tokenize_prepared(text: text, file: ^status_class_probe, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } @@ -62,7 +61,7 @@ test fn an_exact_status_code_parses_holds() -> Bool { status_parses(pattern: "40 test fn a_named_status_parses_holds() -> Bool { status_parses(pattern: "nonzero") } fn token_start(text: String, lexeme: String) -> Int { - match tokenize(text: text, file: ^status_class_probe, rules: dag_lex()) { + match tokenize_prepared(text: text, file: ^status_class_probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => -1 Accepted { value: ts, diagnostics: _ } => fold(ts.all, init: -1, f: fn(acc, t) { if acc < 0 && t.lexeme == lexeme { t.start } else { acc } }) diff --git a/src/v2/test/claim/parse/supplied_token_stream_support.dag b/src/v2/test/claim/parse/supplied_token_stream_support.dag index 2f6304aa0c0..d14cb559c75 100644 --- a/src/v2/test/claim/parse/supplied_token_stream_support.dag +++ b/src/v2/test/claim/parse/supplied_token_stream_support.dag @@ -3,9 +3,8 @@ module v2.test.parse.supplied_token_stream_support import v2.compiler.normalize { normalize } import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.compilers.lexing { Token, TokenStream } import v2.std.diagnostic { Accepted, Outcome, Rejected } import v2.std.node { Node, symbol_lexeme } @@ -44,7 +43,7 @@ fn supplied_stream_projection(ts: TokenStream) -> String { } fn supplied_stream_matches_tokenize(text: String, file: Symbol, supplied: TokenStream) -> Bool { - match tokenize(text: text, file: file, rules: dag_lex()) { + match tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => false Accepted { value: real, diagnostics: _ } => supplied_stream_projection(ts: real) == supplied_stream_projection(ts: supplied) @@ -57,11 +56,10 @@ fn supplied_stream_matches_tokenize(text: String, file: Symbol, supplied: TokenS fn supplied_stream_parse(tokens: TokenStream) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => match parse_module_prepared( tokens: tokens, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/parse/swift.dag b/src/v2/test/claim/parse/swift.dag deleted file mode 100644 index c857128870b..00000000000 --- a/src/v2/test/claim/parse/swift.dag +++ /dev/null @@ -1,43 +0,0 @@ -module v2.test.parse.swift - -data swift_extended_fn_source: String = "func choose(x: Int) -> Int { let y = x + 1\nif y > 0 { return y }\nreturn 0 }" - -fn swift_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^swift_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^swift_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn swift_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn swift_tokenize_and_parse() -> Outcome { - match tokenize( - text: swift_extended_fn_source, - file: ^swift_parse_file, - rules: swift_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: swift_grammar()) - } -} - -data claim_swift_extended_function_parses: TestClaim = EqualsClaim { - label: "Swift lex/grammar accepts an extended function with let, if, return, and binary expressions", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: swift_parse_bool_atom( - b: swift_parse_accepted(out: swift_tokenize_and_parse()) - ), - rhs: swift_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/test_data_decl_parse_test.dag b/src/v2/test/claim/parse/test_data_decl_parse_test.dag index 4dbecc8a3eb..468a5a34e48 100644 --- a/src/v2/test/claim/parse/test_data_decl_parse_test.dag +++ b/src/v2/test/claim/parse/test_data_decl_parse_test.dag @@ -30,7 +30,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn parse_outcome(src: String, file: Symbol) -> String { let lm = dag_language_model() - match tokenize(text: src, file: file, rules: lm.lex) { + match tokenize_prepared(text: src, file: file, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => "TOKENIZE_REFUSED" Accepted { value: ts, diagnostics: _ } => match parse_module(tokens: ts, grammar: lm.grammar) { diff --git a/src/v2/test/claim/parse/test_marker_channel_test.dag b/src/v2/test/claim/parse/test_marker_channel_test.dag index c836fd8a9a8..5ed3cd3d50d 100644 --- a/src/v2/test/claim/parse/test_marker_channel_test.dag +++ b/src/v2/test/claim/parse/test_marker_channel_test.dag @@ -19,7 +19,7 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn test_marker_parsed(src: String, file: Symbol) -> Outcome { let lm = dag_language_model() - match tokenize(text: src, file: file, rules: lm.lex) { + match tokenize_prepared(text: src, file: file, prepared: dag_prepared_lex()) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: ts, diagnostics: _ } => parse_module(tokens: ts, grammar: lm.grammar) } diff --git a/src/v2/test/claim/parse/transport_kind_closed_choice_test.dag b/src/v2/test/claim/parse/transport_kind_closed_choice_test.dag index 7a23224ba00..b46ff637910 100644 --- a/src/v2/test/claim/parse/transport_kind_closed_choice_test.dag +++ b/src/v2/test/claim/parse/transport_kind_closed_choice_test.dag @@ -1,9 +1,8 @@ module v2.test.claim.parse.transport_kind_closed_choice import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_lex } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.std.diagnostic { Accepted, ByteRange, @@ -35,11 +34,11 @@ fn transport_source(kind: String) -> String { fn transport_parse(text: String) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: ^transport_kind_probe, rules: dag_lex()), + o: tokenize_prepared(text: text, file: ^transport_kind_probe, prepared: dag_prepared_lex()), f: fn(token_stream) { - match parse_module_prepared(tokens: token_stream, prepared: prepared, validation_residue: residue) { + match parse_module_prepared(tokens: token_stream, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } @@ -67,7 +66,7 @@ test fn the_local_transport_kind_parses_holds() -> Bool { transport_kind_parses( // The byte offset of the first token spelled `lexeme`, from the same lexer the parse reads. fn token_start(text: String, lexeme: String) -> Int { - match tokenize(text: text, file: ^transport_kind_probe, rules: dag_lex()) { + match tokenize_prepared(text: text, file: ^transport_kind_probe, prepared: dag_prepared_lex()) { Rejected { diagnostics: _ } => -1 Accepted { value: ts, diagnostics: _ } => fold(ts.all, init: -1, f: fn(acc, t) { if acc < 0 && t.lexeme == lexeme { t.start } else { acc } }) diff --git a/src/v2/test/claim/parse/typescript.dag b/src/v2/test/claim/parse/typescript.dag index 29318f9435e..e9272fcfcdc 100644 --- a/src/v2/test/claim/parse/typescript.dag +++ b/src/v2/test/claim/parse/typescript.dag @@ -1,81 +1,15 @@ module v2.test.parse.typescript - -data ts_type_alias_task_source: String = "type Task = { id: TaskId, title: string, status: TaskStatus };" - -data ts_type_alias_minimal_source: String = "type Box = { value: number };" - -data ts_type_alias_missing_eq_source: String = "type Box { value: number };" +import v2.std.verification { EqualsClaim, Integration, ManualAnchorAbsent, TestClaim, TestClassification, Tier1, manual_claim_anchor } +import test.claim.parse_test_typescript_grammar { ts_parse_accepted, ts_parse_bool_atom, ts_tokenize_and_parse_source } data ts_union_task_status_source: String = "const status: 'Open' | 'InProgress' | 'Done' = status;" data ts_union_minimal_source: String = "const bit: 'On' | 'Off' = bit;" -fn ts_parse_bool_atom(b: Bool) -> Node { - match b { - true => Node { kind: TypeNode { connective: Atom { identity: ^ts_parse_bool_true } }, children: [], - occurrence_id: OccurrenceSynthetic -} - false => Node { kind: TypeNode { connective: Atom { identity: ^ts_parse_bool_false } }, children: [], - occurrence_id: OccurrenceSynthetic -} - } -} - -fn ts_parse_accepted(out: Outcome) -> Bool { - match out { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn ts_tokenize_and_parse_source(text: String) -> Outcome { - match tokenize( - text: text, - file: ^ts_parse_file, - rules: ts_lex() - ) { - Rejected { diagnostics: diags } => Rejected { diagnostics: diags } - Accepted { value: tokens, diagnostics: _ } => - parse(tokens: tokens, grammar: ts_grammar()) - } -} - -data claim_ts_type_alias_task_parses: TestClaim = EqualsClaim { - label: "TS grammar accepts the Task record-type alias: type Task = { id: TaskId, title: string, status: TaskStatus };", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: ts_parse_bool_atom( - b: ts_parse_accepted( - out: ts_tokenize_and_parse_source(text: ts_type_alias_task_source) - ) - ), - rhs: ts_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} - -data claim_ts_type_alias_minimal_parses: TestClaim = EqualsClaim { - label: "TS grammar accepts a minimal single-field record-type alias with the spec-required `=` token", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: ts_parse_bool_atom( - b: ts_parse_accepted( - out: ts_tokenize_and_parse_source(text: ts_type_alias_minimal_source) - ) - ), - rhs: ts_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} - -data claim_ts_type_alias_missing_eq_rejected: TestClaim = EqualsClaim { - label: "TS grammar REJECTS the no-`=` form `type Box { value: number };` — parse rejects missing ts_token_eq after ident", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: ts_parse_bool_atom( - b: ts_parse_accepted( - out: ts_tokenize_and_parse_source(text: ts_type_alias_missing_eq_source) - ) - ), - rhs: ts_parse_bool_atom(b: false), - classification: TestClassification { tier: Tier1, layer: Integration } -} +// THE TYPESCRIPT GRAMMAR CLAIMS THAT ARE RED ON MAIN, held here ungated while they are diagnosed +// (v2.test.parse census, gunbc#13372, phase 4 item D). Their green siblings are gated in test.claim.parse_test_typescript_grammar. +// These rows are data TestClaims, which no runner executes; the diagnosis PR, gunbc#13426, converts or frontiers them. data claim_ts_union_task_status_parses: TestClaim = EqualsClaim { label: "TS grammar accepts a 3-variant string-literal discriminated union in a const annotation (TaskStatus surface): const status: 'Open' | 'InProgress' | 'Done' = status;", @@ -100,17 +34,3 @@ data claim_ts_union_minimal_parses: TestClaim = EqualsClaim { rhs: ts_parse_bool_atom(b: true), classification: TestClassification { tier: Tier1, layer: Integration } } - -data ts_typed_fn_list_tasks_source: String = "function list_tasks(): Task[] { return x; }" - -data claim_ts_typed_fn_list_tasks_parses: TestClaim = EqualsClaim { - label: "TS grammar accepts a typed-return fn with array-of-named-type: function list_tasks(): Task[] { return x; }", - anchor: manual_claim_anchor(anchor: ManualAnchorAbsent), - lhs: ts_parse_bool_atom( - b: ts_parse_accepted( - out: ts_tokenize_and_parse_source(text: ts_typed_fn_list_tasks_source) - ) - ), - rhs: ts_parse_bool_atom(b: true), - classification: TestClassification { tier: Tier1, layer: Integration } -} diff --git a/src/v2/test/claim/parse/variant_field_lowering_test.dag b/src/v2/test/claim/parse/variant_field_lowering_test.dag index a7f1c4451ed..8032fe02370 100644 --- a/src/v2/test/claim/parse/variant_field_lowering_test.dag +++ b/src/v2/test/claim/parse/variant_field_lowering_test.dag @@ -77,8 +77,8 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly fn vf_parse(tokens: TokenStream) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => - match parse_module_prepared(tokens: tokens, prepared: prepared, validation_residue: residue) { + Accepted { value: prepared, diagnostics: _ } => + match parse_module_prepared(tokens: tokens, prepared: prepared) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => Accepted { value: artifact.tree, diagnostics: d } } diff --git a/src/v2/test/claim/parse/where_refinement_clause_parse_test.dag b/src/v2/test/claim/parse/where_refinement_clause_parse_test.dag index 03479615671..474e0812fe4 100644 --- a/src/v2/test/claim/parse/where_refinement_clause_parse_test.dag +++ b/src/v2/test/claim/parse/where_refinement_clause_parse_test.dag @@ -2,10 +2,9 @@ module v2.test.parse.where_refinement_clause_parse import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module_prepared } -import v2.compiler.program_assembly { dag_prepared_grammar } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_grammar, dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { - dag_lex, parse_subtree_find_production_captured, ParseSubtreeAbsent, ParseSubtreeFound, @@ -30,14 +29,13 @@ data where_refinement_negative_empty_clause_source: String = "module probe.where fn where_refinement_tokenize_parse(text: String, file: Symbol) -> Outcome { match dag_prepared_grammar() { Rejected { diagnostics: d } => Rejected { diagnostics: d } - Accepted { value: prepared, diagnostics: residue } => + Accepted { value: prepared, diagnostics: _ } => bind_outcome( - o: tokenize(text: text, file: file, rules: dag_lex()), + o: tokenize_prepared(text: text, file: file, prepared: dag_prepared_lex()), f: fn(token_stream) { match parse_module_prepared( tokens: token_stream, - prepared: prepared, - validation_residue: residue + prepared: prepared ) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: artifact, diagnostics: d } => diff --git a/src/v2/test/claim/provenance/parse_memo_frame_replay_test.dag b/src/v2/test/claim/provenance/parse_memo_frame_replay_test.dag index af69e43bfb1..c0b2f9f85d0 100644 --- a/src/v2/test/claim/provenance/parse_memo_frame_replay_test.dag +++ b/src/v2/test/claim/provenance/parse_memo_frame_replay_test.dag @@ -2,94 +2,55 @@ module v2.test.provenance.parse_memo_frame_replay data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// A MEMO HIT MUST RESOLVE EVERY NODE IT RETURNS EXACTLY AS A PARSE WITH NO MEMO DOES. A memo hit -// replays the stored frame -- the ids minted or replayed inside the memoized production -- into -// the current span index, instead of merging the stored index whole. The oracle is independent of -// that mechanism: the same tokens under the same prepared grammar, once with the table's -// materialization Memoize (hits happen) and once with Recompute (the door refuses every store, so -// no hit can happen), must give every minted node in the tree a span-index entry, and the -// pre-order list of their textual-locus resolutions must be identical. A replay that drops an id the -// returned subtree references reds on the entry check; one that adopts the wrong event reds on the -// comparison. +// EVERY NODE A PARSE RETURNS RESOLVES TO ITS OWN SURVIVING-PATH PROVENANCE. Backtracking restores +// the whole ParseProvenanceState on a failed attempt, so occurrence ids are a counter along +// whichever path survives: an id minted on an abandoned attempt is minted again by the next one +// and recorded once. The packrat memo that served captures across paths -- and with them ids +// minted on an abandoned path -- was deleted (operator ruling R2, 2026-10-08; +// gunbc.recurring_failure_mode parse_memo_hit_serves_occurrences_minted_on_an_abandoned_path), and +// this module keeps the grammars that exposed it as regression controls on the memo-free door. // -// Both specimens are hand grammars small enough for the floor's per-witness step budget. The first -// was a real .dag specimen under the real .dag grammar, which cost ~489k eval steps and was refused -// at enrolment; what it uniquely exercised -- a memo hit whose frame holds a NESTED memoized frame -- -// is built directly below. +// The expected results are stated without reference to any memo: every minted occurrence in the +// accepted tree is distinct, every one has a span-index entry, the index holds nothing the +// surviving path did not mint, and the tree's terminals, in token order, each carry their own class +// and resolve through their own id to their own bytes. Every run goes through +// the PRODUCTION DOOR -- prepare_grammar, then parse_production_prepared over the prepared +// analysis, the route parse_module_prepared_seeded takes -- so a door that served a capture across +// paths again reds here. +// +// The module name is kept because the failure-mode row above cites its colliding-grammar claim. -type ParseMemoFrameReplayRun { +type ParsePathRun { tree: Node index: SpanIndex - hits: Int } -fn parse_memo_frame_replay_run_modeled( - tokens: List, - validated: ValidatedGrammarRoot, - analysis: GrammarFirstAnalysis, - materialization: Materialization -) -> Optional { - let root = validated.root - let base = parse_table_for_production_analyzed( - validated: validated, - analysis: analysis, - all_tokens: tokens, - materialization: Memoize - ) - let table = ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: materialization, - grammar_digest: base.grammar_digest, - token_stream_digest: base.token_stream_digest, - entries: base.entries, - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } - match grammar_lookup_production(root: root, name: root.start) { - Violates { diagnostic: _ } => optional_absent() - Holds { value: production } => - match parse_expr( - expr: production.expression, - tokens: tokens, - all_tokens: tokens, - root: root, - table: table, - prov: parse_prov_initial() - ) { - ParseExprRejected { diagnostics: _, table: _ } => optional_absent() - ParseExprAccepted { remaining: _, captured: captured, diagnostics: _, prov: prov, table: final_table } => - optional_present(value: ParseMemoFrameReplayRun { - tree: captured, - index: prov.index, - hits: final_table.memo_hits - }) +fn parse_path_run_through_door(root: GrammarRoot, tokens: List) -> Optional { + match prepare_grammar(grammar: ModeledGrammar { root: root }) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: prepared, diagnostics: _ } => + match prepared { + PreparedVoidGrammar => optional_absent() + PreparedModeled { modeled: modeled } => + match parse_production_prepared( + tokens: tokens, + validated: modeled.validated, + analysis: modeled.analysis, + production_name: modeled.validated.root.start + ) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: artifact, diagnostics: _ } => + optional_present(value: ParsePathRun { tree: artifact.tree, index: artifact.span_index }) + } } } } -// THROUGH A TYPED PARAMETER, deliberately. Inside this module a field read on a variable bound by -// `Present { value: r }` from an Optional of this module's own record type evaluates to an error type -// under the seed interpreter's inference ("error type cascade" at the read), while the same read on a -// declared parameter, or from any other module, types correctly. The defect is recorded on the PR -// that added these witnesses; this accessor is the one spelling that reads the field here. -fn parse_memo_frame_replay_hits(run: ParseMemoFrameReplayRun) -> Int { - run.hits -} - -// Absent when any minted node has no span-index entry -- the fact a dropped replay breaks. +// Absent when any minted node has no span-index entry -- the fact a dropped record breaks. // Otherwise each minted node's textual-locus resolution in pre-order. Resolution itself is not // total (a DerivedBy over children with no minted id exhausts its chain), so an unresolved node is -// recorded as Absent and compared, never treated as failure. -fn parse_memo_frame_replay_loci(run: ParseMemoFrameReplayRun) -> Optional>> { +// recorded as Absent, never treated as failure. +fn parse_path_run_loci(run: ParsePathRun) -> Optional>> { fold_list( xs: node_subtree_nodes(root: run.tree), empty: optional_present(value: std.algebra.Empty), @@ -118,22 +79,129 @@ fn parse_memo_frame_replay_loci(run: ParseMemoFrameReplayRun) -> Optional List { + fold_list( + xs: node_subtree_nodes(root: tree), + empty: std.algebra.Empty, + cons: fn(acc, n) { + match n.occurrence_id { + OccurrenceMinted { id: id } => list_snoc_item(xs: acc, item: id) + OccurrenceSynthetic => acc + OccurrenceProjected { id: _, caused_by: _ } => acc + OccurrencePending { caused_by: _ } => acc + } + } + ) +} + +fn occurrence_id_set_empty() -> Map { + empty_map() +} + +fn parse_path_minted_ids_distinct(run: ParsePathRun) -> Bool { + let ids = parse_path_minted_ids(tree: run.tree) + let distinct = fold_list( + xs: ids, + empty: occurrence_id_set_empty(), + cons: fn(acc, id) { map_insert(m: acc, key: id, value: true) } + ) + length(xs: map_keys(distinct)) == length(xs: ids) +} + +// The index holds exactly the surviving path's mints: one entry per minted node in the tree, none +// left over from an abandoned attempt. +fn parse_path_index_is_the_tree(run: ParsePathRun) -> Bool { + length(xs: map_keys(run.index.entries)) == length(xs: parse_path_minted_ids(tree: run.tree)) +} + +// A terminal is a childless minted Atom, stamped by its token class. Each one is paired with the +// locus ITS OWN id resolves to, in pre-order -- token order -- so the comparison below asks which +// bytes each terminal owns, not merely whether some node owns a given range. Absent when a +// terminal's id does not resolve to a textual locus. +type TerminalAt { + class: Symbol + locus: Locus +} + +fn parse_path_terminals(run: ParsePathRun) -> Optional> { + fold_list( + xs: node_subtree_nodes(root: run.tree), + empty: optional_present(value: std.algebra.Empty), + cons: fn(acc, n) { + match acc { + Absent => acc + Present { value: terminals } => + if length(xs: n.children) != 0 { + acc + } else { + match n.kind { + TypeNode { connective: Atom { identity: class } } => + match n.occurrence_id { + OccurrenceMinted { id: id } => + match span_index_resolve_textual_locus(index: run.index, id: id) { + Holds { value: locus } => + optional_present(value: list_snoc_item(xs: terminals, item: TerminalAt { class: class, locus: locus })) + Violates { diagnostic: _ } => optional_absent() + } + OccurrenceSynthetic => acc + OccurrenceProjected { id: _, caused_by: _ } => acc + OccurrencePending { caused_by: _ } => acc + } + _ => acc + } + } + } + } + ) +} + +type TerminalCursor { + agrees: Bool + rest: List +} + +// The accepted tree's terminals, in order, are exactly `want`: same count, and each one carries the +// expected class and resolves to the expected bytes. Swapping two terminals' span records reds here +// even though both ranges are still present in the index. +fn parse_path_terminals_are(run: ParsePathRun, want: List) -> Bool { + match parse_path_terminals(run: run) { + Absent => false + Present { value: got } => + let walked = fold_list( + xs: want, + empty: TerminalCursor { agrees: true, rest: got }, + cons: fn(cursor, w) { + match cursor.rest { + Empty => TerminalCursor { agrees: false, rest: cursor.rest } + Cons { head: g, tail: tail } => + TerminalCursor { + agrees: cursor.agrees && g.class == w.class && g.locus == w.locus, + rest: tail + } + } + } + ) + walked.agrees && length(xs: walked.rest) == 0 + } +} + +fn expected_terminal_at(token_class: Symbol, start: Int) -> TerminalAt { + TerminalAt { class: token_class, locus: abandoned_locus(start: start) } +} + +fn abandoned_path_terminals() -> List { + [ + expected_terminal_at(token_class: ^abandoned_tok_a, start: 0), + expected_terminal_at(token_class: ^abandoned_tok_a, start: 1), + expected_terminal_at(token_class: ^abandoned_tok_b, start: 2), + expected_terminal_at(token_class: ^abandoned_tok_y, start: 3) + ] +} + +fn abandoned_locus(start: Int) -> Locus { + Textual { file: ^abandoned_path_file, extent: ByteRange { start: start, end: start + 1 } } +} -// THE ACCEPTED ARTIFACT CARRIES ONLY ACCEPTED-PATH PROVENANCE. A memo-free parse discards a failed -// branch's provenance with the branch, so its index holds exactly the entries minted on the accepted -// path. The stored index a memo hit used to merge whole also held entries the MISS path minted -// before the memoized production started. Those only survive when the miss path minted more ids -// before that production than the hit path did -- a shorter hit path re-mints the low ids itself, -// and the base entry wins -- so the real .dag specimen above cannot show the difference, and this -// hand grammar is built to: -// -// S = R X z | a a X y R = a a X = b tokens: a a b y -// -// Alternative one mints four ids before X (a, a, their conjunction, R's wrap), memoizes X at -// position 2, and fails at z. Alternative two mints three ids before X and hits that memo entry, -// whose stored index carries alternative one's fourth id. Replaying the memoized frame imports -// nothing but X's own ids, so the memoized index holds exactly the memo-free index's entry count; -// the whole-index merge held one more. fn abandoned_token(token_class: Symbol, offset: Int) -> Token { Token { class: token_class, lexeme: std.algebra.Empty, file: ^abandoned_path_file, start: offset, end: offset + 1 } } @@ -142,6 +210,22 @@ fn abandoned_terminal(token_class: Symbol) -> GrammarExpr { Terminal { token_class: token_class, stamp: StampClass } } +fn abandoned_path_tokens() -> List { + Cons { head: abandoned_token(token_class: ^abandoned_tok_a, offset: 0), + tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_a, offset: 1), + tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_b, offset: 2), + tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_y, offset: 3), + tail: std.algebra.Empty } } } } +} + +// THE ABANDONED-PATH DIRECTION. +// +// S = R X z | a a X y R = a a X = b tokens: a a b y +// +// Alternative one mints four ids before X (a, a, their conjunction, R's wrap), parses X at position +// 2, and fails at z. Alternative two mints three ids before X and parses X again. Nothing the first +// alternative minted may survive into the accepted artifact: the index holds exactly the tree's +// minted ids, they are distinct, and every terminal resolves to its own byte. fn abandoned_path_root() -> GrammarRoot { GrammarRoot { start: ^abandoned_path_s, @@ -188,95 +272,73 @@ fn abandoned_path_root() -> GrammarRoot { } } -fn abandoned_path_tokens() -> List { - Cons { head: abandoned_token(token_class: ^abandoned_tok_a, offset: 0), - tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_a, offset: 1), - tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_b, offset: 2), - tail: Cons { head: abandoned_token(token_class: ^abandoned_tok_y, offset: 3), - tail: std.algebra.Empty } } } } -} - -fn abandoned_path_run(materialization: Materialization) -> Optional { - let contract_locus = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^abandoned_path_grammar_contract) - match grammar_validate_for_parse(root: abandoned_path_root(), at: contract_locus) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: validated, diagnostics: _ } => - parse_memo_frame_replay_run_modeled( - tokens: abandoned_path_tokens(), - validated: validated, - analysis: compute_grammar_first_analysis(root: validated.root), - materialization: materialization - ) +fn abandoned_run_holds(run: ParsePathRun) -> Bool { + match parse_path_run_loci(run: run) { + Absent => false + Present { value: _ } => + parse_path_minted_ids_distinct(run: run) + && parse_path_index_is_the_tree(run: run) + && parse_path_terminals_are(run: run, want: abandoned_path_terminals()) } } -fn parse_memo_frame_replay_entry_count(run: ParseMemoFrameReplayRun) -> Int { - length(xs: map_keys(run.index.entries)) -} - -test fn parse_memo_hit_carries_no_abandoned_path_provenance() -> Bool { - match abandoned_path_run(materialization: Memoize) { +test fn abandoned_path_parse_carries_only_surviving_path_provenance() -> Bool { + match parse_path_run_through_door(root: abandoned_path_root(), tokens: abandoned_path_tokens()) { Absent => false - Present { value: memo_run } => - match abandoned_path_run(materialization: Recompute) { - Absent => false - Present { value: free_run } => - match parse_memo_frame_replay_loci(run: memo_run) { - Absent => false - Present { value: memo_loci } => - match parse_memo_frame_replay_loci(run: free_run) { - Absent => false - Present { value: free_loci } => - (parse_memo_frame_replay_hits(run: memo_run) > 0) - && (parse_memo_frame_replay_hits(run: free_run) == 0) - && (memo_loci == free_loci) - && (parse_memo_frame_replay_entry_count(run: memo_run) == parse_memo_frame_replay_entry_count(run: free_run)) - } - } - } + Present { value: run } => abandoned_run_holds(run: run) } } -// A HIT WHOSE FRAME HOLDS A NESTED MEMOIZED FRAME. +// THE COLLIDING DIRECTION, and the specimen behind gunbc.recurring_failure_mode +// parse_memo_hit_serves_occurrences_minted_on_an_abandoned_path. The mirror of abandoned_path_root: // -// S = X z | (X y) z* X = W b W = a tokens: a b y +// S = a a X z | R X y R = a a X = b tokens: a a b y // -// The second alternative is nested so it does not share a sequence-left atom with the first. -// SharedPrefix would parse X once and never hit the memo. OrderedBacktrack tries X z (miss, -// nested W miss, fail at z), then (X y) z* (hit X, whose stored frame holds W). -fn nested_frame_root() -> GrammarRoot { +// Alternative one mints three ids before X, then X's own, and fails at z. Alternative two mints FOUR +// ids before X (a, a, their conjunction, R's wrap) -- one more than the first -- so an X served from +// the first attempt would carry a b whose id the surviving path has already given to R's wrap. This +// claim was red under the memo and is the permanent regression control for its deletion (DESIGN +// section 4b(4)): every minted occurrence in the accepted tree is distinct, the index is exactly +// the tree's mints, and each terminal -- b among them, at 2..3 -- resolves to its own bytes. +fn collision_path_root() -> GrammarRoot { GrammarRoot { - start: ^nested_frame_s, + start: ^collision_path_s, productions: Cons { head: GrammarProduction { - name: ^nested_frame_s, + name: ^collision_path_s, expression: Choice { left: Sequence { - left: Nonterminal { production: ^nested_frame_x }, - right: abandoned_terminal(token_class: ^nested_tok_z) - }, - right: Sequence { left: Sequence { - left: Nonterminal { production: ^nested_frame_x }, - right: abandoned_terminal(token_class: ^nested_tok_y) + left: abandoned_terminal(token_class: ^abandoned_tok_a), + right: abandoned_terminal(token_class: ^abandoned_tok_a) }, - right: Repeat { element: abandoned_terminal(token_class: ^nested_tok_z) } + right: Sequence { + left: Nonterminal { production: ^collision_path_x }, + right: abandoned_terminal(token_class: ^abandoned_tok_z) + } + }, + right: Sequence { + left: Nonterminal { production: ^collision_path_r }, + right: Sequence { + left: Nonterminal { production: ^collision_path_x }, + right: abandoned_terminal(token_class: ^abandoned_tok_y) + } } }, - emitted: grammar_atom(identity: ^nested_frame_s) + emitted: grammar_atom(identity: ^collision_path_s) }, tail: Cons { head: GrammarProduction { - name: ^nested_frame_x, + name: ^collision_path_r, expression: Sequence { - left: Nonterminal { production: ^nested_frame_w }, - right: abandoned_terminal(token_class: ^nested_tok_b) + left: abandoned_terminal(token_class: ^abandoned_tok_a), + right: abandoned_terminal(token_class: ^abandoned_tok_a) }, - emitted: grammar_atom(identity: ^nested_frame_x) + emitted: grammar_atom(identity: ^collision_path_r) }, tail: Cons { head: GrammarProduction { - name: ^nested_frame_w, - expression: abandoned_terminal(token_class: ^nested_tok_a), - emitted: grammar_atom(identity: ^nested_frame_w) + name: ^collision_path_x, + expression: abandoned_terminal(token_class: ^abandoned_tok_b), + emitted: grammar_atom(identity: ^collision_path_x) }, tail: std.algebra.Empty } } }, @@ -284,97 +346,64 @@ fn nested_frame_root() -> GrammarRoot { } } -fn nested_frame_tokens() -> List { - Cons { head: abandoned_token(token_class: ^nested_tok_a, offset: 0), - tail: Cons { head: abandoned_token(token_class: ^nested_tok_b, offset: 1), - tail: Cons { head: abandoned_token(token_class: ^nested_tok_y, offset: 2), - tail: std.algebra.Empty } } } -} - -fn nested_frame_run(materialization: Materialization) -> Optional { - let contract_locus = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^nested_frame_grammar_contract) - match grammar_validate_for_parse(root: nested_frame_root(), at: contract_locus) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: validated, diagnostics: _ } => - parse_memo_frame_replay_run_modeled( - tokens: nested_frame_tokens(), - validated: validated, - analysis: compute_grammar_first_analysis(root: validated.root), - materialization: materialization - ) +fn collision_run_holds(run: ParsePathRun) -> Bool { + match parse_path_run_loci(run: run) { + Absent => false + Present { value: _ } => + parse_path_minted_ids_distinct(run: run) + && parse_path_index_is_the_tree(run: run) + && parse_path_terminals_are(run: run, want: abandoned_path_terminals()) } } -test fn parse_memo_hit_replays_nested_frames_like_a_memo_free_parse() -> Bool { - match nested_frame_run(materialization: Memoize) { +test fn parse_memo_production_door_keeps_occurrences_distinct_on_a_colliding_grammar() -> Bool { + match parse_path_run_through_door(root: collision_path_root(), tokens: abandoned_path_tokens()) { Absent => false - Present { value: warm } => - match nested_frame_run(materialization: Recompute) { - Absent => false - Present { value: cold } => - match parse_memo_frame_replay_loci(run: warm) { - Absent => false - Present { value: warm_loci } => - match parse_memo_frame_replay_loci(run: cold) { - Absent => false - Present { value: cold_loci } => - (parse_memo_frame_replay_hits(run: warm) > 0) - && (parse_memo_frame_replay_hits(run: cold) == 0) - && (length(xs: warm_loci) > 0) - && (warm_loci == cold_loci) - } - } - } + Present { value: run } => collision_run_holds(run: run) } } -// THE PARSE MEMO'S DEMAND CASE IS DERIVED BY THE REAL PRODUCER, AND THE DIRECT ARM PRESERVES THE -// WHOLE PARSE. v2.compiler.parse compute_grammar_first_analysis rules each grammar's -// (nonterminal, position) demand through std.materialization_ladder once, and every table built -// from that analysis carries the answer. Two grammars, one per arm the grammar can reach: -// -// abandoned_path_root above: S's Choice abandons a left branch that demands R and X, so X can be -// asked again at position 2 -- the ladder discharges the demand to the in-process provider and -// the table stores (Memoize). +// A NESTED NONTERMINAL PARSED TWICE AT ONE POSITION. // -// S = R X y R = a a X = b tokens: a a b y -// -// no Choice, Optional or Repeat abandons a nonterminal demand and nothing is nullable, so each -// (nonterminal, position) is asked at most once -- AcceptedSingleRecompute, and the table does -// no memo work (Recompute). +// S = X z | (X y) z* X = W b W = a tokens: a b y // -// The direct arm is qualified on the full result, not on acceptance: the same tokens under the -// same analysis with the store FORCED ON must record zero hits -- the census's claim, checked by -// execution -- and must return the identical remaining input, tree, diagnostics and provenance -// state (allocator, span index, frame). If the census missed a recurrence the forced run hits and -// this reds; if the direct arm changed the parse, the equality reds. -fn linear_root() -> GrammarRoot { +// The first alternative parses X -- and W inside it -- then fails at z; the second parses both again +// at the same position. The second alternative is nested so it does not share a sequence-left atom +// with the first (SharedPrefix would parse X once). The accepted artifact must carry exactly the +// surviving path's ids, distinct, each terminal at its own byte. +fn nested_frame_root() -> GrammarRoot { GrammarRoot { - start: ^linear_s, + start: ^nested_frame_s, productions: Cons { head: GrammarProduction { - name: ^linear_s, - expression: Sequence { - left: Nonterminal { production: ^linear_r }, + name: ^nested_frame_s, + expression: Choice { + left: Sequence { + left: Nonterminal { production: ^nested_frame_x }, + right: abandoned_terminal(token_class: ^nested_tok_z) + }, right: Sequence { - left: Nonterminal { production: ^linear_x }, - right: abandoned_terminal(token_class: ^abandoned_tok_y) + left: Sequence { + left: Nonterminal { production: ^nested_frame_x }, + right: abandoned_terminal(token_class: ^nested_tok_y) + }, + right: Repeat { element: abandoned_terminal(token_class: ^nested_tok_z) } } }, - emitted: grammar_atom(identity: ^linear_s) + emitted: grammar_atom(identity: ^nested_frame_s) }, tail: Cons { head: GrammarProduction { - name: ^linear_r, + name: ^nested_frame_x, expression: Sequence { - left: abandoned_terminal(token_class: ^abandoned_tok_a), - right: abandoned_terminal(token_class: ^abandoned_tok_a) + left: Nonterminal { production: ^nested_frame_w }, + right: abandoned_terminal(token_class: ^nested_tok_b) }, - emitted: grammar_atom(identity: ^linear_r) + emitted: grammar_atom(identity: ^nested_frame_x) }, tail: Cons { head: GrammarProduction { - name: ^linear_x, - expression: abandoned_terminal(token_class: ^abandoned_tok_b), - emitted: grammar_atom(identity: ^linear_x) + name: ^nested_frame_w, + expression: abandoned_terminal(token_class: ^nested_tok_a), + emitted: grammar_atom(identity: ^nested_frame_w) }, tail: std.algebra.Empty } } }, @@ -382,164 +411,30 @@ fn linear_root() -> GrammarRoot { } } -// THE INHABITANCE CLAIM FOR THE DERIVATION, and it runs the PRODUCTION DOOR rather than a -// reconstruction of it. prepare_grammar is where the admission is made -- it analyzes the grammar, -// takes the recurrence census off that analysis, and asks parse_table_memo_materialization_for -- -// so reading the materialization off its PreparedModeled arm is reading exactly the value every -// ParseTable built from this grammar will carry. Supplying a materialization to -// parse_table_for_production_analyzed here instead would have asserted only that the constructor -// copies its argument, which is the thing the derivation is not. -fn derived_memo_materialization(root: GrammarRoot) -> Optional { - match prepare_grammar(grammar: ModeledGrammar { root: root }) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: prepared, diagnostics: _ } => - match prepared { - PreparedVoidGrammar => optional_absent() - PreparedModeled { modeled: _, materialization: m } => optional_present(value: m) - } - } +fn nested_frame_tokens() -> List { + Cons { head: abandoned_token(token_class: ^nested_tok_a, offset: 0), + tail: Cons { head: abandoned_token(token_class: ^nested_tok_b, offset: 1), + tail: Cons { head: abandoned_token(token_class: ^nested_tok_y, offset: 2), + tail: std.algebra.Empty } } } } -// Through a typed parameter: a bare constructor inside the generic optional_present has no -// expected type to resolve against, while a declared Materialization parameter supplies one. -fn derived_memo_materialization_is(root: GrammarRoot, want: Materialization) -> Bool { - match derived_memo_materialization(root: root) { +fn nested_run_holds(run: ParsePathRun) -> Bool { + match parse_path_run_loci(run: run) { Absent => false - Present { value: got } => got == want + Present { value: _ } => + parse_path_minted_ids_distinct(run: run) + && parse_path_index_is_the_tree(run: run) + && parse_path_terminals_are(run: run, want: [ + expected_terminal_at(token_class: ^nested_tok_a, start: 0), + expected_terminal_at(token_class: ^nested_tok_b, start: 1), + expected_terminal_at(token_class: ^nested_tok_y, start: 2) + ]) } } -// THESE TWO CLAIMS CARRY AN ADMISSION ARM, NOT ONLY A REPLAY ONE, and that is a load the module -// name does not advertise. Between them they are the only controls that drive the REAL -// prepare_grammar across both admitted outcomes: Memoize here, from a census that counts a -// discarding backtrack site, and Recompute below, from a census that counts nothing. The -// direct-computation half is also asserted by -// v2.test.claim.compiler_materialization_witness.materialization_control_prepare_is_admitted, -// but nothing over there reaches a store-permitting admission through the production door -- so -// if these are retargeted or rewritten for replay reasons, that coverage leaves with them and -// every remaining control stays green. Keep an admitted-Memoize route through prepare_grammar -// somewhere, or move it deliberately and say where it went. -test fn parse_memo_demand_derived_discharged_when_the_grammar_abandons_a_demand() -> Bool { - derived_memo_materialization_is(root: abandoned_path_root(), want: Memoize) -} - -test fn parse_memo_demand_derived_single_when_the_grammar_cannot_recur() -> Bool { - derived_memo_materialization_is(root: linear_root(), want: Recompute) -} - -type ParseFullRun { - remaining: List - captured: Node - diagnostics: List - prov: ParseProvenanceState - hits: Int -} - -// THE UNFORCED RUN IS THE DERIVED ONE, so the base table's materialization comes from the real -// door asked about THIS grammar's census -- not from a literal, which would make the equality -// claim compare two runs the derivation never chose. -// -// A REFUSAL FAILS THE CLAIM RATHER THAN SUBSTITUTING FOR IT. Standing in Recompute on the -// Rejected arm would be the very move this package exists to remove, one layer down: a refusal -// replaced by a valid operational value so the caller can proceed. The objection is not about -// the store -- Recompute does turn it off and the comparison would still run -- it is about this -// claim's PREMISE. A refused admission means the derived side of the pair does not exist, so the -// claim would go green having evaluated a state it was never written to evaluate, with nothing -// saying so. Refusal is owned elsewhere for COVERAGE (refused_scope_is_not_admitted, and the -// consumer-boundary control over prepare_grammar); it is not owned elsewhere for this claim's -// honesty. optional_absent() reaches the claim's own Absent arm, which is false. -fn linear_run(forced: Optional) -> Optional { - let contract_locus = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^abandoned_path_grammar_contract) - match grammar_validate_for_parse(root: linear_root(), at: contract_locus) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: validated, diagnostics: _ } => - let tokens = abandoned_path_tokens() - let root = validated.root - let analysis = compute_grammar_first_analysis(root: root) - match parse_table_memo_materialization_for(r: analysis.memo_recurrence) { - Rejected { diagnostics: _ } => optional_absent() - Accepted { value: derived, diagnostics: _ } => - let base = parse_table_for_production_analyzed( - validated: validated, - analysis: analysis, - all_tokens: tokens, - materialization: derived - ) - let table = ParseTableRealization { - artifact_kind: base.artifact_kind, - provider_id: base.provider_id, - materialization: match forced { - Absent => base.materialization - Present { value: m } => m - }, - grammar_digest: base.grammar_digest, - token_stream_digest: base.token_stream_digest, - entries: base.entries, - memo_hits: base.memo_hits, - memo_misses: base.memo_misses, - memo_lookup_calls: base.memo_lookup_calls, - furthest: base.furthest, - nullable_set: base.nullable_set, - first_rows: base.first_rows, - productions_by_name: base.productions_by_name, - prepared_exprs: base.prepared_exprs, - layout: base.layout, - refusal: base.refusal - } - match grammar_lookup_production(root: root, name: root.start) { - Violates { diagnostic: _ } => optional_absent() - Holds { value: production } => - match parse_expr( - expr: production.expression, - tokens: tokens, - all_tokens: tokens, - root: root, - table: table, - prov: parse_prov_initial() - ) { - ParseExprRejected { diagnostics: _, table: _ } => optional_absent() - ParseExprAccepted { remaining: remaining, captured: captured, diagnostics: diags, prov: prov, table: final_table } => - optional_present(value: ParseFullRun { - remaining: remaining, - captured: captured, - diagnostics: diags, - prov: prov, - hits: final_table.memo_hits - }) - } - } - } - } -} - -fn linear_run_forced(m: Materialization) -> Optional { - linear_run(forced: optional_present(value: m)) -} - -fn linear_run_forced_memoize() -> Optional { - linear_run_forced(m: Memoize) -} - -fn parse_full_run_same_parse(a: ParseFullRun, b: ParseFullRun) -> Bool { - (a.remaining == b.remaining) - && (a.captured == b.captured) - && (a.diagnostics == b.diagnostics) - && (a.prov == b.prov) -} - -fn parse_full_run_hits(run: ParseFullRun) -> Int { - run.hits -} - -test fn parse_memo_direct_arm_preserves_the_whole_parse() -> Bool { - match linear_run(forced: optional_absent()) { +test fn nested_reparse_carries_only_surviving_path_provenance() -> Bool { + match parse_path_run_through_door(root: nested_frame_root(), tokens: nested_frame_tokens()) { Absent => false - Present { value: direct } => - match linear_run_forced_memoize() { - Absent => false - Present { value: stored } => - (parse_full_run_hits(run: stored) == 0) - && parse_full_run_same_parse(a: direct, b: stored) - } + Present { value: run } => nested_run_holds(run: run) } } diff --git a/src/v2/test/claim/reference_derived_graph_fixture.dag b/src/v2/test/claim/reference_derived_graph_fixture.dag index 615c572a8b9..c79444248e0 100644 --- a/src/v2/test/claim/reference_derived_graph_fixture.dag +++ b/src/v2/test/claim/reference_derived_graph_fixture.dag @@ -8,7 +8,8 @@ import v2.compiler.normalized_tree { no_resource_declarations } import v2.compiler.normalize { record_declaration_capture } import v2.compiler.parse { parse_module } import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } -import v2.compiler.tokenize { tokenize } +import v2.compiler.program_assembly { dag_prepared_lex } +import v2.compiler.tokenize { tokenize_prepared } import v2.extdeps.languages.dag { dag_language_model } import v2.lens.module_graph { LocatedModuleRoot, @@ -118,7 +119,7 @@ fn fixture_ingest() -> SourceRootIngest { fn parse_one(w: DagSourceReadWitness) -> Outcome { let lm = dag_language_model() bind_outcome( - o: tokenize(text: w.source.carried, file: w.compilation_unit, rules: lm.lex), + o: tokenize_prepared(text: w.source.carried, file: w.compilation_unit, prepared: dag_prepared_lex()), f: fn(token_stream) { bind_outcome( o: parse_module(tokens: token_stream, grammar: lm.grammar), diff --git a/src/v2/test/claim/reference_derived_residency_reading_test.dag b/src/v2/test/claim/reference_derived_residency_reading_test.dag index 206bdda0a43..2a10574d00f 100644 --- a/src/v2/test/claim/reference_derived_residency_reading_test.dag +++ b/src/v2/test/claim/reference_derived_residency_reading_test.dag @@ -34,7 +34,13 @@ import v2.lens.reference_derived_residency_reading { carrier_reading_from_type_d import v2.std.diagnostic { Accepted, Rejected } import std.optional { Absent, Present } import v2.std.qualified_name { qualified_name_snoc } -import v2.extdeps.languages.dag { qualified_name_from_module_node } +import v2.extdeps.languages.dag { + qualified_name_from_module_node, + dag_named_edge, + dag_surface_edge, + dag_surface_identity_is_module_member, + dag_surface_identity_is_top_level_item, +} import v2.std.symbol_index { empty_symbol_index, symbol_index_lookup } import v2.test.claim.reference_derived_graph_fixture { xl4_fixture_trees, xl4_item_at, consumer_field_bare_local_path } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -238,6 +244,105 @@ test fn an_alias_typed_field_is_not_readable() -> Bool { !readability_is_recognised(r: reading_of(decl_name: "ReferenceDerivedPool", decl: alias_typed_declaration())) } +fn identity_stamped(id: Symbol) -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [dag_named_edge(name: ^grammar_production_identity_node_projection, target: atom(identity: id))], + occurrence_id: OccurrenceSynthetic, + } +} + +fn module_container_with_surface_members() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + dag_surface_edge(edge: ^dag_surface_module_header, target: atom(identity: ^dag_surface_module_header)), + Edge { label: Authored { name: ^bit }, target: identity_stamped(id: ^dag_surface_fn_decl) }, + Edge { label: Authored { name: ^nested }, target: identity_stamped(id: ^dag_surface_module) }, + Edge { label: Authored { name: ^probe }, target: identity_stamped(id: ^dag_surface_test_fn_decl) }, + Edge { label: Authored { name: ^Flag }, target: identity_stamped(id: ^dag_surface_data_decl) }, + Edge { label: Authored { name: ^Wrapper }, target: identity_stamped(id: ^dag_surface_type_decl) }, + Edge { label: Authored { name: ^Alias }, target: identity_stamped(id: ^dag_surface_alias_decl) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + +fn mixed_declaration_and_hand_built_field() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^Flag }, target: identity_stamped(id: ^dag_surface_data_decl) }, + Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + +fn conj_with_header_edge_and_hand_built_field() -> Node { + Node { + kind: TypeNode { connective: Conj }, + children: [ + dag_surface_edge(edge: ^dag_surface_module_header, target: atom(identity: ^dag_surface_module_header)), + Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }, + ], + occurrence_id: OccurrenceSynthetic, + } +} + +fn hand_built_record_field_declaration() -> Node { + record_of(fields: [Edge { label: Authored { name: ^trees }, target: atom(identity: ^Node) }]) +} + +fn readability_is_member_not_a_binder(r: ProducerCarrierReading, expected: Symbol) -> Bool { + match r.readability { + CarrierMemberNotABinder { member: member } => member == expected + CarrierTypeRecognized => false + CarrierTypeUnrecognized { spelling: _ } => false + } +} + +// A MODULE CONTAINER IS NOT A RECORD PAYLOAD. Its Authored members are dag_surface_module nodes and +// declarations; asking member_edge_type_node of them used to mint CarrierMemberNotABinder. +test fn a_module_container_is_not_read_as_a_non_binder_record() -> Bool { + readability_is_recognised(r: reading_of(decl_name: "ModuleContainer", decl: module_container_with_surface_members())) +} + +// THE RED THE SKIP MUST NOT ERASE: a hand-built name-to-type field on a genuine record still reports +// CarrierMemberNotABinder, named. +test fn a_hand_built_record_field_still_reports_member_not_a_binder() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "HandBuiltCarrier", decl: hand_built_record_field_declaration()), + expected: ^trees, + ) +} + +// A HEADER EDGE ON THE CONJ IS NOT A CONTAINER-WIDE SKIP. The member that is not identity-stamped +// is still CarrierMemberNotABinder; skipping the whole Conj because it carries a header is the +// widen the door-lens ruling forbids. +test fn a_header_edge_on_a_conj_does_not_skip_a_hand_built_field() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "HeaderAndField", decl: conj_with_header_edge_and_hand_built_field()), + expected: ^trees, + ) +} + +test fn top_level_item_identities_come_from_the_grammar_rows() -> Bool { + dag_surface_identity_is_top_level_item(id: ^dag_surface_data_decl) + && dag_surface_identity_is_top_level_item(id: ^dag_surface_import_decl) + && !dag_surface_identity_is_top_level_item(id: ^dag_surface_module) + && dag_surface_identity_is_module_member(id: ^dag_surface_module) +} + +// A DATA DECLARATION SIBLING DOES NOT WIDEN THE SKIP. The identity-stamped member is skipped; +// the hand-built field on the same Conj is still CarrierMemberNotABinder. +test fn a_data_decl_member_does_not_skip_a_hand_built_sibling() -> Bool { + readability_is_member_not_a_binder( + r: reading_of(decl_name: "Mixed", decl: mixed_declaration_and_hand_built_field()), + expected: ^trees, + ) +} + // THE BLOCKING ARM. Under the earlier derivation this qualified -- both axes fell through to their // admitting values -- so a producer could buy the admission by renaming a type while the same corpus // stayed resident (review 68724). It must refuse, and it must refuse NAMING the carrier and the diff --git a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag index 3841a9065f5..fd5463354f6 100644 --- a/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag +++ b/src/v2/test/claim/reference_evidence/declaration_reference_evidence_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.reference_evidence.declaration_reference_evidence +import v2.std.member_declaration_kind { no_member_declaration_kinds } import v2.compiler.resolve { ResolvedTree } import v2.std.symbol_index { symbol_index_lookup } import v2.compiler.infer { infer, inferred_facts_grounding_derived, infer_type_equal_ignoring_provenance } @@ -333,7 +334,8 @@ fn dre_same_leaf_resolved() -> ResolvedTree { symbol_index: call.symbol_index, resolved_declarations: symbol_index_insert(index: call.resolved_declarations, qualified_path: [^p, ^Wrap], resolved: dre_same_leaf_wrap_declaration()), lexical_bindings: call.lexical_bindings, - unavailable_providers: call.unavailable_providers + unavailable_providers: call.unavailable_providers, + member_kinds: no_member_declaration_kinds() } } diff --git a/src/v2/test/claim/resolve/fold_handler_binder_resolve_test.dag b/src/v2/test/claim/resolve/fold_handler_binder_resolve_test.dag new file mode 100644 index 00000000000..2501c1214d9 --- /dev/null +++ b/src/v2/test/claim/resolve/fold_handler_binder_resolve_test.dag @@ -0,0 +1,175 @@ +module v2.test.claim.resolve.fold_handler_binder_resolve + +import v2.compiler.compile { + NativeCensusModule, + NativeCensusModuleFileRefused, + NativeCensusModuleOutcome, + NativeCensusModuleResolveRefused, + NativeCensusModuleResolved, + NativeTestContext, + native_census_module_resolution, + native_census_modules, + native_lane_file_refusal_index, + native_test_context_from_ingest +} +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, NodeLocus, Rejected } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node { Symbol } +import std.optional { Absent, Optional, Present, optional_absent } +import v2.std.text { String } +import std.algebra { Cons, Empty } +import extdeps.communication.medium { Lossless, Medium } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A FOLD HANDLER'S PARAMETERS BIND AT NATIVE RESOLVE, THE SAME WAY AN ORDINARY LAMBDA'S DO. +// +// THE SUBJECT IS v2.compiler.resolve on the native ingest route (native_test_context_from_ingest, +// then native_census_module_resolution). gunbc.recurring_failure_mode +// fold_cons_handler_binder_unbound_on_the_native_resolver recorded a 2026-10-02 native prepare +// refusal at `candidate.path` in a cons handler; the exact prepare-refusal cause is unestablished. +// These claims are today's inhabitance at that boundary, not a replay of that revision. +// +// THREE MODULES, ONE VARIABLE EACH. `fold` is the specimen: the item binder is used in the handler +// body. `lambda` is the already-binding control: the same two-parameter fn literal as a call +// argument, not a fold step. `undeclared` keeps an unbound name in a fold handler as the sole +// refusal at its atom, so the repair did not start accepting names the handler does not bind. +// +// The census walk over those three modules is unrolled, not a fold_list: a fold in THIS file would +// make the claim module itself the class's subject (the vfb_specimen residual). The subject is the +// ingest strings. + +data fhb_fold_source: String = "module v2.test.fhb_fold\n\ntype Item { path: Bool }\n\nfn fhb_probe(xs: Item) -> Bool {\n fold(xs, false, fn(acc, candidate) { acc || candidate.path })\n}\n" + +data fhb_lambda_source: String = "module v2.test.fhb_lambda\n\ntype Item { path: Bool }\n\nfn fhb_apply(f: Bool, v: Bool) -> Bool { v }\n\nfn fhb_probe(p: Item) -> Bool { fhb_apply(f: fn(acc, candidate) { acc || candidate.path }, v: true) }\n" + +data fhb_undeclared_source: String = "module v2.test.fhb_undeclared\n\ntype Item { path: Bool }\n\nfn fhb_probe(xs: Item) -> Bool {\n fold(xs, false, fn(acc, candidate) { acc || fhb_undeclared_item })\n}\n" + +fn fhb_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +data fhb_ingest: SourceRootIngest = Cons { + head: fhb_read(source: fhb_fold_source, id: ^fhb_fold_artifact, unit: ^fhb_fold_cu, path: "src/v2/test/fixture/resolve/fhb_fold.dag"), + tail: Cons { + head: fhb_read(source: fhb_lambda_source, id: ^fhb_lambda_artifact, unit: ^fhb_lambda_cu, path: "src/v2/test/fixture/resolve/fhb_lambda.dag"), + tail: Cons { + head: fhb_read(source: fhb_undeclared_source, id: ^fhb_undeclared_artifact, unit: ^fhb_undeclared_cu, path: "src/v2/test/fixture/resolve/fhb_undeclared.dag"), + tail: Empty + } + } + } + +type FhbVerdict + = FhbResolved + | FhbRefused { reason: Symbol, at_atom: Optional, sole_chain: Bool } + | FhbFileRefused + | FhbAbsent + +type FhbOutcomes + = FhbContextRefused + | FhbOutcomesDecided { fold: FhbVerdict, lambda: FhbVerdict, undeclared: FhbVerdict } + +fn fhb_verdict_of(outcome: NativeCensusModuleOutcome) -> FhbVerdict { + match outcome { + NativeCensusModuleResolved => FhbResolved + NativeCensusModuleFileRefused => FhbFileRefused + NativeCensusModuleResolveRefused { first: d, rest: rest, observation: _ } => + FhbRefused { + reason: d.head.reason, + at_atom: match d.head.at { + NodeLocus { anchor: a } => node_atom_identity_optional(node: a.at) + _ => optional_absent() + }, + sole_chain: match rest { + Empty => true + Cons { head: _, tail: _ } => false + } + } + } +} + +fn fhb_entry_verdict(context: NativeTestContext, entry: NativeCensusModule) -> FhbVerdict { + fhb_verdict_of( + outcome: native_census_module_resolution( + context: context, + refusal_index: native_lane_file_refusal_index(refusals: context.file_refusals), + entry: entry + ).outcome + ) +} + +fn fhb_if_name(entry: NativeCensusModule, verdict: FhbVerdict, module_name: String, acc: FhbVerdict) -> FhbVerdict { + if entry.module == module_name { verdict } else { acc } +} + +fn fhb_outcomes() -> FhbOutcomes { + match native_test_context_from_ingest(ingest: fhb_ingest) { + Rejected { diagnostics: _ } => FhbContextRefused + Accepted { value: context, diagnostics: _ } => + match native_census_modules(ingest: fhb_ingest) { + Cons { head: a, tail: Cons { head: b, tail: Cons { head: c, tail: _ } } } => + let va = fhb_entry_verdict(context: context, entry: a) + let vb = fhb_entry_verdict(context: context, entry: b) + let vc = fhb_entry_verdict(context: context, entry: c) + FhbOutcomesDecided { + fold: fhb_if_name(entry: c, verdict: vc, module_name: "v2.test.fhb_fold", acc: fhb_if_name(entry: b, verdict: vb, module_name: "v2.test.fhb_fold", acc: fhb_if_name(entry: a, verdict: va, module_name: "v2.test.fhb_fold", acc: FhbAbsent))), + lambda: fhb_if_name(entry: c, verdict: vc, module_name: "v2.test.fhb_lambda", acc: fhb_if_name(entry: b, verdict: vb, module_name: "v2.test.fhb_lambda", acc: fhb_if_name(entry: a, verdict: va, module_name: "v2.test.fhb_lambda", acc: FhbAbsent))), + undeclared: fhb_if_name(entry: c, verdict: vc, module_name: "v2.test.fhb_undeclared", acc: fhb_if_name(entry: b, verdict: vb, module_name: "v2.test.fhb_undeclared", acc: fhb_if_name(entry: a, verdict: va, module_name: "v2.test.fhb_undeclared", acc: FhbAbsent))) + } + _ => FhbOutcomesDecided { fold: FhbAbsent, lambda: FhbAbsent, undeclared: FhbAbsent } + } + } +} + +fn fhb_is_resolved(v: FhbVerdict) -> Bool { + match v { + FhbResolved => true + _ => false + } +} + +fn fhb_undeclared_is_sole_unbound_at_its_atom(v: FhbVerdict) -> Bool { + match v { + FhbRefused { reason: r, at_atom: Present { value: id }, sole_chain: sole } => + (r == ^resolve_reason_unbound_symbol) && (id == ^fhb_undeclared_item) && sole + _ => false + } +} + +// ONE FRONT END FOR EVERY CLAIM. Native ingest of even three tiny modules is ~1.1M eval steps, +// over the new-witness enrolment margin (~72k). A single `test fn` therefore blocks the floor +// (enrolment_measured_over_margin on fold_handler_binders_resolve_on_native_ingest, required-ci +// run 37776210394). Three planned claims reaching this nullary producer admit it to +// v2.workflow.floor_pure_producer_share: the fill is netted from the first claim and later claims +// are served. Local `--function` of one name still pays ingest in that process. +test fn a_fold_handler_item_binder_resolves() -> Bool { + match fhb_outcomes() { + FhbOutcomesDecided { fold: v, lambda: _, undeclared: _ } => fhb_is_resolved(v: v) + FhbContextRefused => false + } +} + +test fn a_non_fold_lambda_argument_still_resolves() -> Bool { + match fhb_outcomes() { + FhbOutcomesDecided { fold: _, lambda: v, undeclared: _ } => fhb_is_resolved(v: v) + FhbContextRefused => false + } +} + +test fn an_undeclared_name_in_a_fold_handler_is_the_sole_refusal_at_its_atom() -> Bool { + match fhb_outcomes() { + FhbOutcomesDecided { fold: _, lambda: _, undeclared: v } => + fhb_undeclared_is_sole_unbound_at_its_atom(v: v) + FhbContextRefused => false + } +} diff --git a/src/v2/test/claim/rust_crate_partition_witness_test.dag b/src/v2/test/claim/rust_crate_partition_witness_test.dag index 85c4164ee97..8711fd36cee 100644 --- a/src/v2/test/claim/rust_crate_partition_witness_test.dag +++ b/src/v2/test/claim/rust_crate_partition_witness_test.dag @@ -3,7 +3,7 @@ module v2.test.claim.rust_crate_partition_witness import std.decl_ref { DeclarationRef, WholeDeclaration } import std.perturbation { ForwardSameInSameOut, perturbation_consistent } import std.types { NonEmptyStr } -import v2.std.collection { Absent, Present, empty_map, map_get, map_insert } +import v2.std.collection { Absent, Present, empty_map, map_get_checked, map_insert } import v2.std.diagnostic { Accepted, Rejected } import v2.std.compilers.compilation_unit { CompilationUnitId, @@ -244,7 +244,7 @@ fn witness_policy_two() -> PartitionParameter { } fn witness_owner(assignment: Map, module_id: ModuleId) -> String { - match map_get(assignment, module_id) { + match map_get_checked(assignment, module_id) { Accepted { value: Present { value: unit }, diagnostics: _ } => unit as String Accepted { value: Absent, diagnostics: _ } => "unowned" Rejected { diagnostics: _ } => "unowned" diff --git a/src/v2/test/claim/self_host/closure_emission_test.dag b/src/v2/test/claim/self_host/closure_emission_test.dag index ab2e11ec364..680297c21f1 100644 --- a/src/v2/test/claim/self_host/closure_emission_test.dag +++ b/src/v2/test/claim/self_host/closure_emission_test.dag @@ -30,6 +30,7 @@ import v2.std.algebra { contains, fold_list, length } import v2.std.artifact { Artifact, SourceFile } import v2.std.cross_tree.import_model { DagTree } import v2.std.diagnostic { Accepted, Rejected, diagnostics_fatal_reason } +import v2.std.lens_verdict { lens_verdict_diagnostics_located_chain_text_in_spans } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node { Node, Symbol, node_synthetic } import v2.std.qualified_name { QualifiedName, qualified_name_from_dotted_string, qualified_name_to_dotted_string } @@ -322,9 +323,19 @@ test fn the_emission_renders_through_its_codec_and_reads_back_the_same_identitie // A MODULE IDENTITY DECLARED BY TWO FILES REFUSES THE EMISSION AT THE CENSUS, even when the two // files' declarations are independent and neither is a closure member. test fn two_files_declaring_one_module_refuse_the_emission_at_the_census_holds() -> Bool { - match ce_emission_duplicate_module().outcome { + let located = ce_emission_duplicate_module() + match located.outcome { Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => diagnostics_fatal_reason(d: d) == ^closure_census_duplicate_module + Rejected { diagnostics: d } => + diagnostics_fatal_reason(d: d) == ^closure_census_duplicate_module + && string_contains( + s: lens_verdict_diagnostics_located_chain_text_in_spans(d: d, index: located.spans), + pattern: "ce_duplicate_a_read" + ) + && string_contains( + s: lens_verdict_diagnostics_located_chain_text_in_spans(d: d, index: located.spans), + pattern: "ce_duplicate_b_read" + ) } } diff --git a/src/v2/test/claim/self_host/direct_rust_door_group_algebra_test.dag b/src/v2/test/claim/self_host/direct_rust_door_group_algebra_test.dag index 288c55511fc..3ce02609d86 100644 --- a/src/v2/test/claim/self_host/direct_rust_door_group_algebra_test.dag +++ b/src/v2/test/claim/self_host/direct_rust_door_group_algebra_test.dag @@ -541,8 +541,8 @@ test fn behavioral_advance_still_reachable_with_a_bound_probe() -> Bool { fn residue_headed_diag(fatal: Symbol) -> NonEmptyDiagnostics { NonEmptyDiagnostics { head: Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: invariant_port_locus(port: ^direct_rust_door_group_algebra, invariant: ^parse_grammar_choice_overlap_residue), + reason: ^tokenize_lex_e1_unrecognized_char, + at: invariant_port_locus(port: ^direct_rust_door_group_algebra, invariant: ^tokenize_lex_e1_unrecognized_char), correction: Unavailable { reason: ExternalContractUnknown } }, tail: [ @@ -555,9 +555,9 @@ fn residue_headed_diag(fatal: Symbol) -> NonEmptyDiagnostics { } } -// THE PROGRAM'S RED CONTROL AT THE DIRECT-RUST DOOR. Two assembly refusals share the overlap -// residue as their head and differ in their fatal cause; the observation must name each fatal. -// Reading the head named the residue for both. +// THE PROGRAM'S RED CONTROL AT THE DIRECT-RUST DOOR. Two assembly refusals share a non-fatal +// advisory as their head and differ in their fatal cause; the observation must name each fatal. +// Reading the head named the advisory for both. test fn a_shared_residue_head_yields_each_assembly_refusals_own_fatal_holds() -> Bool { let a = project_direct_rust_door_observation(group: group_of(emission: AssemblyRejected { diagnostics: residue_headed_diag(fatal: ^normalize_rejected) })) let b = project_direct_rust_door_observation(group: group_of(emission: AssemblyRejected { diagnostics: residue_headed_diag(fatal: ^resolve_reason_unbound_symbol) })) diff --git a/src/v2/test/claim/self_host/frontier_probe_assemble_detail_test.dag b/src/v2/test/claim/self_host/frontier_probe_assemble_detail_test.dag new file mode 100644 index 00000000000..cfc9c371b5f --- /dev/null +++ b/src/v2/test/claim/self_host/frontier_probe_assemble_detail_test.dag @@ -0,0 +1,68 @@ +module v2.test.self_host.frontier_probe_assemble_detail + +import v2.std.diagnostic { + Diagnostic, + ExternalContractUnknown, + NonEmptyDiagnostics, + Unavailable, + invariant_port_locus +} +import v2.compiler.self_host.frontier_probe_assemble_detail { + frontier_probe_receipt_from_assemble_rejection +} +import v2.compiler.self_host.frontier_probe_types { + AssembleRejectionDetail, + FrontierProbeDetailAbsent, + NoFrontierProbeDetail, + MissingModuleIdentityUnavailable, + ProbeStageEmit, + frontier_probe_receipt_without_assemble_detail, + RealizationGap +} +import v2.std.algebra { Cons, Empty, length } +import v2.std.node { Symbol } + +fn assemble_detail_test_diagnostic(reason: Symbol) -> Diagnostic { + Diagnostic { + reason: reason, + at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: reason), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +test fn emit_stage_receipt_has_no_assemble_detail() -> Bool { + match frontier_probe_receipt_without_assemble_detail( + module_path: "src/v2/compiler/example.dag", + blocker: RealizationGap, + located_stage: ProbeStageEmit, + located_reason: ^probe_emit_accept_self_host_bar_unbound, + detail: FrontierProbeDetailAbsent + ).assemble_detail { + NoFrontierProbeDetail => true + _ => false + } +} + +// The receipt's located reason and its cause come from the rejection's FATAL (last) diagnostic, and +// the chain keeps the whole list in order: a two-diagnostic rejection reads as head then tail. +test fn assemble_receipt_carries_the_whole_rejection_chain_and_its_fatal_cause() -> Bool { + let receipt = frontier_probe_receipt_from_assemble_rejection( + module_path: "src/v2/compiler/example.dag", + diagnostics: NonEmptyDiagnostics { + head: assemble_detail_test_diagnostic(reason: ^normalize_rejected), + tail: Cons { head: assemble_detail_test_diagnostic(reason: ^resolve_module_not_found), tail: Empty } + }, + detail: FrontierProbeDetailAbsent + ) + (receipt.located_reason == ^resolve_module_not_found) + && match receipt.assemble_detail { + AssembleRejectionDetail { rejection_chain: chain, cause: cause, at: _ } => + (chain.head == ^normalize_rejected) + && (length(xs: chain.tail) == 1) + && match cause { + MissingModuleIdentityUnavailable => true + _ => false + } + _ => false + } +} diff --git a/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag index 78968ae9384..4dfc0d6417b 100644 --- a/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag +++ b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag @@ -1,15 +1,11 @@ module v2.test.claim.self_host.frontier_probe_cause_location -import v2.compiler.parse_diagnostic { - NonEmptyParseStageDiagnostics, - parse_stage_diagnostic_from_diagnostic_only -} import v2.compiler.self_host.frontier_probe_assemble_detail { - frontier_probe_assemble_detail_from_parse_stage + frontier_probe_assemble_detail_from_rejection } import v2.compiler.self_host.frontier_probe_types { AssembleRejectionDetail, - NonEmptySymbolChain, + OtherAssembleRefusal, CauseLocationDeclaration, CauseLocationNodeNotSerializable, CauseLocationInvariantPort, @@ -22,6 +18,7 @@ import std.occurrence_identity { OccurrenceSynthetic } import v2.std.diagnostic { ByteRange, Diagnostic, + NonEmptyDiagnostics, Textual, Unavailable, UserInputBoundary, @@ -43,17 +40,14 @@ import v2.std.text { String } // data and keeps its symbol, and only a Node, which has no manifest rendering, reaches the // not-serializable arm. A two-diagnostic control pins the PAIRING: with a second diagnostic // carrying a different reason at a different position, the reason and the location must both come -// from the head, so neither field can be drawn from a diagnostic the other did not come from. The +// from the FATAL (the last), so neither field can be drawn from a diagnostic the other did not come from. The // location is asserted on the DETAIL and never inside the cause, because a clustering key reads the // cause and two modules refusing for the same reason at different positions are the same detailed // cause. fn detail_location_of(d: Diagnostic) -> Symbol { - match frontier_probe_assemble_detail_from_parse_stage( - d: NonEmptyParseStageDiagnostics { - head: parse_stage_diagnostic_from_diagnostic_only(d: d), - tail: Empty - } + match frontier_probe_assemble_detail_from_rejection( + d: NonEmptyDiagnostics { head: d, tail: Empty } ) { NoFrontierProbeDetail => ^no_detail AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => @@ -99,11 +93,8 @@ fn port_locus_diagnostic() -> Diagnostic { } fn textual_extent_start() -> Int { - match frontier_probe_assemble_detail_from_parse_stage( - d: NonEmptyParseStageDiagnostics { - head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), - tail: Empty - } + match frontier_probe_assemble_detail_from_rejection( + d: NonEmptyDiagnostics { head: textual_diagnostic(), tail: Empty } ) { NoFrontierProbeDetail => 0 - 1 AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => @@ -121,13 +112,10 @@ fn textual_extent_start() -> Int { } } -fn two_diagnostic_stage() -> NonEmptyParseStageDiagnostics { - NonEmptyParseStageDiagnostics { - head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), - tail: Cons { - head: parse_stage_diagnostic_from_diagnostic_only(d: later_diagnostic()), - tail: Empty - } +fn two_diagnostic_stage() -> NonEmptyDiagnostics { + NonEmptyDiagnostics { + head: textual_diagnostic(), + tail: Cons { head: later_diagnostic(), tail: Empty } } } @@ -143,7 +131,7 @@ fn later_diagnostic() -> Diagnostic { } fn paired_start_of_two() -> Int { - match frontier_probe_assemble_detail_from_parse_stage(d: two_diagnostic_stage()) { + match frontier_probe_assemble_detail_from_rejection(d: two_diagnostic_stage()) { NoFrontierProbeDetail => 0 - 1 AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => match a { @@ -161,17 +149,18 @@ fn paired_start_of_two() -> Int { } fn paired_reason_of_two() -> Symbol { - match frontier_probe_assemble_detail_from_parse_stage(d: two_diagnostic_stage()) { + match frontier_probe_assemble_detail_from_rejection(d: two_diagnostic_stage()) { NoFrontierProbeDetail => ^no_detail - AssembleRejectionDetail { rejection_chain: c, cause: _, at: _ } => - match c { - NonEmptySymbolChain { head: h, tail: _ } => h + AssembleRejectionDetail { rejection_chain: _, cause: cause, at: _ } => + match cause { + OtherAssembleRefusal { reason: r } => r + _ => ^cause_not_other } } } test fn location_and_reason_are_drawn_from_the_same_diagnostic_holds() -> Bool { - paired_start_of_two() == 7 && paired_reason_of_two() == ^cause_location_probe_unclassified_reason + paired_start_of_two() == 99 && paired_reason_of_two() == ^cause_location_probe_second_reason } test fn textual_locus_reaches_the_textual_arm_holds() -> Bool { diff --git a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag b/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag deleted file mode 100644 index 9ca8b0bbc87..00000000000 --- a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag +++ /dev/null @@ -1,248 +0,0 @@ -module v2.test.self_host.frontier_probe_overlap_detail - -import v2.compiler.parse_diagnostic { - NoParseDiagnosticEvidence, - NonEmptyParseStageDiagnostics, - ParseStageDiagnostic, - non_empty_diagnostics_from_parse_stage, - parse_overlap_residue_stage_diagnostic_at -} -import v2.std.diagnostic { - Diagnostic, - ExternalContractUnknown, - NonEmptyDiagnostics, - Unavailable, - diagnostics_fatal_reason, - invariant_port_locus -} -import v2.std.grammar_choice_ambiguity { - GrammarChoiceAmbiguityRow, - NonEmptyGrammarChoiceAmbiguityRows -} -import v2.compiler.self_host.frontier_probe_assemble_detail { - assemble_rejection_cause_from_parse_stage, - frontier_probe_assemble_detail_from_parse_stage, - frontier_probe_assemble_detail_from_rejection_with_overlap_stage, - frontier_probe_receipt_from_assemble_rejection -} -import v2.compiler.self_host.frontier_probe_types { - AssembleRejectionDetail, - FrontierProbeDetailAbsent, - GrammarChoiceOverlap, - OtherAssembleRefusal, - NoFrontierProbeDetail, - ProbeStageEmit, - frontier_probe_receipt_without_assemble_detail, - RealizationGap -} -import std.optional { Absent, Present, optional_absent, optional_present } -import v2.std.algebra { Cons, Empty, length } - -fn sample_overlap_row() -> GrammarChoiceAmbiguityRow { - GrammarChoiceAmbiguityRow { - production: ^dag_production_expr, - left_first_terminals: Cons { head: ^dag_token_ident, tail: Empty }, - right_first_terminals: Cons { head: ^dag_token_kw_fn, tail: Empty }, - overlap_terminals: Empty, - both_nullable: true - } -} - -fn overlap_residue_stage_diagnostic(row: GrammarChoiceAmbiguityRow) -> ParseStageDiagnostic { - parse_overlap_residue_stage_diagnostic_at( - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^frontier_probe_overlap_detail_test_grammar_contract), - row: row - ) -} - -test fn overlap_evidence_binds_exact_row_not_global_roster() -> Bool { - let row = sample_overlap_row() - let detail = frontier_probe_assemble_detail_from_parse_stage( - d: NonEmptyParseStageDiagnostics { - head: overlap_residue_stage_diagnostic(row: row), - tail: Empty - } - ) - match detail { - AssembleRejectionDetail { - cause: GrammarChoiceOverlap { rows: rows }, - rejection_chain: _, - at: _ - } => - match rows { - NonEmptyGrammarChoiceAmbiguityRows { - head: bound, - tail: Empty - } => bound.production == row.production - } - _ => false - } -} - -fn residue_headed_stage_chain(row: GrammarChoiceAmbiguityRow, fatal: Symbol) -> NonEmptyParseStageDiagnostics { - NonEmptyParseStageDiagnostics { - head: overlap_residue_stage_diagnostic(row: row), - tail: [ - ParseStageDiagnostic { - diagnostic: Diagnostic { - reason: fatal, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: fatal), - correction: Unavailable { reason: ExternalContractUnknown } - }, - evidence: NoParseDiagnosticEvidence - } - ] - } -} - -// THE PROGRAM'S RED CONTROL AT THE ASSEMBLE CAUSE. Two refusals share the overlap residue as their -// head and differ in their fatal cause; the cause reader must report the two distinct fatals. -// Reading the head (or masking on it) gave both the same residue-shaped answer. -test fn a_shared_residue_head_yields_each_chains_own_fatal_cause_holds() -> Bool { - let row = sample_overlap_row() - let a = assemble_rejection_cause_from_parse_stage(d: residue_headed_stage_chain(row: row, fatal: ^normalize_rejected)) - let b = assemble_rejection_cause_from_parse_stage(d: residue_headed_stage_chain(row: row, fatal: ^body_lowering_reason_wrapper_retained_emitted)) - match a { - OtherAssembleRefusal { reason: ra } => - match b { - OtherAssembleRefusal { reason: rb } => - (ra == ^normalize_rejected) && (rb == ^body_lowering_reason_wrapper_retained_emitted) - _ => false - } - _ => false - } -} - -// THE RENDERING KEEPS THE HEAD. The receipt's rejection_chain is the whole chain in order, so the -// advisory head stays visible to an operator while the cause above is the fatal element. -test fn a_residue_headed_receipt_chain_keeps_the_advisory_head_holds() -> Bool { - let row = sample_overlap_row() - let stage = residue_headed_stage_chain(row: row, fatal: ^normalize_rejected) - let receipt = frontier_probe_receipt_from_assemble_rejection( - module_path: "src/v2/compiler/example.dag", - diagnostics: non_empty_diagnostics_from_parse_stage(d: stage), - detail: FrontierProbeDetailAbsent, - overlap_residue_stage: optional_present(value: stage) - ) - match receipt.assemble_detail { - AssembleRejectionDetail { rejection_chain: chain, cause: OtherAssembleRefusal { reason: r }, at: _ } => - (chain.head == ^parse_grammar_choice_overlap_residue) && (r == ^normalize_rejected) - && (receipt.located_reason == ^normalize_rejected) - _ => false - } -} - -test fn emit_stage_receipt_has_no_assemble_detail() -> Bool { - match frontier_probe_receipt_without_assemble_detail( - module_path: "src/v2/compiler/example.dag", - blocker: RealizationGap, - located_stage: ProbeStageEmit, - located_reason: ^probe_emit_accept_self_host_bar_unbound, - detail: FrontierProbeDetailAbsent - ).assemble_detail { - NoFrontierProbeDetail => true - _ => false - } -} - -test fn assemble_receipt_carries_nonempty_rejection_chain() -> Bool { - let row = sample_overlap_row() - let stage = NonEmptyParseStageDiagnostics { - head: overlap_residue_stage_diagnostic(row: row), - tail: Empty - } - let receipt = frontier_probe_receipt_from_assemble_rejection( - module_path: "src/v2/compiler/example.dag", - diagnostics: non_empty_diagnostics_from_parse_stage(d: stage), - detail: FrontierProbeDetailAbsent, - overlap_residue_stage: optional_present(value: stage) - ) - match receipt.assemble_detail { - AssembleRejectionDetail { rejection_chain: chain, cause: cause, at: _ } => - match cause { - GrammarChoiceOverlap { rows: rows } => - (chain.head == ^parse_grammar_choice_overlap_residue) - && (length(xs: chain.tail) == 0) - && (rows.head.production == row.production) - _ => false - } - _ => false - } -} - -test fn overlap_stage_attaches_evidence_when_diagnostics_stripped() -> Bool { - let row = sample_overlap_row() - let stage = NonEmptyParseStageDiagnostics { - head: overlap_residue_stage_diagnostic(row: row), - tail: Empty - } - let stripped = non_empty_diagnostics_from_parse_stage(d: stage) - let detail = frontier_probe_assemble_detail_from_rejection_with_overlap_stage( - d: stripped, - overlap_residue_stage: optional_present(value: stage) - ) - match detail { - AssembleRejectionDetail { - cause: GrammarChoiceOverlap { rows: rows }, - rejection_chain: _, - at: _ - } => - match rows { - NonEmptyGrammarChoiceAmbiguityRows { - head: bound, - tail: Empty - } => bound.production == row.production - } - _ => false - } -} - -test fn masked_overlap_located_reason_unmasks_terminal_refusal() -> Bool { - let masked = NonEmptyDiagnostics { - head: Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_grammar_choice_overlap_residue), - correction: Unavailable { reason: ExternalContractUnknown } - }, - tail: [ - Diagnostic { - reason: ^body_lowering_reason_wrapper_retained_emitted, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^body_lowering_reason_wrapper_retained_emitted), - correction: Unavailable { reason: ExternalContractUnknown } - } - ] - } - let located = diagnostics_fatal_reason(d: masked) - located == ^body_lowering_reason_wrapper_retained_emitted -} - -test fn overlap_only_chain_keeps_overlap_located_reason() -> Bool { - let overlap_only = NonEmptyDiagnostics { - head: Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_grammar_choice_overlap_residue), - correction: Unavailable { reason: ExternalContractUnknown } - }, - tail: Empty - } - let located = diagnostics_fatal_reason(d: overlap_only) - located == ^parse_grammar_choice_overlap_residue -} - -test fn masked_resolve_not_found_unmasks_located_reason() -> Bool { - let masked = NonEmptyDiagnostics { - head: Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_grammar_choice_overlap_residue), - correction: Unavailable { reason: ExternalContractUnknown } - }, - tail: [ - Diagnostic { - reason: ^resolve_module_not_found, - at: invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^resolve_module_not_found), - correction: Unavailable { reason: ExternalContractUnknown } - } - ] - } - diagnostics_fatal_reason(d: masked) == ^resolve_module_not_found -} diff --git a/src/v2/test/claim/self_host/frontier_probe_semantic_family_receipt_test.dag b/src/v2/test/claim/self_host/frontier_probe_semantic_family_receipt_test.dag deleted file mode 100644 index 288e1c65cfe..00000000000 --- a/src/v2/test/claim/self_host/frontier_probe_semantic_family_receipt_test.dag +++ /dev/null @@ -1,43 +0,0 @@ -module v2.test.claim.self_host.frontier_probe_semantic_family_receipt - -import v2.compiler.self_host.frontier_probe_semantic_family_receipt { - capped_shard_count, - every_capped_shard_exceeds_its_own_cap, - every_measured_chain_is_non_uniform, - measured_shard_count, - shard_count, - shards_naming_body_lowering_beneath_the_mask -} -import v2.std.text { String } - -// What these claims can and cannot do, stated so the coverage is not overread. The rows are -// OBSERVATIONS — a witness cannot re-derive them without re-running a twenty-minute probe per -// readout, so nothing here re-measures the door. What it does enforce is the internal discipline -// the rows must keep to stay readable as evidence: the five shards are partitioned into exactly two -// dispositions with no third state and no shard in both, a capped row's read count actually exceeds -// the cap it names (a row claiming a cap it does not breach would be a refusal with no cause), and -// a measured row's differing-symbol count sits strictly inside its chain length — zero would mean -// the chain was uniform and the masking finding would not follow, and equal to the length would -// mean the head appears nowhere in its own chain. The body-lowering count is pinned at 2 rather -// than asserted as 'both', so adding a third measured shard that lands elsewhere reds this instead -// of silently weakening the finding. - -test fn the_five_shards_partition_into_measured_and_capped_holds() -> Bool { - (shard_count() == 5) && (measured_shard_count() + capped_shard_count() == shard_count()) -} - -test fn two_shards_are_measured_and_three_are_capped_holds() -> Bool { - (measured_shard_count() == 2) && (capped_shard_count() == 3) -} - -test fn every_capped_shard_exceeds_the_cap_it_names_holds() -> Bool { - every_capped_shard_exceeds_its_own_cap() -} - -test fn both_measured_shards_name_body_lowering_beneath_the_mask_holds() -> Bool { - shards_naming_body_lowering_beneath_the_mask() == 2 -} - -test fn every_measured_chain_is_non_uniform_holds() -> Bool { - every_measured_chain_is_non_uniform() -} diff --git a/src/v2/test/claim/self_host/peano_nat_structural_realization_test.dag b/src/v2/test/claim/self_host/peano_nat_structural_realization_test.dag index f97735f41bc..46864307985 100644 --- a/src/v2/test/claim/self_host/peano_nat_structural_realization_test.dag +++ b/src/v2/test/claim/self_host/peano_nat_structural_realization_test.dag @@ -21,14 +21,14 @@ import std.operator_realization { } import std.syntax { BinOp, Lt, Le, Gt, Ge, Eq, Div, Mod, Sub, And, Add } import gunbc.structural_realization_bindings { literal_homomorphism_rows, structural_ordering_rows } -import std.nat { Nat, Zero, Succ, nat_compare } +import std.nat { Nat, Zero, Succ, nat_compare, nat_to_decimal_string, decimal_digit_nat, D0, D7 } import test.fixture.structural_peano_nat { StructuralNat, StructuralZero, StructuralSucc, StructuralNatDifference, StructuralNatSubtrahendExceedsMinuend, StructuralNatQuotientRemainder, StructuralNatDivisionByZero, structural_nat_compare, structural_nat_sub, structural_nat_div_rem, structural_nat_div_rem_by_succ } -import v2.std.integer { integer_nat_to_decimal_string, integer_int_to_decimal_string, decimal_digit_nat, D0, D7 } +import std.integer { int_to_decimal_string } // FALSIFIERS FOR THE STRUCTURAL PEANO NAT REALIZATION (XL-0N, adhoc-aec65f93-b00), the // interpreter-executed half. The emitted-bytes half -- that a kernel literal at a test.fixture.structural_peano_nat.StructuralNat @@ -99,10 +99,13 @@ test fn nat_div_rem_by_succ_has_no_zero_arm_and_divides() -> Bool { fn std_seven() -> Nat { Succ { prev: Succ { prev: Succ { prev: Succ { prev: Succ { prev: Succ { prev: Succ { prev: Zero } } } } } } } } test fn nat_to_decimal_converts_nontrivial_values() -> Bool { - integer_nat_to_decimal_string(value: Zero) == "0" - && integer_nat_to_decimal_string(value: std_seven()) == "7" - && integer_nat_to_decimal_string(value: Succ { prev: Succ { prev: Succ { prev: std_seven() } } }) == "10" - && integer_nat_to_decimal_string(value: 23) == "23" + nat_to_decimal_string(value: Zero) == "0" + && nat_to_decimal_string(value: std_seven()) == "7" + && nat_to_decimal_string(value: Succ { prev: Succ { prev: Succ { prev: std_seven() } } }) == "10" + && nat_to_decimal_string(value: 23) == "23" + && nat_to_decimal_string(value: 9) == "9" + && nat_to_decimal_string(value: 19) == "19" + && nat_to_decimal_string(value: 100) == "100" } test fn decimal_digit_nat_supplies_the_peano_value() -> Bool { @@ -130,10 +133,19 @@ test fn structural_peano_constructors_stay_constructors() -> Bool { } test fn int_to_decimal_is_the_machine_integer_formatter() -> Bool { - integer_int_to_decimal_string(value: 0) == "0" - && integer_int_to_decimal_string(value: 42) == "42" - && integer_int_to_decimal_string(value: 0 - 7) == "-7" - && integer_int_to_decimal_string(value: 1000) == "1000" + int_to_decimal_string(value: 0) == "0" + && int_to_decimal_string(value: 42) == "42" + && int_to_decimal_string(value: 0 - 7) == "-7" + && int_to_decimal_string(value: 1000) == "1000" + && int_to_decimal_string(value: 0 - 10) == "-10" + && int_to_decimal_string(value: 0 - 19) == "-19" + && int_to_decimal_string(value: 99) == "99" +} + +// The minimum host integer has no representable negation, so a renderer that negates before it +// divides overflows here; this one divides the signed value and never negates it. +test fn int_to_decimal_renders_the_minimum_machine_integer() -> Bool { + int_to_decimal_string(value: 0 - 9223372036854775807 - 1) == "-9223372036854775808" } // --------------------------------------------------------------------------------------------- diff --git a/src/v2/test/claim/self_host/stage0_production_target_test.dag b/src/v2/test/claim/self_host/stage0_production_target_test.dag index cb4fca7dedd..4e711038ca4 100644 --- a/src/v2/test/claim/self_host/stage0_production_target_test.dag +++ b/src/v2/test/claim/self_host/stage0_production_target_test.dag @@ -13,6 +13,7 @@ import v2.compiler.self_host.stage0_production_target { stage0_production_target_model } import v2.compiler.source_authority { DagSourceReadWitness, SourceRef } +import v2.std.cross_tree.import_model { V2Tree } import v2.compiler.source_authority_read { source_ref_for_observed_storage_path, source_root_ingest_from_source_refs @@ -232,7 +233,7 @@ type Stage0BoundaryRead fn stage0_boundary_read_of(path: String) -> Stage0BoundaryRead { match source_root_ingest_from_source_refs( - refs: Cons { head: source_ref_for_observed_storage_path(path: path), tail: Empty } + refs: Cons { head: source_ref_for_observed_storage_path(path: path, source_root: V2Tree), tail: Empty } ) { Rejected { diagnostics: _ } => Stage0BoundaryReadRefused Accepted { value: ingest, diagnostics: _ } => diff --git a/src/v2/test/claim/service_realization_conformance_test.dag b/src/v2/test/claim/service_realization_conformance_test.dag index fda1cdc6e06..e05647e8159 100644 --- a/src/v2/test/claim/service_realization_conformance_test.dag +++ b/src/v2/test/claim/service_realization_conformance_test.dag @@ -13,7 +13,7 @@ import v2.std.service_realization_labels { realization_arm_payload_label, realization_status_code_label } -import v2.compiler.symbol_index_fill { symbol_index_fill_containment_node } +import v2.compiler.symbol_index_fill { disj_variant_counts_in_module, symbol_index_fill_containment_node } import v2.extdeps.languages.dag { dag_int_literal_node_from_lexeme, dag_string_literal_node } import v2.std.arrow_signature { declared_signature_arrow } import v2.std.node_query { binder_edge } @@ -208,7 +208,13 @@ fn sr_module(realization_label: Symbol) -> Node { } fn sr_index(module: Node) -> SymbolIndex { - symbol_index_fill_containment_node(index: empty_symbol_index(), path: Cons { head: ^m, tail: Empty }, node: module) + symbol_index_fill_containment_node( + index: empty_symbol_index(), + path: Cons { head: ^m, tail: Empty }, + node: module, + counts: disj_variant_counts_in_module(root: module), + module_qn: Cons { head: ^m, tail: Empty } + ) } fn sr_indexed(index: SymbolIndex, path: List) -> Bool { diff --git a/src/v2/test/claim/source_root_tagging_test.dag b/src/v2/test/claim/source_root_tagging_test.dag index 4130e059635..015d6032123 100644 --- a/src/v2/test/claim/source_root_tagging_test.dag +++ b/src/v2/test/claim/source_root_tagging_test.dag @@ -1,11 +1,21 @@ module v2.test.claim.source_root_tagging -import v2.std.live_tree { LiveTreeDisposition } -import v2.compiler.source_authority { DagSourceReadWitness } +import gunbc.source_root_read { SourceRootFile } +import v2.compiler.source_authority { + DagSourceReadWitness, + SourceRootArgumentAdmitted, + SourceRootArgumentUnrecognized, + SourceRootIngest, + SourceRootIngestFromFilesBuilt, + SourceRootIngestFromFilesUnrecognizedRoot, + admit_source_root_argument, + source_root_ingest_from_files, + source_root_set_from_ingest +} import v2.std.artifact { Artifact, SourceFile } import extdeps.communication.medium { Lossless, Medium } -import v2.compiler.source_authority { DagSourceReadWitness, source_root_set_from_ingest } import v2.std.cross_tree.import_model { DagTree, V2Tree, source_root_set_contains } +import std.algebra { Cons, Empty } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -36,6 +46,78 @@ data sr_tag_dag_read: DagSourceReadWitness = DagSourceReadWitness { data sr_tag_mixed_ingest: SourceRootIngest = [sr_tag_v2_read, sr_tag_dag_read] data sr_tag_v2_only_ingest: SourceRootIngest = [sr_tag_v2_read] +test fn a_dag_source_root_argument_is_dag_tree() -> Bool { + match admit_source_root_argument(argument: "dag") { + SourceRootArgumentAdmitted { tree: DagTree } => true + SourceRootArgumentAdmitted { tree: V2Tree } => false + SourceRootArgumentUnrecognized { argument: _ } => false + } +} + +test fn a_v2_source_root_argument_is_v2_tree() -> Bool { + match admit_source_root_argument(argument: "src/v2") { + SourceRootArgumentAdmitted { tree: V2Tree } => true + SourceRootArgumentAdmitted { tree: DagTree } => false + SourceRootArgumentUnrecognized { argument: _ } => false + } +} + +test fn a_copied_single_root_argument_is_unrecognized() -> Bool { + match admit_source_root_argument(argument: "/tmp/copied-closure") { + SourceRootArgumentUnrecognized { argument: argument } => argument == "/tmp/copied-closure" + SourceRootArgumentAdmitted { tree: _ } => false + } +} + +fn sr_file(path: String, origin: String) -> SourceRootFile { + SourceRootFile { path: path, content: "module fixture.sr_tag\n", origin: origin } +} + +test fn a_dag_file_whose_path_contains_src_is_still_dag_tree() -> Bool { + match source_root_ingest_from_files( + files: [sr_file(path: "dag/pkg/src/leaf.dag", origin: "dag")] + ) { + SourceRootIngestFromFilesUnrecognizedRoot { argument: _, path: _ } => false + SourceRootIngestFromFilesBuilt { ingest: ingest } => + match ingest { + Empty => false + Cons { head: read, tail: rest } => + match rest { + Empty => match read.source_root { + DagTree => true + V2Tree => false + } + Cons { head: _, tail: _ } => false + } + } + } +} + +test fn ingest_from_an_unrecognized_origin_refuses() -> Bool { + match source_root_ingest_from_files( + files: [sr_file(path: "/tmp/copied-closure/src/v2/std/algebra.dag", origin: "/tmp/copied-closure")] + ) { + SourceRootIngestFromFilesUnrecognizedRoot { argument: argument, path: path } => + (argument == "/tmp/copied-closure") + && (path == "/tmp/copied-closure/src/v2/std/algebra.dag") + SourceRootIngestFromFilesBuilt { ingest: _ } => false + } +} + +test fn two_root_origins_tag_each_tree() -> Bool { + match source_root_ingest_from_files( + files: [ + sr_file(path: "dag/std/algebra.dag", origin: "dag"), + sr_file(path: "src/v2/std/algebra.dag", origin: "src/v2") + ] + ) { + SourceRootIngestFromFilesUnrecognizedRoot { argument: _, path: _ } => false + SourceRootIngestFromFilesBuilt { ingest: ingest } => + source_root_set_contains(roots: source_root_set_from_ingest(ingest: ingest), r: DagTree) + && source_root_set_contains(roots: source_root_set_from_ingest(ingest: ingest), r: V2Tree) + } +} + test fn source_root_tag_carried_and_folded_to_grounded_set() -> Bool { let mixed = source_root_set_from_ingest(ingest: sr_tag_mixed_ingest) let v2_only = source_root_set_from_ingest(ingest: sr_tag_v2_only_ingest) diff --git a/src/v2/test/claim/staging_test.dag b/src/v2/test/claim/staging_test.dag index adbed81ab58..325cee68ed7 100644 --- a/src/v2/test/claim/staging_test.dag +++ b/src/v2/test/claim/staging_test.dag @@ -37,16 +37,3 @@ test fn then_outcome_builder_capture() -> Bool { let stage = then_outcome(f: add5, g: s_double) acc_int(o: stage(1), dflt: 0) == 12 } - -fn lookup_always_42(n: Int) -> CacheProbe { Hit { value: 42 } } -fn lookup_never(n: Int) -> CacheProbe { Miss } - -test fn cached_stage_hit_skips_stage() -> Bool { - let staged = cached_stage(lookup: lookup_always_42, stage: s_double) - acc_int(o: staged(5), dflt: 0) == 42 -} - -test fn cached_stage_miss_runs_stage() -> Bool { - let staged = cached_stage(lookup: lookup_never, stage: s_double) - acc_int(o: staged(5), dflt: 0) == 10 -} diff --git a/src/v2/test/claim/std/named_child_lookup_test.dag b/src/v2/test/claim/std/named_child_lookup_test.dag index 913b948c7a1..2da18caa806 100644 --- a/src/v2/test/claim/std/named_child_lookup_test.dag +++ b/src/v2/test/claim/std/named_child_lookup_test.dag @@ -73,6 +73,27 @@ test fn named_child_lookup_decides_all_three_states_in_one_pass() -> Bool { && ncl_is_missing(lookup: named_child_lookup(root: ncl_atom(s: ^ncl_not_a_conj), name: ^ncl_unique)) } +// A lowered call is a ComputationNode whose authored edges carry its named arguments +// (body-lowering mints comp:transform[ *=atom:callee[] @arg=atom:... ]). The lookup must read +// them: this arm goes red against the pre-fix behavior, which answered NamedChildMissing for +// every ComputationNode regardless of its children. +fn ncl_transform() -> Node { + Node { + kind: ComputationNode { behavior: Transform }, + children: [ + Edge { label: Positional, target: ncl_atom(s: ^ncl_callee) }, + ncl_named(name: ^ncl_arg, target: ncl_atom(s: ^ncl_arg_value)) + ], + occurrence_id: OccurrenceSynthetic + } +} + +test fn named_child_lookup_reads_a_lowered_call_s_named_args_holds() -> Bool { + ncl_found_is(lookup: named_child_lookup(root: ncl_transform(), name: ^ncl_arg), expected: ncl_atom(s: ^ncl_arg_value)) + && ncl_is_missing(lookup: named_child_lookup(root: ncl_transform(), name: ^ncl_absent)) + && ncl_is_missing(lookup: named_child_lookup(root: ncl_transform(), name: ^ncl_unique)) +} + test fn find_named_child_projects_the_lookup_with_its_refusal_reasons() -> Bool { ncl_accepted_is(o: find_named_child(root: ncl_conj(), name: ^ncl_unique), expected: ncl_atom(s: ^ncl_unique_target)) && ncl_rejected_with(o: find_named_child(root: ncl_conj(), name: ^ncl_repeated), reason: ^named_child_ambiguous) diff --git a/src/v2/test/claim/symbol_index/containment_test.dag b/src/v2/test/claim/symbol_index/containment_test.dag index 3f999dc7a5e..1cc515dc903 100644 --- a/src/v2/test/claim/symbol_index/containment_test.dag +++ b/src/v2/test/claim/symbol_index/containment_test.dag @@ -99,7 +99,9 @@ data si_unique_index: SymbolIndex = symbol_index_fill_module_root( data si_unique_index_containment_only: SymbolIndex = symbol_index_fill_containment_node( index: empty_symbol_index(), path: si_unique_module_qn, - node: si_unique_module_root_node + node: si_unique_module_root_node, + counts: disj_variant_counts_in_module(root: si_unique_module_root_node), + module_qn: si_unique_module_qn ) test fn symbol_index_materializes_type_variant_paths_holds() -> Bool { @@ -237,18 +239,34 @@ test fn symbol_index_unique_variant_alias_at_module_holds() -> Bool { } } -test fn symbol_index_unique_variant_without_alias_red_control_holds() -> Bool { +// THE COUNT RULE IS THE WITHOUT SIDE. The module-level variant alias exists only where the tag is +// declared by ONE coproduct in the module (disj_variant_counts_in_module): this module declares +// ^Yes twice (^Mood and ^Classical), so the fused containment walk mints nothing for it — the +// WITHOUT side — while the unique module mints for ^Yes — the WITH side. A fill that minted a +// module alias for a twice-declared tag reds the first arm; a containment walk that stopped +// minting reds the second. THE PAIR IS THE EVIDENCE, NOT EITHER ARM. +test fn symbol_index_module_variant_alias_requires_a_unique_count_holds() -> Bool { match symbol_index_lexical_lookup( - index: si_unique_index_containment_only, - position: si_unique_module_qn, + index: si_index, + position: si_module_qn, name: ^Yes ) { + LexicalUnbound => + match symbol_index_lexical_lookup( + index: si_unique_index, + position: si_unique_module_qn, + name: ^Yes + ) { + LexicalHit { node: _, path: _ } => true + LexicalAmbiguous { candidates: _ } => false + LexicalUnbound => false + } LexicalHit { node: _, path: _ } => false LexicalAmbiguous { candidates: _ } => false - LexicalUnbound => true } } + test fn symbol_index_variant_visibility_equivalence_holds() -> Bool { match symbol_index_lexical_lookup( index: si_unique_index, diff --git a/src/v2/test/claim/variant_field_infer_probe_test.dag b/src/v2/test/claim/variant_field_infer_probe_test.dag new file mode 100644 index 00000000000..b45cbb9214f --- /dev/null +++ b/src/v2/test/claim/variant_field_infer_probe_test.dag @@ -0,0 +1,720 @@ +module v2.test.claim.variant_field_infer_probe + +import v2.compiler.infer { + ConstructIsRecord, + ConstructIsVariantArm, + ConstructNotARecord, + infer, + infer_record_construct_read, + inferred_facts_resolved_type +} +import v2.std.qualified_name { declaration_reference_path_optional, qualified_name_last_segment } +import v2.std.witness { Holds, Violates } +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.program_assembly { assemble_program_from_ingest } +import v2.compiler.source_authority { DagSourceReadWitness } +import v2.extdeps.languages.dag { dag_language_model } +import extdeps.communication.medium { Lossless, Medium } +import std.algebra { Cons, Empty, list_append } +import v2.std.cross_tree.import_model { V2Tree } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, None, Outcome, Rejected, Some } +import v2.std.algebra { contains, list_map } +import v2.compiler.inferred_tree { ObligatedInferredTree } +import v2.std.node_query { construct_tag_optional } +import v2.std.node { + Atom, + ComputationNode, + CoreMarker, + FieldProjectionBaseEdge, + Match, + MatchArmBodyEdge, + Node, + Positional, + StructuralLabel, + Symbol, + TypeNode, + symbol_lexeme +} +import std.optional { Absent, Optional, Present } +import v2.std.text { String } +import v2.std.collection { List } +import gunbc.cli_wire { CliWireResponse, CliWirePrintable } +import std.process { ExitSuccess } + +fn vfp_assemble(src: String) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: vfp_variant_artifact, + compilation_unit: ^vfp_variant_cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: ^p, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +fn vfp_reasons(r: NonEmptyDiagnostics) -> List { + Cons { head: r.head.reason, tail: list_map(xs: r.tail, f: fn(d) { d.reason }) } +} + +fn vfp_last(xs: List) -> Symbol { + fold(xs, init: ^vfp_no_reason, f: fn(acc, x) { x }) +} + +fn vfp_find_match(n: Node) -> Optional { + match n.kind { + ComputationNode { behavior: Match } => Present { value: n } + _ => fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => vfp_find_match(n: e.target) + } + }) + } +} + +fn vfp_find_construction(n: Node) -> Optional { + match construct_tag_optional(n: n) { + Present { value: _ } => Present { value: n } + Absent => fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => vfp_find_construction(n: e.target) + } + }) + } +} + +// THE PROJECTION FINDER: preorder for the FieldProjection conj (base edge first). The projection +// now stands as a fn body (pick), not an arm body, so it needs its own walk. +fn vfp_projection_shape(n: Node) -> Optional { + match n.kind { + TypeNode { connective: Conj } => + fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match e.label { + StructuralLabel { label: CoreMarker { marker: FieldProjectionBaseEdge } } => Present { value: n } + _ => Absent + } + } + }) + _ => Absent + } +} + +fn vfp_find_projection(n: Node) -> Optional { + match vfp_projection_shape(n: n) { + Present { value: p } => Present { value: p } + Absent => fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => vfp_find_projection(n: e.target) + } + }) + } +} + +// THE MATCH SCRUTINEE: the match's first positional child (the caller's match is optional, so the +// optional unwraps here like vfp_arm_body_first does). +fn vfp_match_scrutinee(m: Optional) -> Optional { + match m { + Present { value: n } => + fold(n.children, init: Absent, f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match e.label { + Positional => Present { value: e.target } + _ => Absent + } + } + }) + Absent => Absent + } +} + +fn vfp_type_name(t: Node) -> Symbol { + match declaration_reference_path_optional(node: t) { + Present { value: path } => + match qualified_name_last_segment(qn: path) { + Present { value: s } => s + Absent => ^vfp_empty_path + } + Absent => + match t.kind { + TypeNode { connective: Atom { identity: s } } => s + _ => ^vfp_type_not_named + } + } +} + + +fn vfp_fact_in(inferred: ObligatedInferredTree, subject: Optional) -> Symbol { + match subject { + Absent => ^vfp_subject_absent + Present { value: s } => + match inferred.facts.lookup(s) { + Absent => ^vfp_no_facts + Present { value: facts } => + match inferred_facts_resolved_type(facts: facts) { + Violates { diagnostic: _ } => ^vfp_underived + Holds { value: t } => vfp_type_name(t: t) + } + } + } +} + +fn vfp_reports(reading: VfpReading, reason: Symbol) -> Bool { + contains(xs: reading.reasons, item: reason, eq: fn(a, b) { a == b }) +} + +fn vfp_infers(reading: VfpReading) -> Bool { + match reading.reasons { + Cons { head: h, tail: _ } => h == ^infers + Empty => false + } +} + +// THE CONSTRUCT READ, DECODED: what infer_record_construct_read answers for the fixture's +// construction, read directly on the resolved tree. The variant arm is named by its declaring +// path's last segment (^Boxed when the read mints the variant row); the sentinels say where the +// read stopped instead. TEMPORARY decode instrument: goes out with the RFM retirement once the +// per-identity evidence is recorded. +fn vfp_construct_read_decode(resolved: ResolvedTree) -> Symbol { + match vfp_find_construction(n: resolved.root) { + Absent => ^vfp_read_subject_absent + Present { value: node } => + match infer_record_construct_read(node: node, resolved: resolved) { + ConstructIsRecord { construct: _ } => ^vfp_read_record + ConstructIsVariantArm { construct: c } => + match qualified_name_last_segment(qn: c.arm_path) { + Present { value: s } => s + Absent => ^vfp_read_arm_path_absent + } + ConstructNotARecord => ^vfp_read_not_a_record + } + } +} + +// THE CARRIER HALF OF THE SAME DECODE: what the read names as the variant's declaring coproduct +// (^Seal for the fixture) -- the field the alias mechanism exists to fill, since the tag's own +// path (the module-level alias `p.Boxed`) does not carry it. TEMPORARY: goes out with the RFM +// retirement. +fn vfp_construct_read_carrier_decode(resolved: ResolvedTree) -> Symbol { + match vfp_find_construction(n: resolved.root) { + Absent => ^vfp_read_subject_absent + Present { value: node } => + match infer_record_construct_read(node: node, resolved: resolved) { + ConstructIsRecord { construct: _ } => ^vfp_read_record + ConstructIsVariantArm { construct: c } => + match qualified_name_last_segment(qn: c.variant_path) { + Present { value: s } => s + Absent => ^vfp_read_no_carrier + } + ConstructNotARecord => ^vfp_read_not_a_record + } + } +} + +// WHAT THE FIXTURE ESTABLISHES, decided once per program by one nullary producer enrolled WARM in +// v2.workflow.floor_pure_producer_share. `reasons` is the whole chain in order: a resolve or infer +// refusal's reasons, or `^infers` followed by the accepted inference's advisories. A FACT field is +// the picked node's resolved type name on an accepted inference, or the sentinels where the route +// stops: ^vfp_subject_absent when the finder found nothing, ^vfp_no_facts when the node carries no +// facts, ^vfp_underived when the facts do not resolve, or the refusal's reason (the head at resolve, +// the last at infer). +type VfpReading { + reasons: List + construction_fact: Symbol + match_fact: Symbol + arm_body_fact: Symbol + construct_read: Symbol + construct_read_carrier: Symbol +} + +// THE COMBINED FIXTURE'S READING: one module exercising the shapes the enrolled claims measure +// (a named payload binder, a nullary arm, a variant construction, a monomorphic record +// projection). ONE nullary producer so every planned claim reaches one +// closed computation identity and the derived producer-share admits it -- per-claim re-runs of +// this fill refused the enrolment margin (~609k eval steps each). +type VfpFixtureReading { + reasons: List + construct_read: Symbol + construct_read_carrier: Symbol + rt_construction_fact: Symbol + rt_match_fact: Symbol + rt_arm_fact: Symbol + nullary_arm_fact: Symbol + projection_fact: Symbol +} + +// THE ARROW FIXTURE'S READING: the module whose match scrutinee is a NULLARY CALL -- the N7 shape +// (g_tokenize_parse matching dag_prepared_grammar()). call_result_fact reads the scrutinee, i.e. +// the call's derived result type; arrow_arm_fact reads the arm's payload binder. +type VfpArrowReading { + reasons: List + construct_read: Symbol + call_result_fact: Symbol + arrow_arm_fact: Symbol +} + +// PREORDER COLLECTORS over the resolved tree: all matches, and a match's arm bodies in order. +fn vfp_matches_of(n: Node) -> List { vfp_matches_of_at(n: n, depth: 24) } + +fn vfp_matches_of_at(n: Node, depth: Int) -> List { + match depth <= 0 { + true => Empty + false => + match n.kind { + ComputationNode { behavior: Match } => + Cons { head: n, tail: fold(n.children, init: Empty, f: fn(acc, e) { list_append(left: acc, right: vfp_matches_of_at(n: e.target, depth: depth - 1)) }) } + _ => + fold(n.children, init: Empty, f: fn(acc, e) { list_append(left: acc, right: vfp_matches_of_at(n: e.target, depth: depth - 1)) }) + } + } +} + +fn vfp_arm_bodies_of(m: Node) -> List { vfp_arm_bodies_of_at(m: m, depth: 24) } + +// The arm-body edges hang under the match's ARM nodes, not under the match itself, so the walk +// descends and stays bound (a fixture has no nested matches; the bound guards a cyclic tree). +fn vfp_arm_bodies_of_at(m: Node, depth: Int) -> List { + match depth <= 0 { + true => Empty + false => + fold(m.children, init: Empty, f: fn(acc, e) { + match e.label { + StructuralLabel { label: CoreMarker { marker: MatchArmBodyEdge } } => list_append(left: acc, right: Cons { head: e.target, tail: vfp_arm_bodies_of_at(m: e.target, depth: depth - 1) }) + _ => list_append(left: acc, right: vfp_arm_bodies_of_at(m: e.target, depth: depth - 1)) + } + }) + } +} + +fn vfp_first_of(xs: List) -> Optional { + match xs { + Cons { head: h, tail: _ } => Present { value: h } + Empty => Absent + } +} + +fn vfp_second_of(xs: List) -> Optional { + match xs { + Cons { head: _, tail: t } => vfp_first_of(xs: t) + Empty => Absent + } +} + +fn vfp_arm_body_first(m: Optional) -> Optional { + match m { + Absent => Absent + Present { value: n } => vfp_first_of(xs: vfp_arm_bodies_of(m: n)) + } +} + +fn vfp_arm_body_second(m: Optional) -> Optional { + match m { + Absent => Absent + Present { value: n } => vfp_second_of(xs: vfp_arm_bodies_of(m: n)) + } +} + +// THE EXHAUSTIVENESS CONTROL: the same carrier with one arm dropped, so the match must refuse for +// want of coverage -- decided by the single inhabitance relation, which surfaces the established +// non-exhaustive reason (the same one the inline count it replaced reported). + +// THE TEXT ROUTE (DESIGN 3: the subject as authored source). The fact-level probes read the REAL +// pipeline -- assemble_program_from_ingest, the same tokenize/parse/normalize/resolve/admission the +// corpus compile runs -- over authored fixture modules. The earlier hand-lowered route +// (ClaimLoweredModule values built node by node) is retired: reproducing the real lowering's +// conventions by hand made the probe a second copy of the lowering rather than an observation of +// it, and its binder minting refused on exactly the conventions it copied (DESIGN 5: the reading +// must come from the thing itself). The resolve-side and refusal controls ride the same route +// (vfp_partial_src). + +data vfp_fixture_src: String = "module vfp_fixture\n\ntype ParseTree {\n root: Int\n}\n\ntype Verdict = Pass { at: Int } | Fail\n\nfn make() -> Verdict {\n Pass { at: 1 }\n}\n\nfn unbox(s: Verdict) -> Int {\n match s {\n Pass { at: b1 } => b1\n Fail => 0\n }\n}\n\nfn pick(t: ParseTree) -> Int {\n t.root\n}" + +// THE ARROW FIXTURE (the N7 shape): h's match scrutinee is the NULLARY CALL g(), and the arm binds +// the payload with the SHORTHAND binder (name == field name) -- the two halves of the shape that +// dies on the native route (g_tokenize_parse matching dag_prepared_grammar(), binding `d`). +data vfp_arrow_src: String = "module vfp_arrow\n\ntype Result2 = Ok { v: Int } | Bad\n\nfn g() -> Result2 {\n Ok { v: 7 }\n}\n\nfn h() -> Int {\n match g() {\n Ok { v } => v\n Bad => 0\n }\n}" + +// Each fixture assembles under its own artifact so the two programs never share a source identity; +// the admission subject must match the module's own declaration. +fn vfp_assemble_module(src: String, name: Symbol, artifact: Artifact, cu: Symbol) -> Outcome { + assemble_program_from_ingest( + ingest: Cons { + head: DagSourceReadWitness { + source: Medium { carried: src, fidelity: Lossless }, + artifact: artifact, + compilation_unit: cu, + source_root: V2Tree + }, + tail: Empty + }, + admission: Admission { subject: ResolutionSubject { name: Cons { head: name, tail: Empty } }, imports: Empty }, + lm: dag_language_model() + ) +} + +data vfp_variant_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^variant_field_infer_probe_variant_artifact, + file_path: "src/v2/pilot/variant_field_infer_probe_variant_pilot.dag" +} + +data vfp_fixture_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^variant_field_infer_probe_fixture_artifact, + file_path: "src/v2/pilot/variant_field_infer_probe_fixture_pilot.dag" +} + +data vfp_arrow_artifact: Artifact = Artifact { + kind: SourceFile, + id: ^variant_field_infer_probe_arrow_artifact, + file_path: "src/v2/pilot/variant_field_infer_probe_arrow_pilot.dag" +} + +// THE WARM PRODUCERS: vfp_fixture_resolved is what the probe claims share; vfp_fixture_resolved_direct +// is its own identity so the long-home claim that must bill its own frame is not admitted by the +// derived producer-share. +fn vfp_fixture_resolved() -> Outcome { + vfp_assemble_module(src: vfp_fixture_src, name: ^vfp_fixture, artifact: vfp_fixture_artifact, cu: ^vfp_fixture_cu) +} + +fn vfp_fixture_resolved_direct() -> Outcome { + vfp_assemble_module(src: vfp_fixture_src, name: ^vfp_fixture, artifact: vfp_fixture_artifact, cu: ^vfp_fixture_cu) +} + +fn vfp_arrow_resolved() -> Outcome { + vfp_assemble_module(src: vfp_arrow_src, name: ^vfp_arrow, artifact: vfp_arrow_artifact, cu: ^vfp_arrow_cu) +} + +fn vfp_partial_reading() -> VfpReading { vfp_reading(src: vfp_partial_src) } + +// THE ARROW FIXTURE, ITS OWN MODULE: the payload field's type is an arrow whose RESULT is the +// formal (`f: fn(ParseTree) -> T`) -- the one shape the match_binder fixture never exercises, and +// the current suspect for the grounding cascade. It rides alone so a refusal here cannot take the +// other readings' facts down with it, and it carries a second consumer claim (its resolve-side +// construct read) so the derived producer-share admits its fill. + +data vfp_partial_src: String = "module p\n\ntype Seal = Boxed(Int) | Held { value: Int }\n\nfn partial(s: Seal) -> Int {\n match s {\n Boxed(v) => v\n }\n}" + + + +fn vfp_reading(src: String) -> VfpReading { + match vfp_assemble(src: src) { + Rejected { diagnostics: r } => VfpReading { + reasons: vfp_reasons(r: r), + construction_fact: r.head.reason, + match_fact: r.head.reason, + arm_body_fact: r.head.reason, + construct_read: ^vfp_refused_at_resolve, + construct_read_carrier: ^vfp_refused_at_resolve + } + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: r } => VfpReading { + reasons: vfp_reasons(r: r), + construction_fact: vfp_last(xs: vfp_reasons(r: r)), + match_fact: vfp_last(xs: vfp_reasons(r: r)), + arm_body_fact: vfp_last(xs: vfp_reasons(r: r)), + construct_read: vfp_construct_read_decode(resolved: resolved), + construct_read_carrier: vfp_construct_read_carrier_decode(resolved: resolved) + } + Accepted { value: inferred, diagnostics: d } => VfpReading { + reasons: match d { + None => [^infers] + Some { diagnostics: r } => Cons { head: ^infers, tail: vfp_reasons(r: r) } + }, + construction_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_find_construction(n: resolved.root) + ), + match_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_find_match(n: resolved.root) + ), + arm_body_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_arm_body_first(m: vfp_find_match(n: resolved.root)) + ), + construct_read: vfp_construct_read_decode(resolved: resolved), + construct_read_carrier: vfp_construct_read_carrier_decode(resolved: resolved) + } + } + } +} + +// THE COMBINED FIXTURE'S PRODUCER: matches appear in fixture declaration order -- unbox, pick, +// unkeep, useit. The first construct-tagged node in preorder is make's `Boxed(n)`. +fn vfp_fixture_reading_over(resolved: Outcome) -> VfpFixtureReading { + match resolved { + Rejected { diagnostics: r } => VfpFixtureReading { + reasons: vfp_reasons(r: r), + construct_read: ^vfp_refused_at_resolve, + construct_read_carrier: ^vfp_refused_at_resolve, + rt_construction_fact: r.head.reason, + rt_match_fact: r.head.reason, + rt_arm_fact: r.head.reason, + nullary_arm_fact: r.head.reason, + projection_fact: r.head.reason + } + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: r } => VfpFixtureReading { + reasons: Cons { head: ^infers, tail: vfp_reasons(r: r) }, + construct_read: vfp_construct_read_decode(resolved: resolved), + construct_read_carrier: vfp_construct_read_carrier_decode(resolved: resolved), + rt_construction_fact: vfp_last(xs: vfp_reasons(r: r)), + rt_match_fact: vfp_last(xs: vfp_reasons(r: r)), + rt_arm_fact: vfp_last(xs: vfp_reasons(r: r)), + nullary_arm_fact: vfp_last(xs: vfp_reasons(r: r)), + projection_fact: vfp_last(xs: vfp_reasons(r: r)) + } + Accepted { value: inferred, diagnostics: d } => VfpFixtureReading { + reasons: match d { + None => [^infers] + Some { diagnostics: r } => Cons { head: ^infers, tail: vfp_reasons(r: r) } + }, + construct_read: vfp_construct_read_decode(resolved: resolved), + construct_read_carrier: vfp_construct_read_carrier_decode(resolved: resolved), + rt_construction_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_find_construction(n: resolved.root) + ), + rt_match_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_first_of(xs: vfp_matches_of(n: resolved.root)) + ), + rt_arm_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_arm_body_first(m: vfp_first_of(xs: vfp_matches_of(n: resolved.root))) + ), + nullary_arm_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_arm_body_second(m: vfp_first_of(xs: vfp_matches_of(n: resolved.root))) + ), + projection_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_find_projection(n: resolved.root) + ) + } + } + } +} + +// THE COMBINED FIXTURE'S PRODUCER: matches appear in fixture declaration order -- unbox, pick, +// unkeep. The first construct-tagged node in preorder is make's `Boxed(n)`. +fn vfp_fixture_reading() -> VfpFixtureReading { + vfp_fixture_reading_over(resolved: vfp_fixture_resolved()) +} + +// THE COMBINED FIXTURE'S OWN-FRAME READING: a separate computation identity from the warm producer +// so the long-home claim that must bill its own frame is not admitted by the derived +// producer-share. +fn vfp_reading_direct() -> VfpReading { + let r = vfp_fixture_reading_over(resolved: vfp_fixture_resolved_direct()) + VfpReading { + reasons: r.reasons, + construction_fact: r.rt_construction_fact, + match_fact: r.rt_match_fact, + arm_body_fact: r.rt_arm_fact, + construct_read: r.construct_read, + construct_read_carrier: r.construct_read_carrier + } +} + +fn vfp_arrow_reading_over(resolved: Outcome) -> VfpArrowReading { + match resolved { + Rejected { diagnostics: r } => VfpArrowReading { + reasons: vfp_reasons(r: r), + construct_read: ^vfp_refused_at_resolve, + call_result_fact: r.head.reason, + arrow_arm_fact: r.head.reason + } + Accepted { value: resolved, diagnostics: _ } => + match infer(tree: resolved) { + Rejected { diagnostics: r } => VfpArrowReading { + reasons: vfp_reasons(r: r), + construct_read: vfp_construct_read_decode(resolved: resolved), + call_result_fact: vfp_last(xs: vfp_reasons(r: r)), + arrow_arm_fact: vfp_last(xs: vfp_reasons(r: r)) + } + Accepted { value: inferred, diagnostics: d } => VfpArrowReading { + reasons: match d { + None => [^infers] + Some { diagnostics: r } => Cons { head: ^infers, tail: vfp_reasons(r: r) } + }, + construct_read: vfp_construct_read_decode(resolved: resolved), + call_result_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_match_scrutinee(m: vfp_first_of(xs: vfp_matches_of(n: resolved.root))) + ), + arrow_arm_fact: vfp_fact_in( + inferred: inferred, + subject: vfp_arm_body_first(m: vfp_first_of(xs: vfp_matches_of(n: resolved.root))) + ) + } + } + } +} + +// THE ARROW FIXTURE'S PRODUCER. +fn vfp_arrow_reading() -> VfpArrowReading { + vfp_arrow_reading_over(resolved: vfp_arrow_resolved()) +} + +// THE CLAIMS OVER THE ROUND-TRIP FIXTURE. The RESOLVE-side rows (construction carrier, construct +// read, its carrier) ground on the text route. The INFER typed-fact rows over the two-arm +// param-scrutinee match DO NOT: the walk refuses at the match node with infer_match_arm_type_mismatch +// (terminal, under active bisect in #13210) while the resolver's own binder records carry the +// canonical payload type -- so those rows are DECLARED REDS on that specific located defect, not +// positives; the positive evidence for the typed payload-binder facts is the ARROW fixture's arm row +// (accepted-with-advisories walk) and the enrolled native claims on the real route. +// The construction green cannot hold on the fixture module while the two-arm mismatch stands: infer +// refuses the fixture MODULE-WIDE (the match's arm types), so every fact field on this reading reads +// the terminal reason, construction included. The construction-typing invariant is pinned on the +// route by the ARROW fixture's greens (whose module grounds); this row is a DECLARED RED on the +// located module refusal until the two-arm fix flips it. +test fn vfp_the_fixture_construction_fact_reads_the_module_refusal_red() -> Bool { + vfp_fixture_reading().rt_construction_fact == ^infer_match_arm_type_mismatch +} + +test fn vfp_the_construct_read_names_the_variant_holds() -> Bool { + vfp_fixture_reading().construct_read == ^Pass +} + +test fn vfp_the_construct_read_carrier_is_the_declaring_coproduct_holds() -> Bool { + vfp_fixture_reading().construct_read_carrier == ^Verdict +} + +// DECLARED RED (the two-arm mismatch): the terminal diagnostic is anchored at the match node itself, +// and the reading's fact fields read that terminal reason on a refused walk. This row goes green +// exactly when the mismatch is understood and fixed or the fixture's context stops producing it. +test fn vfp_the_two_arm_match_refuses_with_the_located_mismatch_red() -> Bool { + vfp_fixture_reading().rt_match_fact == ^infer_match_arm_type_mismatch +} + +// THE COVERAGE CONTROL: a one-arm match over a two-variant carrier must refuse, and the refusal must +// be the non-exhaustive reason the coverage obligation reports on the inhabitance relation's refusal. +test fn vfp_the_partial_match_refuses_as_not_covering_the_carrier_holds() -> Bool { + vfp_reports(reading: vfp_partial_reading(), reason: ^infer_match_non_exhaustive) +} + +// THE CALL-RESULT SCRUTINEE (the N7 shape): h's match scrutinee is the nullary call `g()`. At the +// assembled route the call's derived result type does not reach the facts (the walk reports +// infer_match_scrutinee_type_underived, advisory), so the coverage check runs undecidable and the +// ARM BINDER still types from the payload -- the row below pins the binder; this row pins the +// KNOWN route gap on the scrutinee's own fact so the gap cannot silently become a pass. +test fn vfp_the_call_result_scrutinee_fact_stays_underived_on_the_assembled_route_red() -> Bool { + vfp_arrow_reading().call_result_fact == ^vfp_underived +} + +// DECLARED RED: same refused walk as the two-arm mismatch row -- the projection's fact reads the +// terminal reason until the mismatch is settled. +test fn vfp_the_projection_fact_reads_the_located_refusal_red() -> Bool { + vfp_fixture_reading().projection_fact == ^infer_match_arm_type_mismatch +} + +// DECLARED RED: the nullary arm rides the same refused walk. When the mismatch settles, this row +// flips to `== ^Int` beside the arrow binder row. +test fn vfp_the_nullary_arm_fact_reads_the_located_refusal_red() -> Bool { + vfp_fixture_reading().nullary_arm_fact == ^infer_match_arm_type_mismatch +} + +// THE PAYLOAD-BINDER REGRESSION CONTROL (the N7 shape, flipped from the declared red per DESIGN +// 4b(4)): before the payload reader landed, the arm binder's fact lookup was Absent -- NO_ENTRY, +// warm-ferret-741's N7 locate of #2891785 (facts.lookup Absent under the #2891781 +// GroundingNotDerived parent) -- and the declared-red row asserting that defect value went red the +// moment the reader landed. Flipped, it does not retire: the binder on the CALL-RESULT scrutinee +// (the shape that dies on the native route) must read its payload field's type -- the regression +// guard the param-scrutinee positive control does not cover. +test fn vfp_the_call_result_arm_binder_types_from_its_payload_field_holds() -> Bool { + vfp_arrow_reading().arrow_arm_fact == ^Int +} + +// THE ARROW FIXTURE'S RESOLVE-SIDE CONTROL (its second consumer claim): resolve must bind the +// module's sole variant tag so the construct read names Ok, separating resolve binding from +// whatever the infer facts say about the call-result scrutinee. +test fn vfp_the_arrow_fixture_resolves_and_names_its_variant_holds() -> Bool { + vfp_arrow_reading().construct_read == ^Ok +} +// THE RESOLVE-SIDE CONTROL ON THE SAME READING: even while infer refuses the partial match for +// want of coverage, resolve has already bound the construction's tag to its variant's declaring +// path -- the control separates the resolve binding (green on both sides of the fix) from the +// infer facts (red on main, green here), and gives the partial reading its second consumer under +// the warm-producer sharing conjunct. +test fn vfp_the_partial_fixture_still_mints_its_variant_row_holds() -> Bool { + vfp_partial_reading().construct_read == ^Boxed +} + +// THE BISECT FIXTURES THE LEDGER CITES (temporary; they go out when their rows' follow-ups +// land): the nullary single-arm sum the single_arm_sum_parses_as_an_alias row's 2026-10-06 +// receipt cites, and the recursive record the grounding-cascade row cites. + + +// TEMPORARY DECODE INSTRUMENT -- the one reading for the combined fixture: prints every field +// through `gunbc run --function` so the actual values are readable, not guessed. DELETED WITH THE +// RFM RETIREMENT RECEIPT. +fn vfp_decode_fixture_reading(r: VfpFixtureReading) -> CliWireResponse { + CliWirePrintable { bytes: "read=" + symbol_lexeme(sym: r.construct_read) + " carr=" + symbol_lexeme(sym: r.construct_read_carrier) + " rt_constr=" + symbol_lexeme(sym: r.rt_construction_fact) + " rt_match=" + symbol_lexeme(sym: r.rt_match_fact) + " rt_arm=" + symbol_lexeme(sym: r.rt_arm_fact) + " null_arm=" + symbol_lexeme(sym: r.nullary_arm_fact) + " proj=" + symbol_lexeme(sym: r.projection_fact) + " reasons=" + fold(r.reasons, init: "", f: fn(acc, sym) { acc + "/" + symbol_lexeme(sym: sym) }), exit: ExitSuccess } +} + +fn vfp_decode_fixture() -> CliWireResponse { vfp_decode_fixture_reading(r: vfp_fixture_reading()) } + +fn vfp_decode_arrow_reading(r: VfpArrowReading) -> CliWireResponse { + CliWirePrintable { bytes: "arr_read=" + symbol_lexeme(sym: r.construct_read) + " scrut=" + symbol_lexeme(sym: r.call_result_fact) + " arr=" + symbol_lexeme(sym: r.arrow_arm_fact) + " arr_reasons=" + fold(r.reasons, init: "", f: fn(acc, sym) { acc + "/" + symbol_lexeme(sym: sym) }), exit: ExitSuccess } +} + +fn vfp_decode_arrow() -> CliWireResponse { vfp_decode_arrow_reading(r: vfp_arrow_reading()) } + +// THE FORMAL-PAYLOAD GATE (side-chat review 5452786886): a pattern binder whose variant field is +// typed by the DECLARING TYPE's formal must refuse the PREPARE stage -- a typed, located refusal +// (^resolve_reason_pattern_payload_formal_uninstantiated, at the payload field), not an advisory +// frontier the module could prepare under and the native lane could report as prepared. The +// concrete sibling beside it runs the same two-arm shape with a CONCRETE Int payload and prepares +// (accepted, with this route's known advisories), isolating the formal as the refusal's cause. Its +// match sits over the fixture's construction because a match over a variant-typed PARAMETER refuses +// on this route with the declared two-arm defect regardless of payload (the red the two-arm claim +// carries), so the parameter shape cannot serve as the preparing sibling. The red flips to grounding +// when the deep-instantiation follow-up installs the scrutinee's arguments into the arm frame. +data vfp_formal_gate_src: String = "module p\n\ntype Out = Acc { value: T } | Rej\n\nfn read(x: Out) -> Int {\n match x {\n Acc { value: v } => v\n Rej => 0\n }\n}" + +data vfp_formal_gate_concrete_src: String = "module p\n\ntype Out = Acc { value: Int } | Rej\n\nfn make() -> Out {\n Acc { value: 5 }\n}\n\nfn id(n: Int) -> Int {\n n\n}\n\nfn read() -> Int {\n match make() {\n Acc { value: v } => id(5)\n Rej => 0\n }\n}" + +fn vfp_formal_gate_reading() -> VfpReading { vfp_reading(src: vfp_formal_gate_src) } + +fn vfp_formal_gate_concrete_reading() -> VfpReading { vfp_reading(src: vfp_formal_gate_concrete_src) } + +// ONE WARM NULLARY PRODUCER for both formal-gate claims (the match_binder pattern +// mbt_program_reading uses): two claims demanding one producer put its least common ancestor at +// the shared preparation frame, so the two fixture fills run once and each claim's own frame is +// its field reads -- per-fixture fills measured 341,777 and 727,954 eval steps against the 72,300 +// new-witness budget on floor run 37757604769 when each claim demanded its own fill. +type VfpFormalGatePair { + formal: VfpReading + concrete: VfpReading +} + +fn vfp_formal_gate_pair() -> VfpFormalGatePair { + VfpFormalGatePair { + formal: vfp_formal_gate_reading(), + concrete: vfp_formal_gate_concrete_reading() + } +} + +test fn vfp_the_formal_payload_binder_refuses_at_prepare_red() -> Bool { + vfp_formal_gate_pair().formal.match_fact == ^resolve_reason_pattern_payload_formal_uninstantiated +} + +test fn vfp_the_concrete_payload_sibling_still_prepares_holds() -> Bool { + vfp_infers(reading: vfp_formal_gate_pair().concrete) +} diff --git a/src/v2/test/lens_common/infer_fixture.dag b/src/v2/test/lens_common/infer_fixture.dag index c6d2cca2bd4..a77e6441a48 100644 --- a/src/v2/test/lens_common/infer_fixture.dag +++ b/src/v2/test/lens_common/infer_fixture.dag @@ -1,4 +1,5 @@ module v2.test.lens_common.infer_fixture +import v2.std.member_declaration_kind { no_member_declaration_kinds } import std.optional { optional_absent } import std.occurrence_identity { OccurrenceSynthetic } @@ -33,7 +34,7 @@ import std.algebra { Cons } // resolution consulted; a supplied tree was never resolved, so it carries an index holding NO // declarations -- the name says so, and a declaration lookup through it finds nothing and refuses. fn claim_resolved_tree_without_declarations(root: Node) -> ResolvedTree { - ResolvedTree { root: root, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [] } + ResolvedTree { root: root, symbol_index: empty_symbol_index(), resolved_declarations: empty_symbol_index(), lexical_bindings: empty_map(), unavailable_providers: [], member_kinds: no_member_declaration_kinds() } } fn claim_atom_node(s: Symbol) -> Node { @@ -234,7 +235,8 @@ fn claim_resolved_call_tree( params: params ), lexical_bindings: empty_map(), - unavailable_providers: [] + unavailable_providers: [], + member_kinds: no_member_declaration_kinds() } } @@ -297,7 +299,8 @@ fn claim_resolved_projection_tree( params: [param] ), lexical_bindings: empty_map(), - unavailable_providers: unavailable + unavailable_providers: unavailable, + member_kinds: no_member_declaration_kinds() } } diff --git a/src/v2/test/lens_lifecycle_carrier/raw_prose_field_flagged_test.dag b/src/v2/test/lens_lifecycle_carrier/raw_prose_field_flagged_test.dag index bb4e93af12f..1525dc4b07d 100644 --- a/src/v2/test/lens_lifecycle_carrier/raw_prose_field_flagged_test.dag +++ b/src/v2/test/lens_lifecycle_carrier/raw_prose_field_flagged_test.dag @@ -1,17 +1,28 @@ module v2.test.lens_lifecycle_carrier.raw_prose_field_flagged import std.occurrence_identity { OccurrenceSynthetic } +import v2.compiler.compile { + lifecycle_carrier_lens, + run_required_lens_gates_on_subtree +} +import v2.compiler.infer { InferredTree } import v2.lens.lifecycle_carrier { lifecycle_carrier_gate } import v2.std.diagnostic { Accepted, Rejected } import v2.std.node { Atom, + Arrow, Conj, Edge, Authored, Node, + Positional, Symbol, TypeNode, } +import v2.test.lens_common.infer_fixture { + claim_inferred_facts, + claim_inferred_tree +} import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.node_query { binder_edge } @@ -112,3 +123,56 @@ test fn ordinary_string_field_is_accepted() -> Bool { Rejected { diagnostics: _ } => false } } + +// RED (parameter domain): fn f(dissolve_on: String) -> Int. The domain Conj is a record +// carrier; fold_node must still reach it under the Arrow (not stop at Arrow). +data arrow_param_domain: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + binder_edge(name: ^dissolve_on, type_node: string_leaf_target) + ], + occurrence_id: OccurrenceSynthetic +} + +data arrow_param_codomain: Node = Node { + kind: TypeNode { connective: Atom { identity: ^Int } }, + children: [], + occurrence_id: OccurrenceSynthetic +} + +data arrow_with_dissolve_on_param: Node = Node { + kind: TypeNode { connective: Arrow }, + children: [ + Edge { label: Positional, target: arrow_param_domain }, + Edge { label: Positional, target: arrow_param_codomain } + ], + occurrence_id: OccurrenceSynthetic +} + +fn arrow_param_tree() -> InferredTree { + claim_inferred_tree( + root: arrow_with_dissolve_on_param, + facts: claim_inferred_facts( + type_symbol: ^arrow_param_type_sym, + algebra_symbol: ^arrow_param_algebra_sym, + descent_symbol: ^arrow_param_descent_sym + ) + ) +} + +test fn arrow_parameter_dissolve_on_string_is_rejected_on_the_domain() -> Bool { + match lifecycle_carrier_gate(node: arrow_param_domain) { + Rejected { diagnostics: r } => r.head.reason == ^lifecycle_carrier_raw_prose_field + Accepted { value: _, diagnostics: _ } => false + } +} + +test fn subtree_walk_reaches_arrow_parameter_dissolve_on() -> Bool { + match run_required_lens_gates_on_subtree( + inferred: arrow_param_tree(), + lenses: [lifecycle_carrier_lens] + ) { + Rejected { diagnostics: r } => r.head.reason == ^lifecycle_carrier_raw_prose_field + Accepted { value: _, diagnostics: _ } => false + } +} diff --git a/src/v2/test/lens_unit_modeling/module_container_not_judged_as_record_carrier_test.dag b/src/v2/test/lens_unit_modeling/module_container_not_judged_as_record_carrier_test.dag new file mode 100644 index 00000000000..16a0f328b00 --- /dev/null +++ b/src/v2/test/lens_unit_modeling/module_container_not_judged_as_record_carrier_test.dag @@ -0,0 +1,324 @@ +module v2.test.lens_unit_modeling.module_container_not_judged_as_record_carrier + +import std.occurrence_identity { OccurrenceSynthetic } +import v2.compiler.compile { + CompileLensSubtree, + required_lenses_at_grain, + run_required_lens_gates_on_subtree +} +import v2.compiler.infer { InferredTree } +import v2.lens.lifecycle_carrier { lifecycle_carrier_gate } +import v2.lens.unit_modeling { unit_modeling_gate } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.node { + Atom, + Arrow, + ComputationNode, + Conj, + Edge, + Authored, + Node, + TypeNode, + Value, +} +import v2.test.lens_common.infer_fixture { + claim_inferred_facts, + claim_inferred_tree +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.node_query { binder_edge } +import v2.extdeps.languages.dag { + dag_grammar_atom, + dag_node_is_module_surface, + dag_node_is_record_field_carrier, + dag_surface_edge_label +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// GREEN: a Conj whose Authored member is a synthetic dag_surface_module Atom. The PARENT +// remains a record-field carrier (a module-surface child does not opt the whole Conj out). +// The member is skipped because its TARGET is a module surface, per-edge. +data module_surface_atom: Node = Node { + kind: TypeNode { connective: Atom { identity: ^dag_surface_module } }, + children: [], + occurrence_id: OccurrenceSynthetic +} + +data module_container_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^child_mod }, target: module_surface_atom } + ], + occurrence_id: OccurrenceSynthetic +} + +fn module_container_tree() -> InferredTree { + claim_inferred_tree( + root: module_container_root, + facts: claim_inferred_facts( + type_symbol: ^mod_container_type_sym, + algebra_symbol: ^mod_container_algebra_sym, + descent_symbol: ^mod_container_descent_sym + ) + ) +} + +test fn module_container_with_a_module_surface_child_is_still_a_record_field_carrier() -> Bool { + dag_node_is_record_field_carrier(node: module_container_root) +} + +test fn unit_modeling_accepts_a_module_container() -> Bool { + match unit_modeling_gate(node: module_container_root) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn lifecycle_carrier_accepts_a_module_container() -> Bool { + match lifecycle_carrier_gate(node: module_container_root) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +// The door used to refuse this shape with member_not_a_binder at the module-surface child +// before later lenses (mandatory_tag at root grain among them) ran. After the carrier +// narrowing, that cause is gone. A later subtree lens may still refuse a different class +// (fact_density on a bare identity Atom is a separate subject). +test fn subtree_required_lenses_do_not_refuse_member_not_a_binder_on_a_module_container() -> Bool { + match run_required_lens_gates_on_subtree( + inferred: module_container_tree(), + lenses: required_lenses_at_grain(grain: CompileLensSubtree) + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: r } => r.head.reason != ^member_not_a_binder + } +} + +data malformed_record_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^x }, + target: Node { + kind: TypeNode { connective: Atom { identity: ^Text } }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +// RED: a payload Conj whose Authored member is not a binder is still refused. The carrier +// narrowing must not swallow this wall. +test fn a_hand_built_record_field_is_still_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: malformed_record_root) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +// RED (review 77884): a user field whose name happens to be a parse-projection spelling +// is still a record field. Skip those names only on a module-surface carrier. +data projection_named_record_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^grammar_production_identity_node_projection }, + target: Node { + kind: TypeNode { connective: Atom { identity: ^Text } }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_projection_spelled_record_field_is_still_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: projection_named_record_root) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +data record_with_binder: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + binder_edge( + name: ^note, + type_node: Node { + kind: TypeNode { connective: Atom { identity: ^Text } }, + children: [], + occurrence_id: OccurrenceSynthetic + } + ) + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_binder_payload_is_still_a_record_field_carrier() -> Bool { + dag_node_is_record_field_carrier(node: record_with_binder) +} + +data mixed_int_leaf: Node = Node { + kind: TypeNode { connective: Atom { identity: ^dag_binding_type_int } }, + children: [], + occurrence_id: OccurrenceSynthetic +} + +// RED (review 38602): a Conj that CONTAINS a module-surface member must still judge its +// other members. A clock_hz binder typed as bare Int was refused before the container-wide +// skip and must stay refused. +data mixed_module_and_unit_field_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^child_mod }, target: module_surface_atom }, + binder_edge(name: ^clock_hz, type_node: mixed_int_leaf) + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_mixed_conj_still_refuses_a_bare_int_unit_field() -> Bool { + match unit_modeling_gate(node: mixed_module_and_unit_field_root) { + Rejected { diagnostics: r } => r.head.reason == ^unit_modeling_flat_scalar_unit_leaf_fixable + Accepted { value: _, diagnostics: _ } => false + } +} + +// RED (review 77837): skipping every Conj/Arrow/ComputationNode member would swallow a +// real record field. Only module-surface and fn-decl identities skip. +data conj_shaped_member_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^payload }, + target: Node { + kind: TypeNode { connective: Conj }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_conj_shaped_record_field_is_still_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: conj_shaped_member_root) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +data arrow_shaped_member_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^payload }, + target: Node { + kind: TypeNode { connective: Arrow }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn an_arrow_shaped_record_field_is_still_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: arrow_shaped_member_root) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +data computation_shaped_member_root: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^payload }, + target: Node { + kind: ComputationNode { behavior: Value }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_computation_shaped_record_field_is_still_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: computation_shaped_member_root) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +data unstamped_conj_with_module_header_edge: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: dag_surface_edge_label(edge: ^dag_surface_module_header), + target: dag_grammar_atom(id: ^dag_surface_module_header) + }, + Edge { + label: Authored { name: ^x }, + target: Node { + kind: TypeNode { connective: Atom { identity: ^Text } }, + children: [], + occurrence_id: OccurrenceSynthetic + } + } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn an_unstamped_conj_is_not_a_module_surface_just_because_it_has_a_header_edge() -> Bool { + dag_node_is_module_surface(node: unstamped_conj_with_module_header_edge) == false +} + +test fn an_unstamped_conj_with_a_header_edge_is_still_a_record_field_carrier() -> Bool { + dag_node_is_record_field_carrier(node: unstamped_conj_with_module_header_edge) +} + +test fn an_unstamped_header_edge_conj_still_refuses_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: unstamped_conj_with_module_header_edge) { + Rejected { diagnostics: r } => r.head.reason == ^member_not_a_binder + Accepted { value: _, diagnostics: _ } => false + } +} + +// GREEN: the exception is producer identity dag_surface_fn_decl on the member, not Conj shape. +data fn_decl_identity_atom: Node = Node { + kind: TypeNode { connective: Atom { identity: ^dag_surface_fn_decl } }, + children: [], + occurrence_id: OccurrenceSynthetic +} + +data stamped_fn_shell: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { + label: Authored { name: ^grammar_production_identity_node_projection }, + target: fn_decl_identity_atom + } + ], + occurrence_id: OccurrenceSynthetic +} + +data module_with_stamped_fn_member: Node = Node { + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Authored { name: ^f }, target: stamped_fn_shell } + ], + occurrence_id: OccurrenceSynthetic +} + +test fn a_stamped_fn_decl_member_is_not_member_not_a_binder() -> Bool { + match unit_modeling_gate(node: module_with_stamped_fn_member) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: r } => r.head.reason != ^member_not_a_binder + } +} diff --git a/src/v2/test/manual/parse_forensics_scaling_witness.dag b/src/v2/test/manual/parse_forensics_scaling_witness.dag deleted file mode 100644 index 0dc39364715..00000000000 --- a/src/v2/test/manual/parse_forensics_scaling_witness.dag +++ /dev/null @@ -1,70 +0,0 @@ -module v2.test.manual.parse_forensics_scaling_witness - -// adhoc-c328b166-bca interpreted-parse cost forensics: per-file packrat memo stats -// (hits/misses/lookup_calls) at increasing corpus-file sizes, timed externally via `time -// ./target/release/gunbc run --claim-run`. One fn per fixture file so each can be timed as an -// isolated process (amortizes nothing; each pays full corpus-load overhead, which is the point -- -// wall time here is (fixed load) + (parse cost) and the parse-cost delta across sizes is read off -// the growth curve, not the absolute number). - -fn forensics_scaling_stats_for_path(path: String) -> ParseMemoStats { - let lm = dag_language_model() - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_forensics_scaling_witness_grammar_contract) - let source_text = filesystem_read(path: path).content - match tokenize(text: source_text, file: ^parse_forensics_scaling_file_anchor, rules: lm.lex) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: stream, diagnostics: _ } => - match lm.grammar { - ModeledGrammar { root: root } => - match grammar_validate_for_parse(root: root, at: at) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: validated, diagnostics: _ } => - parse_production_memo_stats( - tokens: token_stream_remaining(stream: stream), - validated: validated, - production_name: validated.root.start, - materialization: Memoize - ) - } - VoidGrammar => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - } - } -} - -fn forensics_scaling_report(path: String) -> List { - let stats = forensics_scaling_stats_for_path(path: path) - Cons { - head: stats.hits, - tail: Cons { - head: stats.misses, - tail: Cons { head: stats.lookup_calls, tail: std.algebra.Empty } - } - } -} - -fn witness_forensics_scale_tiny_report() -> List { - forensics_scaling_report(path: "src/v2/std/witness.dag") -} - -fn witness_forensics_scale_small_report() -> List { - forensics_scaling_report(path: "dag/std/execution_mode.dag") -} - -fn witness_forensics_scale_medium_report() -> List { - forensics_scaling_report(path: "src/v2/std/datetime.dag") -} - -fn witness_forensics_scale_bisect_report() -> List { - forensics_scaling_report(path: "/tmp/claude-1000/-home-briansrls--worktrees-gunbc-neat-stag-780/8faadfe5-0411-48d9-980a-f8a213df0415/scratchpad/bisect/datetime_half.dag") -} - -fn witness_forensics_scale_mediumlarge_report() -> List { - forensics_scaling_report(path: "src/v2/std/node.dag") -} - -fn witness_forensics_scale_large_report() -> List { - forensics_scaling_report(path: "src/v2/std/grammar.dag") -} diff --git a/src/v2/test/manual/parse_table_memo_scale_witness.dag b/src/v2/test/manual/parse_table_memo_scale_witness.dag deleted file mode 100644 index ec83d974965..00000000000 --- a/src/v2/test/manual/parse_table_memo_scale_witness.dag +++ /dev/null @@ -1,85 +0,0 @@ -module v2.test.manual.parse_table_memo_scale_witness - - - -data parse_memo_scale_tokenize_path: String = "src/v1/01_tokenize.dag" - -fn parse_memo_scale_stats_for_grammar( - source_text: String, - grammar: ParseGrammar -) -> ParseMemoStats { - let lm = dag_language_model() - let at = invariant_port_locus(port: ^parse_stage_locus_port, invariant: ^parse_table_memo_scale_witness_grammar_contract) - match tokenize(text: source_text, file: ^parse_memo_scale_file_anchor, rules: lm.lex) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: stream, diagnostics: _ } => - match grammar { - ModeledGrammar { root: root } => - match grammar_validate_for_parse(root: root, at: at) { - Rejected { diagnostics: _ } => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - Accepted { value: validated, diagnostics: _ } => - parse_production_memo_stats( - tokens: token_stream_remaining(stream: stream), - validated: validated, - production_name: validated.root.start, - materialization: Memoize - ) - } - VoidGrammar => - ParseMemoStats { hits: 0, misses: 0, lookup_calls: 0 } - } - } -} - -fn parse_memo_scale_tokenize_stats() -> ParseMemoStats { - let source_text = filesystem_read(path: parse_memo_scale_tokenize_path).content - parse_memo_scale_stats_for_grammar( - source_text: source_text, - grammar: dag_language_model().grammar - ) -} - -fn witness_parse_memo_scale_tokenize_hits() -> Int { - parse_memo_scale_tokenize_stats().hits -} - -fn witness_parse_memo_scale_tokenize_misses() -> Int { - parse_memo_scale_tokenize_stats().misses -} - -fn witness_parse_memo_scale_tokenize_lookup_calls() -> Int { - parse_memo_scale_tokenize_stats().lookup_calls -} - -fn witness_parse_memo_scale_tokenize_report() -> List { - let stats = parse_memo_scale_tokenize_stats() - Cons { - head: stats.hits, - tail: Cons { - head: stats.misses, - tail: Cons { head: stats.lookup_calls, tail: std.algebra.Empty } - } - } -} - -fn witness_stage_tokenize_only_holds() -> Bool { - let lm = dag_language_model() - let source_text = filesystem_read(path: parse_memo_scale_tokenize_path).content - match tokenize(text: source_text, file: ^parse_memo_scale_file_anchor, rules: lm.lex) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -fn witness_stage_parse_only_holds() -> Bool { - let source_text = filesystem_read(path: parse_memo_scale_tokenize_path).content - match parse_memo_scale_stats_for_grammar( - source_text: source_text, - grammar: dag_language_model().grammar - ) { - ParseMemoStats { hits: _, misses: misses, lookup_calls: calls } => - (misses > 0) && (calls > 0) - } -} diff --git a/src/v2/test/manual/s1_stall_probe.dag b/src/v2/test/manual/s1_stall_probe.dag index 55302c4c98d..b7ee303b95a 100644 --- a/src/v2/test/manual/s1_stall_probe.dag +++ b/src/v2/test/manual/s1_stall_probe.dag @@ -23,7 +23,7 @@ fn stall_offset(path: String) -> Int { tokens: tokens, all_tokens: tokens, root: validated.root, - table: parse_table_for_production(validated: validated, all_tokens: tokens, materialization: Memoize), + table: parse_table_for_production(validated: validated, all_tokens: tokens), prov: parse_prov_initial() ) { ParseExprRejected { diagnostics: _, table: _ } => 0 - 2 diff --git a/src/v2/test/native_file_refusal_locus_test.dag b/src/v2/test/native_file_refusal_locus_test.dag index 30dcaa4ba95..c39b633509c 100644 --- a/src/v2/test/native_file_refusal_locus_test.dag +++ b/src/v2/test/native_file_refusal_locus_test.dag @@ -23,10 +23,10 @@ fn at_text(file: Symbol, start: Int) -> Diagnostic { } } -fn residue_head() -> Diagnostic { +fn advisory_head() -> Diagnostic { Diagnostic { - reason: ^parse_grammar_choice_overlap_residue, - at: invariant_port_locus(port: ^native_file_refusal_locus_port, invariant: ^grammar_choice_overlap), + reason: ^tokenize_lex_e1_unrecognized_char, + at: invariant_port_locus(port: ^native_file_refusal_locus_port, invariant: ^tokenize_lex_e1_unrecognized_char), correction: Unavailable { reason: ExternalContractUnknown } } } @@ -39,7 +39,7 @@ fn located_row() -> NativeTestFileRefusal { native_test_file_refusal( path: ^native_file_refusal_locus_fixture, module: optional_absent(), - d: NonEmptyDiagnostics { head: residue_head(), tail: [at_text(file: ^native_file_refusal_locus_fixture, start: 4)] }, + d: NonEmptyDiagnostics { head: advisory_head(), tail: [at_text(file: ^native_file_refusal_locus_fixture, start: 4)] }, spans: span_index_empty(), text: "\u{00e9}\u{000a}ab" ) diff --git a/src/v2/test/v2_native_route_test.dag b/src/v2/test/v2_native_route_test.dag index 62282253d3f..fe48f961066 100644 --- a/src/v2/test/v2_native_route_test.dag +++ b/src/v2/test/v2_native_route_test.dag @@ -190,7 +190,7 @@ fn receipt_over( false_control: Present { value: NativeTestReturnedFalse }, true_control: Present { value: NativeTestPassed }, malformed_control: MalformedSpecimenRefused { - path: "target/v2-native-lane/malformed-control-root/poison.dag", + path: "target/v2-native-lane/malformed-control-root/dag/poison.dag", reason: ^tokenize_lex_e1_unterminated_string }, old_route_control: OldRouteWithdrawn { withdrawn_executable: "/seed/gunbc" }, @@ -906,7 +906,7 @@ test fn an_unattributed_file_refusal_is_refused() -> Bool { population: [passed_row(module: "v2.test.alpha", declaration: "passes")], file_refusals: [file_refusal_row( path: ^some_path, - head: ^parse_grammar_choice_overlap_residue, + head: ^tokenize_lex_e1_unrecognized_char, fatal: ^some_unowned_cause )] )), @@ -921,10 +921,10 @@ test fn an_unattributed_file_refusal_is_refused() -> Bool { // cause has no ownership row still fails it, so the owned rows did not become a catch-all. fn body_lowering_frontier_refusals() -> List { [ - file_refusal_row(path: ^bl_a, head: ^parse_grammar_choice_overlap_residue, fatal: ^body_lowering_reason_call_argument_unread), - file_refusal_row(path: ^bl_b, head: ^parse_grammar_choice_overlap_residue, fatal: ^body_lowering_reason_operator_operand_unread), - file_refusal_row(path: ^bl_c, head: ^parse_grammar_choice_overlap_residue, fatal: ^body_lowering_reason_type_annotation_not_carried), - file_refusal_row(path: ^bl_d, head: ^parse_grammar_choice_overlap_residue, fatal: ^body_lowering_reason_paren_group_unread) + file_refusal_row(path: ^bl_a, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^body_lowering_reason_call_argument_unread), + file_refusal_row(path: ^bl_b, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^body_lowering_reason_operator_operand_unread), + file_refusal_row(path: ^bl_c, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^body_lowering_reason_type_annotation_not_carried), + file_refusal_row(path: ^bl_d, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^body_lowering_reason_paren_group_unread) ] } @@ -949,7 +949,7 @@ test fn an_unknown_fatal_cause_beside_the_owned_ones_still_refuses_unowned() -> module_source_index: [], population: [], file_refusals: Cons { - head: file_refusal_row(path: ^bl_e, head: ^parse_grammar_choice_overlap_residue, fatal: ^some_unowned_cause), + head: file_refusal_row(path: ^bl_e, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^some_unowned_cause), tail: body_lowering_frontier_refusals() } )) @@ -1025,7 +1025,7 @@ test fn a_context_refusal_backed_by_another_modules_file_is_refused() -> Bool { ], file_refusals: [file_refusal_row( path: ^alpha_path, - head: ^parse_grammar_choice_overlap_residue, + head: ^tokenize_lex_e1_unrecognized_char, fatal: ^parse_g0_tokens_remain )] )), @@ -1056,7 +1056,7 @@ test fn a_context_refusal_at_its_own_path_with_the_wrong_reason_is_refused() -> ], file_refusals: [file_refusal_row( path: ^beta_path, - head: ^parse_grammar_choice_overlap_residue, + head: ^tokenize_lex_e1_unrecognized_char, fatal: ^resolve_module_not_found )] )), @@ -1091,7 +1091,7 @@ test fn a_context_refusal_backed_by_its_file_refusal_is_not_refused_on_that_clau ], file_refusals: [file_refusal_row( path: ^beta_path, - head: ^parse_grammar_choice_overlap_residue, + head: ^tokenize_lex_e1_unrecognized_char, fatal: ^parse_g0_tokens_remain )] )) @@ -1613,8 +1613,8 @@ fn native_route_tally_probe() -> List { module_source_index: [], population: [], file_refusals: [ - file_refusal_row(path: ^tally_a, head: ^parse_grammar_choice_overlap_residue, fatal: ^parse_g0_tokens_remain), - file_refusal_row(path: ^tally_b, head: ^parse_grammar_choice_overlap_residue, fatal: ^parse_g0_tokens_remain), + file_refusal_row(path: ^tally_a, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^parse_g0_tokens_remain), + file_refusal_row(path: ^tally_b, head: ^tokenize_lex_e1_unrecognized_char, fatal: ^parse_g0_tokens_remain), file_refusal_row(path: ^tally_c, head: ^resolve_reason_unbound_symbol, fatal: ^resolve_reason_unbound_symbol) ] )) diff --git a/src/v2/workflow/ci_floor_peak_emit.dag b/src/v2/workflow/ci_floor_peak_emit.dag index b67e21419dd..2dcaffbc265 100644 --- a/src/v2/workflow/ci_floor_peak_emit.dag +++ b/src/v2/workflow/ci_floor_peak_emit.dag @@ -68,21 +68,16 @@ data ci_floor_peak_nearest_ancestor_spec: NearestAncestorContaining = NearestAnc // reimplementing the parser. Peak reset/cat/echo calibration leaves stay raw String — Phase 2 typed // filesystem observation. -data ci_floor_peak_path_model_refused_poison: String = "__GUNBC_PATH_INTENT_LOWERING_REFUSED__ floor-peak cgroup path-intent leaf lowering was rejected by v2.extdeps.languages.bash_orch_if; this marker is not valid workflow shell, so the committed ci.yml drift gate goes red (DESIGN 5: refuse, never widen). WHY A POISON RATHER THAN EMPTY TEXT, on the measured facts rather than the intuitive ones: the Run.command seam is String-typed and cannot carry an Outcome, so the refusal arm has to be SOME string, and the sibling witness file's model-text helpers return \"\" there. An earlier draft of this row justified the poison by claiming an empty while-condition is an always-true loop that would spin forever. THAT IS FALSE, tested 2026-08-05 against /bin/sh, /bin/dash and /bin/bash: 'while ; do' is a SYNTAX ERROR in all three, so an empty leaf would fail loudly rather than hang. The real argument is smaller and survives the correction. An empty String is a well-typed, ordinary-looking value flowing through an untyped seam; it says nothing about which lowering refused or why, and its first observable symptom is a shell parse failure inside a generated workflow — attributed to ci.yml at CI runtime, several layers from the path model that actually refused. The poison carries the cause to the place the reader lands. In practice the drift gate catches either one, because both differ from the committed bytes; what the poison buys is a LOCATED refusal instead of an anonymous one, which is the half of DESIGN section 5 that a mere red does not supply." - fn ci_floor_peak_assign_cgroup_dir_command() -> String { - let cmd = bash_nearest_ancestor_locate_assign_command(spec: ci_floor_peak_nearest_ancestor_spec) - if cmd == "" { ci_floor_peak_path_model_refused_poison } else { cmd } + bash_nearest_ancestor_locate_assign_command(spec: ci_floor_peak_nearest_ancestor_spec) } fn ci_floor_peak_while_cond_command() -> String { - let cmd = bash_nearest_ancestor_locate_cond_command(spec: ci_floor_peak_nearest_ancestor_spec) - if cmd == "" { ci_floor_peak_path_model_refused_poison } else { cmd } + bash_nearest_ancestor_locate_cond_command(spec: ci_floor_peak_nearest_ancestor_spec) } fn ci_floor_peak_while_body_command() -> String { - let cmd = bash_nearest_ancestor_locate_body_command(spec: ci_floor_peak_nearest_ancestor_spec) - if cmd == "" { ci_floor_peak_path_model_refused_poison } else { cmd } + bash_nearest_ancestor_locate_body_command(spec: ci_floor_peak_nearest_ancestor_spec) } data ci_floor_peak_test_f_memory_peak_command: String = "[ -f \"$d/memory.peak\" ]" diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index e8d0bcf3bf7..869e94d081c 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -57,12 +57,12 @@ data known_frontier_causes: List = [ CauseOwnership { cause: ^member_not_a_binder, lane: MigrationOwned, - flip_trigger: "RETIRED WHEN v2.lens.lifecycle_carrier and v2.lens.unit_modeling narrow their carrier predicate so that a node whose Authored members are module surface nodes (a container of modules, not a record or payload type) is never judged as a record carrier; this row is then deleted and the door-ledger long test's member_not_a_binder count returns to zero. DIAGNOSIS (advisories step 2, bold-lynx-438): reading the fatal element exposed this cause on dag/std/error_primitives.dag and dag/std/bit.dag. Each refusal is located at the TARGET of the offending member edge, a synthetic dag_surface_module node: both lenses fold every TypeNode's Authored children through v2.std.node_query member_edge_type_node and refuse a child that is not a binder, and a node carrying modules as Authored members reaches that fold. That is a lens defect (the carrier predicate admits a module container), not a correct refusal of these subjects. It was masked at head grain by parse_grammar_choice_overlap_residue. The fix is owned by its own work item, not this row" + flip_trigger: "KEEP until an executed door-ledger census of dag/std/error_primitives.dag and dag/std/bit.dag shows member_not_a_binder at zero on those subjects (fatals moved), then delete this row. The carrier predicate no longer judges a module-surface Authored member as a record field; that is not by itself the census. DIAGNOSIS (advisories step 2, bold-lynx-438): reading the fatal element exposed this cause on those two files at synthetic dag_surface_module targets. Do not retire on an unrelated histogram substitution." }, CauseOwnership { cause: ^parse_g0_tokens_remain, lane: MigrationOwned, - flip_trigger: "no per-cause trigger — same grammar-surgery class as parse_grammar_choice_overlap_residue (nimble-boar-198). Measured 2026-09-09 by the gunbc.witness_v2_native_route census as the fatal refusal on 706 real v2.test.* modules through the emitted native binary" + flip_trigger: "no per-cause trigger — grammar-surgery class (nimble-boar-198). Measured 2026-09-09 by the gunbc.witness_v2_native_route census as the fatal refusal on 706 real v2.test.* modules through the emitted native binary" }, CauseOwnership { cause: ^tokenize_lex_e1_unrecognized_char, @@ -117,7 +117,7 @@ data known_frontier_causes: List = [ CauseOwnership { cause: ^body_lowering_reason_unrecognised_lowered_shape, lane: SharedSelfHostCriticalPath, - flip_trigger: "a reader in v2.compiler.body_lowering_fold meets a node that is lowered output (no grammar projection edge) but that body_lower_is_core_substrate does not name, and refuses at it instead of re-reading it as raw parse and answering a part of it (body_lower_is_unrecognised_lowered_shape). The MQ-5 census found no producer that reaches this today. Flips when every producer declares the shape it emits (LoweredShape carried in a BodyTerm through the reducers, neat-boar-16 ruling 2026-09-30), so a reader never classifies a node by shape and the reason has no producer (gunbc.recurring_failure_mode lowered_output_reread_as_raw_parse_at_v2_body_lowering)" + flip_trigger: "v2.compiler.body_lowering_fold body_term_of reads a node in the fold's tree that is neither under the sealed Lowered marker a producer writes (body_term_seal), nor a composite the parse stamped, nor a leaf, and refuses at it: lowered output no producer declared, never re-read as raw parse. Since MQ-5 every producer seals its result and the shape allowlist is deleted, so this fires only on an unsealed producer output. Flips when the fold's tree is a typed BodyTerm term tree that normalize consumes, so an unsealed composite has no representation and the reason has no producer (gunbc.recurring_failure_mode lowered_output_reread_as_raw_parse_at_v2_body_lowering)" }, CauseOwnership { cause: ^body_lowering_reason_pattern_form_not_admitted, diff --git a/src/v2/workflow/emitted_closure_crate_partition.dag b/src/v2/workflow/emitted_closure_crate_partition.dag index 4f0a816e80e..5d2dc9ef510 100644 --- a/src/v2/workflow/emitted_closure_crate_partition.dag +++ b/src/v2/workflow/emitted_closure_crate_partition.dag @@ -10,7 +10,7 @@ import v2.lens.module_graph { dependency_resolution_facts_live, module_declaration_facts_live } -import v2.std.collection { empty_map, map_get, map_insert } +import v2.std.collection { empty_map, map_get_checked, map_insert } import v2.std.diagnostic { Accepted, Rejected } import std.optional { Absent, Optional, Present } import v2.std.compilers.compilation_unit { ModuleDag, ModuleDagEdge, ModuleId } @@ -58,7 +58,7 @@ fn emitted_closure_module_by_path( closure: Map ) -> Map { declarations |> fold(init: empty_map(), f: fn(acc, decl) { - match map_get(closure, decl.path) { + match map_get_checked(closure, decl.path) { Accepted { value: Present { value: _ }, diagnostics: _ } => map_insert(acc, decl.path, decl.module) Accepted { value: Absent, diagnostics: _ } => acc Rejected { diagnostics: _ } => acc @@ -67,7 +67,7 @@ fn emitted_closure_module_by_path( } fn emitted_closure_module_at(by_path: Map, path: String) -> Optional { - match map_get(by_path, path) { + match map_get_checked(by_path, path) { Accepted { value: found, diagnostics: _ } => found Rejected { diagnostics: _ } => Absent } @@ -115,7 +115,7 @@ fn closure_edge_step( if !edge.target_declared { unresolved } else { - match map_get(module_set, edge.target_module) { + match map_get_checked(module_set, edge.target_module) { Accepted { value: Present { value: _ }, diagnostics: _ } => if importer == edge.target_module { acc diff --git a/src/v2/workflow/emitted_crate_workspace.dag b/src/v2/workflow/emitted_crate_workspace.dag index 171f33e1c45..addc9f0bb1b 100644 --- a/src/v2/workflow/emitted_crate_workspace.dag +++ b/src/v2/workflow/emitted_crate_workspace.dag @@ -1,7 +1,7 @@ module v2.workflow.emitted_crate_workspace import std.types { List, String, Bool, Int, NonEmptyStr } -import v2.std.collection { empty_map, map_get, map_insert } +import v2.std.collection { empty_map, map_get_checked, map_insert } import std.optional { Absent, Optional, Present, optional_present } import v2.std.diagnostic { Accepted, Rejected } import v2.std.compilers.compilation_unit { @@ -131,7 +131,7 @@ fn emitted_workspace_crate_dir(package: String) -> String { } fn emitted_workspace_lookup(m: Map, key: String) -> Optional { - match map_get(m, key) { + match map_get_checked(m, key) { Accepted { value: found, diagnostics: _ } => found Rejected { diagnostics: _ } => Absent } @@ -264,7 +264,7 @@ fn emitted_workspace_unit_rows(modules: List, assignment: Map filter(m => match map_get(assignment, m) { + modules: modules |> filter(m => match map_get_checked(assignment, m) { Accepted { value: Present { value: u }, diagnostics: _ } => u == unit_id Accepted { value: Absent, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -321,7 +321,7 @@ fn emitted_workspace_derived_rows(unit_rows: List, unit } fn emitted_workspace_unit_of(assignment: Map, module: String) -> Optional { - match map_get(assignment, module as ModuleId) { + match map_get_checked(assignment, module as ModuleId) { Accepted { value: owner, diagnostics: _ } => owner Rejected { diagnostics: _ } => Absent } diff --git a/src/v2/workflow/floor2_prepared_subject.dag b/src/v2/workflow/floor2_prepared_subject.dag index f7139d71458..480778ebd8f 100644 --- a/src/v2/workflow/floor2_prepared_subject.dag +++ b/src/v2/workflow/floor2_prepared_subject.dag @@ -17,11 +17,6 @@ import v2.std.node { Symbol } // eval_wall_ms). One preparation serving every claim is the same work with the // entry removed from the key. -type PreparedSubjectSpec { - source_roots: List - exclusions: List -} - // The exclusion set is PART OF THE SUBJECT, so it is declared beside the roots and // never borrowed from a roster that answers a different question. The probe exclusion // roster is pattern rows UNION a derived strict-resolve failure closure, and its @@ -90,50 +85,6 @@ fn witness_identity_qualified_name(identity: WitnessIdentity) -> String { concat(concat(identity.module_path, "."), identity.function) } -// WHICH EXECUTION KEY A BATCH GETS IS FLOOR POLICY, so it is a row here rather than a -// condition in the seed. The note above claims changing floor policy is a row edit and not -// a Rust edit; landing the routing rule only in cli_run would have made that sentence false -// in the same change that wrote it. -// -// ONE INPUT DECIDES, and it used to be two. An earlier revision of this rule also read -// roster provenance -- walked from scan_dirs versus named by the batch -- because a batch -// naming ten entries would have paid a whole-tree preparation to serve them, which is the -// entry-major cost-shape defect pointing the other way. That ground was real and it is now -// dissolved rather than merely unused: ExecutionMode is not an input to resolution, so the -// seed memoizes the resolved graph by its source digest and a second consumer over the same -// tree pays a file walk and builds its own context. The distinction bought nothing once the -// cost it named was gone, so FloorRosterProvenance and the -// WholeTreePreparationNotAmortized ground are DELETED here rather than left standing beside -// the climb that obsoleted them. -type FloorSelectionPosture - = NoSelectionMachinery - | SelectionRequiresPerEntryFrontier - -// The surviving entry-major arm names its own dissolution in its constructor: a -// selection-bearing lane evaluates a per-entry frontier, and an entry-private context is -// what supplies one. It retires when node_frontier_selection leaves Runnable, at which point -// this coproduct collapses to a single inhabitant and deletes with it. -type FloorExecutionKey - = OnePreparedSubjectKey { subject: PreparedSubjectSpec } - | EntryMajorUntilFrontierRetires - -fn floor_execution_key( - posture: FloorSelectionPosture, - subject: PreparedSubjectSpec -) -> FloorExecutionKey { - match posture { - SelectionRequiresPerEntryFrontier => EntryMajorUntilFrontierRetires - NoSelectionMachinery => OnePreparedSubjectKey { subject: subject } - } -} - -fn floor_execution_key_is_one_prepared_subject(key: FloorExecutionKey) -> Bool { - match key { - OnePreparedSubjectKey { subject: _ } => true - EntryMajorUntilFrontierRetires => false - } -} - // A DECLARED RUNG REGRESSION, measured on this change rather than discovered after it. // // Under one prepared subject every module of the tree is present at once, which is the most diff --git a/src/v2/workflow/floor_cost_debt.dag b/src/v2/workflow/floor_cost_debt.dag index 1aab917f794..4a758c4d515 100644 --- a/src/v2/workflow/floor_cost_debt.dag +++ b/src/v2/workflow/floor_cost_debt.dag @@ -782,8 +782,13 @@ fn floor_cost_debt_censored_chunk_02() -> List { // match_keyword_after_dot_is_not_a_match_expr_sniff_holds, and both Cause R data-record // discriminators (data_record_arrow_lambda_field_is_not_retained_holds, // data_record_fn_literal_field_is_not_retained_holds). +// RETIRED (2026-10-05): single_arm_match two_arm_match_still_accepts_holds and +// three_arm_match_keeps_every_arm_holds. Run with no cost budget, they reached FAIL, not an +// interrupt: a semantic red that this row masked. They now pass after the parse repair +// (gunbc.recurring_failure_mode memo_retained_occurrence_id_reminted_after_a_rejected_attempt), +// so they return to the floor and its budget. fn floor_cost_debt_censored_chunk_03() -> List { - Cons { head: "v2.test.claim.body_lowering.single_arm_match.single_arm_wildcard_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.two_arm_match_still_accepts_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.three_arm_match_keeps_every_arm_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.one_arm_constructor_pattern_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.one_arm_shorthand_field_pattern_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_renamed_other_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_loop_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_type_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.infix_field_named_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.data_initializer_fn_value.lambda_in_fn_body_control_holds", tail: Empty {} } } } } } } } } } } } + Cons { head: "v2.test.claim.body_lowering.single_arm_match.single_arm_wildcard_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.one_arm_constructor_pattern_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.single_arm_match.one_arm_shorthand_field_pattern_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_renamed_other_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_loop_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.field_named_type_control_holds", tail: Cons { head: "v2.test.claim.body_lowering.match_as_field_name.infix_field_named_match_normalizes_holds", tail: Cons { head: "v2.test.claim.body_lowering.data_initializer_fn_value.lambda_in_fn_body_control_holds", tail: Empty {} } } } } } } } } } } // CENSORED: thirteen identities interrupted_before_verdict on this PR's floor of @@ -1035,8 +1040,40 @@ fn floor_cost_debt_proven_chunk_21() -> List { Cons { head: "v2.test.parse.expression_bodied_fn_decl_parse.braced_fn_decl_survives_normalize_holds", tail: Empty {} } } +// PROVEN: reached its verdict (passed) over the new-witness budget on the required floor of +// record at 8528a59b45 (floor run 2026-10-06T14:35Z; the same gate re-measured it at run +// 37499673683 / floor job 112393139664, head d4ec92f) -- the figures live in those runs, not +// here. The only real-path inhabitance claim for the former drop +// gunbc.rung_drop.admit_callers_discarded_on_the_native_route. Row admitted by +// operator escalation, default-approved (lively-ram-153 relaying, 2026-10-07). The cost is the +// interpreter's realization of the parse+lower pipeline, not the claim's shape: 02_parse +// documents that parsing even a trivial source dominates under the tree-walking v1 +// interpreter, and every cheaper shape (hand-built parse shells) was rejected by review 77470 +// as fixture-standing. Owner: the long-home changed-witness lane, where required_floor +// long_home_prefixes admit module v2.test.claim.long.* as ChangedCostDebtVerdictOnly; the +// claim also PASSES when run for its verdict without the eval budget (runner receipt, PR +// #13108). Retirement: structural (the native emitter stops billing claims for the interpreter's +// pipeline) or observational -- this identity measures below the charged budget on a required run +// and is re-enrolled in required execution (side chat review 5439746794). +fn floor_cost_debt_proven_chunk_22() -> List { + Cons { head: "v2.test.claim.long.caller_admission_native_wall_red.the_native_route_refuses_exactly_the_outside_cross_module_calls_holds", tail: Empty {} } +} + +// PROVEN, ADMITTED AS THE ONE REAL-PATH INHABITANCE CLAIM OF ITS MODULE (DESIGN section 3, the +// pairing obligation). v2.test.long.cwc_resolution_probe takes every other input at the interface +// under test as a supplied value; this claim runs the real tokenize -> parse -> normalize -> resolve +// route and checks that each supplied fixture is what its producer emits, so it cannot itself be +// cheap. Measure it with claim_batch --entry src/v2/test/claim/long/cwc_resolution_probe_test.dag +// --functions cwc_cwc_module_resolve_accepts and read the [witness] eval_steps line. +// ADMISSION AUTHORITY: operator decision B, default-approved, escalation msg_e427bf73 (2026-10-05, +// relayed by clever-crane-847). It is admitted through v2.workflow.floor_cost_debt_verdict, which +// runs a newly added row unbudgeted for its verdict and refuses it if the claim does not pass. +fn floor_cost_debt_proven_chunk_23() -> List { + Cons { head: "v2.test.long.cwc_resolution_probe.cwc_cwc_module_resolve_accepts", tail: Empty {} } +} + fn floor_cost_debt_proven_chunks() -> List> { - Cons { head: floor_cost_debt_proven_chunk_00(), tail: Cons { head: floor_cost_debt_proven_chunk_01(), tail: Cons { head: floor_cost_debt_proven_chunk_02(), tail: Cons { head: floor_cost_debt_proven_chunk_03(), tail: Cons { head: floor_cost_debt_proven_chunk_04(), tail: Cons { head: floor_cost_debt_proven_chunk_05(), tail: Cons { head: floor_cost_debt_proven_chunk_06(), tail: Cons { head: floor_cost_debt_proven_chunk_07(), tail: Cons { head: floor_cost_debt_proven_chunk_08(), tail: Cons { head: floor_cost_debt_proven_chunk_09(), tail: Cons { head: floor_cost_debt_proven_chunk_10(), tail: Cons { head: floor_cost_debt_proven_chunk_11(), tail: Cons { head: floor_cost_debt_proven_chunk_12(), tail: Cons { head: floor_cost_debt_proven_chunk_13(), tail: Cons { head: floor_cost_debt_proven_chunk_14(), tail: Cons { head: floor_cost_debt_proven_chunk_15(), tail: Cons { head: floor_cost_debt_proven_chunk_16(), tail: Cons { head: floor_cost_debt_proven_chunk_17(), tail: Cons { head: floor_cost_debt_proven_chunk_18(), tail: Cons { head: floor_cost_debt_proven_chunk_19(), tail: Cons { head: floor_cost_debt_proven_chunk_20(), tail: Cons { head: floor_cost_debt_proven_chunk_21(), tail: Empty {} } } } } } } } } } } }}}}}}}}}}}} + Cons { head: floor_cost_debt_proven_chunk_00(), tail: Cons { head: floor_cost_debt_proven_chunk_01(), tail: Cons { head: floor_cost_debt_proven_chunk_02(), tail: Cons { head: floor_cost_debt_proven_chunk_03(), tail: Cons { head: floor_cost_debt_proven_chunk_04(), tail: Cons { head: floor_cost_debt_proven_chunk_05(), tail: Cons { head: floor_cost_debt_proven_chunk_06(), tail: Cons { head: floor_cost_debt_proven_chunk_07(), tail: Cons { head: floor_cost_debt_proven_chunk_08(), tail: Cons { head: floor_cost_debt_proven_chunk_09(), tail: Cons { head: floor_cost_debt_proven_chunk_10(), tail: Cons { head: floor_cost_debt_proven_chunk_11(), tail: Cons { head: floor_cost_debt_proven_chunk_12(), tail: Cons { head: floor_cost_debt_proven_chunk_13(), tail: Cons { head: floor_cost_debt_proven_chunk_14(), tail: Cons { head: floor_cost_debt_proven_chunk_15(), tail: Cons { head: floor_cost_debt_proven_chunk_16(), tail: Cons { head: floor_cost_debt_proven_chunk_17(), tail: Cons { head: floor_cost_debt_proven_chunk_18(), tail: Cons { head: floor_cost_debt_proven_chunk_19(), tail: Cons { head: floor_cost_debt_proven_chunk_20(), tail: Cons { head: floor_cost_debt_proven_chunk_21(), tail: Cons { head: floor_cost_debt_proven_chunk_22(), tail: Cons { head: floor_cost_debt_proven_chunk_23(), tail: Empty {} } } } } } } } } } } } }}}}}}}}}}}}} } fn floor_cost_debt_censored_chunks() -> List> { diff --git a/src/v2/workflow/floor_enrolment_dead_band.dag b/src/v2/workflow/floor_enrolment_dead_band.dag index a042186f362..5c369cb9b0c 100644 --- a/src/v2/workflow/floor_enrolment_dead_band.dag +++ b/src/v2/workflow/floor_enrolment_dead_band.dag @@ -80,10 +80,6 @@ fn mtcollins1_boot_matrix_enrolment_dead_band_observed_only() -> List = [ + EvalStepCostDropMeasurement { + identity: "test.claim.parse_test_kotlin_grammar.claim_kotlin_extended_function_parses_holds" as NonEmptyStr, + measured_by: "v2.test.parse census phase 4, claim_batch BuildBuddy invocations 2c8f9d0b and 9241511f on main of 2026-10-05 plus this change: verdict pass, the claim's own marginal over the new-witness budget even with its grammar preparation billed apart; re-derived by claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/parse/parse_test_kotlin_grammar_test.dag --functions claim_kotlin_extended_function_parses_holds. The billed work is one tokenize and one real parse of an extended function with the language's own grammar" as NonEmptyStr, + }, + EvalStepCostDropMeasurement { + identity: "test.claim.parse_test_rust_grammar.claim_rust_extended_function_parses_holds" as NonEmptyStr, + measured_by: "v2.test.parse census phase 4, claim_batch BuildBuddy invocations 2c8f9d0b and 9241511f on main of 2026-10-05 plus this change: verdict pass, the claim's own marginal over the new-witness budget even with its grammar preparation billed apart; re-derived by claim_batch --source-root dag --source-root src/v2 --entry src/v2/test/claim/parse/parse_test_rust_grammar_test.dag --functions claim_rust_extended_function_parses_holds. The billed work is one tokenize and one real parse of an extended function with the language's own grammar" as NonEmptyStr, + }, + ] + // THE COPRODUCT FRAGMENT-LOWERING CLAIMS (v2.test.parse census phase 3; `gunbc.rung_drop` // `coproduct_fragment_normalize_new_witness_eval_step_cost`). Three identities, each parsing ONE type // declaration fragment at its production and lowering it with normalize_node (two, for the two @@ -442,8 +462,15 @@ data floor_eval_step_cost_drop_coproduct_fragment_normalize_rows: List = [ + EvalStepCostDropMeasurement { + identity: "test.claim.machine_intake_arrival_converge_witness.w_bmc_secure_apply_with_the_mtjade1_standing_grant_converges_over_the_dry_world" as NonEmptyStr, + measured_by: "claim_batch --hermetic --source-root dag --source-root src/v2 --entry dag/test/claim/machine_intake/machine_intake_arrival_converge_witness_test.dag --function w_bmc_secure_apply_with_the_mtjade1_standing_grant_converges_over_the_dry_world (BuildBuddy, GUNBC_BIND_MEMORY_CGROUP_BYTES=30064771072): PASS, eval_steps over the new-witness budget. The billed work is converge_arrival_through_bmc_secure over the production prefix captures plus dry Apply+grant, which this claim is the only Apply execution of" as NonEmptyStr, + }, +] + fn floor_eval_step_cost_drop_all_rows() -> List { - concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(floor_eval_step_cost_drop_rows, floor_eval_step_cost_drop_apply_render_rows), floor_eval_step_cost_drop_live_forecast_rows), floor_eval_step_cost_drop_page_style_rows), floor_eval_step_cost_drop_interpreted_crypto_rows), floor_eval_step_cost_drop_app_attest_verifier_rows), floor_eval_step_cost_drop_span_program_rows), floor_eval_step_cost_drop_python_to_typescript_inhabitance_rows), floor_eval_step_cost_drop_boot_matrix_rows), floor_eval_step_cost_drop_dark_install_render_rows), floor_eval_step_cost_drop_build_job_membership_rows), floor_eval_step_cost_drop_dag_emit_round_trip_rows), floor_eval_step_cost_drop_approval_intent_digest_rows), floor_eval_step_cost_drop_dag_text_round_trip_rows), floor_eval_step_cost_drop_coproduct_fragment_normalize_rows) + concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(concat(floor_eval_step_cost_drop_rows, floor_eval_step_cost_drop_apply_render_rows), floor_eval_step_cost_drop_live_forecast_rows), floor_eval_step_cost_drop_page_style_rows), floor_eval_step_cost_drop_interpreted_crypto_rows), floor_eval_step_cost_drop_app_attest_verifier_rows), floor_eval_step_cost_drop_span_program_rows), floor_eval_step_cost_drop_python_to_typescript_inhabitance_rows), floor_eval_step_cost_drop_boot_matrix_rows), floor_eval_step_cost_drop_dark_install_render_rows), floor_eval_step_cost_drop_build_job_membership_rows), floor_eval_step_cost_drop_dag_emit_round_trip_rows), floor_eval_step_cost_drop_approval_intent_digest_rows), floor_eval_step_cost_drop_dag_text_round_trip_rows), floor_eval_step_cost_drop_coproduct_fragment_normalize_rows), floor_eval_step_cost_drop_foreign_grammar_extended_parse_rows), floor_eval_step_cost_drop_bmc_secure_apply_converge_rows) } // THE PROJECTION THE SEED RUNNER READS AND THE MODEL TESTS AGAINST. The runner refuses an empty or diff --git a/src/v2/workflow/floor_expected_red.dag b/src/v2/workflow/floor_expected_red.dag index 023c612a157..3bdd05a31b9 100644 --- a/src/v2/workflow/floor_expected_red.dag +++ b/src/v2/workflow/floor_expected_red.dag @@ -691,7 +691,7 @@ fn floor_expected_red_chunk_20() -> List { // spelling collides with a builtin or module free function; the gate is widened against a // measured population, never speculatively. fn floor_expected_red_chunk_21() -> List { - Cons { head: "v2.test.claim.local_binding_shadow.a_noncallable_local_binding_does_not_fall_through_to_the_module_function", tail:Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_extra_argument_must_refuse_at_compile", tail: Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_missing_argument_must_refuse_at_compile", tail: Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_wrong_argument_type_must_refuse_at_compile", tail: Cons { head: "test.claim.fabric_cell_evidence_algebra_witness.evidence_full_value_is_order_independent", tail: Empty {} } } } } } + Cons { head: "v2.test.claim.local_binding_shadow.a_noncallable_local_binding_does_not_fall_through_to_the_module_function", tail:Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_extra_argument_must_refuse_at_compile", tail: Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_missing_argument_must_refuse_at_compile", tail: Cons { head: "test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_wrong_argument_type_must_refuse_at_compile", tail: Empty {} } } } } } // THE SHADOW CHUNK IS DELETED AND THE ROSTER IS RENUMBERED RATHER THAN LEFT WITH A HOLE. The chunk @@ -805,6 +805,14 @@ fn floor_expected_red_chunk_interpreter_first_optional_divergence() -> List List { + Cons { head: "v2.test.long.cwc_resolution_probe.cwc_malformed_nary_coproduct_normalize_rejects", tail: Empty {} } +} + // THIRTEEN FURTHER IDENTITIES RETIRED 2026-08-26 BY ONE REPAIR, and the number is the finding. The // local-binding-shadow repair -- a call site spelling a name held by both a local binding and a // module-level free function now reaches the LOCAL -- was enrolled with ONE row of its own. On the @@ -1275,7 +1283,7 @@ fn floor_expected_red_chunk_infer_checking_mode() -> List { } fn floor_expected_red_chunks() -> List> { - Cons { head: floor_expected_red_chunk_text_wall_generic_formal(), tail: Cons { head: floor_expected_red_chunk_00(), tail: Cons { head: floor_expected_red_chunk_02(), tail: Cons { head: floor_expected_red_chunk_03(), tail: Cons { head: floor_expected_red_chunk_04(), tail: Cons { head: floor_expected_red_chunk_05(), tail: Cons { head: floor_expected_red_chunk_06(), tail: Cons { head: floor_expected_red_chunk_07(), tail: Cons { head: floor_expected_red_chunk_08(), tail: Cons { head: floor_expected_red_chunk_09(), tail: Cons { head: floor_expected_red_chunk_10(), tail: Cons { head: floor_expected_red_chunk_13(), tail: Cons { head: floor_expected_red_chunk_14(), tail: Cons { head: floor_expected_red_chunk_15(), tail: Cons { head: floor_expected_red_chunk_16(), tail: Cons { head: floor_expected_red_chunk_17(), tail: Cons { head: floor_expected_red_chunk_18(), tail: Cons { head: floor_expected_red_chunk_19(), tail: Cons { head: floor_expected_red_chunk_20(), tail: Cons { head: floor_expected_red_chunk_21(), tail: Cons { head: floor_expected_red_chunk_24(), tail: Cons { head: floor_expected_red_chunk_interpreter_first_optional_divergence(), tail: Cons { head: floor_expected_red_chunk_optional_equality_by_representation(), tail: Cons { head: floor_expected_red_chunk_disjoint_refinement_chains(), tail: Cons { head: floor_expected_red_chunk_shell_dag_cron_entry_line_builder(), tail: Cons { head: floor_expected_red_chunk_shell_dag_floor_peak_calibration_rows(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_fmt_gate_line(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_deploy_invoke_prelude(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_heal_regen_invoke(), tail: Cons { head: floor_expected_red_chunk_shell_dag_codex_app_server_trip_script(), tail: Cons { head: floor_expected_red_chunk_shell_dag_live_deploy_install_owned(), tail: Cons { head: floor_expected_red_chunk_shell_dag_live_deploy_restart_tailscale(), tail: Cons { head: floor_expected_red_chunk_live_tree_admission(), tail: Cons { head: floor_expected_red_chunk_emitted_add_algebra_ref_ungrounded(), tail: Cons { head: floor_expected_red_chunk_infer_checking_mode(), tail: Cons { head: floor_expected_red_chunk_product_at_kernel_scalar_formal(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } + Cons { head: floor_expected_red_chunk_text_wall_generic_formal(), tail: Cons { head: floor_expected_red_chunk_00(), tail: Cons { head: floor_expected_red_chunk_02(), tail: Cons { head: floor_expected_red_chunk_03(), tail: Cons { head: floor_expected_red_chunk_04(), tail: Cons { head: floor_expected_red_chunk_05(), tail: Cons { head: floor_expected_red_chunk_06(), tail: Cons { head: floor_expected_red_chunk_07(), tail: Cons { head: floor_expected_red_chunk_08(), tail: Cons { head: floor_expected_red_chunk_09(), tail: Cons { head: floor_expected_red_chunk_10(), tail: Cons { head: floor_expected_red_chunk_13(), tail: Cons { head: floor_expected_red_chunk_14(), tail: Cons { head: floor_expected_red_chunk_15(), tail: Cons { head: floor_expected_red_chunk_16(), tail: Cons { head: floor_expected_red_chunk_17(), tail: Cons { head: floor_expected_red_chunk_18(), tail: Cons { head: floor_expected_red_chunk_19(), tail: Cons { head: floor_expected_red_chunk_20(), tail: Cons { head: floor_expected_red_chunk_21(), tail: Cons { head: floor_expected_red_chunk_24(), tail: Cons { head: floor_expected_red_chunk_interpreter_first_optional_divergence(), tail: Cons { head: floor_expected_red_chunk_optional_equality_by_representation(), tail: Cons { head: floor_expected_red_chunk_disjoint_refinement_chains(), tail: Cons { head: floor_expected_red_chunk_shell_dag_cron_entry_line_builder(), tail: Cons { head: floor_expected_red_chunk_shell_dag_floor_peak_calibration_rows(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_fmt_gate_line(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_deploy_invoke_prelude(), tail: Cons { head: floor_expected_red_chunk_shell_dag_ci_heal_regen_invoke(), tail: Cons { head: floor_expected_red_chunk_shell_dag_codex_app_server_trip_script(), tail: Cons { head: floor_expected_red_chunk_shell_dag_live_deploy_install_owned(), tail: Cons { head: floor_expected_red_chunk_shell_dag_live_deploy_restart_tailscale(), tail: Cons { head: floor_expected_red_chunk_live_tree_admission(), tail: Cons { head: floor_expected_red_chunk_emitted_add_algebra_ref_ungrounded(), tail: Cons { head: floor_expected_red_chunk_infer_checking_mode(), tail: Cons { head: floor_expected_red_chunk_product_at_kernel_scalar_formal(), tail: Cons { head: floor_expected_red_chunk_coproduct_arm_without_constructor_name(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } // The three compile_accepted_unevaluable_program_control rows in chunk 21 are live again: run diff --git a/src/v2/workflow/floor_grandfathered_roster.dag b/src/v2/workflow/floor_grandfathered_roster.dag index f358282b043..28732672de6 100644 --- a/src/v2/workflow/floor_grandfathered_roster.dag +++ b/src/v2/workflow/floor_grandfathered_roster.dag @@ -374,6 +374,59 @@ data floor_grandfathered_removals: List = [ identity: "v2.test.claim.realize_advisory_soundness_test.witness_window_reds_on_one_unsound" as NonEmptyStr, deleted_in: "Advisories 5.4: v2.workflow.realize_advisory_soundness judged pairs from the GUNBC_REALIZE_ADVISORY logger, which no run sets and whose feeding runs were cut; the law, its test module and the logger are deleted together" as NonEmptyStr }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.assemble_receipt_carries_nonempty_rejection_chain" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.masked_overlap_located_reason_unmasks_terminal_refusal" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.masked_overlap_tail_preserves_overlap_evidence_and_remaining_chain" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.masked_resolve_not_found_unmasks_located_reason" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.overlap_evidence_binds_exact_row_not_global_roster" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.overlap_only_chain_keeps_overlap_located_reason" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.self_host.frontier_probe_overlap_detail.overlap_stage_attaches_evidence_when_diagnostics_stripped" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: grammar validation REFUSES every choice-overlap row, located at the owning production, so parse_grammar_choice_overlap_residue has no producer and the parse-stage overlap evidence carrier (v2.compiler.parse_diagnostic) is deleted. v2.test.self_host.frontier_probe_overlap_detail witnessed that carrier's merge and masking; its subject no longer exists" as NonEmptyStr, + }, + WitnessRenamed { + previous_identity: "v2.test.self_host.frontier_probe_overlap_detail.emit_stage_receipt_has_no_assemble_detail" as NonEmptyStr, + new_identity: "v2.test.self_host.frontier_probe_assemble_detail.emit_stage_receipt_has_no_assemble_detail" as NonEmptyStr, + renamed_in: "the grammar-overlap cut: the claim is unchanged and its subject survives, but its module was named for the overlap-detail carrier the cut deletes, so it moved to v2.test.self_host.frontier_probe_assemble_detail and is judged against the new-witness tier" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.self_host.frontier_probe_semantic_family_receipt.both_measured_shards_name_body_lowering_beneath_the_mask_holds" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: v2.compiler.self_host.frontier_probe_semantic_family_receipt recorded a 2026-08 observation whose subject was the grammar-overlap residue masking a deeper chain. Validation now refuses every overlap, so that mask cannot exist and the module's own note required a fresh survey to replace its rows; the module and its consistency claims are deleted, the receipt stays in git history" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.self_host.frontier_probe_semantic_family_receipt.every_capped_shard_exceeds_the_cap_it_names_holds" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: v2.compiler.self_host.frontier_probe_semantic_family_receipt recorded a 2026-08 observation whose subject was the grammar-overlap residue masking a deeper chain. Validation now refuses every overlap, so that mask cannot exist and the module's own note required a fresh survey to replace its rows; the module and its consistency claims are deleted, the receipt stays in git history" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.self_host.frontier_probe_semantic_family_receipt.every_measured_chain_is_non_uniform_holds" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: v2.compiler.self_host.frontier_probe_semantic_family_receipt recorded a 2026-08 observation whose subject was the grammar-overlap residue masking a deeper chain. Validation now refuses every overlap, so that mask cannot exist and the module's own note required a fresh survey to replace its rows; the module and its consistency claims are deleted, the receipt stays in git history" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.self_host.frontier_probe_semantic_family_receipt.the_five_shards_partition_into_measured_and_capped_holds" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: v2.compiler.self_host.frontier_probe_semantic_family_receipt recorded a 2026-08 observation whose subject was the grammar-overlap residue masking a deeper chain. Validation now refuses every overlap, so that mask cannot exist and the module's own note required a fresh survey to replace its rows; the module and its consistency claims are deleted, the receipt stays in git history" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.self_host.frontier_probe_semantic_family_receipt.two_shards_are_measured_and_three_are_capped_holds" as NonEmptyStr, + deleted_in: "the grammar-overlap cut: v2.compiler.self_host.frontier_probe_semantic_family_receipt recorded a 2026-08 observation whose subject was the grammar-overlap residue masking a deeper chain. Validation now refuses every overlap, so that mask cannot exist and the module's own note required a fresh survey to replace its rows; the module and its consistency claims are deleted, the receipt stays in git history" as NonEmptyStr, + }, WitnessRenamed { previous_identity: "v2.test.parse.parse_binding_fidelity.witness_ambiguous_export_fail_closed_holds" as NonEmptyStr, new_identity: "test.claim.parse_test_parse_binding_fidelity.witness_ambiguous_export_fail_closed_holds" as NonEmptyStr, @@ -484,6 +537,60 @@ data floor_grandfathered_removals: List = [ new_identity: "test.claim.parse_test_token_stream_cursor.token_stream_cursor_remaining_matches_skip_holds" as NonEmptyStr, renamed_in: "the v2.test.parse ungated census (gunbc#13372) phase 1: v2.test.parse.* modules sat outside required_gate_prefixes and ran only as changed witnesses; the module moved into the gated test.claim.parse_test family with a byte-identical claim body, so the identity changed and the claim is judged against the new-witness tier" as NonEmptyStr, }, + WitnessRenamed { + previous_identity: "v2.test.claim.symbol_index.containment.symbol_index_unique_variant_without_alias_red_control_holds" as NonEmptyStr, + new_identity: "v2.test.claim.symbol_index.containment.symbol_index_module_variant_alias_requires_a_unique_count_holds" as NonEmptyStr, + renamed_in: "the variant-field native binding repair (gunbc#13210): the alias-binding repair files a module variant alias whenever a tag's declaration count is unique, so the state the old red control pinned -- a unique variant whose alias is MISSING -- no longer occurs and the red is unwritable as stated; the successor establishes the two-sided rule the repair implements (alias present when the count is unique, absent when the tag is declared twice), and as a new identity it is judged against the new-witness tier rather than inheriting the cut row's grandfathered one" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.parse_table_memo_provider_preserves_catalog_retention" as NonEmptyStr, + deleted_in: "the parse memo deletion (operator ruling R2 2026-10-08): the v2 packrat parse memo served captures whose occurrence ids were minted on abandoned backtrack paths, so its carrier (v2.compiler.parse ParseTableRealization entries, the extdeps.realization.parse_table_memo catalog row, and the parse-table demand of v2.compiler.materialization_carriers) is deleted; this claim pinned the retention of the deleted parse_table_memo provider, which no longer exists. Its compile-stage sibling compile_stage_memo_provider_preserves_catalog_retention stays" as NonEmptyStr + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.parse_table_carrier_inhabits_realization" as NonEmptyStr, + deleted_in: "the parse memo deletion (operator ruling R2 2026-10-08): the v2 packrat parse memo served captures whose occurrence ids were minted on abandoned backtrack paths, so its carrier (v2.compiler.parse ParseTableRealization entries, the extdeps.realization.parse_table_memo catalog row, and the parse-table demand of v2.compiler.materialization_carriers) is deleted; this claim pinned the deleted ParseTableRealization carrier inhabiting the Realization model. Its compile-stage sibling compile_stage_carrier_inhabits_realization stays" as NonEmptyStr + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.parse_table_empty_uses_catalog_identity" as NonEmptyStr, + deleted_in: "the parse memo deletion (operator ruling R2 2026-10-08): the v2 packrat parse memo served captures whose occurrence ids were minted on abandoned backtrack paths, so its carrier (v2.compiler.parse ParseTableRealization entries, the extdeps.realization.parse_table_memo catalog row, and the parse-table demand of v2.compiler.materialization_carriers) is deleted; this claim pinned the empty parse table's catalog identity (the deleted parse_table_memo_id)" as NonEmptyStr + }, + WitnessRenamed { + previous_identity: "v2.test.claim.compiler_materialization_witness.parse_table_memo_plural_reds_without_provider" as NonEmptyStr, + new_identity: "v2.test.claim.compiler_materialization_witness.compile_stage_memo_plural_reds_without_provider" as NonEmptyStr, + renamed_in: "the parse memo deletion (operator ruling R2 2026-10-08): the v2 packrat parse memo served captures whose occurrence ids were minted on abandoned backtrack paths, so its carrier (v2.compiler.parse ParseTableRealization entries, the extdeps.realization.parse_table_memo catalog row, and the parse-table demand of v2.compiler.materialization_carriers) is deleted; the plural-demand red control was retargeted from the deleted parse-table demand to the compile-stage demand, and it is named for the demand it now exercises" as NonEmptyStr + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.compiler_materialization_ladder_holds" as NonEmptyStr, + deleted_in: "the compile-stage memo deletion (roadmap native-memory-inert-carriers, operator ruling 2026-10-08): the compile-stage memo's only lookup answered Miss unconditionally, so its carrier (v2.compiler.materialization_carriers compile-stage demand, provider, governed door and admission controls, and the extdeps.realization.compile_stage_memo catalog row) is deleted; this claim pinned that carrier, and the generic ladder obligations it exercised stay enrolled on fixture subjects in test.claim.materialization_ladder_witness" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.compile_stage_carrier_inhabits_realization" as NonEmptyStr, + deleted_in: "the compile-stage memo deletion (roadmap native-memory-inert-carriers, operator ruling 2026-10-08): the compile-stage memo's only lookup answered Miss unconditionally, so its carrier (v2.compiler.materialization_carriers compile-stage demand, provider, governed door and admission controls, and the extdeps.realization.compile_stage_memo catalog row) is deleted; this claim pinned that carrier, and the generic ladder obligations it exercised stay enrolled on fixture subjects in test.claim.materialization_ladder_witness" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.compiler_materialization_witness.compile_stage_memo_provider_preserves_catalog_retention" as NonEmptyStr, + deleted_in: "the compile-stage memo deletion (roadmap native-memory-inert-carriers, operator ruling 2026-10-08): the compile-stage memo's only lookup answered Miss unconditionally, so its carrier (v2.compiler.materialization_carriers compile-stage demand, provider, governed door and admission controls, and the extdeps.realization.compile_stage_memo catalog row) is deleted; this claim pinned that carrier, and the generic ladder obligations it exercised stay enrolled on fixture subjects in test.claim.materialization_ladder_witness" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.staging.cached_stage_hit_skips_stage" as NonEmptyStr, + deleted_in: "the compile-stage memo deletion (roadmap native-memory-inert-carriers, operator ruling 2026-10-08): v2.std.staging cached_stage, resolve_probe and CacheProbe had one production caller, the always-Miss compile-stage memo, and are deleted with it; this claim exercised only that lookup wrapper, while then_outcome and its claims remain" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.staging.cached_stage_miss_runs_stage" as NonEmptyStr, + deleted_in: "the compile-stage memo deletion (roadmap native-memory-inert-carriers, operator ruling 2026-10-08): v2.std.staging cached_stage, resolve_probe and CacheProbe had one production caller, the always-Miss compile-stage memo, and are deleted with it; this claim exercised only that lookup wrapper, while then_outcome and its claims remain" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.native_cache_fusion_test.witness_warm_marker_agrees_share" as NonEmptyStr, + deleted_in: "the inert-carrier deletion (roadmap native-memory-inert-carriers, census #5): v2.workflow.native_cache_fusion stated a synthetic agreement law between a modeled host warm-marker observation and std.emit_on_demand lookup; no production code read it, so the module and its own claims are deleted and the real emit-host native build cache is untouched" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.native_cache_fusion_test.witness_cold_marker_absent_agrees_recompute" as NonEmptyStr, + deleted_in: "the inert-carrier deletion (roadmap native-memory-inert-carriers, census #5): v2.workflow.native_cache_fusion stated a synthetic agreement law between a modeled host warm-marker observation and std.emit_on_demand lookup; no production code read it, so the module and its own claims are deleted and the real emit-host native build cache is untouched" as NonEmptyStr, + }, + WitnessDeleted { + identity: "v2.test.claim.native_cache_fusion_test.witness_cold_control_forces_recompute_agreement" as NonEmptyStr, + deleted_in: "the inert-carrier deletion (roadmap native-memory-inert-carriers, census #5): v2.workflow.native_cache_fusion stated a synthetic agreement law between a modeled host warm-marker observation and std.emit_on_demand lookup; no production code read it, so the module and its own claims are deleted and the real emit-host native build cache is untouched" as NonEmptyStr, + }, ] fn floor_grandfathered_chunk_000() -> List { @@ -567,7 +674,7 @@ fn floor_grandfathered_chunk_019() -> List { } fn floor_grandfathered_chunk_020() -> List { - Cons { head: "test.claim.prose_citation_census_witness.a_longer_identifier_ending_in_the_suffix_is_not_read_as_its_tail", tail: Cons { head: "test.claim.prose_citation_census_witness.a_name_retired_from_one_path_resolves_its_locality", tail: Cons { head: "test.claim.prose_citation_census_witness.a_name_retired_from_several_paths_refuses_to_pick_a_locality", tail: Cons { head: "test.claim.prose_citation_census_witness.an_annotation_citation_in_another_file_is_one_site_at_its_line", tail: Cons { head: "test.claim.prose_citation_census_witness.an_annotation_line_and_a_prose_row_line_are_different_carriers", tail: Cons { head: "test.claim.prose_citation_census_witness.a_partial_module_index_splits_the_rows_across_both_lists", tail: Cons { head: "test.claim.prose_citation_census_witness.a_populated_module_index_leaves_nothing_unidentified", tail: Cons { head: "test.claim.prose_citation_census_witness.a_program_data_line_is_read_as_not_a_prose_row_not_as_malformed", tail: Cons { head: "test.claim.prose_citation_census_witness.a_prose_row_citation_is_a_prose_row_site", tail: Cons { head: "test.claim.prose_citation_census_witness.a_row_still_declared_in_its_own_module_is_not_retired", tail: Cons { head: "test.claim.prose_citation_census_witness.a_row_whose_bare_name_survives_only_in_another_module_is_retired", tail: Cons { head: "test.claim.prose_citation_census_witness.a_semantic_line_naming_a_retired_row_is_not_a_citation_site", tail: Cons { head: "test.claim.prose_citation_census_witness.a_word_that_merely_starts_with_the_suffix_is_not_a_name", tail: Cons { head: "test.claim.prose_citation_census_witness.every_baseline_line_lands_in_exactly_one_arm", tail: Cons { head: "test.claim.prose_citation_census_witness.prose_naming_a_row_outside_the_retired_index_is_not_a_site", tail: Cons { head: "test.claim.prose_citation_census_witness.the_census_totals_are_projections_of_its_own_site_list", tail: Cons { head: "test.claim.prose_citation_census_witness.the_module_index_reads_module_lines_and_ignores_others", tail: Cons { head: "test.claim.prose_citation_census_witness.the_produced_site_identities_join_with_the_expected_ones_both_ways", tail: Cons { head: "test.claim.prose_citation_census_witness.the_qualified_identity_is_the_module_dot_name_spelling", tail: Cons { head: "test.claim.prose_citation_census_witness.the_same_path_seen_twice_is_still_one_origin", tail: Cons { head: "test.claim.prose_citation_census_witness.the_sample_is_the_twelve_sites_that_were_drawn", tail: Cons { head: "test.claim.prose_citation_census_witness.the_scan_is_non_constant_in_its_retired_index", tail: Cons { head: "test.claim.prose_citation_census_witness.the_trailing_blank_line_is_blank_not_malformed", tail: Cons { head: "test.claim.prose_citation_census_witness.the_twelve_sampled_specimens_are_all_recognised", tail: Cons { head: "test.claim.resolved_call_emission_identity_witness_test.w_divergent_map_get_keeps_declared_outcome_identity", tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } + Cons { head: "test.claim.prose_citation_census_witness.a_longer_identifier_ending_in_the_suffix_is_not_read_as_its_tail", tail: Cons { head: "test.claim.prose_citation_census_witness.a_name_retired_from_one_path_resolves_its_locality", tail: Cons { head: "test.claim.prose_citation_census_witness.a_name_retired_from_several_paths_refuses_to_pick_a_locality", tail: Cons { head: "test.claim.prose_citation_census_witness.an_annotation_citation_in_another_file_is_one_site_at_its_line", tail: Cons { head: "test.claim.prose_citation_census_witness.an_annotation_line_and_a_prose_row_line_are_different_carriers", tail: Cons { head: "test.claim.prose_citation_census_witness.a_partial_module_index_splits_the_rows_across_both_lists", tail: Cons { head: "test.claim.prose_citation_census_witness.a_populated_module_index_leaves_nothing_unidentified", tail: Cons { head: "test.claim.prose_citation_census_witness.a_program_data_line_is_read_as_not_a_prose_row_not_as_malformed", tail: Cons { head: "test.claim.prose_citation_census_witness.a_prose_row_citation_is_a_prose_row_site", tail: Cons { head: "test.claim.prose_citation_census_witness.a_row_still_declared_in_its_own_module_is_not_retired", tail: Cons { head: "test.claim.prose_citation_census_witness.a_row_whose_bare_name_survives_only_in_another_module_is_retired", tail: Cons { head: "test.claim.prose_citation_census_witness.a_semantic_line_naming_a_retired_row_is_not_a_citation_site", tail: Cons { head: "test.claim.prose_citation_census_witness.a_word_that_merely_starts_with_the_suffix_is_not_a_name", tail: Cons { head: "test.claim.prose_citation_census_witness.every_baseline_line_lands_in_exactly_one_arm", tail: Cons { head: "test.claim.prose_citation_census_witness.prose_naming_a_row_outside_the_retired_index_is_not_a_site", tail: Cons { head: "test.claim.prose_citation_census_witness.the_census_totals_are_projections_of_its_own_site_list", tail: Cons { head: "test.claim.prose_citation_census_witness.the_module_index_reads_module_lines_and_ignores_others", tail: Cons { head: "test.claim.prose_citation_census_witness.the_produced_site_identities_join_with_the_expected_ones_both_ways", tail: Cons { head: "test.claim.prose_citation_census_witness.the_qualified_identity_is_the_module_dot_name_spelling", tail: Cons { head: "test.claim.prose_citation_census_witness.the_same_path_seen_twice_is_still_one_origin", tail: Cons { head: "test.claim.prose_citation_census_witness.the_sample_is_the_twelve_sites_that_were_drawn", tail: Cons { head: "test.claim.prose_citation_census_witness.the_scan_is_non_constant_in_its_retired_index", tail: Cons { head: "test.claim.prose_citation_census_witness.the_trailing_blank_line_is_blank_not_malformed", tail: Cons { head: "test.claim.prose_citation_census_witness.the_twelve_sampled_specimens_are_all_recognised", tail: Cons { head: "test.claim.resolved_call_emission_identity_witness_test.w_divergent_map_get_checked_keeps_declared_outcome_identity", tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } fn floor_grandfathered_chunk_021() -> List { diff --git a/src/v2/workflow/floor_preparation.dag b/src/v2/workflow/floor_preparation.dag deleted file mode 100644 index 41a5aae7161..00000000000 --- a/src/v2/workflow/floor_preparation.dag +++ /dev/null @@ -1,443 +0,0 @@ -module v2.workflow.floor_preparation - -import std.types { Bool, List, Map, String } -import std.content_hash { - Fnv1a64Structural, - content_hash_atom, - content_hash_combine_structural, - serialize_content_hash, -} -import std.materialization_provider { - ArtifactRequest, - MaterializedArtifact, - ProviderProbe, - ProviderLookup, - ProviderHit, - ResolveClosureRequest, - compiler_identity_input, - provider_serve, - request_key, -} -import std.realization_schedule { - ScheduleWitnessEntry, - CorpusWitnessKind, - ExecutionWitnessKind, - NativeBundleWitnessKind, -} -import v2.workflow.floor_discovery { - FloorDiscoveryRequest, - floor_discovery_request_identity, -} -import v2.std.algebra { list_reverse } -import std.optional { Absent, Present } - -// The prepared floor subject is an opaque semantic identity over the exact discovery request, -// selected witness identities, and the existing std.materialization_provider ResolveClosureRequest -// keys. It deliberately does NOT carry MultiEntryIndex or any other compiler container. -// ProviderProbe is the host read-back seam and provider_serve remains the one request/artifact -// binding authority. A miss, damaged artifact, incomplete artifact, wrong key, wrong content, or -// wrong hash family remains the corresponding typed ProviderLookup inside -// PreparedFloorProviderRefused. There is no cold-construction or fallback arm. -// SequentialChildProcess may therefore realize this identity as verified materialized -// resolved-closure artifacts while SharedWalkProcess may realize it as live capabilities; neither -// realization changes the semantic identity. FIRST CONSUMER: the scoped-witness population named by -// ROADMAP ci-scoped-worker-shared-substrate. The old scoped cold constructor may delete only after -// that consumer can serve every exact selected identity through this contract. - -type FloorPreparationPolicy - = ResolveSelectedClosuresV1 {} - -type PreparedFloorEntryRequest { - witness: ScheduleWitnessEntry - closure_digest: Fnv1a64Structural -} - -type PreparedFloorSubjectRequest { - discovery: FloorDiscoveryRequest - selected: List - policy: FloorPreparationPolicy -} - -type PreparedClaimSubjectIdentity sole_constructor { - discovery_request_identity: Fnv1a64Structural - selected_identity_digest: Fnv1a64Structural - preparation_policy_identity: Fnv1a64Structural - artifact_set_identity: Fnv1a64Structural -} - -type PreparedFloorEntryProbe { - witness: ScheduleWitnessEntry - probe: ProviderProbe -} - -type PreparedFloorEntryArtifact { - witness: ScheduleWitnessEntry - artifact: MaterializedArtifact -} - -type PreparedFloorSubjectRefusal - = PreparedFloorDuplicateSelectedIdentities { duplicates: List } - | PreparedFloorDuplicateProbeIdentities { duplicates: List } - | PreparedFloorMissingProbeIdentities { missing: List } - | PreparedFloorUnexpectedProbeIdentities { unexpected: List } - | PreparedFloorProviderRefused { witness: ScheduleWitnessEntry, lookup: ProviderLookup } - -type PreparedFloorSubjectServe - = PreparedFloorSubjectReady { - identity: PreparedClaimSubjectIdentity - entries: List - } - | PreparedFloorSubjectRefused { cause: PreparedFloorSubjectRefusal } - -fn floor_preparation_policy_identity(policy: FloorPreparationPolicy) -> Fnv1a64Structural { - match policy { - ResolveSelectedClosuresV1 {} => content_hash_atom(value: "floor-preparation.resolve-selected-closures-v1") - } -} - -fn prepared_floor_witness_kind_identity(witness: ScheduleWitnessEntry) -> String { - match witness.kind { - CorpusWitnessKind => "corpus" - ExecutionWitnessKind => "execution" - NativeBundleWitnessKind => "native-bundle" - } -} - -fn prepared_floor_witness_identity(witness: ScheduleWitnessEntry) -> Fnv1a64Structural { - let with_entry = content_hash_combine_structural( - left: content_hash_atom(value: "prepared-floor-witness-v1"), - right: content_hash_atom(value: witness.entry), - ) - let with_function = content_hash_combine_structural( - left: with_entry, - right: content_hash_atom(value: witness.function), - ) - content_hash_combine_structural( - left: with_function, - right: content_hash_atom(value: prepared_floor_witness_kind_identity(witness: witness)), - ) -} - -fn prepared_floor_selected_identity( - selected: List, -) -> Fnv1a64Structural { - selected |> fold( - init: content_hash_atom(value: "prepared-floor-selected-v1"), - f: (acc, entry) => content_hash_combine_structural( - left: acc, - right: prepared_floor_witness_identity(witness: entry.witness), - ) - ) -} - -fn prepared_floor_entry_artifact_request( - discovery: FloorDiscoveryRequest, - entry: PreparedFloorEntryRequest, -) -> ArtifactRequest { - ResolveClosureRequest { - closure_digest: entry.closure_digest, - compiler: compiler_identity_input(digest: discovery.subject.tool_identity), - } -} - -fn prepared_floor_artifact_set_identity( - request: PreparedFloorSubjectRequest, -) -> Fnv1a64Structural { - request.selected |> fold( - init: content_hash_atom(value: "prepared-floor-artifact-set-v1"), - f: (acc, entry) => content_hash_combine_structural( - left: acc, - right: content_hash_atom( - value: serialize_content_hash( - hash: request_key( - req: prepared_floor_entry_artifact_request( - discovery: request.discovery, - entry: entry, - ) - ) - ) - ), - ) - ) -} - -fn prepared_claim_subject_identity( - request: PreparedFloorSubjectRequest, -) -> PreparedClaimSubjectIdentity { - PreparedClaimSubjectIdentity { - discovery_request_identity: floor_discovery_request_identity(request: request.discovery), - selected_identity_digest: prepared_floor_selected_identity(selected: request.selected), - preparation_policy_identity: floor_preparation_policy_identity(policy: request.policy), - artifact_set_identity: prepared_floor_artifact_set_identity(request: request), - } -} - -type PreparedFloorWitnessMultiplicityIndex { - witnessed: Map - duplicated: Map -} - -fn prepared_floor_identity_map_contains( - identities: Map, - witness: ScheduleWitnessEntry, -) -> Bool { - match identities.lookup(witness) { - Absent => false - Present { value: _ } => true - } -} - -fn prepared_floor_witness_multiplicity_step( - index: PreparedFloorWitnessMultiplicityIndex, - witness: ScheduleWitnessEntry, -) -> PreparedFloorWitnessMultiplicityIndex { - if prepared_floor_identity_map_contains(identities: index.witnessed, witness: witness) { - PreparedFloorWitnessMultiplicityIndex { - witnessed: index.witnessed, - duplicated: map_insert(index.duplicated, witness, true), - } - } else { - PreparedFloorWitnessMultiplicityIndex { - witnessed: map_insert(index.witnessed, witness, true), - duplicated: index.duplicated, - } - } -} - -fn prepared_floor_selected_multiplicity_index( - selected: List, -) -> PreparedFloorWitnessMultiplicityIndex { - selected |> fold( - init: PreparedFloorWitnessMultiplicityIndex { - witnessed: empty_map(), - duplicated: empty_map(), - }, - f: (index, entry) => prepared_floor_witness_multiplicity_step( - index: index, - witness: entry.witness, - ) - ) -} - -type PreparedFloorProbeIndex { - multiplicity: PreparedFloorWitnessMultiplicityIndex - by_witness: Map -} - -fn prepared_floor_probe_index_step( - index: PreparedFloorProbeIndex, - entry: PreparedFloorEntryProbe, -) -> PreparedFloorProbeIndex { - PreparedFloorProbeIndex { - multiplicity: prepared_floor_witness_multiplicity_step( - index: index.multiplicity, - witness: entry.witness, - ), - by_witness: map_insert(index.by_witness, entry.witness, entry), - } -} - -fn prepared_floor_probe_index( - probes: List, -) -> PreparedFloorProbeIndex { - probes |> fold( - init: PreparedFloorProbeIndex { - multiplicity: PreparedFloorWitnessMultiplicityIndex { - witnessed: empty_map(), - duplicated: empty_map(), - }, - by_witness: empty_map(), - }, - f: (index, entry) => prepared_floor_probe_index_step(index: index, entry: entry) - ) -} - -type PreparedFloorIdentityIndex { - selected: PreparedFloorWitnessMultiplicityIndex - probes: PreparedFloorProbeIndex -} - -fn prepared_floor_identity_index( - selected: List, - probes: List, -) -> PreparedFloorIdentityIndex { - PreparedFloorIdentityIndex { - selected: prepared_floor_selected_multiplicity_index(selected: selected), - probes: prepared_floor_probe_index(probes: probes), - } -} - -fn prepared_floor_probe_for_witness( - index: PreparedFloorIdentityIndex, - witness: ScheduleWitnessEntry, -) -> PreparedFloorEntryProbe? { - index.probes.by_witness.lookup(witness) -} - -// THE SELECTED/PROBE JOIN IS INDEXED ON THE EXISTING ScheduleWitnessEntry IDENTITY. The former -// duplicate, missing, unexpected, and serve lookups filtered an entire population once per row, so -// preparing n witnesses performed quadratic identity comparisons before any artifact could be -// served. Each side now makes one pass through the existing native Map substrate: the witnessed map -// establishes membership, the duplicated map preserves multiplicity standing, and the probe map -// serves the already-proven singleton row. The four refusal projections then make one linear pass -// each while preserving input order. There is no digest-key shortcut and therefore no collision -// state — every map key is ScheduleWitnessEntry itself, whose structural equality is the same -// entry/function/kind grain as schedule_witness_entry_eq. - -fn prepared_floor_duplicate_selected_identities( - selected: List, - index: PreparedFloorIdentityIndex, -) -> List { - selected - |> filter(entry => prepared_floor_identity_map_contains( - identities: index.selected.duplicated, - witness: entry.witness, - )) - |> map(entry => entry.witness) -} - -fn prepared_floor_duplicate_probe_identities( - probes: List, - index: PreparedFloorIdentityIndex, -) -> List { - probes - |> filter(entry => prepared_floor_identity_map_contains( - identities: index.probes.multiplicity.duplicated, - witness: entry.witness, - )) - |> map(entry => entry.witness) -} - -fn prepared_floor_missing_probe_identities( - selected: List, - index: PreparedFloorIdentityIndex, -) -> List { - selected - |> filter(entry => !prepared_floor_identity_map_contains( - identities: index.probes.multiplicity.witnessed, - witness: entry.witness, - )) - |> map(entry => entry.witness) -} - -fn prepared_floor_unexpected_probe_identities( - probes: List, - index: PreparedFloorIdentityIndex, -) -> List { - probes - |> filter(entry => !prepared_floor_identity_map_contains( - identities: index.selected.witnessed, - witness: entry.witness, - )) - |> map(entry => entry.witness) -} - -type PreparedFloorServeFold - = PreparedFloorServeContinue { entries: List } - | PreparedFloorServeStopped { cause: PreparedFloorSubjectRefusal } - -fn prepared_floor_serve_one( - request: PreparedFloorSubjectRequest, - index: PreparedFloorIdentityIndex, - state: PreparedFloorServeFold, - entry: PreparedFloorEntryRequest, -) -> PreparedFloorServeFold { - match state { - PreparedFloorServeStopped { cause: c } => PreparedFloorServeStopped { cause: c } - PreparedFloorServeContinue { entries: entries } => { - match prepared_floor_probe_for_witness( - index: index, - witness: entry.witness, - ) { - Absent => PreparedFloorServeStopped { - cause: PreparedFloorMissingProbeIdentities { missing: [entry.witness] } - } - Present { value: prepared_probe } => match provider_serve( - req: prepared_floor_entry_artifact_request( - discovery: request.discovery, - entry: entry, - ), - probe: prepared_probe.probe, - ) { - ProviderHit { artifact: artifact, identity: _ } => - PreparedFloorServeContinue { - entries: concat([PreparedFloorEntryArtifact { - witness: entry.witness, - artifact: artifact, - }], entries), - } - other => PreparedFloorServeStopped { - cause: PreparedFloorProviderRefused { - witness: entry.witness, - lookup: other, - } - } - } - } - } - } -} - -// THE SERVE FOLD PREPENDS AND REVERSES ONCE. Right-appending one artifact to the growing prefix -// copied the accumulated list on every selected witness, making the success path quadratic even -// after identity lookup was indexed. prepared_floor_serve_one now conses the singleton artifact -// before the accumulator; prepared_floor_subject_serve reverses exactly once at the successful -// boundary so PreparedFloorSubjectReady.entries remains in selected-request order. The -// exact_prepared_artifact_set_serves control observes that order and reds if the terminal reverse -// is removed. - -fn prepared_floor_subject_serve( - request: PreparedFloorSubjectRequest, - probes: List, -) -> PreparedFloorSubjectServe { - let index = prepared_floor_identity_index(selected: request.selected, probes: probes) - let duplicate_selected = prepared_floor_duplicate_selected_identities( - selected: request.selected, - index: index, - ) - let duplicate_probes = prepared_floor_duplicate_probe_identities( - probes: probes, - index: index, - ) - let missing = prepared_floor_missing_probe_identities( - selected: request.selected, - index: index, - ) - let unexpected = prepared_floor_unexpected_probe_identities( - probes: probes, - index: index, - ) - if duplicate_selected.count() > 0 { - PreparedFloorSubjectRefused { - cause: PreparedFloorDuplicateSelectedIdentities { duplicates: duplicate_selected } - } - } else if duplicate_probes.count() > 0 { - PreparedFloorSubjectRefused { - cause: PreparedFloorDuplicateProbeIdentities { duplicates: duplicate_probes } - } - } else if missing.count() > 0 { - PreparedFloorSubjectRefused { - cause: PreparedFloorMissingProbeIdentities { missing: missing } - } - } else if unexpected.count() > 0 { - PreparedFloorSubjectRefused { - cause: PreparedFloorUnexpectedProbeIdentities { unexpected: unexpected } - } - } else { - match request.selected |> fold( - init: PreparedFloorServeContinue { entries: [] }, - f: (state, entry) => prepared_floor_serve_one( - request: request, - index: index, - state: state, - entry: entry, - ) - ) { - PreparedFloorServeContinue { entries: entries } => PreparedFloorSubjectReady { - identity: prepared_claim_subject_identity(request: request), - entries: list_reverse(xs: entries), - } - PreparedFloorServeStopped { cause: cause } => PreparedFloorSubjectRefused { cause: cause } - } - } -} diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 28d3129de9e..3efe867e1e0 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -17,7 +17,7 @@ import std.materialization_ladder { import std.measure { Millisecond, Nanosecond, byte_size, millisecond_count, nanosecond, nanosecond_count } import v2.workflow.required_floor { required_floor_new_witness_envelope_ms } import v2.std.algebra { any, list_flat_map, list_map } -import v2.std.collection { empty_map, map_get, map_insert } +import v2.std.collection { empty_map, map_get_checked, map_insert } import v2.std.diagnostic { Accepted, Rejected, Outcome } import std.optional { Absent, Present, Optional } @@ -401,7 +401,7 @@ fn share_map_lookup_causes(o: Outcome>) -> List, claim: String) -> Bool { - let o = map_get(wholes, claim) + let o = map_get_checked(wholes, claim) if length(xs: share_map_lookup_causes(o: o)) > 0 { false } else { @@ -414,7 +414,7 @@ fn reads_whole_value(wholes: Map, claim: String) -> Bool { } fn projection_count(counts: Map, field: String) -> Int { - let o = map_get(counts, field) + let o = map_get_checked(counts, field) if length(xs: share_map_lookup_causes(o: o)) > 0 { 0 } else { @@ -432,14 +432,14 @@ type ShareCountState { } fn add_projection_count(state: ShareCountState, wholes: Map, claim: String, field: String) -> ShareCountState { - let whole_o = map_get(wholes, claim) + let whole_o = map_get_checked(wholes, claim) let whole_unread = share_map_lookup_causes(o: whole_o) if length(xs: whole_unread) > 0 { ShareCountState { counts: state.counts, unread: concat(state.unread, whole_unread) } } else if reads_whole_value(wholes: wholes, claim: claim) { state } else { - let count_o = map_get(state.counts, field) + let count_o = map_get_checked(state.counts, field) let count_unread = share_map_lookup_causes(o: count_o) if length(xs: count_unread) > 0 { ShareCountState { counts: state.counts, unread: concat(state.unread, count_unread) } @@ -475,7 +475,7 @@ fn sole_projections_from_state(reads: List, counts: Map - let count_o = map_get(counts, f) + let count_o = map_get_checked(counts, f) let unread = share_map_lookup_causes(o: count_o) if length(xs: unread) > 0 { [] @@ -798,7 +798,25 @@ fn floor_single_claim_fill_debt_active_claims() -> List { // steps each against the 72300 new-witness budget). That run names exactly these nine. // Twelve more were added from probe 37238532455, for the sixteen rows gunbc#13187 added to the hand // roster (v2.test.claim.compiler.generic_formal_instantiation): all twelve claims of the module. +// One more was added from gunbc#13308's own floor run 37270043819 (head dc9a7079), which measured it +// COMPLETED-OVER-COST-REQUIREMENT as a single-claim demander: v2.test.claim.native_census.infer_cascade +// census_infer_provider_cascade_is_one_root_with_fan_out_one_holds, the one real-route inhabitance +// claim; assemble + resolve + infer of the cascade fixture. Its module's other controls supply values +// at the folds they judge (DESIGN section 3 witness rule) and are not members. Trigger: assemble + infer +// of a one-module fixture under the per-claim budget. // Re-derive with that probe; do not extend by hand. +// gunbc#13320 (XL-2): its ONE real-path inhabitance claim, kept per the DESIGN section 3 witness rule (every +// other claim in the module supplies its input; sharp-raven-357 approved this member). Over the 72,300 +// new-witness budget on its floor run at a2485f0 by the pipeline's per-claim fixed +// cost of assembling and inferring a one-module fixture, not the test's own work. Trigger: assemble plus infer +// of a one-module fixture runs under the per-claim budget (gunbc.recurring_failure_mode +// shared_precondition_re_derived_once_per_claim_frame). +// gunbc#13307 (XL-2): its ONE real-path inhabitance claim, kept per the DESIGN section 3 witness rule (every +// other claim in the module supplies its input; sharp-raven-357 approved this member). Over the 72,300 +// new-witness budget on that PR's floor run 37240332005 (886,357 steps) by the pipeline's per-claim fixed cost +// of assembling and inferring a one-module fixture, not the test's own work. Trigger: assemble plus infer of a +// one-module fixture runs under the per-claim budget (gunbc.recurring_failure_mode +// shared_precondition_re_derived_once_per_claim_frame). // // THE xl2 MEMBERS (test.claim.namespace_xl2_rehearsal_census) were the bundle `xl2_control_runs`, // declined as BundleOfDisjointProjections on floor run 37194676016: each reads its own rehearsal @@ -815,13 +833,76 @@ fn floor_single_claim_fill_debt_active_claims() -> List { // is SHARED; what remains over the new-witness budget on floor run 37207183924 is each claim's OWN // stage (its route, infer or front end), which the bundle had been netting from the claim silently. // That transfer is named here per claim instead. +// Two more, from gunbc#12942 (body lowering's sealed term), on the same ground as every member above: +// each claim is the one demander of a nullary parse producer that change had added to the hand +// roster before main deleted it (door_data_literal_normalized, ulsh_route_postfix), and each is that +// module's ONE inhabitance claim on the real front-end path, so supplying its subject is not available +// without removing the last real execution (DESIGN section 3, the pairing obligation). These two were +// written in the change itself rather than read from a probe pair, because the change and the +// roster's deletion met only at the merge; re-derive them with the probe like every other member. +// One more for gunbc#13325, from floor run 37247019357 (930,914 eval steps): the ONE real-path +// inhabitance claim of v2.test.claim.compiler.declared_return_result, whose other five claims supply +// their inputs at the result-type interface (DESIGN section 3). Its fixture program, drr_route_source, +// has no other demander, so derived sharing cannot cover it. // gunbc#13207 (XL-2 collection concat): its ONE real-path inhabitance claim, kept per the DESIGN section 3 // witness rule (every other claim in the module supplies its input; sharp-raven-357 approved this member). // Measured on that PR's floor run 37238836376: 902,997 eval steps against the 72,300 new-witness budget -- the // pipeline's per-claim fixed cost of assembling and inferring a one-module fixture, not the test's own work. // Trigger: assemble plus infer of a one-module fixture runs under the per-claim budget // (gunbc.recurring_failure_mode shared_precondition_re_derived_once_per_claim_frame). +// gunbc#13511 (kernel-value join): the ONE real-path inhabitance of v2.test.claim.compiler.generic_closure_outcome_construct_probe, +// gbo_a_kernel_literal_inhabits_an_alias_of_the_corpus_int_holds (red with the join deleted and with input-only recognition). Its own-frame eval steps are +// re-derived by claim_batch --hermetic --entry src/v2/test/claim/compiler/generic_closure_outcome_construct_probe_test.dag +// --functions gbo_a_kernel_literal_inhabits_an_alias_of_the_corpus_int_holds (receipt: BuildBuddy a63835e8, pinned +// 46da0c8fa0; red with the join deleted, BuildBuddy 50c58a76, and with input-only recognition, BuildBuddy 2002ea86). +// Trigger: shared_precondition_re_derived_once_per_claim_frame, as above. +// gunbc#13621 (a data reference uses its value): its ONE real-path inhabitance claim, +// drv_a_data_reference_is_typed_at_its_declared_type_and_evaluates_to_its_value, kept per the DESIGN section 3 +// witness rule (the module's other six claims supply their input; sharp-raven-357 approved this member). Measured +// by that PR's required floor (claim_executor --required-ci --required-lane witnesses) on run 37941635939 at +// 315f7ec8: 86,566 eval steps against the 72,300 new-witness budget, after the fixture was reduced to one +// constant, one fn and one call, assembled once (a served shared fill) and inferred once. The residual is the +// one inference, which the floor cannot share or net because an inferred tree is not a portable served value +// (cross-claim-share-store-declined cause=ServeCacheValueNotPortable). Trigger: the served-value tier admits +// an InferredTree as a portable value, SUFFICIENT FOR a one-claim inference fixture to be netted like an +// assembly -- or the shared_precondition_re_derived_once_per_claim_frame trigger above, whichever lands first. data floor_single_claim_fill_debt: List = [ + SingleClaimFillDebtModule { + module: "v2.test.claim.body_lowering_data_literal_door", + claims: [ + SingleClaimFillDebtClaim { claim: "a_data_declaration_with_an_int_literal_normalizes_holds", standing: ActiveFillDebt } + ] + }, + SingleClaimFillDebtModule { + module: "v2.test.claim.body_lowering_unrecognised_lowered_shape", + claims: [ + SingleClaimFillDebtClaim { claim: "a_passed_through_atom_leaves_the_fold_sealed", standing: ActiveFillDebt } + ] + }, + SingleClaimFillDebtModule { + module: "v2.test.claim.compiler.literal_elaboration", + claims: [ + SingleClaimFillDebtClaim { claim: "lel_route_runs_on_the_real_path", standing: ActiveFillDebt }, + ] + }, + SingleClaimFillDebtModule { + module: "v2.test.claim.compiler.collection_count_realization", + claims: [ + SingleClaimFillDebtClaim { claim: "ccc_route_runs_on_the_real_path", standing: ActiveFillDebt }, + ] + }, + SingleClaimFillDebtModule { + module: "v2.test.claim.compiler.declared_return_result", + claims: [ + SingleClaimFillDebtClaim { claim: "drr_the_real_route_judges_a_projected_generic_call_result", standing: ActiveFillDebt } + ] + }, + SingleClaimFillDebtModule { + module: "v2.test.claim.compiler.generic_closure_outcome_construct_probe", + claims: [ + SingleClaimFillDebtClaim { claim: "gbo_a_kernel_literal_inhabits_an_alias_of_the_corpus_int_holds", standing: ActiveFillDebt } + ] + }, SingleClaimFillDebtModule { module: "v2.test.claim.body_lowering.fold_operand_structure", claims: [ @@ -838,6 +919,12 @@ data floor_single_claim_fill_debt: List = [ SingleClaimFillDebtClaim { claim: "xl2_nested_reference_is_residual_after_the_strip_holds", standing: ActiveFillDebt } ] }, + SingleClaimFillDebtModule { + module: "v2.test.claim.compiler.data_reference_value", + claims: [ + SingleClaimFillDebtClaim { claim: "drv_a_data_reference_is_typed_at_its_declared_type_and_evaluates_to_its_value", standing: ActiveFillDebt } + ] + }, SingleClaimFillDebtModule { module: "v2.test.claim.compiler.collection_concat_realization", claims: [ @@ -907,6 +994,12 @@ data floor_single_claim_fill_debt: List = [ SingleClaimFillDebtClaim { claim: "ccr_piped_map_head_binds_its_row", standing: ActiveFillDebt } ] }, + SingleClaimFillDebtModule { + module: "v2.test.claim.native_census.infer_cascade", + claims: [ + SingleClaimFillDebtClaim { claim: "census_infer_provider_cascade_is_one_root_with_fan_out_one_holds", standing: ActiveFillDebt } + ] + }, SingleClaimFillDebtModule { module: "v2.test.claim.native_census.cascade_root_fan_out", claims: [ @@ -1145,7 +1238,7 @@ data floor_single_claim_fill_debt: List = [ claims: [ SingleClaimFillDebtClaim { claim: "rcf_a_bool_declared_field_value_of_the_wrong_type_refuses_holds", standing: ActiveFillDebt }, SingleClaimFillDebtClaim { claim: "rcf_a_construct_is_its_declared_record_at_a_declared_type_holds", standing: ActiveFillDebt }, - SingleClaimFillDebtClaim { claim: "rcf_a_cross_module_records_field_value_is_counted_not_judged_holds", standing: ActiveFillDebt }, + SingleClaimFillDebtClaim { claim: "rcf_a_cross_module_records_field_value_of_the_wrong_type_refuses_holds", standing: ActiveFillDebt }, SingleClaimFillDebtClaim { claim: "rcf_a_cross_module_records_missing_field_refuses_holds", standing: ActiveFillDebt }, SingleClaimFillDebtClaim { claim: "rcf_a_generic_record_at_its_instantiation_is_accepted_and_decided_holds", standing: ActiveFillDebt }, SingleClaimFillDebtClaim { claim: "rcf_a_generic_records_second_field_must_inhabit_the_instance_holds", standing: ActiveFillDebt }, @@ -1413,7 +1506,7 @@ data floor_single_claim_fill_debt: List = [ ] }, SingleClaimFillDebtModule { - module: "v2.test.claim.parse.pattern_and_let_sugar", + module: "test.claim.parse_test_pattern_and_let_sugar", claims: [ SingleClaimFillDebtClaim { claim: "a_bare_assignment_lowers_identically_to_let_holds", standing: ActiveFillDebt }, SingleClaimFillDebtClaim { claim: "a_generic_pattern_refuses_at_its_generic_params_holds", standing: ActiveFillDebt }, diff --git a/src/v2/workflow/floor_route_gap.dag b/src/v2/workflow/floor_route_gap.dag index dbd2cd7655a..8290b11b7dc 100644 --- a/src/v2/workflow/floor_route_gap.dag +++ b/src/v2/workflow/floor_route_gap.dag @@ -487,6 +487,8 @@ fn floor_route_gap_expectation_chunk_00() -> List { tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.codex_app_server_press_wet_witness_test.press_account_trip_yields_standing_or_typed_refusal_never_turn_started_holds", operation: "Dir", ground: NoMockResponse {} }, tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.srv3_seeded_install_media_real_execution.a_seed_artifact_is_fetched_read_back_and_an_escaping_name_moves_no_byte_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.codex_package_delivery_wet_witness_test.acquire_required_codex_tarball_sha512_matches_lock", operation: "Dir", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.codex_package_delivery_wet_witness_test.acquire_exact_target_pair_admits_wrapper_and_platform", operation: "KernelName", ground: NoMockResponse {} }, @@ -520,7 +522,7 @@ fn floor_route_gap_expectation_chunk_00() -> List { head: FloorRouteGapExpectation { identity: "test.claim.codex_supervised_turn_wet_witness_test.wet_finish_observe_native_digest_only", operation: "Check", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.codex_supervised_turn_wet_witness_test.wet_finish_observe_toolchain_only", operation: "Check", ground: NoMockResponse {} }, - tail: Empty {} + tail: Empty {} } } } } @@ -1149,6 +1151,8 @@ fn floor_route_gap_expectation_chunk_09() -> List { head: FloorRouteGapExpectation { identity: "test.claim.materialization_store_local_wet_witness.a_refused_part_publish_leaves_no_temporary_copy_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.materialization_store_local_wet_witness.a_leftover_copy_that_cannot_be_removed_leaves_the_store_unwritable_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.materialization_store_local_wet_witness.n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Empty {} } } @@ -1180,6 +1184,7 @@ fn floor_route_gap_expectation_chunk_09() -> List { } } } + } } } } @@ -1190,22 +1195,18 @@ fn floor_route_gap_expectation_chunk_09() -> List { } } -// MegaRAC committed-artifact sha256sum: first host effect is shell.Mktemp.Dir (checksum +// Committed SOL host payload sha256sum: first host effect is shell.Mktemp.Dir (checksum // file), which declares no mock_response. Triple with local_repo_wet_schedule and the -// LocalRepoWetLane exclusion on megarac_spx_ui_surface_artifact_integrity_witness_test.dag. -// The catalog-contents Read of the same checkout path is hermetic input, not a route gap. +// LocalRepoWetLane exclusion on mtcollins_sol_host_payload_integrity_witness_test.dag. // Memory capture verification consumes DigestStdin over the bytes read once. fn floor_route_gap_expectation_chunk_10() -> List { Cons { head: FloorRouteGapExpectation { identity: "test.claim.memory_capture_real_execution_witness.w_capture_digest_binds_consumed_content", operation: "DigestStdin", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.pre_os_capture_replay_witness.w_early_exit_refuses_absence_on_real_bytes", operation: "DigestStdin", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.pre_os_capture_replay_witness.w_four_real_captures_retain_statement_inventory_and_missing_receipts", operation: "DigestStdin", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.pre_os_capture_replay_witness.w_truncated_configuration_cannot_support_absence", operation: "DigestStdin", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.pre_os_capture_replay_witness.w_real_host_userspace_positive_and_truncated_negative", operation: "DigestStdin", ground: NoMockResponse {} }, tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.megarac_spx_ui_surface_artifact_integrity_witness.committed_spx_ui_surface_hashes_to_the_catalog_identity", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.megarac_spx_ui_surface_artifact_integrity_witness.committed_sol_host_payload_hashes_to_the_positive_control_identity", operation: "Dir", ground: NoMockResponse {} }, + head: FloorRouteGapExpectation { identity: "test.claim.mtcollins_sol_host_payload_integrity_witness.committed_sol_host_payload_hashes_to_the_positive_control_identity", operation: "Dir", ground: NoMockResponse {} }, tail: Empty {} } } } } - } } } @@ -1292,6 +1293,10 @@ fn floor_route_gap_expectation_chunk_12() -> List { tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.durable_cas_file_store_wet_witness.a_racing_writer_under_a_window_retries_and_loses_no_commit_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.durable_cas_file_store_wet_witness.a_window_observation_at_generation_1500_terminates_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.durable_cas_file_store_wet_witness.reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.filesystem_link_create_new_wet_witness.a_link_publishes_the_source_bytes_and_keeps_the_source_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.filesystem_link_create_new_wet_witness.an_existing_target_refuses_as_occupied_and_is_unchanged_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, @@ -1307,6 +1312,8 @@ fn floor_route_gap_expectation_chunk_12() -> List { } } } + } + } } } } @@ -1447,7 +1454,7 @@ fn floor_route_gap_expectation_chunk_14() -> List { } }}} -// The ten test.claim.durable_exclusive_hold_file_store_wet_witness identities: the hold protocol +// The test.claim.durable_exclusive_hold_file_store_wet_witness identities: the hold protocol // over the real file store -- absent acquires at 1, held refuses without writing (another owner and // its own spelling), release to N+1 and reacquire at N+2, two contenders over one free observation, // store refusal distinct from contention, refused releases leave no next generation, and a commit @@ -1477,7 +1484,16 @@ fn floor_route_gap_expectation_chunk_15() -> List { head: FloorRouteGapExpectation { identity: "test.claim.durable_exclusive_hold_file_store_wet_witness.a_write_time_store_refusal_is_reported_as_unavailable_not_as_a_lost_race_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.durable_exclusive_hold_file_store_wet_witness.a_live_release_plan_carries_the_observed_generation_and_the_free_bytes_by_real_execution", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.durable_exclusive_hold_file_store_wet_witness.w_after_acquire_invokes_the_real_slot_release", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.durable_exclusive_hold_file_store_wet_witness.w_failed_acquire_runs_no_body", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.durable_exclusive_hold_file_store_wet_witness.w_refused_acquire_body_writes_a_marker_when_called_directly", operation: "DirWithTemplate", ground: NoMockResponse {} }, tail: Empty {} + } + } + } } } } @@ -2098,6 +2114,23 @@ fn floor_route_gap_expectation_chunk_41() -> List { } } +// The two wet witnesses of the workspace source pack: test.claim.seeded_filler_real_execution (the +// seeded filler by real truncate/openssl) and test.claim.workspace_source_pack_archive_real_execution +// (the two-root tar and its digest readback). Each first mints a directory with shell.Mktemp.Dir, +// which the hermetic route has no arm for. Triple enrollment as for chunk_31: these expectations, a +// WetScheduledClaim row per function in local_repo_wet_schedule, and the gunbc.ci_layer_roots +// LocalRepoWetLane rows for both files. +fn floor_route_gap_expectation_chunk_42() -> List { + [ + FloorRouteGapExpectation { identity: "test.claim.seeded_filler_real_execution.a_seeded_filler_is_exactly_its_size_and_reproducible_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + FloorRouteGapExpectation { identity: "test.claim.seeded_filler_real_execution.two_seeds_give_different_fillers_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + FloorRouteGapExpectation { identity: "test.claim.seeded_filler_real_execution.a_zero_stand_in_compresses_and_the_filler_does_not_red_control_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + FloorRouteGapExpectation { identity: "test.claim.workspace_source_pack_archive_real_execution.the_two_root_archive_carries_the_placeholder_and_reads_back_clean_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + FloorRouteGapExpectation { identity: "test.claim.workspace_source_pack_archive_real_execution.same_size_real_content_at_the_placeholder_path_is_refused_by_digest_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + FloorRouteGapExpectation { identity: "test.claim.workspace_source_pack_archive_real_execution.one_ordinary_file_and_zero_placeholders_packs_clean_by_real_execution", operation: "Dir", ground: NoMockResponse {} } + ] +} + // The test.claim.scm_repository_save_real_execution_witness identities: the guarded save's real // write over the committed v7 fixture (first effect Filesystem.Write), and a first save from // absence on a throwaway path (first effect shell.Mktemp.DirWithTemplate). Triple enrollment as @@ -2130,40 +2163,13 @@ fn floor_route_gap_expectation_chunk_30() -> List { } } -// THE mtcollins1 KVM OBSERVER WET CONTROLS (gunbc#12492) and the SOL channel-loss control that asks the -// screen run on the local-repo wet lane (v2.workflow.local_repo_wet_terminal local_repo_wet_schedule), -// where all held on required-floor run 36583319535 (srv4-03). The same run measured each identity's -// hermetic route gap: shell.Mktemp.Dir / DirWithTemplate declares no mock_response. +// The SOL channel-loss control that asks the screen runs on the local-repo wet lane +// (v2.workflow.local_repo_wet_terminal local_repo_wet_schedule); its hermetic route gap is +// shell.Mktemp.Dir, which declares no mock_response. fn floor_route_gap_expectation_chunk_29() -> List { Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_busy_viewer_slot_refuses_the_observer_and_the_handoff", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_connection_lost_mid_boot_is_reported_and_no_still_is_claimed_after_it", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_held_observer_is_admitted_and_its_triggered_still_is_hash_bound", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_launch_that_cannot_publish_its_pid_stops_its_child", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.an_owned_process_is_released_and_observed_gone", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.an_unreadable_canvas_is_acquisition_failed_with_the_browsers_words", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.an_unreadable_process_state_never_skips_the_kill_of_an_unpublished_child", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_record_naming_another_process_is_foreign_and_not_released", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_term_ignoring_child_with_an_unpublished_record_is_observed_stopped", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_transport_that_exits_at_once_is_not_serving", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_relative_observer_directory_is_resolved_once", operation: "DirWithTemplate", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_root_page_that_navigates_cannot_destroy_the_login", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { - head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness.a_viewer_without_its_feature_list_never_opens_kvm", operation: "Dir", ground: NoMockResponse {} }, - tail: Cons { head: FloorRouteGapExpectation { identity: "test.claim.machine_intake.sol_hold_stdin_wet_witness.a_channel_loss_asks_the_screen_and_the_ask_does_not_change_the_verdict", operation: "DirWithTemplate", ground: NoMockResponse {} }, - tail: Empty {} } } } } } } } } } } } } } } + tail: Empty {} } } // test.claim.roadmap.session_placement_wet_witness (dogfood G5, gunbc#13074): all five reach @@ -2323,7 +2329,7 @@ fn floor_route_gap_expectation_chunk_37() -> List { } fn floor_route_gap_expectation_chunks() -> List> { - Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Cons { head: floor_route_gap_expectation_chunk_20(), tail: Cons { head: floor_route_gap_expectation_chunk_21(), tail: Cons { head: floor_route_gap_expectation_chunk_22(), tail: Cons { head: floor_route_gap_expectation_chunk_23(), tail: Cons { head: floor_route_gap_expectation_chunk_24(), tail: Cons { head: floor_route_gap_expectation_chunk_25(), tail: Cons { head: floor_route_gap_expectation_chunk_26(), tail: Cons { head: floor_route_gap_expectation_chunk_27(), tail: Cons { head: floor_route_gap_expectation_chunk_28(), tail: Cons { head: floor_route_gap_expectation_chunk_29(), tail: Cons { head: floor_route_gap_expectation_chunk_30(), tail: Cons { head: floor_route_gap_expectation_chunk_31(), tail: Cons { head: floor_route_gap_expectation_chunk_32(), tail: Cons { head: floor_route_gap_expectation_chunk_33(), tail: Cons { head: floor_route_gap_expectation_chunk_34(), tail: Cons { head: floor_route_gap_expectation_chunk_35(), tail: Cons { head: floor_route_gap_expectation_chunk_36(), tail: Cons { head: floor_route_gap_expectation_chunk_37(), tail: Cons { head: floor_route_gap_expectation_chunk_38(), tail: Cons { head: floor_route_gap_expectation_chunk_39(), tail: Cons { head: floor_route_gap_expectation_chunk_40(), tail: Cons { head: floor_route_gap_expectation_chunk_41(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }} + Cons { head: floor_route_gap_expectation_chunk_00(), tail: Cons { head: floor_route_gap_expectation_chunk_01(), tail: Cons { head: floor_route_gap_expectation_chunk_02(), tail: Cons { head: floor_route_gap_expectation_chunk_03(), tail: Cons { head: floor_route_gap_expectation_chunk_04(), tail: Cons { head: floor_route_gap_expectation_chunk_05(), tail: Cons { head: floor_route_gap_expectation_chunk_06(), tail: Cons { head: floor_route_gap_expectation_chunk_07(), tail: Cons { head: floor_route_gap_expectation_chunk_08(), tail: Cons { head: floor_route_gap_expectation_chunk_09(), tail: Cons { head: floor_route_gap_expectation_chunk_10(), tail: Cons { head: floor_route_gap_expectation_chunk_11(), tail: Cons { head: floor_route_gap_expectation_chunk_12(), tail: Cons { head: floor_route_gap_expectation_chunk_13(), tail: Cons { head: floor_route_gap_expectation_chunk_14(), tail: Cons { head: floor_route_gap_expectation_chunk_15(), tail: Cons { head: floor_route_gap_expectation_chunk_16(), tail: Cons { head: floor_route_gap_expectation_chunk_17(), tail: Cons { head: floor_route_gap_expectation_chunk_18(), tail: Cons { head: floor_route_gap_expectation_chunk_19(), tail: Cons { head: floor_route_gap_expectation_chunk_20(), tail: Cons { head: floor_route_gap_expectation_chunk_21(), tail: Cons { head: floor_route_gap_expectation_chunk_22(), tail: Cons { head: floor_route_gap_expectation_chunk_23(), tail: Cons { head: floor_route_gap_expectation_chunk_24(), tail: Cons { head: floor_route_gap_expectation_chunk_25(), tail: Cons { head: floor_route_gap_expectation_chunk_26(), tail: Cons { head: floor_route_gap_expectation_chunk_27(), tail: Cons { head: floor_route_gap_expectation_chunk_28(), tail: Cons { head: floor_route_gap_expectation_chunk_29(), tail: Cons { head: floor_route_gap_expectation_chunk_30(), tail: Cons { head: floor_route_gap_expectation_chunk_31(), tail: Cons { head: floor_route_gap_expectation_chunk_32(), tail: Cons { head: floor_route_gap_expectation_chunk_33(), tail: Cons { head: floor_route_gap_expectation_chunk_34(), tail: Cons { head: floor_route_gap_expectation_chunk_35(), tail: Cons { head: floor_route_gap_expectation_chunk_36(), tail: Cons { head: floor_route_gap_expectation_chunk_37(), tail: Cons { head: floor_route_gap_expectation_chunk_38(), tail: Cons { head: floor_route_gap_expectation_chunk_39(), tail: Cons { head: floor_route_gap_expectation_chunk_40(), tail: Cons { head: floor_route_gap_expectation_chunk_41(), tail: Cons { head: floor_route_gap_expectation_chunk_42(), tail: Cons { head: floor_route_gap_expectation_chunk_43(), tail: Empty {} } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } } }} } @@ -2381,7 +2387,9 @@ fn floor_route_gap_chunk_05() -> List { head: "test.claim.repository_convergence_wet_witness_test.unreadable_head_refuses_before_fetch_or_mutation", tail: Cons { head: "test.claim.repository_convergence_wet_witness_test.wrong_primary_standing_refuses_from_gits_roster", - tail: Empty {} } } } } } } } + tail: Cons { + head: "test.claim.repository_convergence_wet_witness_test.a_legacy_derived_roster_file_is_retired_and_read_back_absent", + tail: Empty {} } } } } } } } } } fn floor_route_gap_chunks() -> List> { @@ -2402,8 +2410,28 @@ fn floor_route_gap_roster() -> List { fn floor_route_gap_expectation_chunk_38() -> List { Cons { head: FloorRouteGapExpectation { identity: "test.claim.workspace_network_real_reader_witness_test.real_reader_takes_the_route_the_reader_identity_dictates", operation: "Read", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.workspace_transcript_rate_real_execution.a_real_tree_grows_gains_a_transcript_and_truncates_one_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.workspace_transcript_rate_real_execution.a_root_that_does_not_exist_refuses_by_real_execution", operation: "Dir", ground: NoMockResponse {} }, tail: Empty {} - } + } } } +} + +// test.claim.roadmap.roadmap_belt_exit_drain_wet_witness (dogfood #4/#6, the exit drain and the +// collected-worker read over a real spool): every control's first effect is fresh_spool, which is +// shell.Mktemp.DirWithTemplate and declares no mock_response, so the nominal floor holds each identity +// here and the local-repo wet schedule runs it. Added BY PREDICTION, as chunk 32's post-gate controls +// were: each identity's first effect is that unconditional DirWithTemplate. +fn floor_route_gap_expectation_chunk_43() -> List { + Cons { + head: FloorRouteGapExpectation { identity: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness.a_recorded_worker_exit_is_handed_to_its_attempt_and_leaves_the_inbox", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness.a_reviewer_exit_queues_no_launch_and_a_malformed_entry_is_refused", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Cons { + head: FloorRouteGapExpectation { identity: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness.a_collected_workers_exit_is_read_from_the_spool_alone", operation: "DirWithTemplate", ground: NoMockResponse {} }, + tail: Empty {} + } } } } // THE ADMISSION PARTITION, MODELED ON THE AUTHORITY THAT OWNS THE REGISTER. This is the single diff --git a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag index 792a8896cb0..5ea10db532f 100644 --- a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag +++ b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag @@ -166,7 +166,7 @@ data unimported_bare_provider_dispositions: List WetEvidenceRequirement { // this population and false for BinWitnessWet, whose members also download ISOs, fetch over the // network, build with real cargo and provision remote hosts. // +// The repository-convergence legacy-roster retirement member (a mktemp scratch repository and bare +// remote, git plumbing over them, one planted file deleted through extdeps.filesystem and read back +// absent, then RecursiveForce) has the same local-repo-only property: no network, no cargo, no +// remote host. It is scheduled so the retirement route reaches a terminal verdict on the candidate +// rather than standing as a held hermetic route gap. +// // THE ROSTER DOES NOT LAND EMPTY, and that is deliberate rather than incidental. An empty roster // would make the lane's coverage claim vacuously true and leave nothing proving the executor runs // anything at all -- a gate permanently green by construction, which DESIGN section 4b calls a @@ -495,6 +501,24 @@ fn local_repo_wet_schedule() -> List { function: "an_undecodable_slot_is_unreadable_and_never_absent", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness", function: "a_recorded_worker_exit_is_handed_to_its_attempt_and_leaves_the_inbox" }, + entry: "dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag", + function: "a_recorded_worker_exit_is_handed_to_its_attempt_and_leaves_the_inbox", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness", function: "a_reviewer_exit_queues_no_launch_and_a_malformed_entry_is_refused" }, + entry: "dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag", + function: "a_reviewer_exit_queues_no_launch_and_a_malformed_entry_is_refused", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.roadmap.roadmap_belt_exit_drain_wet_witness", function: "a_collected_workers_exit_is_read_from_the_spool_alone" }, + entry: "dag/test/claim/roadmap/roadmap_belt_exit_drain_wet_witness_test.dag", + function: "a_collected_workers_exit_is_read_from_the_spool_alone", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.roadmap.session_placement_wet_witness", function: "a_slot_with_no_placement_record_is_kept_in_window_and_released_after" }, entry: "dag/test/claim/roadmap/session_placement_wet_witness_test.dag", @@ -987,6 +1011,12 @@ fn local_repo_wet_schedule() -> List { function: "install_media_remaster_ensure_grub_cmdline_inserts_by_real_execution", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.srv3_seeded_install_media_real_execution", function: "a_seed_artifact_is_fetched_read_back_and_an_escaping_name_moves_no_byte_by_real_execution" }, + entry: "dag/test/claim/srv3/srv3_seeded_install_media_real_execution_witness_test.dag", + function: "a_seed_artifact_is_fetched_read_back_and_an_escaping_name_moves_no_byte_by_real_execution", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.mtcollins1_census_image_local_wet", function: "the_attempt_three_capture_binds_and_reads_by_real_execution" }, entry: "dag/test/claim/machine_intake/mtcollins1_census_image_local_wet_test.dag", @@ -1875,6 +1905,18 @@ fn local_repo_wet_schedule() -> List { function: "a_racing_writer_under_a_window_retries_and_loses_no_commit_by_real_execution", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.durable_cas_file_store_wet_witness", function: "a_window_observation_at_generation_1500_terminates_by_real_execution" }, + entry: "dag/test/claim/durable_cas_file_store_wet_witness_test.dag", + function: "a_window_observation_at_generation_1500_terminates_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.durable_cas_file_store_wet_witness", function: "reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution" }, + entry: "dag/test/claim/durable_cas_file_store_wet_witness_test.dag", + function: "reclaim_stops_before_a_later_generation_when_an_earlier_delete_fails_by_real_execution", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.filesystem_link_create_new_wet_witness", function: "a_link_publishes_the_source_bytes_and_keeps_the_source_by_real_execution" }, entry: "dag/test/claim/filesystem_link_create_new_wet_witness_test.dag", @@ -1959,6 +2001,24 @@ fn local_repo_wet_schedule() -> List { function: "a_live_release_plan_carries_the_observed_generation_and_the_free_bytes_by_real_execution", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.durable_exclusive_hold_file_store_wet_witness", function: "w_after_acquire_invokes_the_real_slot_release" }, + entry: "dag/test/claim/durable_exclusive_hold_file_store_wet_witness_test.dag", + function: "w_after_acquire_invokes_the_real_slot_release", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.durable_exclusive_hold_file_store_wet_witness", function: "w_failed_acquire_runs_no_body" }, + entry: "dag/test/claim/durable_exclusive_hold_file_store_wet_witness_test.dag", + function: "w_failed_acquire_runs_no_body", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.durable_exclusive_hold_file_store_wet_witness", function: "w_refused_acquire_body_writes_a_marker_when_called_directly" }, + entry: "dag/test/claim/durable_exclusive_hold_file_store_wet_witness_test.dag", + function: "w_refused_acquire_body_writes_a_marker_when_called_directly", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.fabric.fabric_control_plane_wet_witness", function: "the_right_reservation_releases_its_cell_to_free_at_the_next_generation_by_real_execution" }, entry: "dag/test/claim/fabric/fabric_control_plane_wet_witness_test.dag", @@ -2217,6 +2277,12 @@ fn local_repo_wet_schedule() -> List { function: "a_leftover_copy_that_cannot_be_removed_leaves_the_store_unwritable_by_real_execution", expectation: ExpectedToHold {} }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.materialization_store_local_wet_witness", function: "n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution" }, + entry: "dag/test/claim/materialization_store_local_wet_witness_test.dag", + function: "n_commits_under_one_held_session_acquire_the_hold_once_by_real_execution", + expectation: ExpectedToHold {} + }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.pre_os_capture_replay_witness", function: "w_early_exit_refuses_absence_on_real_bytes" }, entry: "dag/test/claim/machine_intake/pre_os_capture_replay_witness_test.dag", @@ -2242,20 +2308,8 @@ fn local_repo_wet_schedule() -> List { expectation: ExpectedToHold {} }, WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.megarac_spx_ui_surface_artifact_integrity_witness", function: "committed_spx_ui_surface_hashes_to_the_catalog_identity" }, - entry: "dag/test/claim/bmc/megarac_spx_ui_surface_artifact_integrity_witness_test.dag", - function: "committed_spx_ui_surface_hashes_to_the_catalog_identity", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.megarac_spx_ui_surface_artifact_integrity_witness", function: "committed_spx_ui_surface_agrees_with_every_served_ui_surface_standing" }, - entry: "dag/test/claim/bmc/megarac_spx_ui_surface_artifact_integrity_witness_test.dag", - function: "committed_spx_ui_surface_agrees_with_every_served_ui_surface_standing", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.megarac_spx_ui_surface_artifact_integrity_witness", function: "committed_sol_host_payload_hashes_to_the_positive_control_identity" }, - entry: "dag/test/claim/bmc/megarac_spx_ui_surface_artifact_integrity_witness_test.dag", + identity: WitnessIdentity { module_path: "test.claim.mtcollins_sol_host_payload_integrity_witness", function: "committed_sol_host_payload_hashes_to_the_positive_control_identity" }, + entry: "dag/test/claim/bmc/mtcollins_sol_host_payload_integrity_witness_test.dag", function: "committed_sol_host_payload_hashes_to_the_positive_control_identity", expectation: ExpectedToHold {} }, @@ -2679,90 +2733,6 @@ fn local_repo_wet_schedule() -> List { function: "a_channel_loss_asks_the_screen_and_the_ask_does_not_change_the_verdict", expectation: ExpectedToHold {} }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_held_observer_is_admitted_and_its_triggered_still_is_hash_bound" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_held_observer_is_admitted_and_its_triggered_still_is_hash_bound", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_root_page_that_navigates_cannot_destroy_the_login" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_root_page_that_navigates_cannot_destroy_the_login", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_viewer_without_its_feature_list_never_opens_kvm" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_viewer_without_its_feature_list_never_opens_kvm", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_busy_viewer_slot_refuses_the_observer_and_the_handoff" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_busy_viewer_slot_refuses_the_observer_and_the_handoff", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_connection_lost_mid_boot_is_reported_and_no_still_is_claimed_after_it" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_connection_lost_mid_boot_is_reported_and_no_still_is_claimed_after_it", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "an_unreadable_canvas_is_acquisition_failed_with_the_browsers_words" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "an_unreadable_canvas_is_acquisition_failed_with_the_browsers_words", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_stale_listening_line_from_another_pid_is_not_readiness" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_stale_listening_line_from_another_pid_is_not_readiness", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_transport_that_exits_at_once_is_not_serving" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_transport_that_exits_at_once_is_not_serving", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_launch_that_cannot_publish_its_pid_stops_its_child" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_launch_that_cannot_publish_its_pid_stops_its_child", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_term_ignoring_child_with_an_unpublished_record_is_observed_stopped" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_term_ignoring_child_with_an_unpublished_record_is_observed_stopped", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "an_unreadable_process_state_never_skips_the_kill_of_an_unpublished_child" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "an_unreadable_process_state_never_skips_the_kill_of_an_unpublished_child", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_relative_observer_directory_is_resolved_once" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_relative_observer_directory_is_resolved_once", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_record_naming_another_process_is_foreign_and_not_released" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "a_record_naming_another_process_is_foreign_and_not_released", - expectation: ExpectedToHold {} - }, - WetScheduledClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "an_owned_process_is_released_and_observed_gone" }, - entry: "dag/test/claim/machine_intake/mtcollins1_kvm_observer_protocol_wet_witness_test.dag", - function: "an_owned_process_is_released_and_observed_gone", - expectation: ExpectedToHold {} - }, WetScheduledClaim { identity: WitnessIdentity { module_path: "test.claim.machine_intake.sol_hold_stdin_wet_witness", function: "an_unreadable_incident_is_reported_and_is_not_success" }, entry: "dag/test/claim/machine_intake/sol_hold_stdin_wet_witness_test.dag", @@ -3050,6 +3020,60 @@ fn local_repo_wet_schedule() -> List { entry: "dag/test/claim/runner/browser_ready_label_real_path_witness_test.dag", function: "revoke_executes_the_dropin_removal_for_real_on_a_scratch_root", expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.workspace_transcript_rate_real_execution", function: "a_real_tree_grows_gains_a_transcript_and_truncates_one_by_real_execution" }, + entry: "dag/test/claim/workspace_transcript_rate_real_execution_witness_test.dag", + function: "a_real_tree_grows_gains_a_transcript_and_truncates_one_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.workspace_transcript_rate_real_execution", function: "a_root_that_does_not_exist_refuses_by_real_execution" }, + entry: "dag/test/claim/workspace_transcript_rate_real_execution_witness_test.dag", + function: "a_root_that_does_not_exist_refuses_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.seeded_filler_real_execution", function: "a_seeded_filler_is_exactly_its_size_and_reproducible_by_real_execution" }, + entry: "dag/test/claim/seeded_filler_real_execution_witness_test.dag", + function: "a_seeded_filler_is_exactly_its_size_and_reproducible_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.seeded_filler_real_execution", function: "two_seeds_give_different_fillers_by_real_execution" }, + entry: "dag/test/claim/seeded_filler_real_execution_witness_test.dag", + function: "two_seeds_give_different_fillers_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.seeded_filler_real_execution", function: "a_zero_stand_in_compresses_and_the_filler_does_not_red_control_by_real_execution" }, + entry: "dag/test/claim/seeded_filler_real_execution_witness_test.dag", + function: "a_zero_stand_in_compresses_and_the_filler_does_not_red_control_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.workspace_source_pack_archive_real_execution", function: "the_two_root_archive_carries_the_placeholder_and_reads_back_clean_by_real_execution" }, + entry: "dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag", + function: "the_two_root_archive_carries_the_placeholder_and_reads_back_clean_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.workspace_source_pack_archive_real_execution", function: "same_size_real_content_at_the_placeholder_path_is_refused_by_digest_by_real_execution" }, + entry: "dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag", + function: "same_size_real_content_at_the_placeholder_path_is_refused_by_digest_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.workspace_source_pack_archive_real_execution", function: "one_ordinary_file_and_zero_placeholders_packs_clean_by_real_execution" }, + entry: "dag/test/claim/workspace_source_pack_archive_real_execution_witness_test.dag", + function: "one_ordinary_file_and_zero_placeholders_packs_clean_by_real_execution", + expectation: ExpectedToHold {} + }, + WetScheduledClaim { + identity: WitnessIdentity { module_path: "test.claim.repository_convergence_wet_witness_test", function: "a_legacy_derived_roster_file_is_retired_and_read_back_absent" }, + entry: "dag/test/claim/repository_convergence_wet_witness_test.dag", + function: "a_legacy_derived_roster_file_is_retired_and_read_back_absent", + expectation: ExpectedToHold {} } ] } @@ -3068,26 +3092,6 @@ fn local_repo_wet_schedule() -> List { // without it is never handed this lane (docs/plans/wet-witness-host-premise-census.md). fn local_repo_wet_premise_roster() -> List { [ - WetPremisedClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_held_observer_is_admitted_and_its_triggered_still_is_hash_bound" }, - premise: RunnerBrowserToolchainReadyForJobUser {} - }, - WetPremisedClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_busy_viewer_slot_refuses_the_observer_and_the_handoff" }, - premise: RunnerBrowserToolchainReadyForJobUser {} - }, - WetPremisedClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_connection_lost_mid_boot_is_reported_and_no_still_is_claimed_after_it" }, - premise: RunnerBrowserToolchainReadyForJobUser {} - }, - WetPremisedClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "an_unreadable_canvas_is_acquisition_failed_with_the_browsers_words" }, - premise: RunnerBrowserToolchainReadyForJobUser {} - }, - WetPremisedClaim { - identity: WitnessIdentity { module_path: "test.claim.machine_intake.mtcollins1_kvm_observer_protocol_wet_witness", function: "a_relative_observer_directory_is_resolved_once" }, - premise: RunnerBrowserToolchainReadyForJobUser {} - }, WetPremisedClaim { identity: WitnessIdentity { module_path: "test.claim.roadmap.allocation_client_execution_wet_witness", function: "client_json_reject_keeps_intent" }, premise: RunnerBrowserToolchainReadyForJobUser {} diff --git a/src/v2/workflow/native_cache_fusion.dag b/src/v2/workflow/native_cache_fusion.dag deleted file mode 100644 index 8f8295ff094..00000000000 --- a/src/v2/workflow/native_cache_fusion.dag +++ /dev/null @@ -1,44 +0,0 @@ -module v2.workflow.native_cache_fusion - -import std.emit_on_demand { - EmitOnDemandLookup, EmitOnDemandHit, EmitOnDemandMiss, - EmitOnDemandCacheReceipt, materialization_for_lookup -} -import std.content_hash { ContentHash } -import std.content_hash { content_hash_atom, as_content_hash_structural } -import std.realization { Materialization, Share, Recompute } - -// P2/host fusion law (census R6 projection, 2026-07-22): the host transport decides warmness as -// compile_skipped = !cold_control && marker_exists (v1_interpreter.rs -// emit_host_run_transport_cached_in_workspace, the .native_ready rule), while the P2 model decides -// Share|Recompute via emit_on_demand_lookup over the artifact store. These are two realizations of -// ONE decision and must agree: marker-present under no control == store hit == Share; marker-absent -// or control-forced == miss == Recompute. This module states that agreement as the executable law -// over a modeled host observation (mirroring the cited Rust rule), so divergence between the model -// and the realization is a red, not silent drift. Full fusion — the host transport reading the P2 -// store itself — is the R6 construction end-state; until then this law is the tripwire. - -type HostWarmObservation { - marker_exists: Bool - cold_control: Bool -} - -fn host_compile_skipped(o: HostWarmObservation) -> Bool { - o.cold_control == false && o.marker_exists -} - -fn model_lookup_for(o: HostWarmObservation, key: ContentHash) -> EmitOnDemandLookup { - if host_compile_skipped(o: o) { - EmitOnDemandHit { - receipt: EmitOnDemandCacheReceipt { key: key, compile_skipped: true } - } - } else { - EmitOnDemandMiss - } -} - -fn fusion_agreement_holds_for(o: HostWarmObservation) -> Bool { - let key = as_content_hash_structural(structural: content_hash_atom(value: "fusion-law-synthetic-key")) - let model_share = materialization_for_lookup(lookup: model_lookup_for(o: o, key: key)) == Share - host_compile_skipped(o: o) == model_share -} diff --git a/src/v2/workflow/realization_attempt.dag b/src/v2/workflow/realization_attempt.dag index b0c0be62455..95ca8a7a922 100644 --- a/src/v2/workflow/realization_attempt.dag +++ b/src/v2/workflow/realization_attempt.dag @@ -1,5 +1,6 @@ module v2.workflow.realization_attempt +import v2.std.member_declaration_kind { no_member_declaration_kinds } import v2.compiler.ingested_fixture_arrows { ingested_resolved_module_from_source } import v2.compiler.program_assembly { assemble_program_from_ingest } import v2.compiler.resolve { ResolvedTree } @@ -70,7 +71,7 @@ type EntryRealizationAttempt { } // The exact cause is the chain's FATAL element (its last), never its head: an advisory prepended -// to the refusal (the grammar overlap residue heads every module's chain) only rode along. +// to the refusal by rejected_with_pending only rode along. fn first_located_cause(ds: NonEmptyDiagnostics) -> Symbol { diagnostics_fatal_reason(d: ds) } @@ -287,7 +288,7 @@ fn attempt_joined(program: ResolvedTree, fn_name: Symbol, entry: String) -> Entr match list_at_optional(xs: joined, index: 0) { Absent => attempt_refused(entry: entry, phase: PhaseResolve, cause: ^realization_attempt_identity_absent) Present { value: decl } => - match infer_and_discharge(tree: ResolvedTree { root: decl, symbol_index: program.symbol_index, resolved_declarations: program.resolved_declarations, lexical_bindings: program.lexical_bindings, unavailable_providers: program.unavailable_providers }) { + match infer_and_discharge(tree: ResolvedTree { root: decl, symbol_index: program.symbol_index, resolved_declarations: program.resolved_declarations, lexical_bindings: program.lexical_bindings, unavailable_providers: program.unavailable_providers, member_kinds: no_member_declaration_kinds() }) { Rejected { diagnostics: ds } => attempt_refused_at(entry: entry, phase: PhaseInfer, cause: first_located_cause(ds: ds), located: first_located_file(ds: ds)) Accepted { value: inferred, diagnostics: _ } => diff --git a/src/v2/workflow/realization_sweep.dag b/src/v2/workflow/realization_sweep.dag index 1df3540b6c9..a02f1b1451e 100644 --- a/src/v2/workflow/realization_sweep.dag +++ b/src/v2/workflow/realization_sweep.dag @@ -27,7 +27,7 @@ import v2.compiler.source_authority_read { SourceRootIngestBuildReadFailed, source_root_ingest_build_from_source_refs } -import v2.std.integer { integer_int_to_decimal_string } +import std.integer { int_to_decimal_string } import std.optional { Optional, Present, Absent } import v2.std.algebra { fold_list, length } import v2.std.collection { list_at_optional } @@ -372,15 +372,15 @@ fn sweep_histogram_tsv(rows: List) -> String { SweepHistogramState { prev: k, count: 1, - out: if st.out == "" { concat(st.prev, "\t", integer_int_to_decimal_string(value: st.count)) } else { concat(st.out, "\n", st.prev, "\t", integer_int_to_decimal_string(value: st.count)) } + out: if st.out == "" { concat(st.prev, "\t", int_to_decimal_string(value: st.count)) } else { concat(st.out, "\n", st.prev, "\t", int_to_decimal_string(value: st.count)) } } } }) if folded.count == 0 { folded.out } else if folded.out == "" { - concat(folded.prev, "\t", integer_int_to_decimal_string(value: folded.count)) + concat(folded.prev, "\t", int_to_decimal_string(value: folded.count)) } else { - concat(folded.out, "\n", folded.prev, "\t", integer_int_to_decimal_string(value: folded.count)) + concat(folded.out, "\n", folded.prev, "\t", int_to_decimal_string(value: folded.count)) } } diff --git a/src/v2/workflow/rust_crate_partition.dag b/src/v2/workflow/rust_crate_partition.dag index ce1c8b2a30a..a3e72b02fb8 100644 --- a/src/v2/workflow/rust_crate_partition.dag +++ b/src/v2/workflow/rust_crate_partition.dag @@ -24,7 +24,7 @@ import v2.lens.common.construction_justification { ConstructionJustification, Wa import v2.std.algebra { any, fold_list, length } import v2.std.collection { empty_map, - map_get, + map_get_checked, map_insert } import std.optional { @@ -683,7 +683,7 @@ fn emitted_target_reach(module_dag: ModuleDag, emitted: List) -> Ma match edge.to { EmittedCrateRootTarget => acc EmittedModuleTarget { module: target } => - match map_get(acc, target) { + match map_get_checked(acc, target) { Accepted { value: Present { value: _ }, diagnostics: _ } => acc Accepted { value: _, diagnostics: _ } => map_insert(acc, target, @@ -699,7 +699,7 @@ fn emitted_target_reach(module_dag: ModuleDag, emitted: List) -> Ma } fn string_member(set: Map, key: String) -> Bool { - match map_get(set, key) { + match map_get_checked(set, key) { Accepted { value: Present { value: _ }, diagnostics: _ } => true Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -727,7 +727,7 @@ fn emitted_edge_merge_step( refusal: optional_present(value: EmittedEdgeEndpointOutsideDag { from: edge.from, to: target }) } } else { - let target_reaches_source = match map_get(reach, target) { + let target_reaches_source = match map_get_checked(reach, target) { Accepted { value: Present { value: reached }, diagnostics: _ } => string_member(set: reached, key: edge.from) Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => false @@ -879,7 +879,7 @@ fn assignment_owner( assignment: Map, module_id: ModuleId ) -> Optional { - match map_get(assignment, module_id) { + match map_get_checked(assignment, module_id) { Accepted { value: owner, diagnostics: _ } => owner Rejected { diagnostics: _ } => Absent } @@ -912,7 +912,7 @@ fn assignment_first_stranger( match acc { Present { value: _ } => acc Absent => - match map_get(known, module_id) { + match map_get_checked(known, module_id) { Accepted { value: Present { value: _ }, diagnostics: _ } => Absent Accepted { value: Absent, diagnostics: _ } => Present { value: module_id } Rejected { diagnostics: _ } => Present { value: module_id }